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?
Related
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("");
I am fairly new to Javascript and have a basic question. I have an HTML form with first_name and last_name input fields. I have the following Javascript code in the header but after the code runs, the focus goes to the next field (last_name). Why is that and how do I correct it?
Thank you.
<script>
function validateForm()
{
valid = true;
//validate first name
if (document.contactform.first_name.value == "")
{
//alert user first name is blank
alert("You must enter a first name");
document.getElementById("first_name").focus();
return false;
}
return valid;
}
</script>
and the form field code is:
input type="text" name="first_name" id="first_name" maxlength="50" size="30" onBlur="validateForm()"
A fix for this is to add a slight delay.. like so:
setTimeout(function() {
document.getElementById('first_name').focus()
}, 10);
Here is your example with this fix in jsfiddle: http://jsfiddle.net/FgHrg/1/
It seems to be a common Firefox problem.. I don't know exactly why but it has something to do with Firefox loading the javascript before the DOM is fully loaded.. in otherwords getElementById('first_name') returns null. But adding the slight delay fixes this problem.
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);
}
I'm attempting to use a custom directive to make a conditional required statement. The first condition I've added is 'firstInArray' to make the element required if it's the first in array of choices (necessary for a UI where you need to pick at least one item, but you could pick indefinitely many):
.directive('variableRequired', [
()->
return {
require: 'ngModel',
link: (scope, el, attrs, ctrl)->
vars = attrs.variableRequired.split(',')
condition = vars[0]
if condition is 'firstInArray'
item = vars[1]
arr = vars[2]
if scope[item] == scope[arr][0]
$(el).removeAttr('variable-required')
$(el).attr('required', 'required')
}
])
When I add scope.$apply() within my directive, the app freezes up (seems like infinite recursion).
Is there a better way to approach this than a custom directive? If not, what's wrong with my directive?
You may be able to use the undocumented ng-required directive, instead of your own custom directive:
<li ng-repeat="itemObj in items">
<input type="text" ng-model="itemObj.text" ng-required="$first">
</li>
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.
: )