1 define(function(require) {
6 var _ = require('libs/underscore');
9 var INSERTION = function(implementation) {
10 var toret = function(node, options) {
11 var insertion = this.getNodeInsertion(node),
12 nodeWasContained = this.document.containsNode(insertion.ofNode),
15 options = options || {};
16 if(!(this.document.containsNode(this)) || !insertion.isNew) {
17 nodeParent = insertion.ofNode.parent();
19 if(!insertion.insertsNew && insertion.ofNode.isSurroundedByTextNodes()) {
20 var prev = insertion.ofNode.prev(),
21 next = insertion.ofNode.next();
22 prev.setText(prev.getText()+next.getText());
25 returned = implementation.call(this, insertion.ofNode);
26 if(!options.silent && returned.sameNode(insertion.ofNode)) {
27 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
34 var documentNodeTransformations = {
36 var parent = this.parent(),
37 existed = this.document.containsNode(this);
40 this.triggerChangeEvent('nodeDetached', {parent: parent});
42 // This was the root of the document
43 this.document._defineDocumentProperties(null);
49 replaceWith: function(node) {
52 return this.document.replaceRoot(node);
55 toret = this.after(node);
59 throw new Error('Cannot replace node without a parent.');
62 after: INSERTION(function(node) {
63 var next = this.next();
64 if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
65 next.setText(node.getText() + next.getText());
69 this._$.after(node.nativeNode);
73 before: INSERTION(function(node) {
74 var prev = this.prev();
75 if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
76 prev.setText(prev.getText() + node.getText());
80 this._$.before(node.nativeNode);
84 wrapWith: function(node) {
85 var insertion = this.getNodeInsertion(node);
87 if(this.parent() || this.isRoot()) {
88 this.replaceWith(insertion.ofNode);
90 insertion.ofNode.append(this);
91 return insertion.ofNode;
95 * Removes parent of a node if node has no siblings.
101 var parent = this.parent(),
103 if(parent.contents().length === 1) {
104 grandParent = parent.parent();
105 parent.unwrapContent();
111 var elementNodeTransformations = {
115 if(this.parent() && this.isSurroundedByTextNodes()) {
117 this.prev().appendText(next.getText());
120 return this.__super__.detach();
123 setTag: function(tagName) {
124 var node = this.document.createDocumentNode({tagName: tagName});
126 this.getAttrs().forEach(function(attribute) {
127 node.setAttr(attribute.name, attribute.value);
130 this.contents().forEach(function(child) {
134 node.setData(this.getData());
136 this.replaceWith(node);
140 setAttr: function(name, value, silent) {
141 var oldVal = this.getAttr(name);
142 this._$.attr(name, value);
144 this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
148 append: INSERTION(function(node) {
149 var last = _.last(this.contents());
150 if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
151 last.setText(last.getText() + node.getText());
155 this._$.append(node.nativeNode);
160 prepend: INSERTION(function(node) {
161 var first = this.contents()[0];
162 if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
163 first.setText(node.getText() + first.getText());
167 this._$.prepend(node.nativeNode);
172 insertAtIndex: function(nativeNode, index) {
173 var contents = this.contents();
174 if(index < contents.length) {
175 return contents[index].before(nativeNode);
176 } else if(index === contents.length) {
177 return this.append(nativeNode);
181 unwrapContent: function() {
182 var parent = this.parent();
187 var myContents = this.contents(),
188 myIdx = parent.indexOf(this);
191 if(myContents.length === 0) {
192 return this.detach();
196 var childrenLength = this.contents().length,
199 this.contents().forEach(function(child) {
200 var returned = this.before(child);
201 if(first && !(returned.sameNode(child))) {
210 element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
211 element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
215 wrapText: function(params) {
216 return this.document._wrapText(_.extend({inside: this}, params));
220 var textNodeTransformations = {
222 impl: function(t, text) {
223 t.oldText = this.getText();
224 this.nativeNode.data = text;
225 this.triggerTextChangeEvent();
228 this.setText(t.oldText);
232 before: INSERTION(function(node) {
233 if(node.nodeType === Node.TEXT_NODE) {
234 this.prependText(node.getText());
238 return this.__super__.before(node, {silent:true});
242 after: INSERTION(function(node) {
243 if(node.nodeType === Node.TEXT_NODE) {
244 this.appendText(node.getText());
248 return this.__super__.after(node, {silent:true});
252 append: function(node) {
253 if(node.nodeType === Node.TEXT_NODE) {
254 this.appendText(node.getText());
259 prepend: function(node) {
260 if(node.nodeType === Node.TEXT_NODE) {
261 this.prependText(node.getText());
267 appendText: function(text) {
268 this.nativeNode.data = this.nativeNode.data + text;
269 this.triggerTextChangeEvent();
272 prependText: function(text) {
273 this.nativeNode.data = text + this.nativeNode.data;
274 this.triggerTextChangeEvent();
277 wrapWith: function(desc) {
278 if(typeof desc.start === 'number' && typeof desc.end === 'number') {
279 return this.document._wrapText({
280 inside: this.parent(),
281 textNodeIdx: this.parent().indexOf(this),
282 offsetStart: Math.min(desc.start, desc.end),
283 offsetEnd: Math.max(desc.start, desc.end),
284 _with: {tagName: desc.tagName, attrs: desc.attrs}
287 return this.__super__.wrapWith.call(this, desc);
291 split: function(params) {
292 var parentElement = this.parent(),
294 succeedingChildren = [],
295 prefix = this.getText().substr(0, params.offset),
296 suffix = this.getText().substr(params.offset);
298 parentElement.contents().forEach(function(child) {
300 succeedingChildren.push(child);
302 if(child.sameNode(this)) {
307 if(prefix.length > 0) {
308 this.setText(prefix);
315 parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
316 var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
317 parentElement.after(newElement);
319 succeedingChildren.reverse().forEach(function(child) {
320 newElement.prepend(child);
322 if(suffix.length > 0) {
323 newElement.prepend({text: suffix});
326 return {first: parentElement, second: newElement};
329 divideWithElementNode: function(node, params) {
330 var insertion = this.getNodeInsertion(node),
331 myText = this.getText();
333 if(params.offset === myText.length) {
334 return this.after(node);
336 if(params.offset === 0) {
337 return this.before(node);
340 var lhsText = myText.substr(0, params.offset),
341 rhsText = myText.substr(params.offset),
342 rhsTextNode = this.document.createDocumentNode({text: rhsText});
344 this.setText(lhsText);
345 this.after(insertion.ofNode);
346 insertion.ofNode.after(rhsTextNode);
347 return insertion.ofNode;
351 var documentTransformations = {
352 wrapNodes: function(params) {
353 if(!(params.node1.parent().sameNode(params.node2.parent()))) {
354 throw new Error('Wrapping non-sibling nodes not supported.');
357 var parent = params.node1.parent(),
358 parentContents = parent.contents(),
359 wrapper = this.createDocumentNode({
360 tagName: params._with.tagName,
361 attrs: params._with.attrs}),
362 idx1 = parent.indexOf(params.node1),
363 idx2 = parent.indexOf(params.node2);
371 var insertingMethod, insertingTarget;
373 insertingMethod = 'prepend';
374 insertingTarget = parent;
376 insertingMethod = 'after';
377 insertingTarget = parentContents[idx1-1];
380 for(var i = idx1; i <= idx2; i++) {
381 wrapper.append(parentContents[i].detach());
384 insertingTarget[insertingMethod](wrapper);
388 _wrapText: function(params) {
389 params = _.extend({textNodeIdx: 0}, params);
390 if(typeof params.textNodeIdx === 'number') {
391 params.textNodeIdx = [params.textNodeIdx];
394 var contentsInside = params.inside.contents(),
395 idx1 = Math.min.apply(Math, params.textNodeIdx),
396 idx2 = Math.max.apply(Math, params.textNodeIdx),
397 textNode1 = contentsInside[idx1],
398 textNode2 = contentsInside[idx2],
399 sameNode = textNode1.sameNode(textNode2),
400 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
401 prefixInside = textNode1.getText().substr(params.offsetStart),
402 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
403 suffixOutside = textNode2.getText().substr(params.offsetEnd)
406 if(!(textNode1.parent().sameNode(textNode2.parent()))) {
407 throw new Error('Wrapping text in non-sibling text nodes not supported.');
410 var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
411 textNode1.after(wrapperElement);
414 if(prefixOutside.length > 0) {
415 wrapperElement.before({text:prefixOutside});
418 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
419 wrapperElement.append({text: core});
422 if(prefixInside.length > 0) {
423 wrapperElement.append({text: prefixInside});
425 for(var i = idx1 + 1; i < idx2; i++) {
426 wrapperElement.append(contentsInside[i]);
428 if(suffixInside.length > 0) {
429 wrapperElement.append({text: suffixInside});
432 if(suffixOutside.length > 0) {
433 wrapperElement.after({text: suffixOutside});
435 return wrapperElement;
437 replaceRoot: function(node) {
438 var insertion = this.getNodeInsertion(node);
440 this._defineDocumentProperties(insertion.ofNode._$);
441 insertion.ofNode.triggerChangeEvent('nodeAdded');
442 return insertion.ofNode;
444 deleteText: function(params) {
445 var ptr, next, toDetach, middle, text;
447 if(params.from.node.sameNode(params.to.node)) {
448 ptr = params.from.node;
449 text = ptr.getText();
450 ptr.setText(text.substr(0, params.from.offset) + text.substr(params.to.offset));
454 // Both edge text nodes need to be edited before anything else happen in case that
455 // they get merged when detaching content between them.
456 params.from.node.setText(params.from.node.getText().substr(0, params.from.offset));
457 params.to.node.setText(params.to.node.getText().substr(params.to.offset));
459 ptr = params.from.node;
462 while(next || ptr.parent()) {
464 if(next.sameNode(params.to.node)) {
467 else if(next.nodeType === Node.ELEMENT_NODE && next.containsNode(params.to.node)) {
481 if(!this.containsNode(params.to.node)) {
482 // The end node was merged during detaching nodes above - there is nothing more left to do.
486 ptr = middle.contents()[0];
487 while(ptr && !ptr.sameNode(params.to.node)) {
488 if(ptr.nodeType === Node.ELEMENT_NODE && ptr.containsNode(params.to.node)) {
489 ptr = ptr.contents()[0];
501 transformations: documentTransformations
504 transformations: documentNodeTransformations
507 transformations: elementNodeTransformations
510 transformations: textNodeTransformations