Get tag name of start container in range - range

I'm an absolute newb at this so forgive the simplicity of this question. I have a contenteditable div. All the text in this div are wrapped in link tags.
If the user makes a selection that spans 2 or more of these link nodes, I'd like to identify the name of the link tag at the startContainer and also at the endContainer.
Unfortunately, more often than not, the startContainer node is a formatting node such a paragraph or a bold tag as seen in the example html below.
<div id="myarea" onmouseup="getSelectionHtml();" contenteditable="true">
<a id="1" href=#>text1 <b>text1 text1 </b></a>
<a id="2" href=#>text2 <b>text2 text2 </b></a>
<a id="3" href=#>text3 <b>text3 text3 </b></a>
</div>
So I figure my approach should be to first find the nameTag of the startContainer. If it is not a link tag, then query for it's parent node. If that is not a link tag, query again for the next node up the hierarchy until I find the link tag and can get it's id.
As pitifully short as it is, this is all the code that I have so far. I wish to find tagName of the startContainer, but I'm getting an alert of "undefined". I've been reading as much documentation on the range object as I can but it's all scattered and a little difficult for me to comprehend.
function getSelectionHtml() {
var userSelection;
if (window.getSelection) {
userSelection = window.getSelection();
var selRange = userSelection.getRangeAt(0);
alert(selRange.startContainer.tagName);
}
By the way, if anyone has a better conceptual solution for grabbing the link tag of the beginning and end of a contentEditable selection, I'd be much obliged.
tia

A range's startContainer and endContainer properties may be references to either text nodes or elements. When you're getting an undefined tagName property, it's because you've got a text node.
Here's a simple function for getting hold of the <a> element containing a node:
function getAncestorWithTagName(node, tagName) {
tagName = tagName.toUpperCase();
while (node) {
if (node.nodeType == 1 && node.tagName.toUpperCase() == tagName) {
return node;
}
node = node.parentNode;
}
return null;
}
Example:
var link = getAncestorWithTagName(selRange.startContainer);
if (link) {
alert("Start is inside link with ID " + link.id);
}

Related

How deep down the DOM tree is contenteditable-ity retained?

Background:
I wish to execute work based off whether a node is content editable or not. The node is captured onkeyup through event.target. Consider this complex contenteditable node structure:
<div contenteditable="true">
<div contenteditable="true">
<ol>
<li>
<a href="google.com">
<span style="color: red;">Hi!</span>
</a>
</li>
</ol>
"A text node"
<p>Some more text</p>
</div>
</div>
I wish a keyup inside the span node, as well as the text node, to detect them as a contenteditable node and perform work.
So, I wrote this function (long but straightforward):
// callForParent: flag to prevent infinite recursion
window.isContentEditable = function(node, callForParent){
var tgN = node && node.tagName,
attr, parentCount, parent, MAX_PARENTS_CHECKED = INTEGER_VALUE (3,4,5,etc.);
// insanity checks first
if(!node || tgN === "TEXTAREA" || tgN === "INPUT" || !node.getAttribute)
return false;
else{
attr = node.attr("contenteditable");
// empty string to support <element contenteditable> markup
if(attr === "" || attr === "true" || attr === "plaintext-only")
return true;
// avoid infinite recursion
if(callForParent) return false;
parentCount = 1;
parent = node;
do{
parent = parent.parentNode;
parentCount++;
if(!parent) return false;
// parent check call
if(isContentEditable(parent, true)) return true;
}while(parentCount <= MAX_PARENTS_CHECKED);
return false;
}
};
Problem:
Notice the counter limit MAX_PARENTS_CHECKED above. That makes sure how many levels up the DOM tree I go in search of a parent contenteditable node.
Does that counter affect the logical correctness of my program? I mean, like in the HTML structure I showed above, we need the limit to be 4 to cover all cases. There might be need of more.
But I fear that increasing the limit greater than some point might make non-contenteditable nodes to be detected to be detected as contenteditable. Is this fear of mine true/possible? Are there examples to show that deep descendants of contenteditable node can be non-CE node?
I don't want to execute things in a deep-down non-CE node if say, it CANNOT be edited by the user and then my app gives an error.
Sorry if I sound nitpicky but I need confirmation
UPDATE: As confirmed by user3297291 in the comments, there exist instances where keyup on a non-CE child of a CE node are detected as having event.target == to the parent and NOT the child, contrary to what one might expect. These are situations where my app might fire, and expect a caret Range, Selection, etc. to perform some work, and there it will give an error.
Does there exist some fix/proper way of doing this?

Using dojo dom.byId is not getting an element added programmatically

I'm creating a dom element programatically using dojo and I can "see" it in the dom with its id, but when I attempt a dom.byId("myId") it returns null.
I have a similar jsfiddle that is actually working (so it doesn't reproduce my problem, but it gives an idea of what I'm trying to do): if you click the button (ignore the lack of styling) in the run output panel, it alerts the content of the element retrieved by dom.byId. But similar code within my dojo widget is not working. Here's the code:
var content = lang.replace(selectFilterTemplate, {
"layer-id": layer.id,
"layer-index": idx,
"filter-name": filter.name
}); // this gets template HTML code similar to what's in the HTML panel of the jsfiddle, only it has placeholder tags {} instead of literals, and the tags are replaced with the attributes of the layer, idx, and filter objects here
// Use dojo dom-construct to create a div with the HTML from above
var node = domConstruct.create("div", { "innerHTML": content });
// put the new div into a dojo ContentPane
var filterPanel = new ContentPane({
"id": layer.id + "-filter-" + idx + "-panel",
"content": node,
"style": "width: 200px; float: left;"
});
// Get the dom element:
var mstag = dom.byId(layer.id + "-filter-" + idx + "-ms-tag")
// this is the same as the "var ms = dom.byId("IssuePoints-filter-1-ms-tag")" in the jsfiddle, but this one returns null. If I view the contents of the 'node' variable in the browser debugging console at this point, I can see the <select> tag with the id I'm referencing.
Why would I be getting null in my dom.byId() if I can see that element in the dom in the debugging console?
It seems that the element is added to the dom at a later point. You may see it with the debugger but it is not yet available the moment you call byId().
In the code you posted you create the filterPanel element but you do not place it in the dom. I assume this happens at a later stage. In contrast, the jsfiddle places the Button element with placeAt() directly after constructing it.

knockout.js - help dealing with UI state changes when polling for updates

I'm having a problem losing UI state changes after my observables change and was hoping for some suggestions.
First off, I'm polling my server for updates. Those messages are in my view model and the <ul> renders perfectly:
When my user clicks the "reply" or "assign to" buttons, I'm displaying a little form to perform those actions:
My problem at this point was that when my next polling call returned, the list re-binds and I lose the state of where the form should be open at. I went through adding view model properties for "currentQuestionID" so I could use a visible: binding and redisplay the form after binding.
Once that was complete, the form displays properly on the "current item" after rebinding but the form values are lost. That is to say, it rebinds, rebuilds the form elements, shows them, but any user input disappears (which of course makes sense since the HTML was just regenerated).
I attempted to follow the same pattern (using a value: binding to set the value and an event: {change: responseChanged} binding to update an observable with the values). The HTML fragment looks like this:
<form action="#" class="tb-reply-form" data-bind="visible: $root.showMenu($data, 'reply')">
<textarea id="tb-response" data-bind="value: $root.currentResponse, event: {keyup: $root.responseChanged}"></textarea>
<input type="button" id="tb-submitResponse" data-bind="click: $root.submitResponse, clickBubble: false" value="Send" />
</form>
<form action="#" class="tb-assign-form" data-bind="visible: $root.showMenu($data, 'assign')">
<select id="tb-assign" class="tb-assign" data-bind="value: $root.currentAssignee, options: $root.mediators, optionsText: 'full_name', optionsValue: 'access_token', optionsCaption: 'Select one...', event: {change: $root.assigneeChanged}">
</select>
<input type="button" id="tb-submitAssignment" data-bind="click: $root.submitAssignment, clickBubble: false" value="Assign"/>
</form>
Now, I end up with what seems like an infinite loop where setting the value causes change to happen, which in turn causes value... etc.
I thought "screw it" just move it out of the foreach... By moving the form outside of each <li> in the foreach: binding and doing a little DOM manipulation to move the form into the "current item", I figured I wouldn't lose user inputs.
replyForm.appendTo(theContainer).show();
It works up until the first poll return & rebind. Since the HTML is regenerated for the <ul>, the DOM no longer has my form and my attempt to grab it and do the .appendTo(container) does nothing. I suppose here, I might be able to copy the element into the active item instead of moving it?
So, this all seems like I'm missing something basic because someone has to have put a form into a foreach loop in knockout!
Does anybody have a strategy for maintaining form state inside a bound item in knockout?
Or, possibly, is there a way to make knockout NOT bind anything that's already bound and only generate "new" elements.
Finally, should I just scrap knockout for this and manually generate for "new items" myself when each polling call returns.
Just one last bit of info; if I set my polling interval to something like 30 seconds, all the bits "work" in that it submits, saves, rebinds, etc. I just need the form and it's contents to live through the rebinding.
Thanks a ton for any help!
Well, I figured it out on my own. And it's embarrassing.
Here is a partial bit of my VM code:
function TalkbackViewModel( id ) {
var self = this;
talkback.state.currentTalkbackId = "";
talkback.state.currentAction = "";
talkback.state.currentResponse = "";
talkback.state.currentAssignee = "";
self.talkbackQueue = ko.observableArray([]);
self.completeQueue = ko.observableArray([]);
self.mediators = ko.observableArray([]);
self.currentTalkbackId = ko.observable(talkback.state.currentTalkbackId);
self.currentAction = ko.observable(talkback.state.currentAction);
self.currentResponse = ko.observable(talkback.state.currentResponse);
self.currentAssignee = ko.observable(talkback.state.currentAssignee);
self.showActionForm = function(data, action) {
return ko.computed(function() {
var sameAction = (self.currentAction() == action);
var sameItem = (self.currentTalkbackId() == data.talkback_id());
return (sameAction && sameItem);
}, this);
};
self.replyToggle = function(model, event) {
// we're switching from one item to another. clear input values.
if (self.currentTalkbackId() != model.talkback_id() || self.currentAction() != "reply") {
self.currentResponse("");
self.currentAssignee("");
self.currentTalkbackId(model.talkback_id());
}
My first mistake was trying to treat the textarea & dropdown the same. I noticed the dropdown was saving value & reloading but stupidly tried to keep the code the same as the textarea and caused my own issue.
So...
First off, I went back to the using the $root view model properties for currentAssignee and currentResponse to store the values off and rebind using value: bindings on those controls.
Next, I needed to remove the event handlers:
event: { change: xxxChanged }
because they don't make sense (two way binding!!!!). The drop down value changes and updates automatically by using the value: binding.
The textarea ONLY updated on blur, causing me to think I needed onkeyup,onkeydown, etc. I got rid of those handlers because they were 1) wrong, 2) screwing up the value: binding creating an infinite loop.
I only needed this on the textarea to get up-to-date value updates to my viewmodel property:
valueUpdate: 'input'
At this point everything saves off & rebinds and I didn't lose my values but my caret position was incorrect in the textarea. I added a little code to handle that:
var item = element.find(".tb-assign");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
Some browsers behave OK if you just do item.focus().val(item.val()); but i needed to actually cause the value to "change" in my case to get the caret at the end so I saved the value, cleared it, then restored it. I did this in the event handler for when the event data is returned to the browser:
$(window).on("talkback.retrieved", function(event, talkback_queue, complete_queue) {
var open_mappings = ko.mapping.fromJS(talkback_queue);
self.talkbackQueue(open_mappings);
if (talkback_queue) self.queueLength(talkback_queue.length);
var completed_mappings = ko.mapping.fromJS(complete_queue);
self.completeQueue(completed_mappings);
if (self.currentTalkbackId()) {
var element = $("li[talkbackId='" + self.currentTalkbackId() + "']");
if (talkback.state.currentAction == "assign") {
var item = element.find(".tb-assign");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
} else {
var item = element.find(".tb-response");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
}
}
}
);
So, my final issue is that if I used my observables in my method "clearing" the values when a new "current item" is selected (replyToggle & assignToggle), they don't seem to work.
self.currentResponse("");
self.currentAssignee("");
I cannot get the values to clear. I had to do some hack-fu and added the line below that to just work around it for now:
$(".tb-assign").val("");

#each with call to helper ignores first entry in array

I'm trying to generate a menu with handlebars, based on this array (from coffeescript):
Template.moduleHeader.modules = -> [
{ "moduleName": "dashboard", "linkName": "Dashboard" }
{ "moduleName": "roomManager", "linkName": "Raumverwaltung" }
{ "moduleName": "userManager", "linkName": "Benutzerverwaltung" }
]
The iteration looks like this (from the html code):
{{#each modules}}
<li {{this.isActive this.moduleName}}>
<a class="{{this.moduleName}}" href="#">{{this.linkName}}</a>
</li>
{{/each}}
{{this.isActive}} is defined like this (coffeescript code again):
Template.moduleHeader.isActive = (currentModuleName) ->
if currentModuleName is Session.get 'module'
"class=active"
Based on Session.get 'module' the appropriate menu item is highlighted with the css class active.
On reload, the Session-variable module contains 'dashboard', which is the first entry in that array, however, it does not get the active-class. If I add an empty object as the first item to the modules-array, the 'dashboard'-item is properly highlighted.
The strange thing is, that the first item (dashboard) is rendered fine, but it does not have the active-class, which raises the impression, that the function this.isActive is not called for the first item.
Am I doing it wrong?
Really going out on a whim here but my traditional approach would be below. I don't know whether it will work but it was too big for a comment, yet again it's not a guaranteed fix. Let me know how it goes:
Replace the HTML with
{{#each modules}}
<li {{isActive}}>
<a class="{{moduleName}}" href="#">{{linkName}}</a>
</li>
{{/each}}
Replace the CS with
Template.moduleHeader.isActive = () ->
currentModule = this
currentModuleName = currentModule.moduleName
if currentModuleName is Session.get 'module'
"class=active"
If its rendering, the each block is working correctly. There must be an issue in the isActive helper. Most likely the if block isn't working as you think it should be. Put a console.log in there and see if it is entered, and put another inside the if (printf debugging). I find this is usually the easiest way to do quick debugging of handlebars helpers.
A useful helper in js for inspecting values inside of a context is as follows
Handlebars.registerHelper("debug", function(optionalValue) {
console.log("Current Context");
console.log("====================");
console.log(this);
if (optionalValue) {
console.log("Value");
console.log("====================");
console.log(optionalValue);
}
});
Then you can call this inside an each block and see all the values the current context/a specific value has.

Weird KRL foreach behavior

I've been getting some odd behavior using a foreach today. I have a dataset that's pulling in a JSON document. Part of it is an array, which I pick() out and send to the foreach. Here's my global block:
global {
dataset appserver <- "http://imaj-app.lddi.org:8010/list/popular" cachable for 1 hour;
popular = appserver.pick("$..images")
}
There's one rule first that sets up the page. It looks like this:
rule setup {
select when web pageview "www\.google\.com"
pre {
imagelist = <<
<div id="462popular" style="margin-left:auto;margin-right:auto;width:450px">
<p>Popular images from the CS 462 Image Project</p>
<span class="image"></span>
</div>
>>;
}
prepend('#footer', imagelist);
}
And here's the rule that's not working:
rule images {
select when web pageview "www\.google\.com"
foreach popular setting (image)
pre {
thumburl = image.pick("$..thumburl");
viewurl = "http://imaj-web.lddi.org/view?imagekey=" + image.pick("$..imagekey");
html = <<
<span class="image"><img src="#{thumburl}" style="border:none"/></span>
>>;
}
after('#462popular .image', html);
}
I get something like this (notice how small the scrollbar thumb is):
Any ideas what's going on here?
You have a recursion problem with your html structure and your after selector to insert new content.
Your selector for inserting new content is
#462popular .image
which means that the contents of html will be inserted after every element with the class of image inside an element with the id of #462popular.
Inside the html that you are inserting you have an element with the class name of image which means you are multiplying the number of elements with the class of image inside #462popular every time you go through the loop.
: )