jQuery UI - Slider unbinds after one use - javascript

I'm still a novice, so please go easy on me!
I'm making a JavaScript game. The game works fine, as do the basics of the user interface, like making menu selections or switching screens. But I'm also trying to implement jQuery UI sliders in one of my options menus, which is where I run into trouble.
I can only use the slider once, after which it becomes "stuck." It responds to mouseover - it'll highlight as though it's ready to scroll - but will not budge if I try to move it again.
So far, I've ruled out any problems with the build of jQuery/jQUI I'm using; the demo page works fine.
I have no idea what the problem might be, but I suspect it has something to do with the way I've put together my UI. The way my UI works is by creating a "View" object that contains pointers to a parent DOM element. I then use jQuery to construct its children and use the "loadElement" method to add it to the view's list of children elements:
function CView (parent, target, visible, jQElements) {
this.parent = parent;
this.visible = visible;
this.parentDisplay = parent.css("display");
this.parentPosition = parent.css("position");
this.elements = [];
for(element in jQElements) {
this.elements.push(element);
}
if (!this.visible) {
this.parent.css({ // Default to hidden state
"opacity": 0,
"display": "none"
});
}
this.parent.appendTo(target);
};
CView.prototype.loadElement = function(element) {
element.appendTo(this.parent);
this.elements.push(element);
return element;
};
All these elements can be shown and hidden together with a method call on the View object. Currently, hiding a view unbinds all event listeners in the elements of that view. I don't think this is the problem, since I get this problem immediately after creating a new view.
The issue, I think, might be in this code, which is for swapping views- Perhaps I'm unbinding some kind of document-level listener that jQUI uses?
var swapView = GameUI.swapView = function(view, callbacks) {
$(document).off(); // unbind key listeners
currentView && currentView.hideView(); // also unbinds event listeners
currentView = view;
view.showView(callbacks);
};
There's one more thing that might be relevant, the way I construct the slider and put it in:
var $volumeSlider = jQuery("<div/>", {
class: "options-menu-volume-slider"
});
var resetVolumeSlider = function () {
$volumeSlider.slider({
range: "min",
value: GameUI.options.volume,
min: 0,
max: 100
})
};
resetVolumeSlider();
If you need to see more code, let me know. I really am not sure what's going wrong here. Any and all help is appreciated. (Also, I don't know how to host my game online to demo it. It's basically just an HTML page that runs a bunch of JS.)

It turns out that this problem was caused by my call to $(document).off(), which I used to remove potentially dangling document-level keypress handlers. This had the unfortunate result of also destroying event handlers for jQuery UI.
In the future, my views will have keypresses bound at the parent div level with tab indices set for each div, so that I don't have to make the call to $(document).off() and can simply use hideView() to unbind.

Related

How to trigger a class change event when the code that changes the class is out of reach

I need to trigger an event on a class when that class changes
The only known change noticed in the DOM is that the class obtains a second class (say the class is "selectable", it becomes "selectable selected")
https://jsfiddle.net/zn1xj7wb/1/
In this fiddle, the blue squares may be selected and the css change happens when the class changes (adds "selected")
The goal is to be able to do something in another part of my code like that:
$("[class*='selectable']").on('classChange', function() {
//do stuff like change the background color or add text
//alert("this selectable div has been selected");
});
I am unsure how to proceed as jquery has no event for a class change, and I cannot add "classChange" the trigger to the hidden part of the code that adds and removes the "selected" class for it to be picked up by my code.
EDIT: the reason I need the trigger to be the class change is that it is a graph that uses up the first click to change the class (select the node of the graph) and so a first click on the div of that class does not register, only the second time, and I cannot have to click twice to //do stuff.
I'm not sure I understand your problem, but what I would do is atach the event to the document, like this:
$(document).on("click",".selectable", function() {
//do your stuff here
});
Now, as I've read you need to do something right after you add the class "selected" to "selectable", so you could do it in the function by checking wether it has the class or not and then do your stuff after you add the class "selected".
$(document).on("click",".selectable", function() {
if($(this).hasClass("selected")){
$(this).removeClass("selected")
//do your stuff
}else{
$(this).addClass("selected")
//do some different stuff
}
});
EDIT: Okay, so that won't work (see comments). However, I was able to come up with another solution. While you could regularly scan the whole DOM for changes using an external library, in this instance, you can make the app more performant by limiting your scope to just the selectable items.
What the following code does (jsfiddle link below) is take an initial sampling of the selected elements on the page. Then, once per event loop, it re-samples those selected elements. For each element that wasn't there before, it triggers a custom event:
$(document).ready(function() {
$('.selectable').on('customSelectEvent', (e) =>{
console.log("hello, world!");
// Do your stuff here
});
// Get the starting list of selectable elements
var selecteds = $('.selected');
// Using setInterval to make sure this runs at the back of the event loop
setInterval(() => {
let loopSelecteds = $('.selected');
$.each(loopSelecteds, function(loopIndex, loopSelected) {
let alreadySelected = false;
$.each(selecteds, function(index, selected) {
if ($(selected).get(0) === $(loopSelected).get(0)) {
alreadySelected = true;
}
});
if (!alreadySelected) {
$(loopSelected).trigger('customSelectEvent');
}
});
selecteds = loopSelecteds;
}, 0);
})
Some things to note here:
setInterval(()=>{...}, 0) is being used to cast this operation to the back of the event loop, so it will evaluate once per turn. Use caution when doing this, because if you do it too much, it can impact performance.
$().get(0) === $().get(0) is testing the DOM elements to see if they are the same element. We don't want to trigger the event if they are. Credit: https://stackoverflow.com/a/19546658/10430668
I'm using $.each() here because it's intelligent enough to handle collections of jQuery objects, which other loops weren't (without some fiddling).
Someone spot check me on this, but you may be able to put the custom event listener elsewhere in the code.
JS Fiddle: https://jsfiddle.net/zn1xj7wb/15/
This is my first answer, which doesn't work in this use case. I'll include it so that users who aren't so stuck can benefit from it:
Is there any reason you can't bind another listener to the click event
and test if it's got the right class? Such as:
$(document).ready(function() {
$(".selectable").click((e) => {
const currentElement = $(e.currentTarget);
// This is a little tricky: don't run the code if it has the class pre-setTimeout()
if (currentElement.hasClass('selected')) {
return;
}
// Using setTimeout to cast the evaluation to the end of the event loop
setTimeout(()=>{
if (currentElement.hasClass('selected')) {
// Do your things here.
alert("selected!");
}
},0);
})
})

Javascript: addEventListener not taking updated DOM into account?

I'm very new to javascript and I'm trying to give some dynamic features to a site I'm working on. In particular, I want to have an unfolding menu item that unfolds and folds back whenever the mouse is on and off of it, respectively.
I got the unfolding part down but the event listener that triggers the folding back does it whenever the mouse if off of the area where the menu item used to be, event though it's just been extended.
The HTML markup looks like this:
<nav id="nav">
<ul>
<li id="elemPlaces"><ul id="drop"><li>Places</li></ul></li>
</ul>
</nav>
The event listeners are declared like this:
<script type="text/javascript">
var extended = false;
var listPlace = (<?php echo json_encode($list_place); ?>);
document.getElementById("elemPlaces").addEventListener("mouseover", extend);
document.getElementById("elemPlaces").addEventListener("mouseout", retract);
</script>
and the extend and retract functions are the following:
function extend()
{
if(!extended)
{
var drop = document.getElementById("drop");
var form = document.createElement("form");
form.setAttribute("action", "place.php");
form.setAttribute("method", "get");
drop.appendChild(form);
for(var i = 0; i < listPlace.length; i++)
{
var li = document.createElement("li");
var input = document.createElement("input");
li.setAttribute("class", "dropOption");
input.setAttribute("type", "submit");
input.setAttribute("name", "location");
input.setAttribute("value", listPlace[i]);
li.appendChild(input);
form.appendChild(li);
}
extended = true;
}
}
function retract()
{
var dropOption = document.getElementsByClassName("dropOption");
while(dropOption[0])
{
dropOption[0].parentNode.removeChild(dropOption[0]);
}
extended = false;
}
I realize it all must look amateurish but like I said, I'm new to this. I'd really appreciate it if someone could help me.
Okay JSFiddle seems to be down, so here is a Plunker
Basically :
Use mouseenter and mouseleave (see this question)
I believe in your case you just want to redirect to a page passing a GET option. You can do it using <a href="yourpage.php?yourOption=yourValue">. It is simpler. You don't need forms for this.
As I said, here it's a bad idea to modify the dom. Or well, it depends on where your list comes from. If your $list_place comes from an AJAX asynchronous request, then obviously you can't do anything else but modify the DOM dynamically. However if you know beforehand what the content of your list will be, best it to just write everything to the HTML, and add classes like class="unexpanded/expanded", and have a CSS .unexpanded{display: none}. Then you just need to toggle/change the class
So basically what you want to so, is make sure the menu stays unfolded a certain amount of time before folding back ? Here are some ideas
Instead of calling retract on mouseout, you could call another function with a timeout, like retract_after
function retract_after(){
setTimeout(function(){ retract() }, 3000);
}
But this could lead to weird situations (imagine the user moves the mouse on the menu just after moving it out, before the timeout expired...). So you might want to lookup how to empty the queue or remove EventListeners.
Or, what you could do, is only attach the "retract" eventListener at the end of the mouseover function (and also eventually with a timeout), and remove the eventlistener while or after it is retracting.
Also the animate() function of jquery library already somewhat produces by default the behavior you're looking (it queues animation, but if a user quickly triggers mouseenter and mouseout event listeners, the menu will keep folding/unfolding till it empties the queue)

Dynamic menu for a RichCombo box in CKEditor

I've written a plugin which adds a RichCombo box to my CKEditor. I want to be able to update the content in the ListBox within this RichCombo
Here's my code.
var merge_fields = [];
CKEDITOR.plugins.add('mergefields',
{
requires: ['richcombo'], //, 'styles' ],
init: function (editor) {
var config = editor.config,
lang = editor.lang.format;
// Gets the list of tags from the settings.
var tags = merge_fields; //new Array();
// Create style objects for all defined styles.
editor.ui.addRichCombo('tokens',
{
label: "Merge",
title: "title",
voiceLabel: "voiceLabel",
className: 'cke_format',
multiSelect: false,
panel:
{
css: [config.contentsCss, CKEDITOR.getUrl(CKEDITOR.skin.getPath('editor') + 'editor.css')],
voiceLabel: lang.panelVoiceLabel
},
init: function () {
// this.startGroup("mergefields");
for (var this_tag in tags) {
this.add(tags[this_tag], tags[this_tag], tags[this_tag]);
}
},
onClick: function (value) {
editor.focus();
editor.fire('saveSnapshot');
editor.insertText(value);
editor.fire('saveSnapshot');
}
});
}
});
Unfortunately this list is not update when merge_fields changes. Is there a way to reinitialize the plugin, or failing that remove it and re-add it with updated content?
Note Id prefer NOT to have to remove the entire editor and replace it, as this looks very unpleasant to the user
UPDATE
As requested, here's a jsfiddle to help
http://jsfiddle.net/q8r0dkc4/
In this JSFiddle, you'll see that the menu is dynamically created the first time it is accessed. It should should the checkboxes which are selected. However, every subsequent time it is accessed, it keeps the same values and is not updated. The only way to update it is to reinitialise the editor using the reinit button I have provided, but this causes the editor to disappear and reappear, so I don't want to have to do this.
200 points of a bounty to someone who can make the dropdown dynamically update EVERY TIME it is called.
How about using CKEditors custom events something like this ?
First get reference to CKEditors instance
var myinstance = CKEDITOR.instances.editor1;
Since the checkboxes are outside the scope of CKEditor add a change handler to checkboxes
$(':checkbox').change(function () {
myinstance.fire('updateList'); // here is where you will fire the custom event
});
Inside plugin definition add a event listener on the editor like this
editor.on("updateList", function () { // this is the checkbox change listener
self.buildList(); // the entire build is created again here
});
Instead of directly attaching events on checkboxes (which are outside the scope of CKEditor) inside the plugin, I am using CKEditor's custom events. Now the editor instance and checkboxes are decoupled.
Here is a DEMO
Hope this helps
Update
Option 2
Plugin's methods can directly be called like this
$(':checkbox').change(function () {
CKEDITOR.instances.editor1.ui.instances.Merge.buildList(); //this method should build the entire list again
});
Even though this seems very straightforward, I don't think it is completely decoupled. (But still works)
Here is a demo for option 2
I think I've got a fix for you.
In your richCombo's init function add this line:
$('input').on('click', rebuildList);
Pretty simple JQuery fix, but every time I click those input boxes it will rebuild the list.
Fiddle for proof: https://jsfiddle.net/q8r0dkc4/7/

Make (possibly dynamically loaded) element clickable via JavaScript, but give precedence to links contained within

I am adding a custom data attribute data-js-href to various HTML elements, and these elements should behave just like a link when clicked. If a link within such an element is clicked, the link should take precedence and the data-js-href functionality should be ignored, though. Furthermore, the solution also needs to work with elements that are dynamically added at a later time.
So far, I have come up with the following solution. It basically checks if the click was performed on a link, or any child element of a link (think <a href='…'><img src='…' alt='…' /></a>).
// Make all elements with a `data-js-href` attribute clickable
$$('body').addEvent('click:relay([data-js-href])',
function(event, clicked) {
var link = clicked.get('data-js-href');
if (link && !event.target.match('a')) {
var parents = event.target.getParents();
for (var i = 0; i < parents.length && parents[i] != clicked; i++) {
if (parents[i].match('a')) {
return;
}
}
document.location.href = link;
}
});
It works, but it feels very clumsy, and I think that there has to be a more elegant solution. I tried something along the lines of
$$('body').addEvent('click:relay([data-js-href] a)',
function(event, clicked) {
event.stopPropagation();
}
but to no avail. (I littered the code with some console.log() messages to verify the behavior.) Any idea is welcome.
you can do this with 2 delegated events - no reverse lookups and it's cheap as they will share the same event. the downside is, it is the same event so it will fire for both and there's no stopping it via the event methods (already bubbled, it's a single event that stacks up multiple pseudo event callbacks and executes them in order--the event has stopped but the callbacks continue) That's perhaps an inconsistency in mootools event vs delegation implementation but it's a subject of another issue.
Workarounds for now can be:
to have the 2 event handlers communicate through each other. It will scale and work with any new els added.
to add the delegators on 2 different elements. eg. document.body and #mainWrap.
http://jsfiddle.net/dimitar/J59PD/4/
var showURL = function(howLong) {
// debug.
return function() {
console.log(window.location.href);
}.delay(howLong || 1000);
};
document.id(document.body).addEvents({
"click:relay([data-js-href] a))": function(e) {
// performance on lookup for repeat clicks.
var parent = this.retrieve("parent");
if (!parent) {
parent = this.getParent("[data-js-href]");
this.store("parent", parent);
}
// communicate it's a dummy event to parent delegator.
parent.store("linkEvent", e);
// let it bubble...
},
"click:relay([data-js-href])": function(e) {
// show where we have gone.
showURL(1500);
if (this.retrieve("linkEvent")) {
this.eliminate("linkEvent");
return;
}
var prop = this.get("data-js-href");
if (prop)
window.location.href = prop;
}
});
Discussed this with Ibolmo and Keeto from the mootools team on IRC as well when my initial attempt failed to work and both callbacks fired despite the event.stop: http://jsfiddle.net/dimitar/J59PD/
As a result, there was briefly a ticket open on the mootools github issues: https://github.com/mootools/mootools-core/issues/2105 but it then went into a discussion of what the right thing to do from the library standpoint is and how viable it is to pursue changing the way things work so...

preventing effects to be applied to the same object twice when adding objects with ajax

I'm having a little issue with an application I'm making. I have a page where the user edits a document via dragging modules into the page or "canvas" area.
http://thinktankdesign.ca/temp_img.jpg
When the page is loaded, javascript haves the modules collapsible (like above). However after the user drags in a new module the effect is applied again some new modules can collapse as well. here is the problem. each time a module loads the same effect gets applied to the modules that already can collapse. It ends up breaking their animations.
heres the code that gets executed on page load.
//make colapsible
$("h1.handle").click(function() {
var object = $(this);
v$(this).next().toggle("fast", colapsible_class(object));
vreturn false;
}).addClass("open");
and heres the code that gets executed in the creation of a module via ajax
function get_module(id){
var template = $('input[name=template]').val();
$.post(window.location.href, { template: template, module: id, mode: 'create' },
function(data){
$(data).insertBefore(".target_wrapper");
//enable deletion of module
$(".js_no_modules").slideUp("slow");
$(enable_module_deletion());
//show delete button
$("button[name=delete]").show();
//make colapsible
$("h1.handle").click(function() {
var object = $(this);
$(this).next().toggle("fast", colapsible_class(object));
return false;
}).addClass("open");
}
);
}
I need a solid way of preventing the toggle effect to be applied to the same module twice
Use jQuery 1.3 live events instead.
//make colapsible
$("h1.handle").live("click", function() {
var object = $(this);
v$(this).next().toggle("fast", colapsible_class(object));
vreturn false;
}).addClass("open");
and then eliminate the click declaration in the second block of code, changing it to $("h1.handle").addClass("open");
Live events bind all current and future matching elements with an event.
In your Ajax success handler try the following:
//make collapsible
$("h1.handle:not(.open)").click(function() {
var object = $(this);
$(this).next().toggle("fast", colapsible_class(object));
return false;
}).addClass("open");
The best way to solve your problem is, instead of using $("h1.handle") on the AJAX callback, go for $(data).find("h1.handle"). Something like,
var x = $(data);
x.insertBefore(...);
/* your other code */
x.find('h1.handle').click(...).addClass(...);
Like that, only the newly added items will have the event bounded. The already present ones will not be touched.
If we want to answer your question instead of just solving your problem, then we have several alternatives, such as:
store, in your objects, that the onclick event handler has been set so that you don't set it twice
always bind the onclick event, but always unbind it first
use jQuery's live events and the addClass open only on the newly created items.
IMO, the first one is the easiest. You can accomplish it by using jQuery's data(). Then you could do something like:
$("h1.handle").each(function() {
var me = $(this);
// if already has click handler, don't do anything
if (me.data('click_set') != null) { return true; }
// otherwise, store the data and bind the click event
me.data('click_set', true).click(function() {
/* the code you already have on the click handler */
}).addClass('open');
}
The second alternative involves storing the function that you pass inline to the click event binder in a variable, and then using jQuery's unbind to disable it.

Categories