jQuery widget _on delegating Element - javascript

I am using the Widget factory from jQuery UI into a existing production environment.
This environment uses a binded event called sm2.validateNext which triggers before the page changes. However when trying to bind to this event using _on, it would try to split this event name and delegate it. (Which i believe is correct, bot not the expected functionality for me).
Code (inside the $.widget):
this._on(true, document, {
"sm2.validateNext": function () { ... },
});
And debugging this code, it gets delegated (not my expected behaviour) (from jquery-ui.widget.js)
var match = event.match( /^(\w+)\s*(.*)$/ ),
eventName = match[1] + instance.eventNamespace,
selector = match[2];
if ( selector ) {
delegateElement.delegate( selector, eventName, handlerProxy );
} else {
element.bind( eventName, handlerProxy );
}
Because of the . (dot) it gets a match and selector becomes valid delegating the event. However I need to get binded with its handlerProxy.
Is there a way to avoid get "trapped" into that match? I tried to escape sm2\.validateNext without result.
Note: $(document).on("sm2.validateNext"...) Doesnt work for me as I need the instance to be at the widget object.
Thanks for your help!

Found my solution by using $.proxy
Instead of:
this._on(true, document, {
"sm2.validateNext": function () { ... },
});
I used:
$(document).on("sm2.validateNext", $.proxy(function () { ... }, this));
And now it works just as I expected.

Related

Inline Editing of Textarea – How to get the id?

I use jqInlineEdit for inline editing on a web page. Everything works, except I don't know how to get the id of the item which I need for saving the change to the database(via Django).
The HTML looks like this:
<div id="remark14756" class="remark" data-cid="14756">
Sample Text
</div>
That's the JavaScript:
<script src="/static/inline-edit.jquery.js"></script>
<script>
$(".remark").inlineEdit({
type: 'textarea',
onChange: function (e, text, html) {
// Executes when exiting inline edit mode and a change has been made
c_id = $(this).attr("data-cid");
alert("Test: ", c_id)
}
});
</script>
Obviously, $(this) does not work in this context. I tried everything and searched a lot but I can't find how to do it the right way. Does anybody know the answer?
The inlineEdit docs say:
onChange(this, text, html) - Executes when exiting inline edit mode and a change has been made
with the use of this being quite misleading.
therefore the first param is actually the Element.
$(".remark").inlineEdit({
type: 'textarea',
onChange: function (elem, text, html) {
// `this` refers to inlineEdit instance plugin
// `elem` is the currently edited element
const c_id = $(elem).attr("data-cid");
alert(c_id); // 14756
}
});
That plugin is not performing in an expected "jQuery Plugin" way.
Usually properly written plugins should:
bind all methods to the Element callee,
(in case of Event methods) the first parameter should always refer to the original Event.
allowing a developer to reference it using the this keyword to get the native JS Element or either doing $(this) inside the exposed public Methods just like we're expected from native jQuery Methods, and to have accessible the Event (i.e: useful in case we use arrow functions to extract the currentTarget since the inexistence of this keyword)
$someElem.on('click', function(evt) {
const $el = $(this); // what we're used to
});
$someElem.on('click', (evt) => {
const $el = $(evt.currentTarget); // the importance of always passing the Event as first param
});
clearly not implemented in that plugin.

document.querySelector() on elements not in DOM?

I'm working on a website, with jQuery but I'm trying to not use it anymore. In jQuery you can add an even listener on a element that wasn't on the website or wasn't created yet and no problem. I have elements that are only on the DOM when you're logged in, and I only have one JS file for the whole website.
Problem is, for example, when you're logged in you can't see the "log in" button, it's not even in the DOM, but it still have the event listener in the code, no error on the console, script runs well.
$("#logInButton").on("click", somefunction);
But, using document.querySelector("#logInButton").onclick = somefunction and being logged in already, it throws an error because document.querySelector("#logInButton") is null.
I can do like:
let logInButton = document.querySelector("#logInButton");
logInButton ? logInButton.onclick = somefunction : "";
And it works well, but I know it's not a good practice. Any workaround or improvement to that, not using jQuery?
JSFiddle if what happens. (See console)
And it works well, but I know it's not a good practice.
If having #logInButton on the page is optional, that's perfectly good practice — other than using onclick rather than addEventListener (but that's probably a matter of style). Naturally, you'd have this code in a script linked at the end of the document, just prior to the </body> tag (or trigger it via a DOMContentLoaded callback).
But if you want the equivalent of the jQuery, you need to think in jQuery's "set-based" mindset and use querySelectorAll:
// Not very efficient
document.querySelectorAll("#logInButton").forEach(function() {
// Set up the handler here using `this`
});
Except that jQuery optimizes queries using #id format to a getElementById call (which is dramatically faster) and then uses an if (like yours) to build the set with either one element or zero.
Perhaps in your quest to not use jQuery, you might give yourself a couple of helper functions to take its place, as the DOM API is quite verbose. If you like jQuery's set-based nature, you might even make them set-based:
function MyQuery(selector) {
if (!selector) {
this.data = [];
} else if (typeof selector === "string") {
// (jQuery takes it further than this, search in an unminified version for `rquickExpr`)
var id = /#([\w-]+)/.match(selector);
if (id) {
var e = document.getElementById(id[0]);
this.data = e ? [e] : [];
} else {
this.data = Array.from(document.querySelector(selector));
}
} else {
/* ...handle other things, such as DOM elements or arrays of them...? */
this.data = /*...*/;
}
}
MyQuery.prototype = {
constructor: MyQuery,
on: function(eventName, handler) {
this.data.forEach(function(element) {
element.addEventListener(eventName, handler);
});
return this;
}
// ...etc...
};
function qset(selector) {
return new MyQuery(selector);
}
Then
qset("#logInButton").on("click", /*...*/);
Of course, you might find yourself basically recreating jQuery. But if you keep it lean...
Side note: Using forEach on the return value of querySelectorAll requires an up-to-date browser, or that you polyfill it:
if (typeof NodeList !== "undefined" &&
NodeList.prototype &&
!NodeList.prototype.forEach) {
Object.defineProperty(NodeList.prototype, "forEach", {
value: Array.prototype.forEach
});
}
For truly obsolete browsers (like IE8), you'd have to polyfill Array.prototype.forEach first.
You can do it the same way jQuery does it, using event bubbling.
document.addEventListener('click', function (ev) {
if (ev.target.id === 'someIdHere') {
console.log('click');
}
});

How to trigger an event with a custom event (specifically a custom dataTransfer property)?

I'm currently attempting to test some code that uses drag-and-drop. I found some other questions that were kinda related to this, but they were way too specific to help me, or not related enough.
This being a test, I'm struggling on trying to automatically execute code inside a .on('drop',function(e){....} event. The main issue is not that I can't run the code inside, but it's that I can't transfer the dataTransfer property, and I can't seem to fake it because it's read-only. Is there anyway to fake the dataTransfer property or otherwise get around it?
I came up with this JSFiddle that serves as a template of what I'm trying to do: https://jsfiddle.net/gnq50hsp/53/
Essentially if you are able to explain to me (if this is at all possible) how I can possibly fake the dataTransfer property, I should be all set.
Side notes:
I'm totally open to other ways of somehow getting inside that code, like for example, maybe its possible to trigger the event and pass in a fake event object with a fake dataTransfer object.
To see the drag-drop behavior, change the JavaScript load type from no-wrap head to on-Load, then you should see what I'm trying to simulate.
Important to note that I cannot modify any of the code inside the event handlers, only inside the outside function
Using Karma/Jasmine so use of those tools are also possible like spies
Also, I'm using Chrome.
Thanks in advance, and let me know for any questions/clarifications!
You should be able to override pretty much everything you want using Object.defineProperty. Depending on what you want to test it can be very simple or very complex. Faking the dataTransfer can be a bit tricky, since there's a lot of restrictions and behaviors linked to it, but if you simply want to test the drop function, it's fairly easy.
Here's a way, this should give you some ideas as to how to fake some events and data:
//Event stuff
var target = $('#target');
var test = $('#test');
test.on('dragstart', function(e) {
e.originalEvent.dataTransfer.setData("text/plain", "test");
});
target.on('dragover', function(e) {
//e.dataTransfer.setData('test');
e.preventDefault();
e.stopPropagation();
});
target.on('dragenter', function(e) {
e.preventDefault();
e.stopPropagation();
});
//What I want to simulate:
target.on('drop', function(e) {
console.log(e)
//Issue is that I can't properly override the dataTransfer property, since its read-only
document.getElementById('dataTransferDisplay').innerHTML = e.originalEvent.dataTransfer.getData("text");
});
function simulateDrop() {
// You'll need the original event
var fakeOriginalEvent = new DragEvent('drop');
// Using defineProperty you can override dataTransfer property.
// The original property works with a getter and a setter,
// so assigning it won't work. You need Object.defineProperty.
Object.defineProperty(fakeOriginalEvent.constructor.prototype, 'dataTransfer', {
value: {}
});
// Once dataTransfer is overridden, you can define getData.
fakeOriginalEvent.dataTransfer.getData = function() {
return 'test'
};
// TO have the same behavior, you need a jquery Event with an original event
var fakeJqueryEvent = $.Event('drop', {
originalEvent: fakeOriginalEvent
});
target.trigger(fakeJqueryEvent)
}
https://jsfiddle.net/0tbp4wmk/1/
As per jsfiddel link you want to achieve drag and drop feature. jQuery Draggable UI already provides this feature why you can not use that?
For create custom event on your way you have to follow two alternative ways
$('your selector').on( "myCustomEvent", {
foo: "bar"
}, function( event, arg1, arg2 ) {
console.log( event.data.foo ); // "bar"
console.log( arg1 ); // "bim"
console.log( arg2 ); // "baz"
});
$( document ).trigger( "myCustomEvent", [ "bim", "baz" ] );
On above example
In the world of custom events, there are two important jQuery methods: .on() and .trigger(). In the Events chapter, we saw how to use these methods for working with user events; for this chapter, it's important to remember two things:
.on() method takes an event type and an event handling function as arguments. Optionally, it can also receive event-related data as its second argument, pushing the event handling function to the third argument. Any data that is passed will be available to the event handling function in the data property of the event object. The event handling function always receives the event object as its first argument.
.trigger() method takes an event type as its argument. Optionally, it can also take an array of values. These values will be passed to the event handling function as arguments after the event object.
Here is an example of the usage of .on() and .trigger() that uses custom data in both cases:
OR
jQuery.event.special.multiclick = {
delegateType: "click",
bindType: "click",
handle: function( event ) {
var handleObj = event.handleObj;
var targetData = jQuery.data( event.target );
var ret = null;
// If a multiple of the click count, run the handler
targetData.clicks = ( targetData.clicks || 0 ) + 1;
if ( targetData.clicks % event.data.clicks === 0 ) {
event.type = handleObj.origType;
ret = handleObj.handler.apply( this, arguments );
event.type = handleObj.type;
return ret;
}
}
};
// Sample usage
$( "p" ).on( "multiclick", {
clicks: 3
}, function( event ) {
alert( "clicked 3 times" );
});
On above example
This multiclick special event maps itself into a standard click event, but uses a handle hook so that it can monitor the event and only deliver it when the user clicks on the element a multiple of the number of times specified during event binding.
The hook stores the current click count in the data object, so multiclick handlers on different elements don't interfere with each other. It changes the event type to the original multiclick type before calling the handler and restores it to the mapped "click" type before returning:

jQuery delegate object instead of selector as first argument

Is is possible to pass an object instead of a selector as the first argument for jQuery delegate?
var ancestor = $('ancestor'),
children = ancestor.find('a');
ancestor.delegate(children, eventType, handler);
Instead of the regular:
ancestor.delegate('a', eventType, handler);
EDIT
Motivation:
var children = $('a[href^="#"]').map($.proxy(function(i, current) {
var href = $(current).attr('href');
if(href.length > 1 && givenElement.find(href).length === 1) return $(current);
},
this));
$(document).delegate(children, eventType, handler);
I want to delegate only the anchor elements that are hash linked to any element as a child of a given element. Basically I want to do something you can't do with just a selector only.
You could always just set up the delegation and then do your predicate inside the event handler:
ancestor.delegate('a[href^="#"]', 'click', function(ev) {
if (someElement.find($(ev.target).attr('href')).length > 0) {
// do whatever with ev.target
}
});
If you wanted to avoid the runtime price of doing that jQuery DOM search inside the handler, you could pre-tag all the "good" tags:
$('a[href^="#"]').each(function() {
if (someElement.find($(this).attr('href')).length > 0)
$(this).addClass("special");
});
Then your delegated event handler can just check
if ($(ev.target).hasClass('special')) {
// do stuff
}
which will perform well enough to not be a problem under any circumstances.
The reason you have to start with a selector for ".delegate()" to work is that that's the way it's implemented. The event handler always does something like:
function genericDelegateHandler(ev) {
if ($(ev.target).is(theSelector)) {
userHandler.call(this, ev);
}
}
Now, clearly it could also try and compare the actual elements in the case that you set up a delegate without a selector, but it just doesn't.
edit — #DADU (the OP) correctly points out that if you go to the trouble to mark everything with a class name, then you don't even need a fancy event handler that tests; an ordinary ".delegate()" will do it. :-)

Problem binding Mootools events and calling class methods

What i'm trying to do is a combination of a mootools class and raphael. The problem i got is mainly mootools event binding i guess.
I'm trying to append an event to a raphael element (dom node) and when firing the event another class method should be called.
This is no problem when coding without a mootools class. But this (the right) way i have some problems. When binding the events, the raphael element cannot be longer used because "this" now refers to the mootools class.
Please take a look at this code and i guess you will understand what my problem is:
// mootools class
var test = new Class({
...
initPlane: function() {
// just an JSON object array
this.objects = [{"pid":"2","sx":"685","sy":"498","dx":"190","dy":"540"},{"pid":"3","sx":"156","sy":"341","dx":"691","dy":"500"}];
// place the objects on stage and append some events to them
this.objects.each(function(item, idx){
item.gfx = this.gfx.image("assets/img/enemy.png", item.sx, item.sy, 32, 32);
// #### differnt approaches to bind the events. all not working
// first attempt with mootools event
item.gfx.node.addEvent('click', function(e) {
console.log(this.attr('x')); // not working because this is bound to the class i guess
this.info();
}.bind(this));
// second attempt with mootools event
item.gfx.node.addEvent('click', function(e) {
console.log(this.attr('x')); // not working
parent.info(this); // no binding and not working
});
// first attempt with raphael event
item.gfx.click( function(e) {
console.log(this.attr('x')); // works !
this.info(this); // not working because this refers to raphael element.
});
}.bind(this))
},
// this method should be called after click event and output element attribs
info: function(event) {
console.log(event.attr('x'));
},
...
});
your .each is wrong.
Object.each(obj, function(el, key, obj) {
}, bind);
http://mootools.net/docs/core/Types/Object#Object:Object-each
although you actually have this.objects as array, did not notice :)
Array.each(function(el, index) {
}, bind);
when you need this to be bound to element on click, that's fine. just store a copy of this into self and call self.info() instead. alternatively, keep the bind and reference e.target as the trigger element instead, whilst this is your instance
although it may seem 'neater' to try to keep this bound to the class wherever possible, mootools-core devs tend to prefer the var self = this; way as it avoids the extra callback to bind etc (look at the mootools source, very common)
also, say you want to have the click event go to a method directly:
element.addEvent("click", this.info.bind(this));
which will send the event as the 1st argument to info (so reference event.target).
bind can usually apply arguments as well as the scope (depending on the implementation), and that allows you to write function (raphaelObj, node) { ... }.bind(null, this, item.gfx.node) to bind two arguments.

Categories

Resources