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 returned = implementation.call(this, insertion.ofNode);
20 if(!options.silent && returned.sameNode(insertion.ofNode)) {
21 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
28 var documentNodeTransformations = {
30 var parent = this.parent(),
31 existed = this.document.containsNode(this);
34 this.triggerChangeEvent('nodeDetached', {parent: parent});
36 // This was the root of the document
37 this.document._defineDocumentProperties(null);
43 replaceWith: function(node) {
46 return this.document.replaceRoot(node);
49 toret = this.after(node);
53 throw new Error('Cannot replace node without a parent.');
56 after: INSERTION(function(node) {
57 var next = this.next();
58 if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
59 next.setText(node.getText() + next.getText());
63 this._$.after(node.nativeNode);
67 before: INSERTION(function(node) {
68 var prev = this.prev();
69 if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
70 prev.setText(prev.getText() + node.getText());
74 this._$.before(node.nativeNode);
78 wrapWith: function(node) {
79 var insertion = this.getNodeInsertion(node);
81 if(this.parent() || this.isRoot()) {
82 this.replaceWith(insertion.ofNode);
84 insertion.ofNode.append(this);
85 return insertion.ofNode;
89 * Removes parent of a node if node has no siblings.
95 var parent = this.parent(),
97 if(parent.contents().length === 1) {
98 grandParent = parent.parent();
99 parent.unwrapContent();
105 var elementNodeTransformations = {
109 if(this.parent() && this.isSurroundedByTextElements()) {
111 this.prev().appendText(next.getText());
114 return this.__super__.detach();
117 setTag: function(tagName) {
118 var node = this.document.createDocumentNode({tagName: tagName});
120 this.getAttrs().forEach(function(attribute) {
121 node.setAttr(attribute.name, attribute.value);
124 this.contents().forEach(function(child) {
128 node.setData(this.getData());
130 this.replaceWith(node);
134 setAttr: function(name, value, silent) {
135 var oldVal = this.getAttr(name);
136 this._$.attr(name, value);
138 this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
142 append: INSERTION(function(node) {
143 var last = _.last(this.contents());
144 if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
145 last.setText(last.getText() + node.getText());
149 this._$.append(node.nativeNode);
154 prepend: INSERTION(function(node) {
155 var first = this.contents()[0];
156 if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
157 first.setText(node.getText() + first.getText());
161 this._$.prepend(node.nativeNode);
166 insertAtIndex: function(nativeNode, index) {
167 var contents = this.contents();
168 if(index < contents.length) {
169 return contents[index].before(nativeNode);
170 } else if(index === contents.length) {
171 return this.append(nativeNode);
175 unwrapContent: function() {
176 var parent = this.parent();
181 var myContents = this.contents(),
182 myIdx = parent.indexOf(this);
185 if(myContents.length === 0) {
186 return this.detach();
190 var childrenLength = this.contents().length,
193 this.contents().forEach(function(child) {
194 var returned = this.before(child);
195 if(first && !(returned.sameNode(child))) {
204 element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
205 element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
209 wrapText: function(params) {
210 return this.document._wrapText(_.extend({inside: this}, params));
214 var textNodeTransformations = {
216 impl: function(t, text) {
217 t.oldText = this.getText();
218 this.nativeNode.data = text;
219 this.triggerTextChangeEvent();
222 this.setText(t.oldText);
226 before: INSERTION(function(node) {
227 if(node.nodeType === Node.TEXT_NODE) {
228 this.prependText(node.getText());
232 return this.__super__.before(node, {silent:true});
236 after: INSERTION(function(node) {
237 if(node.nodeType === Node.TEXT_NODE) {
238 this.appendText(node.getText());
242 return this.__super__.after(node, {silent:true});
246 append: function(node) {
247 if(node.nodeType === Node.TEXT_NODE) {
248 this.appendText(node.getText());
253 prepend: function(node) {
254 if(node.nodeType === Node.TEXT_NODE) {
255 this.prependText(node.getText());
261 appendText: function(text) {
262 this.nativeNode.data = this.nativeNode.data + text;
263 this.triggerTextChangeEvent();
266 prependText: function(text) {
267 this.nativeNode.data = text + this.nativeNode.data;
268 this.triggerTextChangeEvent();
271 wrapWith: function(desc) {
272 if(typeof desc.start === 'number' && typeof desc.end === 'number') {
273 return this.document._wrapText({
274 inside: this.parent(),
275 textNodeIdx: this.parent().indexOf(this),
276 offsetStart: Math.min(desc.start, desc.end),
277 offsetEnd: Math.max(desc.start, desc.end),
278 _with: {tagName: desc.tagName, attrs: desc.attrs}
281 return this.__super__.wrapWith.call(this, desc);
285 split: function(params) {
286 var parentElement = this.parent(),
288 succeedingChildren = [],
289 prefix = this.getText().substr(0, params.offset),
290 suffix = this.getText().substr(params.offset);
292 parentElement.contents().forEach(function(child) {
294 succeedingChildren.push(child);
296 if(child.sameNode(this)) {
301 if(prefix.length > 0) {
302 this.setText(prefix);
309 parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
310 var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
311 parentElement.after(newElement);
313 if(suffix.length > 0) {
314 newElement.append({text: suffix});
316 succeedingChildren.forEach(function(child) {
317 newElement.append(child);
320 return {first: parentElement, second: newElement};
323 divideWithElementNode: function(node, params) {
324 var insertion = this.getNodeInsertion(node),
325 myText = this.getText();
327 if(params.offset === myText.length) {
328 return this.after(node);
330 if(params.offset === 0) {
331 return this.before(node);
334 var lhsText = myText.substr(0, params.offset),
335 rhsText = myText.substr(params.offset),
336 rhsTextNode = this.document.createDocumentNode({text: rhsText});
338 this.setText(lhsText);
339 this.after(insertion.ofNode);
340 insertion.ofNode.after(rhsTextNode);
341 return insertion.ofNode;
345 var documentTransformations = {
346 wrapNodes: function(params) {
347 if(!(params.node1.parent().sameNode(params.node2.parent()))) {
348 throw new Error('Wrapping non-sibling nodes not supported.');
351 var parent = params.node1.parent(),
352 parentContents = parent.contents(),
353 wrapper = this.createDocumentNode({
354 tagName: params._with.tagName,
355 attrs: params._with.attrs}),
356 idx1 = parent.indexOf(params.node1),
357 idx2 = parent.indexOf(params.node2);
365 var insertingMethod, insertingTarget;
367 insertingMethod = 'prepend';
368 insertingTarget = parent;
370 insertingMethod = 'after';
371 insertingTarget = parentContents[idx1-1];
374 for(var i = idx1; i <= idx2; i++) {
375 wrapper.append(parentContents[i].detach());
378 insertingTarget[insertingMethod](wrapper);
382 _wrapText: function(params) {
383 params = _.extend({textNodeIdx: 0}, params);
384 if(typeof params.textNodeIdx === 'number') {
385 params.textNodeIdx = [params.textNodeIdx];
388 var contentsInside = params.inside.contents(),
389 idx1 = Math.min.apply(Math, params.textNodeIdx),
390 idx2 = Math.max.apply(Math, params.textNodeIdx),
391 textNode1 = contentsInside[idx1],
392 textNode2 = contentsInside[idx2],
393 sameNode = textNode1.sameNode(textNode2),
394 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
395 prefixInside = textNode1.getText().substr(params.offsetStart),
396 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
397 suffixOutside = textNode2.getText().substr(params.offsetEnd)
400 if(!(textNode1.parent().sameNode(textNode2.parent()))) {
401 throw new Error('Wrapping text in non-sibling text nodes not supported.');
404 var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
405 textNode1.after(wrapperElement);
408 if(prefixOutside.length > 0) {
409 wrapperElement.before({text:prefixOutside});
412 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
413 wrapperElement.append({text: core});
416 if(prefixInside.length > 0) {
417 wrapperElement.append({text: prefixInside});
419 for(var i = idx1 + 1; i < idx2; i++) {
420 wrapperElement.append(contentsInside[i]);
422 if(suffixInside.length > 0) {
423 wrapperElement.append({text: suffixInside});
426 if(suffixOutside.length > 0) {
427 wrapperElement.after({text: suffixOutside});
429 return wrapperElement;
431 replaceRoot: function(node) {
432 var insertion = this.getNodeInsertion(node);
434 this._defineDocumentProperties(insertion.ofNode._$);
435 insertion.ofNode.triggerChangeEvent('nodeAdded');
436 return insertion.ofNode;
438 deleteText: function(params) {
439 var ptr, next, toDetach, middle, text;
441 if(params.from.node.sameNode(params.to.node)) {
442 ptr = params.from.node;
443 text = ptr.getText();
444 ptr.setText(text.substr(0, params.from.offset) + text.substr(params.to.offset));
448 // Both edge text nodes need to be edited before anything else happen in case that
449 // they get merged when detaching content between them.
450 params.from.node.setText(params.from.node.getText().substr(0, params.from.offset));
451 params.to.node.setText(params.to.node.getText().substr(params.to.offset));
453 ptr = params.from.node;
456 while(next || ptr.parent()) {
458 if(next.sameNode(params.to.node)) {
461 else if(next.nodeType === Node.ELEMENT_NODE && next.containsNode(params.to.node)) {
475 if(!this.containsNode(params.to.node)) {
476 // The end node was merged during detaching nodes above - there is nothing more left to do.
480 ptr = middle.contents()[0];
481 while(ptr && !ptr.sameNode(params.to.node)) {
482 if(ptr.nodeType === Node.ELEMENT_NODE && ptr.containsNode(params.to.node)) {
483 ptr = ptr.contents()[0];
495 transformations: documentTransformations
498 transformations: documentNodeTransformations
501 transformations: elementNodeTransformations
504 transformations: textNodeTransformations