How to run R code from inside a JS section of code? - javascript
I have some experience with R by now but very little knowledge of JS. The below reproducible code uses JS to run the package jsTreeR so the user can custom build a hierarchy tree. I would like to replace the JS line in the reproducible code flagged with comment // HELP!! (about halfway down the code, inside the script <- function) with this bit of R code, for generating a sequential list of capital letters from A to ZZ: c(LETTERS, sapply(LETTERS, function(x) paste0(x, LETTERS)))
Any ideas how to do this?
The code allows the user to drag/drop elements from the "Menu" section of the hierarchy tree to the "Drag here to build tree" section beneath, with the structure reflected in the dataframe to the right.
I did find some related questions online but they were extremely outdated. Maybe things have improved since then, such as a nifty package that translates from R to JS. Who knows.
Reproducible code:
library(jsTreeR)
library(shiny)
nodes <- list(
list(
text = "Menu",
state = list(opened = TRUE),
children = list(
list(text = "Bog",type = "moveable"),
list(text = "Hog",type = "moveable")
)
),
list(
text = "Drag here to build tree",
type = "target",
state = list(opened = TRUE)
)
)
checkCallback <- JS("
function(operation, node, parent, position, more) {
if(operation === 'copy_node') {
var n = parent.children.length;
if(position !== n || parent.id === '#' || node.parent !== 'j1_1' || parent.type !== 'target') {
return false;
}
}
if(operation === 'delete_node') {
if (node.type == 'item'){
text = node.text;
Shiny.setInputValue('deletion', text, {priority: 'event'});
} else if (node.type == 'subitem'){
text = parent.text;
Shiny.setInputValue('deletionsub', text, {priority: 'event'});
}
}
return true;
}"
)
customMenu <- JS("
function customMenu(node) {
var tree = $('#mytree').jstree(true);
var items = {
'delete' : {
'label' : 'Delete',
'action' : function (obj) {
parent = tree.get_node(node.parent);
nodetype = node.type;
orgid = node.orgid;
tree.delete_node(node);
},
'icon' : 'fa fa-trash'
},
};
if (node.type == 'item') {return {'delete':items.delete}}
else return {}
}
"
)
dnd <- list(
always_copy = TRUE,
inside_pos = "last",
is_draggable = JS(
"function(node) {",
" return node[0].type === 'moveable';",
"}"
)
)
mytree <- jstree(
nodes,
dragAndDrop = TRUE, dnd = dnd,
checkCallback = checkCallback,
contextMenu = list(items = customMenu),
types = list(moveable = list(), target = list())
)
script <- '
var LETTERS = ["A", "B", "C", "D", "E", "F"]; // HELP!!
var Visited = {};
function getSuffix(orgid){
if (Object.keys(Visited).indexOf(orgid) === -1){Visited[orgid] = 0;}
else{Visited[orgid]++;}
return LETTERS[Visited[orgid]];
}
$(document).ready(function(){
$("#mytree").on("copy_node.jstree", function(e, data){
var orgid = data.original.id;
var node = data.node;
var id = node.id;
var basename= node.text;
var text = basename + " " + getSuffix(orgid);
Shiny.setInputValue("choice", text, {priority: "event"});
var instance = data.new_instance;
instance.rename_node(node, text);
node.type = "item"
node.basename = basename;
node.orgid = orgid;
var tree = $("#mytree").jstree(true);
});
});
'
ui <- fluidPage(
tags$div(class = "header", checked = NA,tags$p(tags$script(HTML(script)))),
fluidRow(
column(width = 4,jstreeOutput("mytree")),
column(width = 8,verbatimTextOutput("choices"))
)
)
server <- function(input, output, session){
output[["mytree"]] <- renderJstree(mytree)
Choices <- reactiveVal(data.frame(choice = character(0)))
observeEvent(input[["choice"]], {Choices(rbind(Choices(), data.frame(choice = input[["choice"]])))} )
observeEvent(input[["deletion"]], {
item = input[["deletion"]]
matched = which(Choices()$choice == item)
if (length(matched)>0) Choices(Choices()[-matched, , drop = FALSE])
})
output[["choices"]] <- renderPrint({Choices()})
}
shinyApp(ui=ui, server=server)
Starting with a portion of your script (just for demonstration, you use the full thing):
script <- '
var Visited = {};
function getSuffix(orgid){
if (Object.keys(Visited).indexOf(orgid) === -1){Visited[orgid] = 0;}
else{Visited[orgid]++;}
return LETTERS[Visited[orgid]];
}'
We can do this:
script <- paste("var LETTERS =", jsonlite::toJSON(c(LETTERS, sapply(LETTERS, function(x) paste0(x, LETTERS)))), ";", script)
Which gives us:
cat(script, "\n")
var LETTERS = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","AA","AB","AC","AD","AE","AF","AG","AH","AI","AJ","AK","AL","AM","AN","AO","AP","AQ","AR","AS","AT","AU","AV","AW","AX","AY","AZ","BA","BB","BC","BD","BE","BF","BG","BH","BI","BJ","BK","BL","BM","BN","BO","BP","BQ","BR","BS","BT","BU","BV","BW","BX","BY","BZ","CA","CB","CC","CD","CE","CF","CG","CH","CI","CJ","CK","CL","CM","CN","CO","CP","CQ","CR","CS","CT","CU","CV","CW","CX","CY","CZ","DA","DB","DC","DD","DE","DF","DG","DH","DI","DJ","DK","DL","DM","DN","DO","DP","DQ","DR","DS","DT","DU","DV","DW","DX","DY","DZ","EA","EB","EC","ED","EE","EF","EG","EH","EI","EJ","EK","EL","EM","EN","EO","EP","EQ","ER","ES","ET","EU","EV","EW","EX","EY","EZ","FA","FB","FC","FD","FE","FF","FG","FH","FI","FJ","FK","FL","FM","FN","FO","FP","FQ","FR","FS","FT","FU","FV","FW","FX","FY","FZ","GA","GB","GC","GD","GE","GF","GG","GH","GI","GJ","GK","GL","GM","GN","GO","GP","GQ","GR","GS","GT","GU","GV","GW","GX","GY","GZ","HA","HB","HC","HD","HE","HF","HG","HH","HI","HJ","HK","HL","HM","HN","HO","HP","HQ","HR","HS","HT","HU","HV","HW","HX","HY","HZ","IA","IB","IC","ID","IE","IF","IG","IH","II","IJ","IK","IL","IM","IN","IO","IP","IQ","IR","IS","IT","IU","IV","IW","IX","IY","IZ","JA","JB","JC","JD","JE","JF","JG","JH","JI","JJ","JK","JL","JM","JN","JO","JP","JQ","JR","JS","JT","JU","JV","JW","JX","JY","JZ","KA","KB","KC","KD","KE","KF","KG","KH","KI","KJ","KK","KL","KM","KN","KO","KP","KQ","KR","KS","KT","KU","KV","KW","KX","KY","KZ","LA","LB","LC","LD","LE","LF","LG","LH","LI","LJ","LK","LL","LM","LN","LO","LP","LQ","LR","LS","LT","LU","LV","LW","LX","LY","LZ","MA","MB","MC","MD","ME","MF","MG","MH","MI","MJ","MK","ML","MM","MN","MO","MP","MQ","MR","MS","MT","MU","MV","MW","MX","MY","MZ","NA","NB","NC","ND","NE","NF","NG","NH","NI","NJ","NK","NL","NM","NN","NO","NP","NQ","NR","NS","NT","NU","NV","NW","NX","NY","NZ","OA","OB","OC","OD","OE","OF","OG","OH","OI","OJ","OK","OL","OM","ON","OO","OP","OQ","OR","OS","OT","OU","OV","OW","OX","OY","OZ","PA","PB","PC","PD","PE","PF","PG","PH","PI","PJ","PK","PL","PM","PN","PO","PP","PQ","PR","PS","PT","PU","PV","PW","PX","PY","PZ","QA","QB","QC","QD","QE","QF","QG","QH","QI","QJ","QK","QL","QM","QN","QO","QP","QQ","QR","QS","QT","QU","QV","QW","QX","QY","QZ","RA","RB","RC","RD","RE","RF","RG","RH","RI","RJ","RK","RL","RM","RN","RO","RP","RQ","RR","RS","RT","RU","RV","RW","RX","RY","RZ","SA","SB","SC","SD","SE","SF","SG","SH","SI","SJ","SK","SL","SM","SN","SO","SP","SQ","SR","SS","ST","SU","SV","SW","SX","SY","SZ","TA","TB","TC","TD","TE","TF","TG","TH","TI","TJ","TK","TL","TM","TN","TO","TP","TQ","TR","TS","TT","TU","TV","TW","TX","TY","TZ","UA","UB","UC","UD","UE","UF","UG","UH","UI","UJ","UK","UL","UM","UN","UO","UP","UQ","UR","US","UT","UU","UV","UW","UX","UY","UZ","VA","VB","VC","VD","VE","VF","VG","VH","VI","VJ","VK","VL","VM","VN","VO","VP","VQ","VR","VS","VT","VU","VV","VW","VX","VY","VZ","WA","WB","WC","WD","WE","WF","WG","WH","WI","WJ","WK","WL","WM","WN","WO","WP","WQ","WR","WS","WT","WU","WV","WW","WX","WY","WZ","XA","XB","XC","XD","XE","XF","XG","XH","XI","XJ","XK","XL","XM","XN","XO","XP","XQ","XR","XS","XT","XU","XV","XW","XX","XY","XZ","YA","YB","YC","YD","YE","YF","YG","YH","YI","YJ","YK","YL","YM","YN","YO","YP","YQ","YR","YS","YT","YU","YV","YW","YX","YY","YZ","ZA","ZB","ZC","ZD","ZE","ZF","ZG","ZH","ZI","ZJ","ZK","ZL","ZM","ZN","ZO","ZP","ZQ","ZR","ZS","ZT","ZU","ZV","ZW","ZX","ZY","ZZ"] ;
var Visited = {};
function getSuffix(orgid){
if (Object.keys(Visited).indexOf(orgid) === -1){Visited[orgid] = 0;}
else{Visited[orgid]++;}
return LETTERS[Visited[orgid]];
}
Related
How to get the highest level parent of clicked element [duplicate]
HTML <body> <div class="lol"> <a class="rightArrow" href="javascriptVoid:(0);" title"Next image"> </div> </body> Pseudo Code $(".rightArrow").click(function() { rightArrowParents = this.dom(); //.dom(); is the pseudo function ... it should show the whole alert(rightArrowParents); }); Alert message would be: body div.lol a.rightArrow How can I get this with javascript/jquery?
Here is a native JS version that returns a jQuery path. I'm also adding IDs for elements if they have them. This would give you the opportunity to do the shortest path if you see an id in the array. var path = getDomPath(element); console.log(path.join(' > ')); Outputs body > section:eq(0) > div:eq(3) > section#content > section#firehose > div#firehoselist > article#firehose-46813651 > header > h2 > span#title-46813651 Here is the function. function getDomPath(el) { var stack = []; while ( el.parentNode != null ) { console.log(el.nodeName); var sibCount = 0; var sibIndex = 0; for ( var i = 0; i < el.parentNode.childNodes.length; i++ ) { var sib = el.parentNode.childNodes[i]; if ( sib.nodeName == el.nodeName ) { if ( sib === el ) { sibIndex = sibCount; } sibCount++; } } if ( el.hasAttribute('id') && el.id != '' ) { stack.unshift(el.nodeName.toLowerCase() + '#' + el.id); } else if ( sibCount > 1 ) { stack.unshift(el.nodeName.toLowerCase() + ':eq(' + sibIndex + ')'); } else { stack.unshift(el.nodeName.toLowerCase()); } el = el.parentNode; } return stack.slice(1); // removes the html element }
Using jQuery, like this (followed by a solution that doesn't use jQuery except for the event; lots fewer function calls, if that's important): $(".rightArrow").click(function () { const rightArrowParents = []; $(this) .parents() .addBack() .not("html") .each(function () { let entry = this.tagName.toLowerCase(); const className = this.className.trim(); if (className) { entry += "." + className.replace(/ +/g, "."); } rightArrowParents.push(entry); }); console.log(rightArrowParents.join(" ")); return false; }); Live example: $(".rightArrow").click(function () { const rightArrowParents = []; $(this) .parents() .addBack() .not("html") .each(function () { let entry = this.tagName.toLowerCase(); const className = this.className.trim(); if (className) { entry += "." + className.replace(/ +/g, "."); } rightArrowParents.push(entry); }); console.log(rightArrowParents.join(" ")); return false; }); <div class=" lol multi "> Click here </div> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> (In the live examples, I've updated the class attribute on the div to be lol multi to demonstrate handling multiple classes.) That uses parents to get the ancestors of the element that was clicked, removes the html element from that via not (since you started at body), then loops through creating entries for each parent and pushing them on an array. Then we use addBack to add the a back into the set, which also changes the order of the set to what you wanted (parents is special, it gives you the parents in the reverse of the order you wanted, but then addBack puts it back in DOM order). Then it uses Array#join to create the space-delimited string. When creating the entry, we trim className (since leading and trailing spaces are preserved, but meaningless, in the class attribute), and then if there's anything left we replace any series of one or more spaces with a . to support elements that have more than one class (<p class='foo bar'> has className = "foo bar", so that entry ends up being p.foo.bar). Just for completeness, this is one of those places where jQuery may be overkill, you can readily do this just by walking up the DOM: $(".rightArrow").click(function () { const rightArrowParents = []; for (let elm = this; elm; elm = elm.parentNode) { let entry = elm.tagName.toLowerCase(); if (entry === "html") { break; } const className = elm.className.trim(); if (className) { entry += "." + className.replace(/ +/g, "."); } rightArrowParents.push(entry); } rightArrowParents.reverse(); console.log(rightArrowParents.join(" ")); return false; }); Live example: $(".rightArrow").click(function () { const rightArrowParents = []; for (let elm = this; elm; elm = elm.parentNode) { let entry = elm.tagName.toLowerCase(); if (entry === "html") { break; } const className = elm.className.trim(); if (className) { entry += "." + className.replace(/ +/g, "."); } rightArrowParents.push(entry); } rightArrowParents.reverse(); console.log(rightArrowParents.join(" ")); return false; }); <div class=" lol multi "> Click here </div> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> There we just use the standard parentNode property (or we could use parentElement) of the element repeatedly to walk up the tree until either we run out of parents or we see the html element. Then we reverse our array (since it's backward to the output you wanted), and join it, and we're good to go.
I needed a native JS version, that returns CSS standard path (not jQuery), and deals with ShadowDOM. This code is a minor update on Michael Connor's answer, just in case someone else needs it: function getDomPath(el) { if (!el) { return; } var stack = []; var isShadow = false; while (el.parentNode != null) { // console.log(el.nodeName); var sibCount = 0; var sibIndex = 0; // get sibling indexes for ( var i = 0; i < el.parentNode.childNodes.length; i++ ) { var sib = el.parentNode.childNodes[i]; if ( sib.nodeName == el.nodeName ) { if ( sib === el ) { sibIndex = sibCount; } sibCount++; } } // if ( el.hasAttribute('id') && el.id != '' ) { no id shortcuts, ids are not unique in shadowDom // stack.unshift(el.nodeName.toLowerCase() + '#' + el.id); // } else var nodeName = el.nodeName.toLowerCase(); if (isShadow) { nodeName += "::shadow"; isShadow = false; } if ( sibCount > 1 ) { stack.unshift(nodeName + ':nth-of-type(' + (sibIndex + 1) + ')'); } else { stack.unshift(nodeName); } el = el.parentNode; if (el.nodeType === 11) { // for shadow dom, we isShadow = true; el = el.host; } } stack.splice(0,1); // removes the html element return stack.join(' > '); }
Here is a solution for exact matching of an element. It is important to understand that the selector (it is not a real one) that the chrome tools show do not uniquely identify an element in the DOM. (for example it will not distinguish between a list of consecutive span elements. there is no positioning/indexing info) An adaptation from a similar (about xpath) answer $.fn.fullSelector = function () { var path = this.parents().addBack(); var quickCss = path.get().map(function (item) { var self = $(item), id = item.id ? '#' + item.id : '', clss = item.classList.length ? item.classList.toString().split(' ').map(function (c) { return '.' + c; }).join('') : '', name = item.nodeName.toLowerCase(), index = self.siblings(name).length ? ':nth-child(' + (self.index() + 1) + ')' : ''; if (name === 'html' || name === 'body') { return name; } return name + index + id + clss; }).join(' > '); return quickCss; }; And you can use it like this console.log( $('some-selector').fullSelector() ); Demo at http://jsfiddle.net/gaby/zhnr198y/
The short vanilla ES6 version I ended up using: Returns the output I'm used to read in Chrome inspector e.g body div.container input#name function getDomPath(el) { let nodeName = el.nodeName.toLowerCase(); if (el === document.body) return 'body'; if (el.id) nodeName += '#' + el.id; else if (el.classList.length) nodeName += '.' + [...el.classList].join('.'); return getDomPath(el.parentNode) + ' ' + nodeName; };
I moved the snippet from T.J. Crowder to a tiny jQuery Plugin. I used the jQuery version of him even if he's right that this is totally unnecessary overhead, but i only use it for debugging purpose so i don't care. Usage: Html <html> <body> <!-- Two spans, the first will be chosen --> <div> <span>Nested span</span> </div> <span>Simple span</span> <!-- Pre element --> <pre>Pre</pre> </body> </html> Javascript // result (array): ["body", "div.sampleClass"] $('span').getDomPath(false) // result (string): body > div.sampleClass $('span').getDomPath() // result (array): ["body", "div#test"] $('pre').getDomPath(false) // result (string): body > div#test $('pre').getDomPath() Repository https://bitbucket.org/tehrengruber/jquery.dom.path
I've been using Michael Connor's answer and made a few improvements to it. Using ES6 syntax Using nth-of-type instead of nth-child, since nth-of-type looks for children of the same type, rather than any child Removing the html node in a cleaner way Ignoring the nodeName of elements with an id Only showing the path until the closest id, if any. This should make the code a bit more resilient, but I left a comment on which line to remove if you don't want this behavior Use CSS.escape to handle special characters in IDs and node names ~ export default function getDomPath(el) { const stack = [] while (el.parentNode !== null) { let sibCount = 0 let sibIndex = 0 for (let i = 0; i < el.parentNode.childNodes.length; i += 1) { const sib = el.parentNode.childNodes[i] if (sib.nodeName === el.nodeName) { if (sib === el) { sibIndex = sibCount break } sibCount += 1 } } const nodeName = CSS.escape(el.nodeName.toLowerCase()) // Ignore `html` as a parent node if (nodeName === 'html') break if (el.hasAttribute('id') && el.id !== '') { stack.unshift(`#${CSS.escape(el.id)}`) // Remove this `break` if you want the entire path break } else if (sibIndex > 0) { // :nth-of-type is 1-indexed stack.unshift(`${nodeName}:nth-of-type(${sibIndex + 1})`) } else { stack.unshift(nodeName) } el = el.parentNode } return stack }
All the examples from other ответов did not work very correctly for me, I made my own, maybe my version will be more suitable for the rest const getDomPath = element => { let templateElement = element , stack = [] for (;;) { if (!!templateElement) { let attrs = '' for (let i = 0; i < templateElement.attributes.length; i++) { const name = templateElement.attributes[i].name if (name === 'class' || name === 'id') { attrs += `[${name}="${templateElement.getAttribute(name)}"]` } } stack.push(templateElement.tagName.toLowerCase() + attrs) templateElement = templateElement.parentElement } else { break } } return stack.reverse().slice(1).join(' > ') } const currentElement = document.querySelectorAll('[class="serp-item__thumb justifier__thumb"]')[7] const path = getDomPath(currentElement) console.log(path) console.log(document.querySelector(path)) console.log(currentElement)
var obj = $('#show-editor-button'), path = ''; while (typeof obj.prop('tagName') != "undefined"){ if (obj.attr('class')){ path = '.'+obj.attr('class').replace(/\s/g , ".") + path; } if (obj.attr('id')){ path = '#'+obj.attr('id') + path; } path = ' ' +obj.prop('tagName').toLowerCase() + path; obj = obj.parent(); } console.log(path);
hello this function solve the bug related to current element not show in the path check this now $j(".wrapper").click(function(event) { selectedElement=$j(event.target); var rightArrowParents = []; $j(event.target).parents().not('html,body').each(function() { var entry = this.tagName.toLowerCase(); if (this.className) { entry += "." + this.className.replace(/ /g, '.'); }else if(this.id){ entry += "#" + this.id; } entry=replaceAll(entry,'..','.'); rightArrowParents.push(entry); }); rightArrowParents.reverse(); //if(event.target.nodeName.toLowerCase()=="a" || event.target.nodeName.toLowerCase()=="h1"){ var entry = event.target.nodeName.toLowerCase(); if (event.target.className) { entry += "." + event.target.className.replace(/ /g, '.'); }else if(event.target.id){ entry += "#" + event.target.id; } rightArrowParents.push(entry); // } where $j = jQuery Variable also solve the issue with .. in class name here is replace function : function escapeRegExp(str) { return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); } function replaceAll(str, find, replace) { return str.replace(new RegExp(escapeRegExp(find), 'g'), replace); } Thanks
$(".rightArrow") .parents() .map(function () { var value = this.tagName.toLowerCase(); if (this.className) { value += '.' + this.className.replace(' ', '.', 'g'); } return value; }) .get().reverse().join(", ");
How to expand node with Sigmajs and Neo4j database (doubleclick Mouse event)?
I'm using the Sigma.js library to create a webapp on top of a Neo4j graph database. So, I have my graph with nodes and edges and I would like to expand nodes by double click event with javascript. Can you help me to do this? Thank you so much sigma.neo4j.cypher( {url :'http://localhost:7474', user: 'neo4j', password: 'neo4j'}, 'MATCH (n)-[r]->(m) RETURN n,r,m LIMIT 100' , {container:'graph-container'}, function (s) { s.graph.nodes().forEach(function (node){ if (node.neo4j_labels[0] === "Movies"){ node.label = node.neo4j_data['movie']; node.color = '#68BDF6'; node.size = 26; node.type = "star"; } else if (node.neo4j_labels[0] === "Personn"){ node.label = node.neo4j_data['personn']; node.type = "diamond"; node.color = '#303A2B'; node.size = 12; } console.log(s.graph.nodes()); s.settings('touchEnabled', true); s.settings('mouseEnabled', true); s.settings('defaultEdgeLabelSize', 18); s.settings('defaultNodeLabelSize', 18); s.settings('animationsTime', 1000); s.graph.edges().forEach(function(e) { //e.type = 'curvedArrow'; //e.color='#F00'; e.originalColor = e.color; e.type = "curvedArrow"; e.size=2; // e.label = neo4j_type; //console.log(s.graph.edges()) }); s.refresh(); });
You have to bind the sigma event doubleClickNode like this : s.bind("doubleClickNode", function(e) { var node = e.data.node; var query = 'MATCH (n)-[r]-(m) WHERE id(n)=' + node.id+ ' RETURN n,r,m'; sigma.neo4j.cypher( neo4jConfig, query, function(graph) { // adding node if not existing graph.nodes.forEach(function (node){ if(s.graph.nodes(node.id) === undefined) { s.graph.addNode(node); } }); // adding edge if not existing graph.edges.forEach(function (edge){ if(s.graph.edges(edge.id) === undefined) { s.graph.addEdge(edge); } }); // apply style applyStyle(s); } ); }); My complete example is available here : https://jsfiddle.net/sim51/qkc0g58o/34/ Before to use it, you need to accept Neo4j SSL certificate, by opening this link https://localhost:7473/browser and adding a security exception
Autocompleting XML values and attributes in Ace editor
I'm looking to autocomplete XML tags and attributes. The valid values will come from the server. For example, If I have a tag such as, <status></status> and my cursor is inside the open and closing tags, I'd like to hit control + space and have only valid values appear in the drop-down. Such as: ok, error, warning, ... Similarly for attributes, <status audience="">ok</status> If my cursor has focus inside the quotes I'd like only valid audiences to appear in the drop-down when hitting control + space. Here's what I have so far. This completer completes words I'm typing. I just can't figure out how to know what kind of tag I'm inside and how to send specific values for that tag or attribute. Any ideas or examples to point me to? Thanks, /w function loadEditor() { var langTools = ace.require("ace/ext/language_tools"); editor = ace.edit("editor"); editor.setOptions({ enableBasicAutocompletion: true, enableLiveAutocompletion: true, enableSnippets: true, }); editor.getSession().setMode("ace/mode/xml"); var myCompleter = { getCompletions: function(editor, session, pos, prefix, callback) { if (prefix.length === 0) { callback(null, []); return; } $.getJSON("completions.php?a=completions&prefix=" + prefix + "&content=" + session, function(json) { callback(null, json.map(function(c) { console.log("value: " + c.value); return {value: c.value, caption: c.caption, meta: c.meta, score:c.score}; })); }) } }; langTools.addCompleter(myCompleter); }
So far I haven't been able to find any projects with XML completion, so this is what I have implemented. The XhtmlTagInterpreter has one function getCompleteInfo() that returns a JavaScript object of the form {completeType: "attribute", tagName: "feline", attributeName: "breed"}. In this example it would try to auto-complete the breed attribute of <feline breed="" />. It sends that data to the server for the appropriate breed values. This service is up to you to implement. e.g. https://www.example.com/services/mock/autocompleter/attribute.json?tagName=feline&attributeName=breed The JSON returned will be something like this. [ {"score":"1000","meta":"cats","caption":"siamese","value":"siamese"}, {"score":"1000","meta":"cats","caption":"burmese","value":"burmese"}, {"score":"1000","meta":"cats","caption":"bengal","value":"bengal"} ] Here is the working JavaScript. function XHtmlTagInterpreter(row, col, session) { "use strict"; this.row = row; this.col = col; this.session = session; this.leftOfCursor = null; this.rightOfCursor = null; this.leftType = null; this.rightType = null; } /** * Sets the left of cursor property used by other methods. This is a * string without new lines from the beginning of the document to the * letter just before the cursor. */ XHtmlTagInterpreter.prototype.setLeftOfCursor = function() { "use strict"; this.leftOfCursor = ""; for (var r=0; r<=this.row; r++) { if (r === this.row) { var line = this.session.getLine(r); for (var c=0; c<this.col; c++) { this.leftOfCursor += line[c]; } } else { this.leftOfCursor += this.session.getLine(r); } } }; /** * Sets the right of cursor property used by other methods. This is a * string without new lines from the letter just to the right of the cursor * to the end of the document. */ XHtmlTagInterpreter.prototype.setRightOfCursor = function() { "use strict"; this.rightOfCursor = ""; for (var r=this.row; r<=this.session.getLength(); r++) { if (r === this.row) { var line = this.session.getLine(r); for (var c=this.col; c<line.length; c++) { this.rightOfCursor += line[c]; } } else { this.rightOfCursor += this.session.getLine(r); } } }; /** * Sets the left type depending on first non-whitespace character to the * left of the cursor position. We look for a right angle or a quotation. * If a right angle we assume the cursor is inside a tag. If quotation the * cursor is inside an attribute. We set the left type value to 'value' * or 'attribute'. */ XHtmlTagInterpreter.prototype.setLeftType = function() { "use strict"; this.setLeftOfCursor(); if (this.leftOfCursor === undefined || this.leftOfCursor.length === 0) { this.leftType = ""; return; } for (var i=this.leftOfCursor.length-1; i>=0; i--) { if (this.leftOfCursor[i] === " " || this.leftOfCursor[i] === "\t") { continue; } if (this.leftOfCursor[i] === ">") { this.leftType = "value"; return; } else if (this.leftOfCursor[i] === '"') { this.leftType = "attribute"; return; } else { this.leftType = ""; return; } } }; /** * Sets the right type depending on first non-whitespace character to the * right of the cursor position. We look for a left angle or a quotation. * If a left angle we assume the cursor is inside a tag. If quotation the * cursor is inside an attribute. We set the right type value to 'value' * or 'attribute'. */ XHtmlTagInterpreter.prototype.setRightType = function() { "use strict"; this.setRightOfCursor(); if (this.rightOfCursor === undefined || this.rightOfCursor.length === 0) { this.rightType = ""; return; } for (var i=0; i<this.rightOfCursor.length; i++) { if (this.rightOfCursor[i] === " " || this.rightOfCursor[i] === "\t") { continue; } if (this.rightOfCursor[i] === "<") { this.rightType = "value"; return; } else if (this.rightOfCursor[i] === '"') { this.rightType = "attribute"; return; } else { this.rightType = ""; return; } } }; /** * Returns the tag name to be sent to autocompleter service. * #returns {_L1.XHtmlTagInterpreter.prototype#pro;leftOfCursor#call;trim#call;replace|String} */ XHtmlTagInterpreter.prototype.getCompleteInfo = function() { "use strict"; this.setLeftType(); this.setRightType(); if (this.leftType !== this.rightType) { return ""; } if (this.leftType === "value") { var tagName = this.leftOfCursor.trim() .replace(new RegExp("^.*<([a-z:]+).*?>$"), "$1"); return {completeType: "value", tagName: tagName}; } else if (this.leftType === "attribute") { var tagName = this.leftOfCursor.trim() .replace(new RegExp("^.*<([a-z:]+).*?([a-z:]+)\s*=\s*\"$"), "$1"); var attributeName = this.leftOfCursor.trim() .replace(new RegExp("^.*<([a-z:]+).*?([a-z:]+)\s*=\s*\"$"), "$2"); return {completeType: "attribute", tagName: tagName, attributeName: attributeName}; } else { return null; } }; var loadEditor = function(editor) { var chileCompleter = { getCompletions: function(editor, session, pos, prefix, callback) { if (prefix.length === 0) { var line = session.getLine(pos.row); if (undefined !== line) { var interpreter = new XHtmlTagInterpreter(pos.row, pos.column, session); var completeInfo = interpreter.getCompleteInfo(); if (undefined === completeInfo || completeInfo === null || undefined === completeInfo.completeType || completeInfo.completeType === null || completeInfo.completeType.length === 0 || undefined === completeInfo.tagName || completeInfo.tagName === null || completeInfo.tagName.length === 0) { callback(null, []); return; } $.getJSON(chileContextPath + "services/mock/autocompleter/" + encodeURIComponent(completeInfo.completeType) + ".json?tagName=" + encodeURIComponent(completeInfo.tagName) + "&attributeName=" + encodeURIComponent(completeInfo.attributeName), function(json) { callback(null, json.content.map(function(c) { return {value: c.value, caption: c.caption, meta: c.meta, score:c.score}; })); }) } } else { callback(null, []); return; } } }; editor = ace.edit("chile-editor"); editor.setOptions({ enableBasicAutocompletion: [chileCompleter], enableLiveAutocompletion: true, enableSnippets: true, }); editor.setTheme("ace/theme/clouds"); editor.getSession().setMode("ace/mode/xml"); editor.getSession().setUseWrapMode(true); editor = loadXKbml(editor); return editor; };
Toggle query string variables
I've been banging my head over this. Using jquery or javascript, how can I toggle variables & values and then rebuild the query string? For example, my starting URL is: http://example.com?color=red&size=small,medium,large&shape=round Then, if the user clicks a button labeled "red", I want to end up with: http://example.com?size=small,medium,large&shape=round //color is removed Then, if the user clicks "red" again, I want to end up with: http://example.com?size=small,medium,large&shape=round&color=red //color is added back Then, if the user clicks a button labeled "medium", I want to end up with: http://example.com?size=small,large&shape=round&color=red //medium is removed from list Then, if the user clicks the labeled "medium" again, I want to end up with: http://example.com?size=small,large,medium&shape=round&color=red //medium added back It doesn't really matter what order the variable are in; I've just been tacking them to the end.
function toggle(url, key, val) { var out = [], upd = '', rm = "([&?])" + key + "=([^&]*?,)?" + val + "(,.*?)?(&.*?)?$", ad = key + "=", rmrplr = function(url, p1, p2, p3, p4) { if (p2) { if (p3) out.push(p1, key, '=', p2, p3.substr(1)); else out.push(p1, key, '=', p2.substr(0, p2.length - 1)); } else { if (p3) out.push(p1, key, '=', p3.substr(1)); else out.push(p1); } if (p4) out.push(p4); return out.join('').replace(/([&?])&/, '$1').replace(/[&?]$/, ''); //<!2 }, adrplr = function(s) { return s + val + ','; }; if ((upd = url.replace(new RegExp(rm), rmrplr)) != url) return upd; if ((upd = url.replace(new RegExp(ad), adrplr)) != url) return upd; return url + (/\?.+/.test(url) ? '&' : '?') + key + '=' + val; //<!1 } params self described enough, hope this help. !1: changed from ...? '&' : '' to ... ? '&' : '?' !2: changed from .replace('?&','?')... to .replace(/([&?]&)/,'$1')... http://jsfiddle.net/ycw7788/Abxj8/
I have written a function, which efficiently results in the expected behaviour, without use of any libraries or frameworks. A dynamic demo can be found at this fiddle: http://jsfiddle.net/w8D2G/1/ Documentation Definitions: The shown example values will be used at the Usage section, below - Haystack - The string to search in (default = query string. e.g: ?size=small,medium) - Needle - The key to search for. Example: size - Value - The value to replace/add. Example: medium. Usage (Example: input > output): qs_replace(needle, value) If value exists, remove: ?size=small,medium > ?size=small If value not exists, add: ?size=small > size=small,medium qs_replace(needle, options) Object options. Recognised options: findString. Returns true if the value exists, false otherwise. add, remove or toggleString. Add/remove the given value to/from needle. If remove is used, and the value was the only value, needle is also removed. A value won't be added if it already exists. ignorecaseIgnore case while looking for the search terms (needle, add, remove or find). separatorSpecify a separator to separate values of needle. Default to comma (,). Note : A different value for String haystack can also be defined, by adding it as a first argument: qs_replace(haystack, needle, value) or qs_replace(haystack, needle, options) Code (examples at bottom). Fiddle: http://jsfiddle.net/w8D2G/1/: function qs_replace(haystack, needle, options) { if(!haystack || !needle) return ""; // Without a haystack or needle.. Bye else if(typeof needle == "object") { options = needle; needle = haystack; haystack = location.search; } else if(typeof options == "undefined") { options = needle; needle = haystack; haystack = location.search; } if(typeof options == "string" && options != "") { options = {remove: options}; var toggle = true; } else if(typeof options != "object" || options === null) { return haystack; } else { var toggle = !!options.toggle; if (toggle) { options.remove = options.toggle; options.toggle = void 0; } } var find = options.find, add = options.add, remove = options.remove || options.del, //declare remove sep = options.sep || options.separator || ",", //Commas, by default flags = (options.ignorecase ? "i" :""); needle = encodeURIComponent(needle); //URL-encoding var pattern = regexp_special_chars(needle); pattern = "([?&])(" + pattern + ")(=|&|$)([^&]*)(&|$)"; pattern = new RegExp(pattern, flags); var subquery_match = haystack.match(pattern); var before = /\?/.test(haystack) ? "&" : "?"; //Use ? if not existent, otherwise & var re_sep = regexp_special_chars(sep); if (!add || find) { //add is not defined, or find is used var original_remove = remove; if (subquery_match) { remove = encodeURIComponent(remove); remove = regexp_special_chars(remove); remove = "(^|" + re_sep + ")(" + remove + ")(" + re_sep + "|$)"; remove = new RegExp(remove, flags); var fail = subquery_match[4].match(remove); } else { var fail = false; } if (!add && !fail && toggle) add = original_remove; } if(find) return !!subquery_match || fail; if (add) { //add is a string, defined previously add = encodeURIComponent(add); if(subquery_match) { var re_add = regexp_special_chars(add); re_add = "(^|" + re_sep + ")(" + re_add + ")(?=" + re_sep + "|$)"; re_add = new RegExp(re_add, flags); if (subquery_match && re_add.test(subquery_match[4])) { return haystack; } if (subquery_match[3] != "=") { subquery_match = "$1$2=" + add + "$4$5"; } else { subquery_match = "$1$2=$4" + sep + add + "$5"; } return haystack.replace(pattern, subquery_match); } else { return haystack + before + needle + "=" + add; } } else if(subquery_match){ // Remove part. We can only remove if a needle exist if(subquery_match[3] != "="){ return haystack; } else { return haystack.replace(pattern, function(match, prefix, key, separator, value, trailing_sep){ // The whole match, example: &foo=bar,doo // will be replaced by the return value of this function var newValue = value.replace(remove, function(m, pre, bye, post){ return pre == sep && post == sep ? sep : pre == "?" ? "?" : ""; }); if(newValue) { //If the value has any content return prefix + key + separator + newValue + trailing_sep; } else { return prefix == "?" ? "?" : trailing_sep; //No value, also remove needle } }); //End of haystack.replace } //End of else if } else { return haystack; } // Convert string to RegExp-safe string function regexp_special_chars(s){ return s.replace(/([[^$.|?*+(){}\\])/g, '\\$1'); } } Examples (Fiddle: http://jsfiddle.net/w8D2G/1/): qs_replace('color', 'red'); //Toggle color=red qs_replace('size', {add: 'medium'}); //Add `medium` if not exist to size var starting_url = 'http://example.com?color=red&size=small,medium,large&shape=round' starting_url = qs_replace(starting_url, 'color', 'red'); //Toggle red, thus remove starting_url = qs_replace(starting_url, 'color', 'red'); //Toggle red, so add it alert(starting_url);
This is the solution for your task: http://jsfiddle.net/mikhailov/QpjZ3/12/ var url = 'http://example.com?size=small,medium,large&shape=round'; var params = $.deparam.querystring(url); var paramsResult = {}; var click1 = { size: 'small' }; var click2 = { size: 'xlarge' }; var click3 = { shape: 'round' }; var click4 = { shape: 'square' }; var clickNow = click4; for (i in params) { var clickKey = _.keys(clickNow)[0]; var clickVal = _.values(clickNow)[0]; if (i == clickKey) { var ar = params[i].split(','); if (_.include(ar, clickVal)) { var newAr = _.difference(ar, [clickVal]); } else { var newAr = ar; newAr.push(clickVal); } paramsResult[i] = newAr.join(','); } else { paramsResult[i] = params[i]; } } alert($.param(paramsResult)) // results see below Init params string { size="small, medium,large", shape="round"} // size=small,medium,large&shape=round Results { size="small"} => { size="medium,large", shape="round"} //size=medium%2Clarge&shape=round { size="xlarge"} => { size="small,medium,large,xlarge", shape="round"} // size=small%2Cmedium%2Clarge%2Cxlarge&shape=round { shape="round"} => { size="small,medium,large", shape=""} //size=small%2Cmedium%2Clarge&shape= { shape="square"} => { size="small,medium,large", shape="round,square"} //size=small%2Cmedium%2Clarge&shape=round%2Csquare
productOptions is the only thing you need to modify here to list all the available options and their default state. You only need to use the public API function toggleOption() to toggle an option. (function(){ //Just keep an object with all the options with flags if they are enabled or disabled: var productOptions = { color: { "red": true, "blue": true, "green": false }, size: { "small": true, "medium": true, "large": true }, shape: { "round": true } }; //After this constructing query becomes pretty simple even without framework functions: function constructQuery(){ var key, opts, qs = [], enc = encodeURIComponent, opt, optAr, i; for( key in productOptions ) { opts = productOptions[key]; optAr = []; for( i in opts ) { if( opts[i] ) { optAr.push( i ); } } if( !optAr.length ) { continue; } qs.push( enc( key ) + "=" + enc( optAr.join( "," ) ) ); } return "?"+qs.join( "&" ); }; //To toggle a value and construct the new query, pass what you want to toggle to this function: function toggleOption( optionType, option ) { if( optionType in productOptions && option in productOptions[optionType] ) { productOptions[optionType][option] = !productOptions[optionType][option]; } return constructQuery(); } window.toggleOption = toggleOption; })() Example use: // "%2C" = url encoded version of "," toggleOption(); //Default query returned: "?color=red%2Cblue&size=small%2Cmedium%2Clarge&shape=round" toggleOption( "color", "red" ); //Red color removed: "?color=blue&size=small%2Cmedium%2Clarge&shape=round" toggleOption( "color", "blue" ); //Blue color removed, no color options so color doesn't show up at all: "?size=small%2Cmedium%2Clarge&shape=round" toggleOption( "color", "blue" ); //Blue color enabled again: "?color=blue&size=small%2Cmedium%2Clarge&shape=round" toggleOption( "shape", "round" ); //The only shape option removed "?color=blue&size=small%2Cmedium%2Clarge"
I have tried this and this may give the desire result <script> var url='http://example.com?color=red&size=small,medium,large&shape=round'; var mySplitResult = url.split("?"); var domain=mySplitResult[0]; var qstring=mySplitResult[1]; var proparr=new Array(); var valarr=new Array(); var mySplitArr = qstring.split("&"); for (i=0;i<mySplitArr.length;i++){ var temp = mySplitArr[i].split("="); proparr[i]=temp[0]; valarr[i]=temp[1].split(","); } function toggle(property,value) { var index; var yes=0; for (i=0;i<proparr.length;i++){ if(proparr[i]==property) index=i; } if(index==undefined){ proparr[i]=property; index=i; valarr[index]=new Array(); } for (i=0;i<valarr[index].length;i++){ if(valarr[index][i]==value){ valarr[index].splice(i,1); yes=1; } } if(!yes) { valarr[index][i]=value; } var furl=domain +'?'; var test=new Array(); for(i=0;i<proparr.length;i++) { if(valarr[i].length) { test[i]=valarr[i].join(","); furl +=proparr[i]+"="+test[i]+"&"; } } furl=furl.substr(0,furl.length-1) alert(furl); } </script> <div> <input id="color" type="button" value="Toggle Red" onclick="toggle('color','red')"/> <input id="shape" type="button" value="Toggle shape" onclick="toggle('shape','round')"/> <input id="size" type="button" value="Toggle Small" onclick="toggle('size','small')"/> <input id="size" type="button" value="Toggle large" onclick="toggle('size','large')"/> <input id="size" type="button" value="Toggle medium" onclick="toggle('size','medium')"/> <input id="size" type="button" value="Toggle new" onclick="toggle('new','yes')"/> </div>
How to get nodes lying inside a range with javascript?
I'm trying to get all the DOM nodes that are within a range object, what's the best way to do this? var selection = window.getSelection(); //what the user has selected var range = selection.getRangeAt(0); //the first range of the selection var startNode = range.startContainer; var endNode = range.endContainer; var allNodes = /*insert magic*/; I've been been thinking of a way for the last few hours and came up with this: var getNextNode = function(node, skipChildren){ //if there are child nodes and we didn't come from a child node if (node.firstChild && !skipChildren) { return node.firstChild; } if (!node.parentNode){ return null; } return node.nextSibling || getNextNode(node.parentNode, true); }; var getNodesInRange = function(range){ var startNode = range.startContainer.childNodes[range.startOffset] || range.startContainer;//it's a text node var endNode = range.endContainer.childNodes[range.endOffset] || range.endContainer; if (startNode == endNode && startNode.childNodes.length === 0) { return [startNode]; }; var nodes = []; do { nodes.push(startNode); } while ((startNode = getNextNode(startNode)) && (startNode != endNode)); return nodes; }; However when the end node is the parent of the start node it returns everything on the page. I'm sure I'm overlooking something obvious? Or maybe going about it in totally the wrong way. MDC/DOM/range
Here's an implementation I came up with to solve this: function getNextNode(node) { if (node.firstChild) return node.firstChild; while (node) { if (node.nextSibling) return node.nextSibling; node = node.parentNode; } } function getNodesInRange(range) { var start = range.startContainer; var end = range.endContainer; var commonAncestor = range.commonAncestorContainer; var nodes = []; var node; // walk parent nodes from start to common ancestor for (node = start.parentNode; node; node = node.parentNode) { nodes.push(node); if (node == commonAncestor) break; } nodes.reverse(); // walk children and siblings from start until end is found for (node = start; node; node = getNextNode(node)) { nodes.push(node); if (node == end) break; } return nodes; }
The getNextNode will skip your desired endNode recursively if its a parent node. Perform the conditional break check inside of the getNextNode instead: var getNextNode = function(node, skipChildren, endNode){ //if there are child nodes and we didn't come from a child node if (endNode == node) { return null; } if (node.firstChild && !skipChildren) { return node.firstChild; } if (!node.parentNode){ return null; } return node.nextSibling || getNextNode(node.parentNode, true, endNode); }; and in while statement: while (startNode = getNextNode(startNode, false , endNode));
The Rangy library has a Range.getNodes([Array nodeTypes[, Function filter]]) function.
I made 2 additional fixes based on MikeB's answer to improve the accuracy of the selected nodes. I'm particularly testing this on select all operations, other than range selection made by dragging the cursor along text spanning across multiple elements. In Firefox, hitting select all (CMD+A) returns a range where it's startContainer & endContainer is the contenteditable div, the difference is in the startOffset & endOffset where it's respectively the index of the first and the last child node. In Chrome, hitting select all (CMD+A) returns a range where it's startContainer is the first child node of the contenteditable div, and the endContainer is the last child node of the contenteditable div. The modifications I've added work around the discrepancies between the two. You can see the comments in the code for additional explanation. function getNextNode(node) { if (node.firstChild) return node.firstChild; while (node) { if (node.nextSibling) return node.nextSibling; node = node.parentNode; } } function getNodesInRange(range) { // MOD #1 // When the startContainer/endContainer is an element, its // startOffset/endOffset basically points to the nth child node // where the range starts/ends. var start = range.startContainer.childNodes[range.startOffset] || range.startContainer; var end = range.endContainer.childNodes[range.endOffset] || range.endContainer; var commonAncestor = range.commonAncestorContainer; var nodes = []; var node; // walk parent nodes from start to common ancestor for (node = start.parentNode; node; node = node.parentNode) { nodes.push(node); if (node == commonAncestor) break; } nodes.reverse(); // walk children and siblings from start until end is found for (node = start; node; node = getNextNode(node)) { // MOD #2 // getNextNode might go outside of the range // For a quick fix, I'm using jQuery's closest to determine // when it goes out of range and exit the loop. if (!$(node.parentNode).closest(commonAncestor)[0]) break; nodes.push(node); if (node == end) break; } return nodes; };
below code solve your problem <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>payam jabbari</title> <script src="http://code.jquery.com/jquery-2.0.2.min.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function(){ var startNode = $('p.first').contents().get(0); var endNode = $('span.second').contents().get(0); var range = document.createRange(); range.setStart(startNode, 0); range.setEnd(endNode, 5); var selection = document.getSelection(); selection.addRange(range); // below code return all nodes in selection range. this code work in all browser var nodes = range.cloneContents().querySelectorAll("*"); for(var i=0;i<nodes.length;i++) { alert(nodes[i].innerHTML); } }); </script> </head> <body> <div> <p class="first">Even a week ago, the idea of a Russian military intervention in Ukraine seemed far-fetched if not totally alarmist. But the arrival of Russian troops in Crimea over the weekend has shown that he is not averse to reckless adventures, even ones that offer little gain. In the coming days and weeks</p> <ol> <li>China says military will respond to provocations.</li> <li >This Man Has Served 20 <span class="second"> Years—and May Die—in </span> Prison for Marijuana.</li> <li>At White House, Israel's Netanyahu pushes back against Obama diplomacy.</li> </ol> </div> </body> </html>
Annon, great work. I've modified the original plus included Stefan's modifications in the following. In addition, I removed the reliance on Range, which converts the function into an generic algorithm to walk between two nodes. Plus, I've wrapped everything into a single function. Thoughts on other solutions: Not interested in relying on jquery Using cloneNode lifts the results to a fragment, which prevents many operations one might want to conduct during filtering. Using querySelectAll on a cloned fragment is wonky because the start or end nodes may be within a wrapping node, hence the parser may not have the closing tag? Example: <div> <p>A</p> <div> <p>B</p> <div> <p>C</p> </div> </div> </div> Assume start node is the "A" paragraph, and the end node is the "C" paragraph . The resulting cloned fragment would be: <p>A</p> <div> <p>B</p> <div> <p>C</p> and we're missing closing tags? resulting in funky DOM structure? Anyway, here's the function, which includes a filter option, which should return TRUE or FALSE to include/exclude from results. var getNodesBetween = function(startNode, endNode, includeStartAndEnd, filter){ if (startNode == endNode && startNode.childNodes.length === 0) { return [startNode]; }; var getNextNode = function(node, finalNode, skipChildren){ //if there are child nodes and we didn't come from a child node if (finalNode == node) { return null; } if (node.firstChild && !skipChildren) { return node.firstChild; } if (!node.parentNode){ return null; } return node.nextSibling || getNextNode(node.parentNode, endNode, true); }; var nodes = []; if(includeStartAndEnd){ nodes.push(startNode); } while ((startNode = getNextNode(startNode, endNode)) && (startNode != endNode)){ if(filter){ if(filter(startNode)){ nodes.push(startNode); } } else { nodes.push(startNode); } } if(includeStartAndEnd){ nodes.push(endNode); } return nodes; };
I wrote the perfect code for this and it works 100% for every node : function getNodesInSelection() { var range = window.getSelection().getRangeAt(0); var node = range.startContainer; var ranges = [] var nodes = [] while (node != null) { var r = document.createRange(); r.selectNode(node) if(node == range.startContainer){ r.setStart(node, range.startOffset) } if(node == range.endContainer){ r.setEnd(node, range.endOffset) } ranges.push(r) nodes.push(node) node = getNextElementInRange(node, range) } // do what you want with ranges and nodes } here are some helper functions function getClosestUncle(node) { var parent = node.parentElement; while (parent != null) { var uncle = parent.nextSibling; if (uncle != null) { return uncle; } uncle = parent.nextElementSibling; if (uncle != null) { return uncle; } parent = parent.parentElement } return null } function getFirstChild(_node) { var deep = _node while (deep.firstChild != null) { deep = deep.firstChild } return deep } function getNextElementInRange(currentNode, range) { var sib = currentNode.nextSibling; if (sib != null && range.intersectsNode(sib)) { return getFirstChild(sib) } var sibEl = currentNode.nextSiblingElemnent; if (sibEl != null && range.intersectsNode(sibEl)) { return getFirstChild(sibEl) } var uncle = getClosestUncle(currentNode); var nephew = getFirstChild(uncle) if (nephew != null && range.intersectsNode(nephew)) { return nephew } return null }
bob. the function only returns the startNode and endNode. the nodes in between do not get pushed to the array. seems the while loop returns null on getNextNode() hence that block never gets executed.
here is function return you array of sub-ranges function getSafeRanges(range) { var doc = document; var commonAncestorContainer = range.commonAncestorContainer; var startContainer = range.startContainer; var endContainer = range.endContainer; var startArray = new Array(0), startRange = new Array(0); var endArray = new Array(0), endRange = new Array(0); // ##### If start container and end container is same if (startContainer == endContainer) { return [range]; } else { for (var i = startContainer; i != commonAncestorContainer; i = i.parentNode) { startArray.push(i); } for (var i = endContainer; i != commonAncestorContainer; i = i.parentNode) { endArray.push(i); } } if (0 < startArray.length) { for (var i = 0; i < startArray.length; i++) { if (i) { var node = startArray[i - 1]; while ((node = node.nextSibling) != null) { startRange = startRange.concat(getRangeOfChildNodes(node)); } } else { var xs = doc.createRange(); var s = startArray[i]; var offset = range.startOffset; var ea = (startArray[i].nodeType == Node.TEXT_NODE) ? startArray[i] : startArray[i].lastChild; xs.setStart(s, offset); xs.setEndAfter(ea); startRange.push(xs); } } } if (0 < endArray.length) { for (var i = 0; i < endArray.length; i++) { if (i) { var node = endArray[i - 1]; while ((node = node.previousSibling) != null) { endRange = endRange.concat(getRangeOfChildNodes(node)); } } else { var xe = doc.createRange(); var sb = (endArray[i].nodeType == Node.TEXT_NODE) ? endArray[i] : endArray[i].firstChild; var end = endArray[i]; var offset = range.endOffset; xe.setStartBefore(sb); xe.setEnd(end, offset); endRange.unshift(xe); } } } var topStartNode = startArray[startArray.length - 1]; var topEndNode = endArray[endArray.length - 1]; var middleRange = getRangeOfMiddleElements(topStartNode, topEndNode); startRange = startRange.concat(middleRange); response = startRange.concat(endRange); return response; }
With generator and document.createTreeWalker: function *getNodeInRange(range) { let [start, end] = [range.startContainer, range.endContainer] if (start.nodeType < Node.TEXT_NODE || Node.COMMENT_NODE < start.nodeType) { start = start.childNodes[range.startOffset] } if (end.nodeType < Node.TEXT_NODE || Node.COMMENT_NODE < end.nodeType) { end = end.childNodes[range.endOffset-1] } const relation = start.compareDocumentPosition(end) if (relation & Node.DOCUMENT_POSITION_PRECEDING) { [start, end] = [end, start] } const walker = document.createTreeWalker( document, NodeFilter.SHOW_ALL ) walker.currentNode = start yield start while (walker.parentNode()) yield walker.currentNode if (!start.isSameNode(end)) { walker.currentNode = start while (walker.nextNode()) { yield walker.currentNode if (walker.currentNode.isSameNode(end)) break } } const subWalker = document.createTreeWalker( end, NodeFilter.SHOW_ALL ) while (subWalker.nextNode()) yield subWalker.currentNode }