I want to add a button outside the node in jstree - jstree

I'm a user who just used jstree.
As shown in the code below, I implemented a code that creates the rename/Delete Node button inside the node when the mouse is placed on the node, and it is currently working well.
But when I checked my project proposal again, the button had to be located outside the node selection area no matter what. Is there a way to implement the task with css or jquery?
Or is it possible to find a node that is currently hovering only with jquery without an event listener dedicated to jstree?
It should be implemented in the following form.
This is my code
$('.jstree').on("hover_node.jstree", function (e, data) {
if (data.node.parents.length > 1) {
let hovered_node = $('li#' + data.node.id).not('#new_node, #create_node').find('a#'+data.node.id+'_anchor');
hovered_node.append(`<a href="javascript:;" class='rename_node' onclick='renameCompDir("${data.node.id}");'>mod</a>`);
hovered_node.append(`<a href="javascript:;" class='delete_node' onclick='deleteCompDir("${data.node.id}");'>del</a>`);
}
}).on("dehover_node.jstree", function (e, data) {
$('#' + data.node.id + '_btn').remove();
$('.delete_node').remove();
$('.rename_node').remove();
});

Related

Meteor/Blaze/Mongo/Leaflet - Dynamically filled Leaflet popups do not pass data to button for entry into database?

I'm working with a Leaflet map that displays marker data based on a MongoDB query. The query results are saved into a variable (I know, bad form for large volumes of info but okay if you only have ~25 pieces) as an array, and then I've iterated over that variable and it's stored information using a for loop to create my leaflet map markers and populate their popups with the information specific to each entry. This part works great.
this.autorun(function(){
fsqresults = FsqResults.find().fetch({});
container = $('<div />');
for (i=0; i < fsqresults.length; i++) {
marker = L.marker([fsqresults[i].geometry.coordinates[1], fsqresults[i].geometry.coordinates[0]], {icon: violetIcon}).addTo(mymap);
container.html("<b>" + "Name: " + "</b>" + fsqresults[i].properties.name + "<br>" +
"<b>" + "Address: " + "</b>" + fsqresults[i].properties.address + "<br>" +
"<b>" + "Checkins: " + "</b>" + fsqresults[i].properties.checkIns + "<br>" +
"<b>" + "Users: " + "</b>" + fsqresults[i].properties.usersCount + "<br>" +
"<b>" + "Tips: " + "</b>" + fsqresults[i].properties.tips + "<br>");
marker.bindPopup(container[0]);
} // end for loop
For each marker, there is a button to log a "checkin" event to another Mongo collection to house the checkin entries. The button fires an event successfully, and creates the entry into the second database, but will not bind the dynamically populated data to the entry so I can see which marker the user has clicked on.
container.append($('<button class="btn btn-sm btn-outline-primary js-checkin">').text("Check In"));
container.on('click', '.js-checkin', function() {
var currentVenue = fsqresults[i].properties.name;
console.log(currentVenue);
console.log("You clicked the button!");
if (!Meteor.user()) {
alert("You need to login first!");
}
if (Meteor.user()) {
console.log("Meteor User Verified");
Checkins.insert({user: Meteor.user(), name: currentVenue});
}
});
}); // end this.autorun
The console tells me that currentVenue is undefined. I know this has something to do with the fact that fsqresults is a dynamically populated variable. I have tried to find ways to "solidify" the information in it (i.e. - creating a second variable with an empty array, pushing the data from fsqresults into it, and then having the markers iterate over that variable) but that hasn't worked as the MongoDB query results, despite being in an array format themselves, will not push or concat into the variable with an empty array successfully.
I've been searching for an answer to this problem and I'm coming up short. I'm lost; is there any other solution which could be staring me in the face?
Some things to note: All of this code lives in the Template.map.onRendered() function. Leaflet has scoping issues if I delegate the code into helpers and events, which is why I haven't created a {markers} template and just done {{#each markers}} over it for iteration. Therefore I am relegated to jQuery style coding for creating DOM elements and firing event triggers. The code above is wrapped in a this.autorun function to ensure it does indeed run upon map rendering. I don't think this is the issue (although one can never rule it out!).
As pointed out in the question comments, you have a scope issue with your i index iterator, and there should be no technical problem in integrating Leaflet with Meteor (although with Blaze that may not be totally trivial nor interesting).
1. Iteration scope issue
The console tells me that currentVenue is undefined.
That is because you try to access fsqresults[i].properties.name in your container.on('click' event listener / callback, which will be called on user click, i.e. after your for loop is complete, hence your i index iterator variable will be equal to fsqresults.length.
You are in the case of Example 6 of the accepted answer of: How do JavaScript closures work?
2. Leaflet integration with Meteor (Blaze)
Since you mention having tried helpers, events, and {{#each markers}}, I assume you use Blaze as your Meteor rendering engine.
While React-Leaflet and Vue2Leaflet indeed offer the possibility to use a kind of "<Marker>" component (same for other types of Leaflet Layer), the latter is only for template declaration purpose, i.e. it does not directly render any DOM / HTML, but only calls some Leaflet methods, which will be in charge of manipulating the DOM. As stated on React-Leaflet limitations:
The components exposed are abstractions for Leaflet layers, not DOM elements.
Side note: interesting to see that angular-leaflet-directive and #asymmetrik/ngx-leaflet did not fall into the same temptation and sticked to JS declaration of Leaflet layers.
Therefore trying to create a Template.Marker (used as {{> Marker}}) in Blaze might be overkill, as you would basically just call some Leaflet factories (like L.marker) within your Template.Marker.onCreated (and needing to access somehow the parent map object to add your Marker into…), without rendering any DOM node yourself (i.e. you would have an HTML file with empty <template name="Marker"></template>).
While we forget about a Marker template in Blaze (as you have already done), we can still leverage Blaze events management to handle user clicks in your Leaflet Popup. For that, we would need a few Blaze features, that I admit could benefit being better documented:
We can attach arbitrary JS data to our template instance.
Template events are delegated, hence we do not need to attach them to each <button> before hand.
We can easily access the template instance in event handlers (as the 2nd argument of the event listener).
2.1. Attaching arbitrary JS data to our template instance
As stated in the Template Instances API:
[…] you can assign additional properties of your choice to the object. Use the onCreated and onDestroyed methods to add callbacks performing initialization or clean-up on the object.
Therefore you can store your fsqresults on your Template instance, so that you can access it later on (typically in your event listener):
Template.myTemplate.onRendered(function () {
this.autorun(() => { // Using an arrow function to keep the same `this`, but you could do `const self = this` beforehand.
const fsqresults = this.fsqresults = FsqResults.find().fetch();
});
});
But since we want to access specific Features later on, it might be more interesting to convert fsqresults to a dictionary. Since your ID seems to be feature.properties.name, you could do:
Template.myTemplate.onCreated(function () {
this.autorun(() => {
const fsqresults = this.fsqresults = FsqResults.find().fetch();
const markersDict = this.markersDict = {};
L.geoJSON(fsqresults, {
pointToLayer(feature, latlng) {
const props = feature.properties;
const markerName = props.name;
// Save a direct reference to the Feature data,
// using the `markerName` as key (ID).
markersDict[markerName] = feature;
// Store the `markerName` in the button `dataset`
// (i.e. as a `data-` attribute),
// as already suggested in the question comments,
// so that we can easily retrieve the ID / key
// of the Marker data associated with the button the user clicked on.
return L.marker(latlng).bindPopup(`
<p>${markerName}</p>
<button role="popupClick" data-marker-name="${markerName}">
Popup action
</button>
`);
},
});
});
});
2.2. Template event handler delegation
As stated on the Blaze Overview Details:
DOM engine […] which features […] event delegation
(sorry there does not seem to be any other mention of this feature in the official doc… let me know if you find a better one!)
Therefore, as long as we create a Template event handler with the appropriate selector, we do not need to attach the event listener on each button, which anyway we may not create as a Node but leave it as String passed to Leaflet .bindPopup (as done in the above code sample).
For example:
Template.myTemplate.events({
// Even if the `<button role="popupClick">` are not DOM nodes yet
// (because Leaflet will create them from the HTML String
// only when the user opens the popup by clicking on the Marker),
// the "click" event will bubble up to the template instance,
// which will call this event handler if it matches the selector.
'click button[role="popupClick"]'() {
console.log('clicked on a button that has been built in a Leaflet Popup');
}
});
2.3. Access the template instance, and our Feature data
The Blaze event handler are called with an extra 2nd argument, which is the current template instance:
The handler function receives two arguments: event, an object with information about the event, and template, a template instance for the template where the handler is defined.
Therefore in our case we can easily retrieve the markersDict variable that we have defined in onCreated, and use it to retrieve the exact Marker's Feature data associated with the button the user clicked on:
Template.myTemplate.events({
'click button[role="popupClick"]'(event, templateInstance) {
const button = event.currentTarget;
const markerName = button.dataset.markerName;
const markerFeature = templateInstance.markersDict[markerName];
// Do something with `markerFeature`…
console.log(markerFeature);
}
});
If you only need the property name, then you could even skip step 2.1 and directly use the markerName string retrieved from the <button> dataset.
I've come up with a solution to the first part of my issue - at first a javascript closure/scope issue to the inner and outer function scopes. I spent about 2 days wrapping my head around this SO answer: the concept of using the first for loop to produce individual instances of the function (if this were a play, the first for loop would "set the stage" for the show), and using the second for loop to execute each instance of the function ("lights, camera, action!").
I also decided that I could maintain scope if I declared my variables inside the first for loop - but I still had this issue of it only pulling the last value. Then I tried simply redeclaring my variables as constants. To my surprise, using const allowed me to write each instance to each map marker, and I could reliably access the correct iteration of the data upon each correspondent map marker! So no need for a second for loop.
this.autorun(function(){
fsqresults_fetch = FsqResults.find().fetch({});
// console.log(fsqresults_fetch);
for (i = 0; i < fsqresults_fetch.length; i++) {
container = $('<div />');
const fsq_marker = L.marker([fsqresults_fetch[i].geometry.coordinates[1], fsqresults_fetch[i].geometry.coordinates[0]], {icon: blueIcon}).addTo(mymap);
const fsq_venueAddress = fsqresults_fetch[i].properties.address;
const fsq_venueName = fsqresults_fetch[i].properties.name;
const fsq_geometry = {type: "Point",
coordinates: [fsqresults_fetch[i].geometry.coordinates[0], fsqresults_fetch[i].geometry.coordinates[1]]};
container.html("<b>" + "Name: " + "</b>" + fsqresults_fetch[i].properties.name + "<br>" +
"<b>" + "Address: " + "</b>" + fsqresults_fetch[i].properties.address + "<br>");
container.append($('<button class="btn btn-sm btn-outline-primary" id="js-checkin">').text("Check In"));
fsq_marker.bindPopup(container[0]);
container.on('click', '#js-checkin', function() {
console.log("You clicked the button!");
if (!Meteor.user()) {
alert("You need to login first!");
}
if (Meteor.user()) {
console.log("Meteor User Verified");
Checkins.insert({type: "Feature", geometry: fsq_geometry, properties: {name: fsq_venueName, address: fsq_venueAddress, user: Meteor.user()}});
}
}); //end container.on
} //end for loop
}); //end this.autorun
As I said in the comment on the last response, it's a bit hack-y, but functional enough to do the job successfully.
Now what I'm really curious to try is the solution that #ghybs posted so I have my events grouped and firing as Blaze is supposed to work!

TinyMCE 3 - textarea id which fired blur event

When a TinyMCE editor blur occurs, I am trying to find the element id (or name) of the textarea which fired the blur event. I also want the element id (or name) of the element which gains the focus, but that part should be similar.
I'm getting closer in being able to get the iframe id of the tinymce editor, but I've only got it working in Chrome and I'm sure there is a better way of doing it. I need this to work across different browsers and devices.
For example, this below code returns the iframe id in Chrome which is okay since the iframe id only appends a suffix of "_ifr" to my textarea element id. I would prefer the element id of the textarea, but it's okay if I need to remove the iframe suffix.
EDIT: I think it's more clear if I add a complete TinyMCE Fiddle (instead of the code below):
http://fiddle.tinymce.com/HIeaab/1
setup : function(ed) {
ed.onInit.add(function(ed) {
ed.pasteAsPlainText = true;
/* BEGIN: Added this to handle JS blur event */
/* example modified from: http://tehhosh.blogspot.com/2012/06/setting-focus-and-blur-event-for.html */
var dom = ed.dom,
doc = ed.getDoc(),
el = doc.content_editable ? ed.getBody() : (tinymce.isGecko ? doc : ed.getWin());
tinymce.dom.Event.add(el, 'blur', function(e) {
//console.log('blur');
var event = e || window.event;
var target = event.target || event.srcElement;
console.log(event);
console.log(target);
console.log(target.frameElement.id);
console.log('the above outputs the following iframe id which triggered the blur (but only in Chrome): ' + 'idPrimeraVista_ifr');
})
tinymce.dom.Event.add(el, 'focus', function(e) {
//console.log('focus');
})
/* END: Added this to handle JS blur event */
});
}
Maybe it's better to give a background of what I'm trying to accomplish:
We have multiple textareas on a form which we're trying to "grammarcheck" with software called "languagetool" (that uses TinyMCE version 3.5.6). Upon losing focus on a textarea, we would like to invoke the grammarcheck for the textarea that lost focus and then return the focus to where it's supposed to go after the grammar check.
I've struggled with this for quite some time, and would greatly appreciate any feedback (even if it's general advice for doing this differently).
Many Thanks!
Simplify
TinyMCE provides a property on the Editor object for getting the editor instance ID: Editor.id
It also seems overkill to check for doc.content_editable and tinyMCE.isGecko because Editor.getBody() allows for cross-browser compatible event binding already (I checked IE8-11, and latest versions of Firefox and Chrome).
Note: I actually found that the logic was failing to properly assign ed.getBody() to el in Internet Explorer, so it wasn't achieving the cross-browser functionality you need anyway.
Try the following simplified event bindings:
tinyMCE.init({
mode : "textareas",
setup : function (ed) {
ed.onInit.add(function (ed) {
/* onBlur */
tinymce.dom.Event.add(ed.getBody(), 'blur', function (e) {
console.log('Editor with ID "' + ed.id + '" has blur.');
});
/* onFocus */
tinymce.dom.Event.add(ed.getBody(), 'focus', function (e) {
console.log('Editor with ID "' + ed.id + '" has focus.');
});
});
}
});
…or see this working TinyMCE Fiddle »
Aside: Your Fiddle wasn't properly initializing the editors because the plugin was failing to load. Since you don't need the plugin for this example, I removed it from the Fiddle to get it working.

jstree move, drag and drop

I want to implement the move functionality for a node in jstree. Is it the move that needs to be implemented or the drag and drop? Also, it would be nice to have working code for binding the container to event and the event code.
You only need to use the dnd plugin if you don't need to enforce any move rules(don't allow certain nodes to be moved to other nodes etc)
If you need to enforce move rules, you can add the crrm plugin.
See the Reorder only demo of the dnd pluign documentation for an example of this. The documentation is very poor, so you will have to use the developer tool on your browser to see what the properties of the parameter for the check_move callback are. For the example in the documentation, m.o refers to your dragged node and m.r refers to your destination node.
You will also likely need to be notified when a node is moved, so bind to the move_node.jstree event when you initialize the tree:
$("#treeHost").jstree({
// ...
}).bind("move_node.jstree", function (e, data) {
// data.rslt.o is a list of objects that were moved
// Inspect data using your fav dev tools to see what the properties are
});
$("#demo1").jstree({
....
.bind("move_node.jstree", function (e, data) {
/*
requires crrm plugin
.o - the node being moved
.r - the reference node in the move
.ot - the origin tree instance
.rt - the reference tree instance
.p - the position to move to (may be a string - "last", "first", etc)
.cp - the calculated position to move to (always a number)
.np - the new parent
.oc - the original node (if there was a copy)
.cy - boolen indicating if the move was a copy
.cr - same as np, but if a root node is created this is -1
.op - the former parent
.or - the node that was previously in the position of the moved node */
var nodeType = $(data.rslt.o).attr("rel");
var parentType = $(data.rslt.np).attr("rel");
if (nodeType && parentType) {
// TODO!
}
})
});
The above approaches do not work with the latest versions of jstree (3.3.7 as of today).
The first line of Bojin's answer still holds true. To implement rules, you can use core.check_callback or possibly, the types plugin; the crrm plugin doesn't exist anymore. Bind to move_node.jstree to perform some action on completion of move (drop). By default, the dnd plugin allows re-ordering (dropping between two nodes) and copying (Ctrl + drag), in addition to moving a node. The code snippet below shows how to disable these additional behaviors.
$('#treeElement').jstree({
'core': {
'check_callback': CheckOperation,
...
},
'plugins': ['dnd']
})
.bind("move_node.jstree", function(e, data) {
//data.node was dragged and dropped on data.parent
});
function CheckOperation(operation, node, parent, position, more) {
if (operation == "move_node") {
if (more && more.dnd && more.pos !== "i") { // disallow re-ordering
return false;
}
... more rules if needed ...
else {
return true;
}
}
else if (operation == "copy_node") {
return false;
}
return true;
}

Jstree dblclick binding problem [duplicate]

I try to use good lib jstree but i have some strange problem with dblclick binding.
Here is my code
$("#basic_html").jstree({
themes: {
url: "http://mywork/shinframework/shinfw/themes/redmond/css/jstree/default/style.css"
},
"plugins" : ["themes","html_data","ui","crrm","hotkeys", "core"],
});
$("#basic_html").bind("dblclick.jstree", function (e, data) {
alert(e);
alert(data);
});
When this code runs and i make dblclick for some node i can see 2 alerts. The first is object -right, the second is undefined - BUT i want receive data information.
Please, if some specialist solve this problem give me right way for correct use dblclick and receive "data" information about node who is i clicked.
Thanks
I recommend this approach . . .
$("#basic_html li").live("dblclick", function (data) {
//this object is jsTree node that was double clicked
...
});
First, you usually only need to know if the li was clicked so monitoring the event on the li will give you everything you need. Secondly, use live or delegate for the event binding so you can manipulate the tree without breaking the event.
Once you have the node that was double clicked (the this object) you can then use the built-in functions like this . . .
if (!jsAll.is_selected(this)) { return false; } //cancel operation if dbl-clicked node not selected
Where . . .
jsAll = $.jstree._reference("basic_html")
$("#basic_html").bind("dblclick.jstree", function (event) {
var node = $(event.target).closest("li");//that was the node you double click
});
that's the code you want.

Jstree : dblclick binding parameter data is undefined

I try to use good lib jstree but i have some strange problem with dblclick binding.
Here is my code
$("#basic_html").jstree({
themes: {
url: "http://mywork/shinframework/shinfw/themes/redmond/css/jstree/default/style.css"
},
"plugins" : ["themes","html_data","ui","crrm","hotkeys", "core"],
});
$("#basic_html").bind("dblclick.jstree", function (e, data) {
alert(e);
alert(data);
});
When this code runs and i make dblclick for some node i can see 2 alerts. The first is object -right, the second is undefined - BUT i want receive data information.
Please, if some specialist solve this problem give me right way for correct use dblclick and receive "data" information about node who is i clicked.
Thanks
I recommend this approach . . .
$("#basic_html li").live("dblclick", function (data) {
//this object is jsTree node that was double clicked
...
});
First, you usually only need to know if the li was clicked so monitoring the event on the li will give you everything you need. Secondly, use live or delegate for the event binding so you can manipulate the tree without breaking the event.
Once you have the node that was double clicked (the this object) you can then use the built-in functions like this . . .
if (!jsAll.is_selected(this)) { return false; } //cancel operation if dbl-clicked node not selected
Where . . .
jsAll = $.jstree._reference("basic_html")
$("#basic_html").bind("dblclick.jstree", function (event) {
var node = $(event.target).closest("li");//that was the node you double click
});
that's the code you want.