Why are shadow DOM events in a setTimeout re-targeted? - shadow-dom

A custom element with a shadow DOM that listens for events on itself will (may?) have its events retargeted if those events are read from within a setTimeout:
<script>
class Widget extends HTMLElement {
constructor() {
super();
this.attachShadowDom();
this.registerListener();
}
attachShadowDom() {
let shadow = this.attachShadow({mode: 'open'});
let templateClone = document.querySelector("#widget-template").content.cloneNode(true);
shadow.appendChild(templateClone);
}
registerListener() {
this.shadowRoot.querySelector("#my-input").addEventListener("input", (event) => {
console.log(event.target);
setTimeout(() => console.log(event.target), 0);
});
}
}
document.addEventListener("DOMContentLoaded", () => {
customElements.define("my-widget", Widget);
});
</script>
<html>
<my-widget></my-widget>
</html>
<template id="widget-template">
<input type="text" id="my-input">
</template>
Each character entered into the input element above logs 2 separate event targets to the console: the input element, and the my-widget element. I am wondering if this is by design?

Yes, it is by design.
As explained in javascript.info:
The idea behind shadow tree is to encapsulate internal implementation details of a component [...]
So, to keep the details encapsulated, the browser retargets the event.
And from Google presentation of Shadow DOM:
When an event bubbles up from shadow DOM its target is adjusted to maintain the encapsulation that shadow DOM provides. That is, events are re-targeted to look like they've come from the component rather than internal elements within your shadow DOM.

Related

jQuery UI tooltip on pseudo disabled elements

I would like to show a tooltip on a text input that has a ui-state-disabled class.
I took a peek to the tooltip source code and I couldn't find something that checks against that particular class. So I don't know why it won't show.
As far as I can tell, the elements aren't disabled per se, they just have a class applied to them.
So, how can I show a tooltip on elements that have that class? I don't want to use a wrapper or anything like that. Maybe extending through widget factory...
Here's a sample code
HTML
<input name="#1" class="text" data-tooltip="message A">
<input name="#2" class="text" data-tooltip="message B">
<br>
<button id="disable">disable input #2</button>
<button id="enable">enable input #2</button>
JS
$(".text").each(function()
{
$(this).tooltip({
content: $(this).data("tooltip"),
items: ".text"
});
});
$("#disable").click(function()
{
$("input[name='#2']").addClass("ui-state-disabled");
});
$("#enable").click(function()
{
$("input[name='#2']").removeClass("ui-state-disabled");
});
FIDDLE: https://jsfiddle.net/hn1o4qs2/
See the doc (http://api.jqueryui.com/tooltip/):
In general, disabled elements do not trigger any DOM events.
Therefore, it is not possible to properly control tooltips for
disabled elements, since we need to listen to events to determine when
to show and hide the tooltip. As a result, jQuery UI does not
guarantee any level of support for tooltips attached to disabled
elements. Unfortunately, this means that if you require tooltips on
disabled elements, you may end up with a mixture of native tooltips
and jQuery UI tooltips.
Solution with wrapper
Your HTML:
<span class="input-container" data-tooltip="message A">
<input name="#1" class="text">
</span>
<span class="input-container" data-tooltip="message B">
<input name="#2" class="text">
</span>
<br>
<button id="disable">
disable input #2
</button>
<button id="enable">
enable input #2
</button>
Your Javascript
$(".input-container").each(function()
{
$(this).tooltip({
content: $(this).data("tooltip"),
items: ".input-container"
});
});
// ... The rest is the same
Solution with fake disabled-property
Here you can use a readonly attribute and a custom class for disabled input.
Playground: https://jsfiddle.net/5gkx8qec/
As I've stated in my question, I needed to get this working without adding a container or anything like that. And I was willing to extend the widget somehow...
So I read the source code more carefully and searched throught the whole repository for ui-state-disabled, and found that in widget.js there is an _on() method that at some point performs a check against that class and a flag called suppressDisabledCheck
A comment in code says
// Allow widgets to customize the disabled handling
// - disabled as an array instead of boolean
// - disabled class as method for disabling individual parts
This was very important, it gave me the clue that this check could be overriden. So a quick search in google and the widget factory had the answer:
Automatically handles disabled widgets: If the widget is disabled or
the event occurs on an element with the ui-state-disabled class, the
event handler is not invoked. Can be overridden with the
suppressDisabledCheck parameter.
So basically I did this:
$.widget("ui.tooltip", $.ui.tooltip,
{
options: {
allowOnDisabled: false
},
_on: function()
{
var instance = this;
this._super(instance.options.allowOnDisabled, {
mouseover: "open",
focusin: "open",
mouseleave: "close",
focusout: "close"
});
}
});
And then used it like this:
$(".text").each(function()
{
$(this).tooltip({
allowOnDisabled: true,
content: $(this).data("tooltip"),
items: ".text"
});
});
EDIT 2022-09-15
I was having some trouble with this implementation, so I've changed it a little bit
$.widget("ui.tooltip", $.ui.tooltip,
{
options: {
allowOnDisabled: false
},
_create: function()
{
this._super();
var instance = this;
this._on(instance.options.allowOnDisabled, {
mouseover: "open",
focusin: "open",
mouseleave: "close",
focusout: "close"
});
}
});

How to pass option tags to a custom element as distributed nodes (aka <slot></slot>)

I have a web component custom element defined like so.
<template id="dropdown-template">
<select>
<slot></slot>
</select>
</template>
<script>
class Dropdown extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
let template = document.getElementById('dropdown-template');
shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define("drop-down", Dropdown);
</script>
When trying to use it, I try and pass option tags with values into the custom element.
<drop-down>
<option>one</option>
<option>two</option>
<option>three</option>
</drop-down>
This doesn't work. The select element is shown to have a <slot> element as its direct child, and it doesn't render the options. Is this not possible to do with the <select> tag?
It not possible to do it like that because the parent element of an <option> tag must be a <select> tag. So you can use with Shadow DOM because the <slot> element will break the parent/child hierachy.
Solution 1 : move elements
A workaround is to move the content of the light DOM inside the <select> element in the template.
class Dropdown extends HTMLElement {
constructor() {
super()
const shadowRoot = this.attachShadow( {mode: 'open'} )
let template = document.getElementById( 'dropdown-template' )
shadowRoot.appendChild( template.content.cloneNode(true) )
const select = shadowRoot.querySelector( 'select' )
shadowRoot.addEventListener( 'slotchange', ev => {
let node = this.querySelector( 'option' )
node && select.append( node )
} )
}
}
customElements.define("drop-down", Dropdown);
<template id="dropdown-template">
<select></select>
<slot></slot>
</template>
<drop-down>
<option>one</option>
<option>two</option>
<option>three</option>
</drop-down>
Solution 2: customized <select>
Another possibility is to avoid Shadow DOM and define your drop-down list as a customized built-in <select> element. Maybe this won't fit your needs if you want to customize the layout.
<select is="drop-down">
<option>one</option>
<option>two</option>
<option>tree</option>
</select>

Knockout Binding Not Working with jQueryUI Dialogue

My viewModel has an array called 'Items'. I want to display the contents of 'Items' using a foreach binding. Everything works fine when I use regular HTML. But does not work with a dialogue box which I created using jQueryUI.
HTML:
<div id="skus0">
<div id="skus1">
<ul data-bind="foreach: Items">
<li data-bind="text:Name"></li>
</ul>
</div>
<input type="button" id="openQryItems" class="btn btn-info" value="Open" data-bind="click:openQueryItems" />
</div>
JavaScript:
// my view model
var viewModel = {
Items: [{Name:'Soap'},{Name:'Toothpaste'}]
};
// JS to configure dialogue
$("#skus1").dialog({
autoOpen: false,
width: 500,
modal: true,
buttons: {
"OK": function () {
$(this).dialog("close");
},
"Cancel": function () {
$(this).dialog("close");
}
}
});
// for mapping my model using ko.mapping plugin
var zub = zub || {};
zub.initModel = function (model) {
zub.cycleCountModel = ko.mapping.fromJS(model);
zub.cycleCountModel.openQueryItems = function () {
$("#skus1").dialog("open");
}
ko.applyBindings(zub.cycleCountModel, $("#skus0")[0]);
}
zub.initModel(viewModel);
I have created a fiddle here my fiddle
$.fn.dialog removes the element from its place in the DOM and places it in a new container; this is how it can create a floating window. The problem with this happening is that it breaks data binding, since the dialog DOM is no-longer nested within the top-level data-bound DOM.
Moving the dialog initialization to after ko.applyBindings will enable dialog to yank stuff out of the DOM after the list is populated. Of course, this means that after that point, future changes will still not be reflected, which may be important if you're wanting the opened dialog to change automatically.
If you are wanting the dialog contents to be fully dynamic, you could create a binding handler; we did this in our project. Here's a rough outline of how we did this:
ko.bindingHandlers.dialog = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingCtx) {
var bindingValues = valueAccessor();
var hasAppliedBindings = false;
var elem = $(element);
var options = {
id: ko.utils.unwrapObservable(bindingValues.id),
title: ko.utils.unwrapObservable(bindingValues.title),
// etc...
onOpen: function () {
if (!hasAppliedBindings) {
hasAppliedBindings = true;
var childCtx = bindingCtx.createChildContext(viewModel);
ko.applyBindingsToDescendants(childCtx, element);
}
}
};
elem.dialog(options);
}
return { controlsDescendantBindings: true };
}
...which we used like this:
<div data-bind="dialog: { title: 'some title', id: 'foo', ... }">
<!-- dialog contents -->
</div>
What return { controlsDescendantBindings: true } does is makes sure that outer bindings do not affect anything using the dialog binding handler. Then we create our own Knockout binding "island" after it is pulled out of the DOM, based on the original view model.
Although in our project we also used hybrid jQuery+Knockout, I would highly recommend you avoid this whenever possible. There were so many hacks we had to employ to sustain this type of application. The very best thing you should do is prefer Knockout binding handlers (and I think it has a "component" concept now which I haven't played with) over DOM manipulations to avoid buggy UI management.

How jquery widgets keep track of the initial jquery object? (an event case scenario)

I'll try to be as simple as I can be:
I wrote a jquery widget with the help of jqueryui (go to themeroller, download a theme, unzip it and there at the development-bundle folder you'll find a folder called ui, then include only the jquery.ui.widget.js for this example to run):
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<title>jQuery widget construction & Events</title>
<style>
h1 {
color: #808080;
}
h1:hover {
color: #000000;
text-decoration: none;
cursor: pointer;
}
a {
color: #808080;
background-color: #CCFF33;
}
</style>
</head>
<body>
<h1>Click Me</h1>
<div class=content>
<p>
spekyboy
</p>
<p>
learning jquery
</p>
</div>
<script src="../jquery-1.7.1.js"></script>
<script src="../jquery.ui.widget.js"></script>
<script>
////////////////////////Plugin Creation (Widget Factory)////////////////////////
///////////////////////////mycompany.linkLocation///////////////////////////////
(function( $ ) {
//1. use jQuery.widget() method, the Widget factory, to create plugins
//--------------------------------------------------------------------
$.widget( "mycompany.linkLocation", {
//2. widget properties "foreground" and "background" (see docs)
//-------------------------------------------------------------
options: {
color: "black",
"background-color": "transparent",
done: false,
//3. a callback from the relevant widget event: linkLocation_onapply (see docs)
//-----------------------------------------------------------------------------
//where does this refers to (???)
//evt refers to the custom jquery event object
_onapply: function(evt, options){
var attrs='';
for(prop in this)
attrs+=prop+', ';
alert('from _onapply, this:\n'+attrs);
attrs='';
for(prop in evt)
attrs+=prop+'='+evt[prop]+'\n';
alert('from _onapply 1st arg, evt:\n'+attrs);
//4. where is the reference to widget linkLocation for evt.target?
//----------------------------------------------------------------
$(evt.target).css(options);
}
},
//5. used during construction by the Widget Factory (see docs)
//------------------------------------------------------------
//this refers to the object built by the Widget Factory
_create: function() {
},
//6. used after initialization (see docs)
//---------------------------------------
//called as $("selector").widgetname("option", option_object)
//or,
//$("selector").widgetname("option", "key", "value")
//this refers to the object built by the Widget Factory
_setOption: function(key, value){
this.options[ key ] = value;
},
//7. public method apply()
//------------------------
//this refers to the object built by the Widget Factory
apply: function(evt){
if(!this.options.done){
//create a jquery object that wraps a specific dom element accessed
//through the this.element provided by the Widget Factory
var $this = $(this.element);
this._css($this);
////////////////////////////////////////////////////////////////////////////
//8. maybe one reason we done all these: add memory and execute only once!//
////////////////////////////////////////////////////////////////////////////
//--------------------------------------------------------------------------
this.options.done = true;
//9. if apply() is called once then, _onapply() is called once
//------------------------------------------------------------
//event linkLocation_onapply will trigger method _onapply(), see docs
this._trigger("_onapply", evt, {color: "white", "background-color": "purple"});
}
},
//10. a private method _css()
//--------------------------
//this refers to the object built by the Widget Factory
_css: function($elem){
$elem.css({
"background-color": this.options.background,
"color": this.options.foreground
});
}
});
}( jQuery ));
//11. user overwites public property: this.options
//------------------------------------------------
//for the objects created by the Widget Factory
var options2 = {
color: "blue",
"background-color": "yellow"
};
//12. construct an object for every <a> element using the Widget Factory
$( "a" ).linkLocation(options2);
//13. call widget apply() methods
//-------------------------------
$('h1').on('click', function(evt) {
//14. the jquery $(h1) object
var attrs='';
for(prop in this)
attrs+=prop+', ';
alert('From h1 tag onclick event, this:\n'+attrs);
$( "a" ).linkLocation("apply").on("linklocation_onapply",
//15. never executed???
function(evt, data){
attrs='';
for(prop in data)
attrs+=prop+': '+data[prop]+'\n';
alert('From linklocation_onapply event function, data:\n'+attrs);
}
);
});
</script>
</body>
</html>
(please see my numbers in the code comments)
now, all we do is create (12) a widget with some options (2), that do nothing (5). User can change the default options during construction (11).
User can call public method apply() (7) to change anchor foreground-background colors based on widget options but we also wanted to define a callback that will be triggered on a custom jquery event (9).
Every time user clicks on h1 tag (13) method apply() (7) is executed from every widget called linkLocation and attached to anchor elements (12) BUT only once (we put code inside an if..else). As we said, method apply() changes anchor colors and triggers the callback function.
My questions:
a) is this at callback _onapply() (3) the jquery object that actually call my widget's public method as we can see in the messages from comment (14)?
The messages are:
From h1 tag onclick event, this:
jQuery17102845835909852654, align, click, focus, blur, title, lang, dir, dataset,
itemScope, itemType, itemId, itemRef, itemProp, properties, itemValue, hidden,
tabIndex, accessKey, ...
followed by our widget messages:
from _onapply, this:
jQuery17102845835909852654, toString, href, target, download, ping, rel, hreflang,
type, text, coords, charset, name, rev, shape, protocol, host, hostname ...
and
from _onapply 1st arg, evt:
type=linklocation_onapply
timeStamp=1382545296011
jQuery17102845835909852654=true
...
if you compare you'll find that _onapply has extra properties-why is this so?
toString, href, target, download, ping, rel, hreflang, type, text, coords, charset,
name, rev, shape, protocol, host, hostname, port, pathname, search, hash
b) why is my event handler at comment (15) never been executed?
c) what's this always changing property jQueryn where n=number at the jquery object properties?
It seems that jquery isn't a singleton object and that seems to bahave bandly when we want to have memory on dom element statuses as in the case of the widget objects; they are stored in a jquery object but which one when it is constantly cloning itself?
and d) as you can see at comment (4), where is a reference to my widget linkLocation for the specific element that triggers my custom event?
Really, can't find the abstraction that the library promishes....keep on searching the source code for answers...
Thanks.
After searching for answers I concluded to the following:
for Q.a) function _onapply() at widget's options changes execution context internally as it is asigned to the widget's connected element so, it's the element's reference
for Q.b) the event system implements the observable pattern; someone plays the role of been observed by others and that's the widget. An observable cannot respond to it's own signals (see comment (15)) so, we need observers for this job i.e. attach event handlers to other elements for the specific events signaled by the widget and you have it, a respond by the observables (that's a new comment (16) with code).
What haven't been said or wrote is that we can have two types of observables in the widget construction: a formal one with the use of function this.element.trigger(eventtype, data)-a general trigger-and, a specific one that is called as this._trigger(eventtype, event, data) and is used to call functions attached to the widget AND to signal at the same time the observables-an options trigger!
Under comments (9) and (11) I implemented both types. What's strange with the options trigger is it's ability to call and signal where the event is named as namespacewidgetname with no connection dot or whatever.
An options trigger falls in two sub-categories according the position of the function that calls:
- a construction options trigger that calls functions defined under construction
and
- a user options trigger that calls functions defined by the user and extend widget's semantics even if it's hard to understand it's usability as the developer can't predict user's extensions.
For Q.c) and Q.d) I feel unsafe to answer...
For my title question about the connection between the jquery object and the widget the answer is that there is none as widgets are tight to elements like the event system and jquery acts as an intermediate finding elements and attaching properties...
Let's test it:
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<title>Custom Effect Methods</title>
<style>
h1 {
color: #808080;
}
h1:hover {
color: #000000;
text-decoration: none;
cursor: pointer;
}
a {
color: #808080;
background-color: #CCFF33;
}
</style>
</head>
<body>
<h1>Click Me</h1>
<div class=content>
<p>
spekyboy
</p>
<p>
learning jquery
</p>
</div>
<script src="../jquery-1.7.1.js"></script>
<script src="../jquery.ui.widget.js"></script>
<script>
////////////////////////Plugin Creation (Widget Factory)////////////////////////
///////////////////////////mycompany.linkLocation///////////////////////////////
(function($, window, document, undefined){
//1. use jQuery.widget() method, the Widget factory, to create plugins
//--------------------------------------------------------------------
$.widget( "mycompany.linkLocation", {
//2. widget properties "foreground" and "background" (see docs)
//-------------------------------------------------------------
options: {
that: this,
color: "black",
"background-color": "transparent",
done: false,
//3. a callback from the relevant widget event: linkLocation_onapply (see docs)
//-----------------------------------------------------------------------------
//this refers to the element referencing the plugin (element.data property?)
//evt refers to the custom jquery event object
_onapply: function(evt, options){
//4. where is the reference to widget linkLocation for evt.target?
//----------------------------------------------------------------
$(evt.target).css(options);
}
},
//5. used during construction by the Widget Factory (see docs)
//------------------------------------------------------------
//this refers to the object built by the Widget Factory
_create: function() {
},
//6. used after initialization (see docs)
//---------------------------------------
//called as $("selector").linkLocation("option", option_object)
//or,
//$("selector").linkLocation("option", "key", "value")
//this refers to the object built by the Widget Factory
_setOption: function(key, value){
this.options[ key ] = value;
},
//7. public method apply()
//------------------------
//this refers to the object built by the Widget Factory
apply: function(evt){
if(!this.options.done){
//create a jquery object that wraps a specific dom element accessed
//through the this.element provided by the Widget Factory
var $this = $(this.element);
this._css($this);
////////////////////////////////////////////////////////////////////////////
//8. maybe one reason we done all these: add memory and execute only once!//
////////////////////////////////////////////////////////////////////////////
//--------------------------------------------------------------------------
this.options.done = true;
//9. if apply() main code is executed once then,
//onapply() & trigger are executed only once
//------------------------------------------
//(!!!construction options trigger!!!) register observers for event linkLocation_onapply
//--------------------------------------------------------------------------------------
this._trigger("_onapply", evt, {color: "white", "background-color": "purple"});
//(!!!user options trigger!!!) register observers for event linkLocation_onuserapply
//----------------------------------------------------------------------------------
this._trigger("_onuserapply", evt, {color: "white", "background-color": "orange"});
//(!!!general trigger!!!) register observers for the event shown
//---------------------------------------------------------------
this.element.trigger("mycompany.linkLocation.customevent", this.options);
}
},
//10. a private method _css()
//--------------------------
//this refers to the object built by the Widget Factory
_css: function($elem){
$elem.css({
"background-color": this.options.background,
"color": this.options.foreground
});
},
_destroy: function() {
}
});
}(jQuery, window, document));
//11. user overwites public property: this.options
//------------------------------------------------
//for the objects created by the Widget Factory
var options2 = {
color: "blue",
"background-color": "yellow",
//a user supplied function that will trigger a custom event
_onuserapply: function(evt, options){
$(evt.target).css(options);
}
};
//12. construct an object for every <a> element using the Widget Factory
var widgets = $( "a" ).linkLocation(options2);
//13. call widget apply() methods
//-------------------------------
$('h1').on('click', function(evt){
//14. at this point this refers to h1 element
//...
$( "a" ).linkLocation("apply").on("linklocation_onapply",
//15. an observable cannot catch it's own signals!!!
function(evt, data){
attrs='';
for(prop in data)
attrs+=prop+': '+data[prop]+'\n';
alert('From linklocation_onapply on observable\'s event function, data:\n'+attrs);
}
);
});
//16. register observers on widget mycompany.linkLocation
$("div.content").on({
//an observer to a custom event mycompany.linkLocation.customevent
"mycompany.linkLocation.customevent": function(evt, data){
var attrs='';
for(prop in data)
attrs+=prop+': '+data[prop]+'\n';
alert('From mycompany.linkLocation.customevent event on div.content event function, data:\n'+attrs);
},
//an observer to a custom event linklocation_onapply
"linklocation_onapply": function(evt, data){
var attrs='';
for(prop in data)
attrs+=prop+': '+data[prop]+'\n';
alert('From linklocation_onapply event on div.content event function, data:\n'+attrs);
},
//an observer to a custom event linklocation_onuserapply
"linklocation_onuserapply": function(evt, data){
var attrs='';
for(prop in data)
attrs+=prop+': '+data[prop]+'\n';
alert('From linklocation_onuserapply event on div.content event function, data:\n'+attrs);
}
});
</script>
</body>
</html>

jquery drag and drop question

i want to detect to see if an element was dragged out of container.
for example:
<div id="container">
<img src="xxx.png" />
</div>
if i am drag that img element out of the div. i will remove
the img element, but i do not know how to detect when the img is out of div.
i am use jquery drag and drop library.
There is an easy way to do this.
Set child to draggable
Set parent to droppable
Set a flag which if true on drag stop removes the element
Unset this flag in the parents on drop function
child dragged out of parent == child not dropped into parent
So when you move the child around in the parent nothing happens (removeMe flag is unset) and it moves back to original position.
If you drag the child outside of the parent the removeMe flag isn't unset and the drag stop method removes the child.
javascript
$("#draggable").draggable({
start: function(event, ui) {
// flag to indicate that we want to remove element on drag stop
ui.helper.removeMe = true;
},
stop: function(event, ui) {
// remove draggable if flag is still true
// which means it wasn't unset on drop into parent
// so dragging stopped outside of parent
if (ui.helper.removeMe) {
ui.helper.remove();
}
},
// move back if dropped into a droppable
revert: 'valid'
});
$("#droppable").droppable({
drop: function(event, ui) {
// unset removeMe flag as child is still inside parent
ui.helper.removeMe = false;
}
});
html
<div id="droppable">
<p id="draggable">Drag me!</p>
</div>
thank your provides a solution.
I have found an other solution without need an outer div for this problem.
I am use "distance" option to detect how long mouse has moved, then use
"stop" option to remove element.
$(".droppable").droppable({
drop: function(event, ui) {
var obj = $(ui.draggable).clone();
obj.draggable({
distance: 100,//used to measure how far my mouse moved.
helper: 'clone',
opacity : 0.35,
stop: function(event, ui) {
$(this).remove();//remove this element.
}
}
);//obj.draggable
}//drop
})
you need to add a div outside of your container
<div id="droppableDiv">
<div id="container">
<img src="xxx.png" />
</div>
</div>
and then make it droppable by adding a function similar to:
$("#droppableDiv").droppable ({
drop: function() { alert('dropped'); }
});
instead of the alert('dropped'); part you could add a little bit of code that removes the img element from the container div.
here is a example that does some other thing but makes use of droppable and draggable objects, maybe it can help you understand how it works!
hope this helps
-Fortes

Resources