Is there a way to programmatically distribute (insert) content from lightDOM to ShadowDOM?
I would like to wrap every single child node into an element.
For example :
<my-list>
<span>first element</span>
<div>second element</div>
<a>third element</a>
</my-link>
to be distributed as
<my-list>
<ul>
<li>
<span>first element</span>
</li>
<li>
<div>second element</div>
</li>
<li>
<a>third element</a>
</li>
</ul>
</my-link>
I need it not only to render that way, but also delegate entire HTML behavior (bindings, events, etc..) as each distributed node may contain entire app.
I have tried appending <content select=":nth-child(..)"> elements to template on attached callback
attached: function(){
//ensure any element upgrades have been processed
this.async(function asyncAttached(){
var list = this.$.container;
this.children.array().forEach(function(child, childNo){
var li = document.createElement("LI");
console.log(list, li, child);
li.innerHTML = '<content select=":nth-child('+childNo+')"></content>';
list.appendChild(li);
});
});
}
But it does not work (probably because content was already distributed).
Fiddle here
In general what I would like to achieve is something like http://jsbin.com/hegizi/3/edit, but without hard-coding class names, and make it work with variable number of child nodes.
What is more, it seems, that :nth-child is not supported natively: https://github.com/Polymer/polymer/issues/470
Composition is something Shadow DOM is designed for. If that spec bug gets fixed, the best way to do this would be interesting tricks with <content select=":nth-child(...)"> in a <template repeat>. Since you can't (currently) use :nth-child, you could instead use the distributed nodes and data-binding to wrap the content:
<template repeat="{{node in nodes}}">
<li>
<html-echo html="{{node.outerHTML}}"></html-echo>
</li>
</template>
<content id="c" select="*"></content>
nodes is generated from something like:
this.nodes = Array.prototype.slice.call(this.$.c.getDistributedNodes());
I'm using <html-echo> from this post: https://stackoverflow.com/a/22208332/274673
Working demo: http://jsbin.com/mamawugo/2/edit
There is quite old issue at W3C Bugzilla: #18429 - [Shadow]: Specify imperative API for node distribution
But as for now, there is nothing in spec about that.
Related
I have some HTML that can contain <span>s with a specific class (let's call it marker). These spans can be anywhere in the document under a particular <div> - as direct children, or nested arbitrarily deeply in other nodes.
I then have a particular piece of text selected by the use (so I can use a window.getSelection() call to find the anchor node from Selection.anchorNode). What I want to find out, using Javascript, and jQuery as needed, is the last marker <span> to occur in the documents before that selection. For example:
<div class="container">
<div>
<div>
<span>Some text<span class="marker">Marker 1</span></span>
</div>
<div>
<div>
<span>Foo</span>
<span>THIS IS THE SELECTION ANCHOR NODE</span>
</div>
<span class="marker">Marker 2</span>
</div>
</div><!-- end of container -->
would find Marker 1, even though they are "cousins".
Is there a "standard" approach to determining the relative "linear" positions of an element in the DOM so I can decide if one element is "before" the other?
I am not concerned with the position on the page (x, y), so things like CSS order do not matter.
Things I have thought of, but seem suboptimal:
traversing the parents of each .marker (and the selection span) using [closest()][2] and constructing some kind of lexicographic ordering of nodes, which seems expensive and error-prone
traversing parents of .markers and storing lists of the spans found within
Both of these seem like they need a lot of book-keeping and manual DOM traversal for something that sounds like the DOM already knows (since the document has its specific order).
You can use the Node.prototype.compareDocumentPosition method to find out if an element is prior to an other one:
const anchor = document.getElementById("anchor");
const markers = document.querySelectorAll(".marker"); // get all the markers in DOM order
const previous = [...markers].filter( // get only the markers before
(elem) => anchor.compareDocumentPosition(elem) === Node.DOCUMENT_POSITION_PRECEDING
).pop(); // get the last one
console.log( previous );
<div class="container">
<div>
<div>
<span>Some text<span class="marker">Marker 0</span></span>
<span>Some text<span class="marker">Marker 1</span></span>
</div>
<div>
<div>
<span>Foo</span>
<span id="anchor">THIS IS THE SELECTION ANCHOR NODE</span>
</div>
<span class="marker">Marker 2</span>
</div>
</div><!-- end of container -->
If I understand you correctly, this should get you there:
let xpath = '(//span[#class="marker"][not(.//preceding::*[contains(.,"SELECTION ANCHOR")])])[last()]',
result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
console.log(result.textContent);
I'm trying to figure out how to find all elements that have a specific attribute. I haven't been able to figure it out without knowing the element beforehand. This is using plain vanilla javascript.
Use querySelectorAll and an attribute matching selector:
document.querySelectorAll('[myattribute]')
That should do the trick for you.
An example:
console.log(document.querySelectorAll('[myattr]'));
<div>
<p myattr="test">hello</p>
<ul>
<li myattr="somethingelse">world</li>
<li>!!!</li>
</ul>
</div>
Open the console and you'll see the <p> and <li> that have class myattr are returned.
I have two identical document fragment objects, and I attach a separate event listener to each using jQuery, as you can see in my fiddle.
The contents of the two event listeners should be functionally equivalent, but for some reason only the first event listener acts as expected.
Here is my function for creating document fragments:
function createDocFrag(htmlStr) {
var frag = document.createDocumentFragment();
var temp = document.createElement('div');
temp.innerHTML = htmlStr;
while (temp.firstChild) {
frag.appendChild(temp.firstChild);
}
return frag;
}
That function accepts an HTML string as its argument - here is the markup for said Document Fragments:
<div class='product current'>
<div class='collapsed'>
<p class='name'></p>
<p class='price'><span></span></p>
<div class='info'><p>i</p></div>
</div>\
<div class='expanded'>
<ul>
<li class='base'><span> GHz Base</span></li>
<li class='turbo'><span> GHz Turbo</span></li>
<li class='cores'><span> Cores</span></li>
<li class='threads'><span> Threads</span></li>
</ul>
</div>
</div>
And here are the two event listeners:
$(fragment1).find(".info").on("click", function(){
$(this).parents(".product").toggleClass("expand");
});
$(fragment2).find(".info").on("click", function(){
$(fragment2).find(".product").toggleClass("expand");
});
Would love to know what's causing this behavior (or lack thereof), so any clarity on the subject would be much appreciated. Thanks!
Forget my first attempt. You really did throw me with the use of the function creating DOM fragments.
The reason it does not work is that the fragments are undefined after they have been appended to the body element. The only reason the first one works is that it uses DOM navigation relative to the clicked element. Add this inside the first click handler to check:
alert($(fragment1).html()); // Undefined!!!
Alternatives:
I would suggest doing things in a more "jQuery" way and use templates in the page. This has the advantage of it being easier to maintain HTML in HTML.
JSFiddle: http://jsfiddle.net/7j9bex15/2/
Place your template in a dummy script block with an unknown type so it is ignored by the browser. I use text/template:
<script id="fragment" type="text/template">
<div class='product current'>
<div class='collapsed'>
<p class='name'></p>
<p class='price'><span></span></p>
<div class='info'><p>i</p></div>
</div>
<div class='expanded'>
<ul>
<li class='base'><span> GHz Base</span></li>
<li class='turbo'><span> GHz Turbo</span></li>
<li class='cores'><span> Cores</span></li>
<li class='threads'><span> Threads</span></li>
</ul>
</div>
</div>
</script>
Refer to the html by the id of the block:
var fragment = $('#fragment').html();
And use delegated event handlers, attached to a non-changing ancestor element to catch the click events on dynamically added content:
$(document).on('click', '.info', function(){
});
Use closest (not parents) to find the first closest matching ancestor, then toggleClass as before:
$(this).closest(".product").toggleClass("expand");
Per my SO question here, which has turned to jquery to solve this, but which may be worked back into YUI if I get my thinking straight, I need a selector to exclude descendents.
The solution proposed says something like this:
$( '.revealer:not(.revealer > .revealer)' );
To fit more accurately with my situation, because I have multiple HTML chunks to perform the same test on, I have updated it be:
$( '#_revealerEl_0 .handle:not(#_revealerEl_0 .reveal .handle)' );
The HTML its selecting on (image there are numerous copies of this same chunk on a page, each needing to be treated alone - an id attribute is assigned to each 'revealer'):
<div class="revealer" id="#_revealerEl_0">
<div class="hotspot">
<a class="handle" href="javascript:;">A</a>
<div class="reveal">
<p>Content A.</p>
</div>
<div class="reveal">
<p>Content B.</p>
<!-- nested revealer -->
<div class="revealer">
<div class="hotspot">
<a class="handle" href="javascript:;">A</a>
<div class="reveal">
<p>Sub-content A.</p>
</div>
<div class="reveal">
<p>Sub-content B.</p>
</div>
</div>
</div>
</div>
</div>
</div>
In a nutshell: I need to target 'top level' handles within a 'hotspot', per revealer - and no nested descendents with the same class names.
thanks,
d
EDIT:
It's also quite important that I don't start relying on descendant properties like parentNode, childNode[x], nextSibling, etc ... because currently this module is quite flexible in that its 'reveal' and 'handle' elements can reside within other markup and still be targeted - so long as they're found inside a 'hotspot'.
I don't know which is your #_revealerEl_0 element, but if it's your top-level .revealer, can't you just do this?
$('#_revealerEl_0 > .hotspot > .handle')
Or if the top-level .revealer is itself a descendant of #_revealerEl_0, then this works:
$('#_revealerEl_0 > .revealer > .hotspot > .handle')
The basic premise here is that you chain multiple > child combinators.
This works for me using jQuery:
$('.revealer:first > .hotspot > .reveal')
Given the first revealer, find any hotspots that are DIRECT children, and find any DIRECT reveal items within.
So, to assign handlers to your 'handles':
$('.handle').click(function(){
$(this).closest('.hotspot > .reveal').show();
});
The above translates to:
For any given handle, assign a click event function to the element
When a handle is clicked, find its closest parent hotspot
From the hotspot, find any reveal elements that are direct children of the hotspot
Show those elements if they were hidden with display: none.
Try this:
obj = $('.revealer[id*="revealerEl"]');
//this will give you what you are after
result = $("> .hotspot > .handle",obj)
//if you want to see them in red
$("> .hotspot > .handle",obj).css('color','red');
//or assign a click to it
$("> .hotspot > .handle",obj).click(function(){
//blah ....
})
I have one html structure:
enter code here <ul>
<li><span>aaa</span>
<div>
<ul>
<li><span>bbb</span> </li>
</ul>
</div>
</li>
<li><span>ccc</span>
<div>
<ul>
<li><span>ddd</span> </li>
</ul>
</div>
</li>
</ul>
now what should be the exact code to access
<span>aaa</span>and <span>ccc</span>
but not span with bbb and ddd...I have used $("li span:first-child") and its working fine..is it rite I mean as per standard...bcoz I think it should ref every first child span under any li inside that html file....what should be the exact code?
This maybe because you are nesting li without ol/ul, li should be inside ol/ul not inside another li
Your HTML is not well formed. li elements aren't closed. This could be causing the problem.
So you want all the <span>s which are a direct child of an <li> which has a nested list inside it? Here's my go at it:
$("li:has(ul) > span")
Explanation, step by step:
li // find all <li>s
:has( // which have inside them
ul // a <ul> tag
) // (current context is still at the <li>
> // now find just immediate children (not grandchildren, etc)
span // ..which are spans
The result set should now be a list of <span>s whose parent is an <li> which has a <ul> descendant.
Pay a visit to http://validator.w3.org/. Browsers do amazing things in trying to build a DOM from illegal markup, but the results are often not what you expect and inconsistent across browsers.
Use correct markup — then worry about tools dealing with it in unexpected ways. See GIGO.