XHTML DOM - How to split a tag on IE? - dom

Let's assume I have a part of an html document containing the following code (basic structure) :
<p>
<span class="1">This is my first content</span>
<span class="2">This is my second content</span>
</p>
I'd like to allow the user to select a part of the text and apply a new class to it.
Let's say the user selects "is my first" in the first span, and applies class "3".
I'd like to have the following result :
<p>
<span class="1">This </span>
<span class="3">is my first</span>
<span class="1"> content</span>
<span class="2">This is my second content</span>
</p>
I've managed to do this on Firefox by using the execCommand "InsertHTML", but I can't find a way to do this in IE (before IE9)
The only result I have is a nested span element, like below :
<p>
<span class="1">This <span class="3">is my first</span> content</span>
<span class="2">This is my second content</span>
</p>
Do you have any idea of how I could achieve this ?
Any help would be much appreciated !
By the way, if this looks too simple to you, how would you handle the case of a user selecting a portion of text that spans over 2 or more spans ? over 2 or more ps ?

you can get the selected segment using selection range. I would recommend using rangy, which is a cross browser range module.
Here's some "untested" code using jQuery and Rangy to hopefully point you in the right direction, for your first case:
var splitTag=function(class){
var sel = rangy.getSelection();
// this is your selection, in your example "is my first"
var r0 = sel.getRangeAt(0);
// create a new range
var r1 = rangy.createRange();
// this would be your <p>
var p = r0.endContainer.parentNode;
// set the new range to start at the end of your phrase and to end at <p>
r1.setStart(r0.endContainer, r0.endOffset);
r1.setEnd(p, p.length-1);
// extract the content of your first selection "is my first"
var r0Txt=r0.toHtml();
// make it into an span, with class set to "class argument" which would be 3
var newContent=$("<span/>").html(r0Txt).attr("class", class);
r0.deleteContents();
// insert the new node before r1
r1.insertNode(newContent[0]);
sel.removeAllRanges();
}
this should get you the result for your first situation. for selections across multiple paragraphs, here's a modification of the code:
var splitTag=function(class){
var sel = rangy.getSelection();
var r0 = sel.getRangeAt(0);
var r1 = rangy.createRange();
var p = r0.endContainer.parentNode;
r1.setStart(r0.endContainer, r0.endOffset);
r1.setEnd(p, p.length-1);
var r0Txt=r0.toHtml();
if(!r0.startContainer===r0.endContainer){
// the selection spans multiple dom's
// set the class of all spans in the highlight to 3
var newContent=$(r0Txt).find("span").attr("class",class);
}else{
var newContent=$("<span/>").html(r0Txt).attr("class", class);
}
r0.deleteContents();
r1.insertNode(newContent[0]);
sel.removeAllRanges();
}

Related

Element refreshing in #FindBy with Selenium and java

We have HTML code that has elements which can be expanded or collapsed (depending on what they currently are). They can be expanded only one level. It looks sort of like this
<a href = "#" title = "Expand" id = ...>Row 1 </a>
<a href = "#" title = "Expand" id = ...>Row 2 </a>
<a href = "#" title = "Collapse" id = ...>Row 3 </a>
<a href = "#" id = ...>Subrow 3.1 </a>
<a href = "#" id = ...>Subrow 3.2 </a>
<a href = "#" title = "Expand" id = ...>Row 4 </a>
<a href = "#" title = "Collaps" id = ...>Row 5 </a>
<a href = "#" id = ...>Subrow 5.1 </a>
<a href = "#" id = ...>Subrow 5.2 </a>
There are many more, and it is a bit more complicated than this but you get the picture. Using page object I made a list
#FindBy(xpath = "//a[contains(#title, 'Expand') or contains(#title, 'Collapse')]")
private List<WebElement> expandCollapseElements
And then a method to expand them for instance:
public void expand() {
for (WebElement ele : expndCollapseElements) {
if (ele.getAttribute("title").equals("Expand")) {
ele.click();
}
}
}
In the actual code I also tried waiting until the title changed so I know it worked. Anyway, the problem is that once I expand an element, all the elements underneath apparently become stale. I know when you use #FindBy it refinds the element each time. But I am wondering when the element is refreshed. It seems like it is just done once at the beginning of the loop. Is that true? When is the element list refreshed? And is there a better way to do this? I tried making a new list containing the list and reversing it which kind of worked but not really. The matter is further complicated by the fact that these rows are displayed in its own scrollable div where you need to sacroll to see some of the elements. So any thoughts?
Suggestion: That's the difference between foreach and for.
Try to use "For loop" only.

protractor 4 : how to locate a parent at 'n' level above by class

For instance with following html:
<div class='parent'>
<p>
<span class='child'></span>
</p>
</div>
To locate .parent from .child I'm trying something like :
let child = element(by.css('.child'));
let parent = child.element(by.xpath('../..')); // not working
let parent = child.element(by.xpath('ancestor::.parent')); // not working
What's the best way to achieve that?
You have mentioned incorrect locator combination. That is incuded css value '.parent' in xpath expression. The right way is:
let parent = child.element(by.xpath('ancestor::div'));
OR
let parent = child.element(by.xpath('ancestor::div[1]'));

Issues getting validation working with dynamically generating form inputs?

I have some basic form/input html that works (including validation) if explicitly written as follows:
<form name="forms.create" novalidate>
<div class="si-container">
<div class="si-input-container">
<input class="si-input" name="someNum" placeholder="Enter a number" ng-model="formdata.number" type="number" min="40"/>
</div>
<div class="si-error">
<div ng-show="forms.create.someNum.$error.min">Error! Value must be > 40.</div>
</div>
</div>
</form>
Now what I want to do is create a directive that allows me to write the html below, but result in the html above:
<form name="forms.create" novalidate>
<div special-input name="someNum" placeholder="Enter a number" type="number" ng-model="formdata.number">
<div error-type="min" error-value="40">Error! Value must be > 40.</div>
</div>
</form>
My attempt at the special-input directive (simplified) is as follows:
.directive('specialInput', [function(){
return {
compile: function(elem, attrs){
var input = angular.element('<input class="si-input"/>');
input.attr('placeholder', attrs.placeholder);
input.attr('type', attrs.type);
input.attr('name', attrs.name);
input.attr('ng-model', attrs.ngModel);
var errorCont = angular.element('<div class="si-error"></div>');
var errors = elem.children();
angular.forEach(errors, function(error){
var err = angular.element(error);
var type = err.attr('error-type');
var value = err.attr('error-value');
input.attr(type, value);
var formName = elem.parent().attr('name');
errorCont.append('<div ng-show="' + formName + '.' + attrs.name + '.$error.' + type + '">' + err.html() + '</div>');
});
var cont = angular.element('<div class="si-container"></div>');
cont.append('<div class="si-floating-label">' + attrs.placeholder + '</div>');
cont.append('<div class="si-input-container">' + input[0].outerHTML + '</div>');
cont.append('<div class="si-underline"></div>');
cont.append(errorCont);
elem.replaceWith(cont[0].outerHTML);
}
};
}]);
Now the resultant html using the directive above looks about right. If I put {{formdata.number}} below the form the value changes as expected. The problem is that now the validation never shows.
For example, if I put the value 5 in the input and inspect the form object, I get weird results. $dirty is set to true for form, but not for form.someNum. If I put 55 in the input, $dirty is still set to false for form.someNum, but $modelValue and $viewValue both show 55.
Any ideas or suggestions? Here is a fiddle to help with any testing.
If you put 50 in the input box you should see the value below, but put 5 and the error does not appear
UPDATE
I have managed to get it working by moving the dom changes into the link function instead of the compile function, and adding this:
elem.replaceWith(cont);
$compile(cont)(scope);
I am still puzzled though, as to why this works, while altering the dom in the exact same way in the compile function doesn't work. Is anyone able to explain this?
It's because the original ng-model is still get compiled even the original DOM has already been replaced by the new one in your compile function.
The ng-model directive will register itself to a parent form in its postLink function. Due to the fact that the postLink function will be executed in reverse (child's before parent's), the new ng-model will do the registration first, thus it will be overridden by the one from the original ng-model eventually.
To avoid this problem, you could change the original ng-model to another name such as my-model, then rename it to ng-model later in your compile function.
Example jsfiddle: http://jsfiddle.net/Wr3cJ/1/
Hope this helps.

Search on descendants of an element

With protractor whats the best way to select child elements? Say we have the layout below...
<div id='parent_1'>
<div class='red'>Red</div>
<div class='blue'>Blue</div>
</div>
<div id='parent_2'>
<div class='red'>Red</div>
<div class='blue'>Blue</div>
</div>
With jQuery we'd do something like this.
var p1 = $('#parent_1');
var p1_red = $('.red', p1); //or p1.find('.red');
var p1_blue = $('.blue', p1); //or p1.find('.blue');
But with Protractor does it make sense to first get the parent element?
Since doing this var p1 = element('#parent_1'); doesn't actually retrieve/search for the object until getText() or something is called.
so doing this..
Scenario 1
expect(p1.element('.red')).toBe('red');
expect(p1.element('.blue')).toBe('blue');
OR
Scenario 2
expect(element('#parent_1').element('.red')).toBe('red');
expect(element('#parent_1').element('.blue')).toBe('blue');
OR
Scenario 3
expect(element('#parent_1 > .red')).toBe('red');
expect(element('#parent_1 > .blue')).toBe('blue');
Are there any benefits in one approach over the other?
This is what I'm doing but I don't know if there's any advantage of separating the parent from the cssSelector:
function getChild(cssSelector, parentElement){
return parentElement.$(cssSelector);
}
var parent = $('#parent_1');
var child_red = getChild('.red', parent);
var child_blue = getChild('.blue', parent);
Looking at Protractor's elementFinder I could be doing this:
function getChild(cssSelector, parentCss){
return $(parentCss).$(cssSelector);
}
var child_red = getChild('.red', '#parent_1');
var child_blue = getChild('.blue', '#parent_1');
The advantage of separating the child from the child css selector would only be if you'd like to use the parent for something else. Otherwise, it's slightly faster to do it in one call, like expect(element('#parent_1 > .red')).toBe('red'); since Protractor doesn't need to make two calls to the browser in this case.
Another reason to use the first approach would be if you were using a Locator strategy that cannot be expressed in CSS. For example:
var parent = element(by.css('.foo'));
var child = parent.element(by.binding('childBinding'));
expect(child.getText()).toEqual('whatever');

Dojo events: getting it to work with dynamically added DOM elements

I have a method of a class as follows:
add_file: function(name, id, is_new){
// HTML: <div class="icon mime zip">name.zip <a>x</a></div>
var components = name.split('.');
var extension = components[components.length-1];
this.container.innerHTML += "<div id='"+id+"' class='icon mime "+extension+"'>"+name+" <a id='remove-"+id+"' href='#remove'>x</a></div>";
// Add event to a tag
dojo.connect(dojo.byId('remove-'+id), 'onclick', function(ev){
// here i am
});
},
All is working well, until I run this method more than once. The first time the event is registered correctly, and clicking the 'x' will run the "here i am" function. However, once I add more than one node (and yes, the ID is different), the event is registered to the last node, but removed from any previous ones.
In affect I have this:
<div id="field[photos]-filelist">
<div id="file1" class="icon mime jpg">file1.jpg <a id="remove-file1" href="#remove">x</a></div>
<div id="file2" class="icon mime jpg">file2.jpg <a id="remove-file2" href="#remove">x</a></div>
</div>
...and the remove link only works for the last node (remove-file2 in this case).
The problem is you are using the innerHTML +=
That is going to take the existing html, convert it to plain markup, and then completely create new nodes from the markup. In the process, all of the nodes with events get replaced with nodes that look exactly the same but are not connected to anything.
The correct way to do this is to use dojo.place(newNodeOrHTML, refNode, positionString)
var myNewHTML = "<div id='"+id+"' class='icon mime "+extension+"'>"+name+" <a id='remove-"+id+"' href='#remove'>x</a></div>"
//This won't work as is breaks all the connections between nodes and events
this.container.innerHTML += myNewHTML;
//This will work because it uses proper dom manipulation techniques
dojo.place(myNewHTML, this.container, 'last');