1 /** |
|
2 * Node.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /** |
|
12 * This class is a minimalistic implementation of a DOM like node used by the DomParser class. |
|
13 * |
|
14 * @example |
|
15 * var node = new tinymce.html.Node('strong', 1); |
|
16 * someRoot.append(node); |
|
17 * |
|
18 * @class tinymce.html.Node |
|
19 * @version 3.4 |
|
20 */ |
|
21 define("tinymce/html/Node", [], function() { |
|
22 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { |
|
23 '#text': 3, |
|
24 '#comment': 8, |
|
25 '#cdata': 4, |
|
26 '#pi': 7, |
|
27 '#doctype': 10, |
|
28 '#document-fragment': 11 |
|
29 }; |
|
30 |
|
31 // Walks the tree left/right |
|
32 function walk(node, root_node, prev) { |
|
33 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; |
|
34 |
|
35 // Walk into nodes if it has a start |
|
36 if (node[startName]) { |
|
37 return node[startName]; |
|
38 } |
|
39 |
|
40 // Return the sibling if it has one |
|
41 if (node !== root_node) { |
|
42 sibling = node[siblingName]; |
|
43 |
|
44 if (sibling) { |
|
45 return sibling; |
|
46 } |
|
47 |
|
48 // Walk up the parents to look for siblings |
|
49 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { |
|
50 sibling = parent[siblingName]; |
|
51 |
|
52 if (sibling) { |
|
53 return sibling; |
|
54 } |
|
55 } |
|
56 } |
|
57 } |
|
58 |
|
59 /** |
|
60 * Constructs a new Node instance. |
|
61 * |
|
62 * @constructor |
|
63 * @method Node |
|
64 * @param {String} name Name of the node type. |
|
65 * @param {Number} type Numeric type representing the node. |
|
66 */ |
|
67 function Node(name, type) { |
|
68 this.name = name; |
|
69 this.type = type; |
|
70 |
|
71 if (type === 1) { |
|
72 this.attributes = []; |
|
73 this.attributes.map = {}; |
|
74 } |
|
75 } |
|
76 |
|
77 Node.prototype = { |
|
78 /** |
|
79 * Replaces the current node with the specified one. |
|
80 * |
|
81 * @example |
|
82 * someNode.replace(someNewNode); |
|
83 * |
|
84 * @method replace |
|
85 * @param {tinymce.html.Node} node Node to replace the current node with. |
|
86 * @return {tinymce.html.Node} The old node that got replaced. |
|
87 */ |
|
88 replace: function(node) { |
|
89 var self = this; |
|
90 |
|
91 if (node.parent) { |
|
92 node.remove(); |
|
93 } |
|
94 |
|
95 self.insert(node, self); |
|
96 self.remove(); |
|
97 |
|
98 return self; |
|
99 }, |
|
100 |
|
101 /** |
|
102 * Gets/sets or removes an attribute by name. |
|
103 * |
|
104 * @example |
|
105 * someNode.attr("name", "value"); // Sets an attribute |
|
106 * console.log(someNode.attr("name")); // Gets an attribute |
|
107 * someNode.attr("name", null); // Removes an attribute |
|
108 * |
|
109 * @method attr |
|
110 * @param {String} name Attribute name to set or get. |
|
111 * @param {String} value Optional value to set. |
|
112 * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation. |
|
113 */ |
|
114 attr: function(name, value) { |
|
115 var self = this, attrs, i, undef; |
|
116 |
|
117 if (typeof name !== "string") { |
|
118 for (i in name) { |
|
119 self.attr(i, name[i]); |
|
120 } |
|
121 |
|
122 return self; |
|
123 } |
|
124 |
|
125 if ((attrs = self.attributes)) { |
|
126 if (value !== undef) { |
|
127 // Remove attribute |
|
128 if (value === null) { |
|
129 if (name in attrs.map) { |
|
130 delete attrs.map[name]; |
|
131 |
|
132 i = attrs.length; |
|
133 while (i--) { |
|
134 if (attrs[i].name === name) { |
|
135 attrs = attrs.splice(i, 1); |
|
136 return self; |
|
137 } |
|
138 } |
|
139 } |
|
140 |
|
141 return self; |
|
142 } |
|
143 |
|
144 // Set attribute |
|
145 if (name in attrs.map) { |
|
146 // Set attribute |
|
147 i = attrs.length; |
|
148 while (i--) { |
|
149 if (attrs[i].name === name) { |
|
150 attrs[i].value = value; |
|
151 break; |
|
152 } |
|
153 } |
|
154 } else { |
|
155 attrs.push({name: name, value: value}); |
|
156 } |
|
157 |
|
158 attrs.map[name] = value; |
|
159 |
|
160 return self; |
|
161 } else { |
|
162 return attrs.map[name]; |
|
163 } |
|
164 } |
|
165 }, |
|
166 |
|
167 /** |
|
168 * Does a shallow clones the node into a new node. It will also exclude id attributes since |
|
169 * there should only be one id per document. |
|
170 * |
|
171 * @example |
|
172 * var clonedNode = node.clone(); |
|
173 * |
|
174 * @method clone |
|
175 * @return {tinymce.html.Node} New copy of the original node. |
|
176 */ |
|
177 clone: function() { |
|
178 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; |
|
179 |
|
180 // Clone element attributes |
|
181 if ((selfAttrs = self.attributes)) { |
|
182 cloneAttrs = []; |
|
183 cloneAttrs.map = {}; |
|
184 |
|
185 for (i = 0, l = selfAttrs.length; i < l; i++) { |
|
186 selfAttr = selfAttrs[i]; |
|
187 |
|
188 // Clone everything except id |
|
189 if (selfAttr.name !== 'id') { |
|
190 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; |
|
191 cloneAttrs.map[selfAttr.name] = selfAttr.value; |
|
192 } |
|
193 } |
|
194 |
|
195 clone.attributes = cloneAttrs; |
|
196 } |
|
197 |
|
198 clone.value = self.value; |
|
199 clone.shortEnded = self.shortEnded; |
|
200 |
|
201 return clone; |
|
202 }, |
|
203 |
|
204 /** |
|
205 * Wraps the node in in another node. |
|
206 * |
|
207 * @example |
|
208 * node.wrap(wrapperNode); |
|
209 * |
|
210 * @method wrap |
|
211 */ |
|
212 wrap: function(wrapper) { |
|
213 var self = this; |
|
214 |
|
215 self.parent.insert(wrapper, self); |
|
216 wrapper.append(self); |
|
217 |
|
218 return self; |
|
219 }, |
|
220 |
|
221 /** |
|
222 * Unwraps the node in other words it removes the node but keeps the children. |
|
223 * |
|
224 * @example |
|
225 * node.unwrap(); |
|
226 * |
|
227 * @method unwrap |
|
228 */ |
|
229 unwrap: function() { |
|
230 var self = this, node, next; |
|
231 |
|
232 for (node = self.firstChild; node;) { |
|
233 next = node.next; |
|
234 self.insert(node, self, true); |
|
235 node = next; |
|
236 } |
|
237 |
|
238 self.remove(); |
|
239 }, |
|
240 |
|
241 /** |
|
242 * Removes the node from it's parent. |
|
243 * |
|
244 * @example |
|
245 * node.remove(); |
|
246 * |
|
247 * @method remove |
|
248 * @return {tinymce.html.Node} Current node that got removed. |
|
249 */ |
|
250 remove: function() { |
|
251 var self = this, parent = self.parent, next = self.next, prev = self.prev; |
|
252 |
|
253 if (parent) { |
|
254 if (parent.firstChild === self) { |
|
255 parent.firstChild = next; |
|
256 |
|
257 if (next) { |
|
258 next.prev = null; |
|
259 } |
|
260 } else { |
|
261 prev.next = next; |
|
262 } |
|
263 |
|
264 if (parent.lastChild === self) { |
|
265 parent.lastChild = prev; |
|
266 |
|
267 if (prev) { |
|
268 prev.next = null; |
|
269 } |
|
270 } else { |
|
271 next.prev = prev; |
|
272 } |
|
273 |
|
274 self.parent = self.next = self.prev = null; |
|
275 } |
|
276 |
|
277 return self; |
|
278 }, |
|
279 |
|
280 /** |
|
281 * Appends a new node as a child of the current node. |
|
282 * |
|
283 * @example |
|
284 * node.append(someNode); |
|
285 * |
|
286 * @method append |
|
287 * @param {tinymce.html.Node} node Node to append as a child of the current one. |
|
288 * @return {tinymce.html.Node} The node that got appended. |
|
289 */ |
|
290 append: function(node) { |
|
291 var self = this, last; |
|
292 |
|
293 if (node.parent) { |
|
294 node.remove(); |
|
295 } |
|
296 |
|
297 last = self.lastChild; |
|
298 if (last) { |
|
299 last.next = node; |
|
300 node.prev = last; |
|
301 self.lastChild = node; |
|
302 } else { |
|
303 self.lastChild = self.firstChild = node; |
|
304 } |
|
305 |
|
306 node.parent = self; |
|
307 |
|
308 return node; |
|
309 }, |
|
310 |
|
311 /** |
|
312 * Inserts a node at a specific position as a child of the current node. |
|
313 * |
|
314 * @example |
|
315 * parentNode.insert(newChildNode, oldChildNode); |
|
316 * |
|
317 * @method insert |
|
318 * @param {tinymce.html.Node} node Node to insert as a child of the current node. |
|
319 * @param {tinymce.html.Node} ref_node Reference node to set node before/after. |
|
320 * @param {Boolean} before Optional state to insert the node before the reference node. |
|
321 * @return {tinymce.html.Node} The node that got inserted. |
|
322 */ |
|
323 insert: function(node, ref_node, before) { |
|
324 var parent; |
|
325 |
|
326 if (node.parent) { |
|
327 node.remove(); |
|
328 } |
|
329 |
|
330 parent = ref_node.parent || this; |
|
331 |
|
332 if (before) { |
|
333 if (ref_node === parent.firstChild) { |
|
334 parent.firstChild = node; |
|
335 } else { |
|
336 ref_node.prev.next = node; |
|
337 } |
|
338 |
|
339 node.prev = ref_node.prev; |
|
340 node.next = ref_node; |
|
341 ref_node.prev = node; |
|
342 } else { |
|
343 if (ref_node === parent.lastChild) { |
|
344 parent.lastChild = node; |
|
345 } else { |
|
346 ref_node.next.prev = node; |
|
347 } |
|
348 |
|
349 node.next = ref_node.next; |
|
350 node.prev = ref_node; |
|
351 ref_node.next = node; |
|
352 } |
|
353 |
|
354 node.parent = parent; |
|
355 |
|
356 return node; |
|
357 }, |
|
358 |
|
359 /** |
|
360 * Get all children by name. |
|
361 * |
|
362 * @method getAll |
|
363 * @param {String} name Name of the child nodes to collect. |
|
364 * @return {Array} Array with child nodes matchin the specified name. |
|
365 */ |
|
366 getAll: function(name) { |
|
367 var self = this, node, collection = []; |
|
368 |
|
369 for (node = self.firstChild; node; node = walk(node, self)) { |
|
370 if (node.name === name) { |
|
371 collection.push(node); |
|
372 } |
|
373 } |
|
374 |
|
375 return collection; |
|
376 }, |
|
377 |
|
378 /** |
|
379 * Removes all children of the current node. |
|
380 * |
|
381 * @method empty |
|
382 * @return {tinymce.html.Node} The current node that got cleared. |
|
383 */ |
|
384 empty: function() { |
|
385 var self = this, nodes, i, node; |
|
386 |
|
387 // Remove all children |
|
388 if (self.firstChild) { |
|
389 nodes = []; |
|
390 |
|
391 // Collect the children |
|
392 for (node = self.firstChild; node; node = walk(node, self)) { |
|
393 nodes.push(node); |
|
394 } |
|
395 |
|
396 // Remove the children |
|
397 i = nodes.length; |
|
398 while (i--) { |
|
399 node = nodes[i]; |
|
400 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; |
|
401 } |
|
402 } |
|
403 |
|
404 self.firstChild = self.lastChild = null; |
|
405 |
|
406 return self; |
|
407 }, |
|
408 |
|
409 /** |
|
410 * Returns true/false if the node is to be considered empty or not. |
|
411 * |
|
412 * @example |
|
413 * node.isEmpty({img: true}); |
|
414 * @method isEmpty |
|
415 * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. |
|
416 * @return {Boolean} true/false if the node is empty or not. |
|
417 */ |
|
418 isEmpty: function(elements) { |
|
419 var self = this, node = self.firstChild, i, name; |
|
420 |
|
421 if (node) { |
|
422 do { |
|
423 if (node.type === 1) { |
|
424 // Ignore bogus elements |
|
425 if (node.attributes.map['data-mce-bogus']) { |
|
426 continue; |
|
427 } |
|
428 |
|
429 // Keep empty elements like <img /> |
|
430 if (elements[node.name]) { |
|
431 return false; |
|
432 } |
|
433 |
|
434 // Keep bookmark nodes and name attribute like <a name="1"></a> |
|
435 i = node.attributes.length; |
|
436 while (i--) { |
|
437 name = node.attributes[i].name; |
|
438 if (name === "name" || name.indexOf('data-mce-bookmark') === 0) { |
|
439 return false; |
|
440 } |
|
441 } |
|
442 } |
|
443 |
|
444 // Keep comments |
|
445 if (node.type === 8) { |
|
446 return false; |
|
447 } |
|
448 |
|
449 // Keep non whitespace text nodes |
|
450 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) { |
|
451 return false; |
|
452 } |
|
453 } while ((node = walk(node, self))); |
|
454 } |
|
455 |
|
456 return true; |
|
457 }, |
|
458 |
|
459 /** |
|
460 * Walks to the next or previous node and returns that node or null if it wasn't found. |
|
461 * |
|
462 * @method walk |
|
463 * @param {Boolean} prev Optional previous node state defaults to false. |
|
464 * @return {tinymce.html.Node} Node that is next to or previous of the current node. |
|
465 */ |
|
466 walk: function(prev) { |
|
467 return walk(this, null, prev); |
|
468 } |
|
469 }; |
|
470 |
|
471 /** |
|
472 * Creates a node of a specific type. |
|
473 * |
|
474 * @static |
|
475 * @method create |
|
476 * @param {String} name Name of the node type to create for example "b" or "#text". |
|
477 * @param {Object} attrs Name/value collection of attributes that will be applied to elements. |
|
478 */ |
|
479 Node.create = function(name, attrs) { |
|
480 var node, attrName; |
|
481 |
|
482 // Create node |
|
483 node = new Node(name, typeLookup[name] || 1); |
|
484 |
|
485 // Add attributes if needed |
|
486 if (attrs) { |
|
487 for (attrName in attrs) { |
|
488 node.attr(attrName, attrs[attrName]); |
|
489 } |
|
490 } |
|
491 |
|
492 return node; |
|
493 }; |
|
494 |
|
495 return Node; |
|
496 }); |
|