|
1 /** |
|
2 * DOMUtils.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 * Utility class for various DOM manipulation and retrieval functions. |
|
13 * |
|
14 * @class tinymce.dom.DOMUtils |
|
15 * @example |
|
16 * // Add a class to an element by id in the page |
|
17 * tinymce.DOM.addClass('someid', 'someclass'); |
|
18 * |
|
19 * // Add a class to an element by id inside the editor |
|
20 * tinymce.activeEditor.dom.addClass('someid', 'someclass'); |
|
21 */ |
|
22 define("tinymce/dom/DOMUtils", [ |
|
23 "tinymce/dom/Sizzle", |
|
24 "tinymce/dom/DomQuery", |
|
25 "tinymce/html/Styles", |
|
26 "tinymce/dom/EventUtils", |
|
27 "tinymce/dom/TreeWalker", |
|
28 "tinymce/dom/Range", |
|
29 "tinymce/html/Entities", |
|
30 "tinymce/Env", |
|
31 "tinymce/util/Tools", |
|
32 "tinymce/dom/StyleSheetLoader" |
|
33 ], function(Sizzle, $, Styles, EventUtils, TreeWalker, Range, Entities, Env, Tools, StyleSheetLoader) { |
|
34 // Shorten names |
|
35 var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim; |
|
36 var isIE = Env.ie; |
|
37 var simpleSelectorRe = /^([a-z0-9],?)+$/i; |
|
38 var whiteSpaceRegExp = /^[ \t\r\n]*$/; |
|
39 |
|
40 function setupAttrHooks(domUtils, settings) { |
|
41 var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook; |
|
42 |
|
43 keepUrlHook = { |
|
44 set: function($elm, value, name) { |
|
45 if (settings.url_converter) { |
|
46 value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]); |
|
47 } |
|
48 |
|
49 $elm.attr('data-mce-' + name, value).attr(name, value); |
|
50 }, |
|
51 |
|
52 get: function($elm, name) { |
|
53 return $elm.attr('data-mce-' + name) || $elm.attr(name); |
|
54 } |
|
55 }; |
|
56 |
|
57 attrHooks = { |
|
58 style: { |
|
59 set: function($elm, value) { |
|
60 if (value !== null && typeof value === 'object') { |
|
61 $elm.css(value); |
|
62 return; |
|
63 } |
|
64 |
|
65 if (keepValues) { |
|
66 $elm.attr('data-mce-style', value); |
|
67 } |
|
68 |
|
69 $elm.attr('style', value); |
|
70 }, |
|
71 |
|
72 get: function($elm) { |
|
73 var value = $elm.attr('data-mce-style') || $elm.attr('style'); |
|
74 |
|
75 value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName); |
|
76 |
|
77 return value; |
|
78 } |
|
79 } |
|
80 }; |
|
81 |
|
82 if (keepValues) { |
|
83 attrHooks.href = attrHooks.src = keepUrlHook; |
|
84 } |
|
85 |
|
86 return attrHooks; |
|
87 } |
|
88 |
|
89 /** |
|
90 * Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class. |
|
91 * |
|
92 * @constructor |
|
93 * @method DOMUtils |
|
94 * @param {Document} d Document reference to bind the utility class to. |
|
95 * @param {settings} s Optional settings collection. |
|
96 */ |
|
97 function DOMUtils(doc, settings) { |
|
98 var self = this, blockElementsMap; |
|
99 |
|
100 self.doc = doc; |
|
101 self.win = window; |
|
102 self.files = {}; |
|
103 self.counter = 0; |
|
104 self.stdMode = !isIE || doc.documentMode >= 8; |
|
105 self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode; |
|
106 self.styleSheetLoader = new StyleSheetLoader(doc); |
|
107 self.boundEvents = []; |
|
108 self.settings = settings = settings || {}; |
|
109 self.schema = settings.schema; |
|
110 self.styles = new Styles({ |
|
111 url_converter: settings.url_converter, |
|
112 url_converter_scope: settings.url_converter_scope |
|
113 }, settings.schema); |
|
114 |
|
115 self.fixDoc(doc); |
|
116 self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event; |
|
117 self.attrHooks = setupAttrHooks(self, settings); |
|
118 blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {}; |
|
119 self.$ = $.overrideDefaults(function() { |
|
120 return { |
|
121 context: doc, |
|
122 element: self.getRoot() |
|
123 }; |
|
124 }); |
|
125 |
|
126 /** |
|
127 * Returns true/false if the specified element is a block element or not. |
|
128 * |
|
129 * @method isBlock |
|
130 * @param {Node/String} node Element/Node to check. |
|
131 * @return {Boolean} True/False state if the node is a block element or not. |
|
132 */ |
|
133 self.isBlock = function(node) { |
|
134 // Fix for #5446 |
|
135 if (!node) { |
|
136 return false; |
|
137 } |
|
138 |
|
139 // This function is called in module pattern style since it might be executed with the wrong this scope |
|
140 var type = node.nodeType; |
|
141 |
|
142 // If it's a node then check the type and use the nodeName |
|
143 if (type) { |
|
144 return !!(type === 1 && blockElementsMap[node.nodeName]); |
|
145 } |
|
146 |
|
147 return !!blockElementsMap[node]; |
|
148 }; |
|
149 } |
|
150 |
|
151 DOMUtils.prototype = { |
|
152 $$: function(elm) { |
|
153 if (typeof elm == 'string') { |
|
154 elm = this.get(elm); |
|
155 } |
|
156 |
|
157 return this.$(elm); |
|
158 }, |
|
159 |
|
160 root: null, |
|
161 |
|
162 fixDoc: function(doc) { |
|
163 var settings = this.settings, name; |
|
164 |
|
165 if (isIE && settings.schema) { |
|
166 // Add missing HTML 4/5 elements to IE |
|
167 ('abbr article aside audio canvas ' + |
|
168 'details figcaption figure footer ' + |
|
169 'header hgroup mark menu meter nav ' + |
|
170 'output progress section summary ' + |
|
171 'time video').replace(/\w+/g, function(name) { |
|
172 doc.createElement(name); |
|
173 }); |
|
174 |
|
175 // Create all custom elements |
|
176 for (name in settings.schema.getCustomElements()) { |
|
177 doc.createElement(name); |
|
178 } |
|
179 } |
|
180 }, |
|
181 |
|
182 clone: function(node, deep) { |
|
183 var self = this, clone, doc; |
|
184 |
|
185 // TODO: Add feature detection here in the future |
|
186 if (!isIE || node.nodeType !== 1 || deep) { |
|
187 return node.cloneNode(deep); |
|
188 } |
|
189 |
|
190 doc = self.doc; |
|
191 |
|
192 // Make a HTML5 safe shallow copy |
|
193 if (!deep) { |
|
194 clone = doc.createElement(node.nodeName); |
|
195 |
|
196 // Copy attribs |
|
197 each(self.getAttribs(node), function(attr) { |
|
198 self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); |
|
199 }); |
|
200 |
|
201 return clone; |
|
202 } |
|
203 |
|
204 return clone.firstChild; |
|
205 }, |
|
206 |
|
207 /** |
|
208 * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not |
|
209 * go above the point of this root node. |
|
210 * |
|
211 * @method getRoot |
|
212 * @return {Element} Root element for the utility class. |
|
213 */ |
|
214 getRoot: function() { |
|
215 var self = this; |
|
216 |
|
217 return self.settings.root_element || self.doc.body; |
|
218 }, |
|
219 |
|
220 /** |
|
221 * Returns the viewport of the window. |
|
222 * |
|
223 * @method getViewPort |
|
224 * @param {Window} win Optional window to get viewport of. |
|
225 * @return {Object} Viewport object with fields x, y, w and h. |
|
226 */ |
|
227 getViewPort: function(win) { |
|
228 var doc, rootElm; |
|
229 |
|
230 win = !win ? this.win : win; |
|
231 doc = win.document; |
|
232 rootElm = this.boxModel ? doc.documentElement : doc.body; |
|
233 |
|
234 // Returns viewport size excluding scrollbars |
|
235 return { |
|
236 x: win.pageXOffset || rootElm.scrollLeft, |
|
237 y: win.pageYOffset || rootElm.scrollTop, |
|
238 w: win.innerWidth || rootElm.clientWidth, |
|
239 h: win.innerHeight || rootElm.clientHeight |
|
240 }; |
|
241 }, |
|
242 |
|
243 /** |
|
244 * Returns the rectangle for a specific element. |
|
245 * |
|
246 * @method getRect |
|
247 * @param {Element/String} elm Element object or element ID to get rectangle from. |
|
248 * @return {object} Rectangle for specified element object with x, y, w, h fields. |
|
249 */ |
|
250 getRect: function(elm) { |
|
251 var self = this, pos, size; |
|
252 |
|
253 elm = self.get(elm); |
|
254 pos = self.getPos(elm); |
|
255 size = self.getSize(elm); |
|
256 |
|
257 return { |
|
258 x: pos.x, y: pos.y, |
|
259 w: size.w, h: size.h |
|
260 }; |
|
261 }, |
|
262 |
|
263 /** |
|
264 * Returns the size dimensions of the specified element. |
|
265 * |
|
266 * @method getSize |
|
267 * @param {Element/String} elm Element object or element ID to get rectangle from. |
|
268 * @return {object} Rectangle for specified element object with w, h fields. |
|
269 */ |
|
270 getSize: function(elm) { |
|
271 var self = this, w, h; |
|
272 |
|
273 elm = self.get(elm); |
|
274 w = self.getStyle(elm, 'width'); |
|
275 h = self.getStyle(elm, 'height'); |
|
276 |
|
277 // Non pixel value, then force offset/clientWidth |
|
278 if (w.indexOf('px') === -1) { |
|
279 w = 0; |
|
280 } |
|
281 |
|
282 // Non pixel value, then force offset/clientWidth |
|
283 if (h.indexOf('px') === -1) { |
|
284 h = 0; |
|
285 } |
|
286 |
|
287 return { |
|
288 w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth, |
|
289 h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight |
|
290 }; |
|
291 }, |
|
292 |
|
293 /** |
|
294 * Returns a node by the specified selector function. This function will |
|
295 * loop through all parent nodes and call the specified function for each node. |
|
296 * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end |
|
297 * and the node it found will be returned. |
|
298 * |
|
299 * @method getParent |
|
300 * @param {Node/String} node DOM node to search parents on or ID string. |
|
301 * @param {function} selector Selection function or CSS selector to execute on each node. |
|
302 * @param {Node} root Optional root element, never go below this point. |
|
303 * @return {Node} DOM Node or null if it wasn't found. |
|
304 */ |
|
305 getParent: function(node, selector, root) { |
|
306 return this.getParents(node, selector, root, false); |
|
307 }, |
|
308 |
|
309 /** |
|
310 * Returns a node list of all parents matching the specified selector function or pattern. |
|
311 * If the function then returns true indicating that it has found what it was looking for and that node will be collected. |
|
312 * |
|
313 * @method getParents |
|
314 * @param {Node/String} node DOM node to search parents on or ID string. |
|
315 * @param {function} selector Selection function to execute on each node or CSS pattern. |
|
316 * @param {Node} root Optional root element, never go below this point. |
|
317 * @return {Array} Array of nodes or null if it wasn't found. |
|
318 */ |
|
319 getParents: function(node, selector, root, collect) { |
|
320 var self = this, selectorVal, result = []; |
|
321 |
|
322 node = self.get(node); |
|
323 collect = collect === undefined; |
|
324 |
|
325 // Default root on inline mode |
|
326 root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null); |
|
327 |
|
328 // Wrap node name as func |
|
329 if (is(selector, 'string')) { |
|
330 selectorVal = selector; |
|
331 |
|
332 if (selector === '*') { |
|
333 selector = function(node) { |
|
334 return node.nodeType == 1; |
|
335 }; |
|
336 } else { |
|
337 selector = function(node) { |
|
338 return self.is(node, selectorVal); |
|
339 }; |
|
340 } |
|
341 } |
|
342 |
|
343 while (node) { |
|
344 if (node == root || !node.nodeType || node.nodeType === 9) { |
|
345 break; |
|
346 } |
|
347 |
|
348 if (!selector || selector(node)) { |
|
349 if (collect) { |
|
350 result.push(node); |
|
351 } else { |
|
352 return node; |
|
353 } |
|
354 } |
|
355 |
|
356 node = node.parentNode; |
|
357 } |
|
358 |
|
359 return collect ? result : null; |
|
360 }, |
|
361 |
|
362 /** |
|
363 * Returns the specified element by ID or the input element if it isn't a string. |
|
364 * |
|
365 * @method get |
|
366 * @param {String/Element} n Element id to look for or element to just pass though. |
|
367 * @return {Element} Element matching the specified id or null if it wasn't found. |
|
368 */ |
|
369 get: function(elm) { |
|
370 var name; |
|
371 |
|
372 if (elm && this.doc && typeof elm == 'string') { |
|
373 name = elm; |
|
374 elm = this.doc.getElementById(elm); |
|
375 |
|
376 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick |
|
377 if (elm && elm.id !== name) { |
|
378 return this.doc.getElementsByName(name)[1]; |
|
379 } |
|
380 } |
|
381 |
|
382 return elm; |
|
383 }, |
|
384 |
|
385 /** |
|
386 * Returns the next node that matches selector or function |
|
387 * |
|
388 * @method getNext |
|
389 * @param {Node} node Node to find siblings from. |
|
390 * @param {String/function} selector Selector CSS expression or function. |
|
391 * @return {Node} Next node item matching the selector or null if it wasn't found. |
|
392 */ |
|
393 getNext: function(node, selector) { |
|
394 return this._findSib(node, selector, 'nextSibling'); |
|
395 }, |
|
396 |
|
397 /** |
|
398 * Returns the previous node that matches selector or function |
|
399 * |
|
400 * @method getPrev |
|
401 * @param {Node} node Node to find siblings from. |
|
402 * @param {String/function} selector Selector CSS expression or function. |
|
403 * @return {Node} Previous node item matching the selector or null if it wasn't found. |
|
404 */ |
|
405 getPrev: function(node, selector) { |
|
406 return this._findSib(node, selector, 'previousSibling'); |
|
407 }, |
|
408 |
|
409 // #ifndef jquery |
|
410 |
|
411 /** |
|
412 * Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test". |
|
413 * This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough |
|
414 * on more complex patterns. |
|
415 * |
|
416 * @method select |
|
417 * @param {String} selector CSS level 3 pattern to select/find elements by. |
|
418 * @param {Object} scope Optional root element/scope element to search in. |
|
419 * @return {Array} Array with all matched elements. |
|
420 * @example |
|
421 * // Adds a class to all paragraphs in the currently active editor |
|
422 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); |
|
423 * |
|
424 * // Adds a class to all spans that have the test class in the currently active editor |
|
425 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass') |
|
426 */ |
|
427 select: function(selector, scope) { |
|
428 var self = this; |
|
429 |
|
430 /*eslint new-cap:0 */ |
|
431 return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []); |
|
432 }, |
|
433 |
|
434 /** |
|
435 * Returns true/false if the specified element matches the specified css pattern. |
|
436 * |
|
437 * @method is |
|
438 * @param {Node/NodeList} elm DOM node to match or an array of nodes to match. |
|
439 * @param {String} selector CSS pattern to match the element against. |
|
440 */ |
|
441 is: function(elm, selector) { |
|
442 var i; |
|
443 |
|
444 // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance |
|
445 if (elm.length === undefined) { |
|
446 // Simple all selector |
|
447 if (selector === '*') { |
|
448 return elm.nodeType == 1; |
|
449 } |
|
450 |
|
451 // Simple selector just elements |
|
452 if (simpleSelectorRe.test(selector)) { |
|
453 selector = selector.toLowerCase().split(/,/); |
|
454 elm = elm.nodeName.toLowerCase(); |
|
455 |
|
456 for (i = selector.length - 1; i >= 0; i--) { |
|
457 if (selector[i] == elm) { |
|
458 return true; |
|
459 } |
|
460 } |
|
461 |
|
462 return false; |
|
463 } |
|
464 } |
|
465 |
|
466 // Is non element |
|
467 if (elm.nodeType && elm.nodeType != 1) { |
|
468 return false; |
|
469 } |
|
470 |
|
471 var elms = elm.nodeType ? [elm] : elm; |
|
472 |
|
473 /*eslint new-cap:0 */ |
|
474 return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0; |
|
475 }, |
|
476 |
|
477 // #endif |
|
478 |
|
479 /** |
|
480 * Adds the specified element to another element or elements. |
|
481 * |
|
482 * @method add |
|
483 * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to. |
|
484 * @param {String/Element} name Name of new element to add or existing element to add. |
|
485 * @param {Object} attrs Optional object collection with arguments to add to the new element(s). |
|
486 * @param {String} html Optional inner HTML contents to add for each element. |
|
487 * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements |
|
488 * were passed in. |
|
489 * @example |
|
490 * // Adds a new paragraph to the end of the active editor |
|
491 * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content'); |
|
492 */ |
|
493 add: function(parentElm, name, attrs, html, create) { |
|
494 var self = this; |
|
495 |
|
496 return this.run(parentElm, function(parentElm) { |
|
497 var newElm; |
|
498 |
|
499 newElm = is(name, 'string') ? self.doc.createElement(name) : name; |
|
500 self.setAttribs(newElm, attrs); |
|
501 |
|
502 if (html) { |
|
503 if (html.nodeType) { |
|
504 newElm.appendChild(html); |
|
505 } else { |
|
506 self.setHTML(newElm, html); |
|
507 } |
|
508 } |
|
509 |
|
510 return !create ? parentElm.appendChild(newElm) : newElm; |
|
511 }); |
|
512 }, |
|
513 |
|
514 /** |
|
515 * Creates a new element. |
|
516 * |
|
517 * @method create |
|
518 * @param {String} name Name of new element. |
|
519 * @param {Object} attrs Optional object name/value collection with element attributes. |
|
520 * @param {String} html Optional HTML string to set as inner HTML of the element. |
|
521 * @return {Element} HTML DOM node element that got created. |
|
522 * @example |
|
523 * // Adds an element where the caret/selection is in the active editor |
|
524 * var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content'); |
|
525 * tinymce.activeEditor.selection.setNode(el); |
|
526 */ |
|
527 create: function(name, attrs, html) { |
|
528 return this.add(this.doc.createElement(name), name, attrs, html, 1); |
|
529 }, |
|
530 |
|
531 /** |
|
532 * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in. |
|
533 * |
|
534 * @method createHTML |
|
535 * @param {String} name Name of new element. |
|
536 * @param {Object} attrs Optional object name/value collection with element attributes. |
|
537 * @param {String} html Optional HTML string to set as inner HTML of the element. |
|
538 * @return {String} String with new HTML element, for example: <a href="#">test</a>. |
|
539 * @example |
|
540 * // Creates a html chunk and inserts it at the current selection/caret location |
|
541 * tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line')); |
|
542 */ |
|
543 createHTML: function(name, attrs, html) { |
|
544 var outHtml = '', key; |
|
545 |
|
546 outHtml += '<' + name; |
|
547 |
|
548 for (key in attrs) { |
|
549 if (attrs.hasOwnProperty(key) && attrs[key] !== null && typeof attrs[key] != 'undefined') { |
|
550 outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"'; |
|
551 } |
|
552 } |
|
553 |
|
554 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime |
|
555 if (typeof html != "undefined") { |
|
556 return outHtml + '>' + html + '</' + name + '>'; |
|
557 } |
|
558 |
|
559 return outHtml + ' />'; |
|
560 }, |
|
561 |
|
562 /** |
|
563 * Creates a document fragment out of the specified HTML string. |
|
564 * |
|
565 * @method createFragment |
|
566 * @param {String} html Html string to create fragment from. |
|
567 * @return {DocumentFragment} Document fragment node. |
|
568 */ |
|
569 createFragment: function(html) { |
|
570 var frag, node, doc = this.doc, container; |
|
571 |
|
572 container = doc.createElement("div"); |
|
573 frag = doc.createDocumentFragment(); |
|
574 |
|
575 if (html) { |
|
576 container.innerHTML = html; |
|
577 } |
|
578 |
|
579 while ((node = container.firstChild)) { |
|
580 frag.appendChild(node); |
|
581 } |
|
582 |
|
583 return frag; |
|
584 }, |
|
585 |
|
586 /** |
|
587 * Removes/deletes the specified element(s) from the DOM. |
|
588 * |
|
589 * @method remove |
|
590 * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids. |
|
591 * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be |
|
592 * placed at the location of the removed element. |
|
593 * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements |
|
594 * were passed in. |
|
595 * @example |
|
596 * // Removes all paragraphs in the active editor |
|
597 * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p')); |
|
598 * |
|
599 * // Removes an element by id in the document |
|
600 * tinymce.DOM.remove('mydiv'); |
|
601 */ |
|
602 remove: function(node, keepChildren) { |
|
603 node = this.$$(node); |
|
604 |
|
605 if (keepChildren) { |
|
606 node.each(function() { |
|
607 var child; |
|
608 |
|
609 while ((child = this.firstChild)) { |
|
610 if (child.nodeType == 3 && child.data.length === 0) { |
|
611 this.removeChild(child); |
|
612 } else { |
|
613 this.parentNode.insertBefore(child, this); |
|
614 } |
|
615 } |
|
616 }).remove(); |
|
617 } else { |
|
618 node.remove(); |
|
619 } |
|
620 |
|
621 return node.length > 1 ? node.toArray() : node[0]; |
|
622 }, |
|
623 |
|
624 /** |
|
625 * Sets the CSS style value on a HTML element. The name can be a camelcase string |
|
626 * or the CSS style name like background-color. |
|
627 * |
|
628 * @method setStyle |
|
629 * @param {String/Element/Array} n HTML element/Element ID or Array of elements/ids to set CSS style value on. |
|
630 * @param {String} na Name of the style value to set. |
|
631 * @param {String} v Value to set on the style. |
|
632 * @example |
|
633 * // Sets a style value on all paragraphs in the currently active editor |
|
634 * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red'); |
|
635 * |
|
636 * // Sets a style value to an element by id in the current document |
|
637 * tinymce.DOM.setStyle('mydiv', 'background-color', 'red'); |
|
638 */ |
|
639 setStyle: function(elm, name, value) { |
|
640 elm = this.$$(elm).css(name, value); |
|
641 |
|
642 if (this.settings.update_styles) { |
|
643 elm.attr('data-mce-style', null); |
|
644 } |
|
645 }, |
|
646 |
|
647 /** |
|
648 * Returns the current style or runtime/computed value of an element. |
|
649 * |
|
650 * @method getStyle |
|
651 * @param {String/Element} elm HTML element or element id string to get style from. |
|
652 * @param {String} name Style name to return. |
|
653 * @param {Boolean} computed Computed style. |
|
654 * @return {String} Current style or computed style value of an element. |
|
655 */ |
|
656 getStyle: function(elm, name, computed) { |
|
657 elm = this.$$(elm); |
|
658 |
|
659 if (computed) { |
|
660 return elm.css(name); |
|
661 } |
|
662 |
|
663 // Camelcase it, if needed |
|
664 name = name.replace(/-(\D)/g, function(a, b) { |
|
665 return b.toUpperCase(); |
|
666 }); |
|
667 |
|
668 if (name == 'float') { |
|
669 name = isIE ? 'styleFloat' : 'cssFloat'; |
|
670 } |
|
671 |
|
672 return elm[0] && elm[0].style ? elm[0].style[name] : undefined; |
|
673 }, |
|
674 |
|
675 /** |
|
676 * Sets multiple styles on the specified element(s). |
|
677 * |
|
678 * @method setStyles |
|
679 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set styles on. |
|
680 * @param {Object} o Name/Value collection of style items to add to the element(s). |
|
681 * @example |
|
682 * // Sets styles on all paragraphs in the currently active editor |
|
683 * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'}); |
|
684 * |
|
685 * // Sets styles to an element by id in the current document |
|
686 * tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'}); |
|
687 */ |
|
688 setStyles: function(elm, styles) { |
|
689 elm = this.$$(elm).css(styles); |
|
690 |
|
691 if (this.settings.update_styles) { |
|
692 elm.attr('data-mce-style', null); |
|
693 } |
|
694 }, |
|
695 |
|
696 /** |
|
697 * Removes all attributes from an element or elements. |
|
698 * |
|
699 * @method removeAllAttribs |
|
700 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from. |
|
701 */ |
|
702 removeAllAttribs: function(e) { |
|
703 return this.run(e, function(e) { |
|
704 var i, attrs = e.attributes; |
|
705 for (i = attrs.length - 1; i >= 0; i--) { |
|
706 e.removeAttributeNode(attrs.item(i)); |
|
707 } |
|
708 }); |
|
709 }, |
|
710 |
|
711 /** |
|
712 * Sets the specified attribute of an element or elements. |
|
713 * |
|
714 * @method setAttrib |
|
715 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set attribute on. |
|
716 * @param {String} n Name of attribute to set. |
|
717 * @param {String} v Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove the attribute instead. |
|
718 * @example |
|
719 * // Sets class attribute on all paragraphs in the active editor |
|
720 * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass'); |
|
721 * |
|
722 * // Sets class attribute on a specific element in the current page |
|
723 * tinymce.dom.setAttrib('mydiv', 'class', 'myclass'); |
|
724 */ |
|
725 setAttrib: function(elm, name, value) { |
|
726 var self = this, originalValue, hook, settings = self.settings; |
|
727 |
|
728 if (value === '') { |
|
729 value = null; |
|
730 } |
|
731 |
|
732 elm = self.$$(elm); |
|
733 originalValue = elm.attr(name); |
|
734 |
|
735 if (!elm.length) { |
|
736 return; |
|
737 } |
|
738 |
|
739 hook = self.attrHooks[name]; |
|
740 if (hook && hook.set) { |
|
741 hook.set(elm, value, name); |
|
742 } else { |
|
743 elm.attr(name, value); |
|
744 } |
|
745 |
|
746 if (originalValue != value && settings.onSetAttrib) { |
|
747 settings.onSetAttrib({ |
|
748 attrElm: elm, |
|
749 attrName: name, |
|
750 attrValue: value |
|
751 }); |
|
752 } |
|
753 }, |
|
754 |
|
755 /** |
|
756 * Sets two or more specified attributes of an element or elements. |
|
757 * |
|
758 * @method setAttribs |
|
759 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on. |
|
760 * @param {Object} attrs Name/Value collection of attribute items to add to the element(s). |
|
761 * @example |
|
762 * // Sets class and title attributes on all paragraphs in the active editor |
|
763 * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'}); |
|
764 * |
|
765 * // Sets class and title attributes on a specific element in the current page |
|
766 * tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'}); |
|
767 */ |
|
768 setAttribs: function(elm, attrs) { |
|
769 var self = this; |
|
770 |
|
771 self.$$(elm).each(function(i, node) { |
|
772 each(attrs, function(value, name) { |
|
773 self.setAttrib(node, name, value); |
|
774 }); |
|
775 }); |
|
776 }, |
|
777 |
|
778 /** |
|
779 * Returns the specified attribute by name. |
|
780 * |
|
781 * @method getAttrib |
|
782 * @param {String/Element} elm Element string id or DOM element to get attribute from. |
|
783 * @param {String} name Name of attribute to get. |
|
784 * @param {String} defaultVal Optional default value to return if the attribute didn't exist. |
|
785 * @return {String} Attribute value string, default value or null if the attribute wasn't found. |
|
786 */ |
|
787 getAttrib: function(elm, name, defaultVal) { |
|
788 var self = this, hook, value; |
|
789 |
|
790 elm = self.$$(elm); |
|
791 |
|
792 if (elm.length) { |
|
793 hook = self.attrHooks[name]; |
|
794 |
|
795 if (hook && hook.get) { |
|
796 value = hook.get(elm, name); |
|
797 } else { |
|
798 value = elm.attr(name); |
|
799 } |
|
800 } |
|
801 |
|
802 if (typeof value == 'undefined') { |
|
803 value = defaultVal || ''; |
|
804 } |
|
805 |
|
806 return value; |
|
807 }, |
|
808 |
|
809 /** |
|
810 * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields. |
|
811 * |
|
812 * @method getPos |
|
813 * @param {Element/String} elm HTML element or element id to get x, y position from. |
|
814 * @param {Element} rootElm Optional root element to stop calculations at. |
|
815 * @return {object} Absolute position of the specified element object with x, y fields. |
|
816 */ |
|
817 getPos: function(elm, rootElm) { |
|
818 var self = this, x = 0, y = 0, offsetParent, doc = self.doc, body = doc.body, pos; |
|
819 |
|
820 elm = self.get(elm); |
|
821 rootElm = rootElm || body; |
|
822 |
|
823 if (elm) { |
|
824 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes |
|
825 // Fallback to offsetParent calculations if the body isn't static better since it stops at the body root |
|
826 if (rootElm === body && elm.getBoundingClientRect && $(body).css('position') === 'static') { |
|
827 pos = elm.getBoundingClientRect(); |
|
828 rootElm = self.boxModel ? doc.documentElement : body; |
|
829 |
|
830 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit |
|
831 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position |
|
832 x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - rootElm.clientLeft; |
|
833 y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - rootElm.clientTop; |
|
834 |
|
835 return {x: x, y: y}; |
|
836 } |
|
837 |
|
838 offsetParent = elm; |
|
839 while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { |
|
840 x += offsetParent.offsetLeft || 0; |
|
841 y += offsetParent.offsetTop || 0; |
|
842 offsetParent = offsetParent.offsetParent; |
|
843 } |
|
844 |
|
845 offsetParent = elm.parentNode; |
|
846 while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { |
|
847 x -= offsetParent.scrollLeft || 0; |
|
848 y -= offsetParent.scrollTop || 0; |
|
849 offsetParent = offsetParent.parentNode; |
|
850 } |
|
851 } |
|
852 |
|
853 return {x: x, y: y}; |
|
854 }, |
|
855 |
|
856 /** |
|
857 * Parses the specified style value into an object collection. This parser will also |
|
858 * merge and remove any redundant items that browsers might have added. It will also convert non-hex |
|
859 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. |
|
860 * |
|
861 * @method parseStyle |
|
862 * @param {String} cssText Style value to parse, for example: border:1px solid red;. |
|
863 * @return {Object} Object representation of that style, for example: {border: '1px solid red'} |
|
864 */ |
|
865 parseStyle: function(cssText) { |
|
866 return this.styles.parse(cssText); |
|
867 }, |
|
868 |
|
869 /** |
|
870 * Serializes the specified style object into a string. |
|
871 * |
|
872 * @method serializeStyle |
|
873 * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'} |
|
874 * @param {String} name Optional element name. |
|
875 * @return {String} String representation of the style object, for example: border: 1px solid red. |
|
876 */ |
|
877 serializeStyle: function(styles, name) { |
|
878 return this.styles.serialize(styles, name); |
|
879 }, |
|
880 |
|
881 /** |
|
882 * Adds a style element at the top of the document with the specified cssText content. |
|
883 * |
|
884 * @method addStyle |
|
885 * @param {String} cssText CSS Text style to add to top of head of document. |
|
886 */ |
|
887 addStyle: function(cssText) { |
|
888 var self = this, doc = self.doc, head, styleElm; |
|
889 |
|
890 // Prevent inline from loading the same styles twice |
|
891 if (self !== DOMUtils.DOM && doc === document) { |
|
892 var addedStyles = DOMUtils.DOM.addedStyles; |
|
893 |
|
894 addedStyles = addedStyles || []; |
|
895 if (addedStyles[cssText]) { |
|
896 return; |
|
897 } |
|
898 |
|
899 addedStyles[cssText] = true; |
|
900 DOMUtils.DOM.addedStyles = addedStyles; |
|
901 } |
|
902 |
|
903 // Create style element if needed |
|
904 styleElm = doc.getElementById('mceDefaultStyles'); |
|
905 if (!styleElm) { |
|
906 styleElm = doc.createElement('style'); |
|
907 styleElm.id = 'mceDefaultStyles'; |
|
908 styleElm.type = 'text/css'; |
|
909 |
|
910 head = doc.getElementsByTagName('head')[0]; |
|
911 if (head.firstChild) { |
|
912 head.insertBefore(styleElm, head.firstChild); |
|
913 } else { |
|
914 head.appendChild(styleElm); |
|
915 } |
|
916 } |
|
917 |
|
918 // Append style data to old or new style element |
|
919 if (styleElm.styleSheet) { |
|
920 styleElm.styleSheet.cssText += cssText; |
|
921 } else { |
|
922 styleElm.appendChild(doc.createTextNode(cssText)); |
|
923 } |
|
924 }, |
|
925 |
|
926 /** |
|
927 * Imports/loads the specified CSS file into the document bound to the class. |
|
928 * |
|
929 * @method loadCSS |
|
930 * @param {String} u URL to CSS file to load. |
|
931 * @example |
|
932 * // Loads a CSS file dynamically into the current document |
|
933 * tinymce.DOM.loadCSS('somepath/some.css'); |
|
934 * |
|
935 * // Loads a CSS file into the currently active editor instance |
|
936 * tinymce.activeEditor.dom.loadCSS('somepath/some.css'); |
|
937 * |
|
938 * // Loads a CSS file into an editor instance by id |
|
939 * tinymce.get('someid').dom.loadCSS('somepath/some.css'); |
|
940 * |
|
941 * // Loads multiple CSS files into the current document |
|
942 * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css'); |
|
943 */ |
|
944 loadCSS: function(url) { |
|
945 var self = this, doc = self.doc, head; |
|
946 |
|
947 // Prevent inline from loading the same CSS file twice |
|
948 if (self !== DOMUtils.DOM && doc === document) { |
|
949 DOMUtils.DOM.loadCSS(url); |
|
950 return; |
|
951 } |
|
952 |
|
953 if (!url) { |
|
954 url = ''; |
|
955 } |
|
956 |
|
957 head = doc.getElementsByTagName('head')[0]; |
|
958 |
|
959 each(url.split(','), function(url) { |
|
960 var link; |
|
961 |
|
962 url = Tools._addCacheSuffix(url); |
|
963 |
|
964 if (self.files[url]) { |
|
965 return; |
|
966 } |
|
967 |
|
968 self.files[url] = true; |
|
969 link = self.create('link', {rel: 'stylesheet', href: url}); |
|
970 |
|
971 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug |
|
972 // This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading |
|
973 // It's ugly but it seems to work fine. |
|
974 if (isIE && doc.documentMode && doc.recalc) { |
|
975 link.onload = function() { |
|
976 if (doc.recalc) { |
|
977 doc.recalc(); |
|
978 } |
|
979 |
|
980 link.onload = null; |
|
981 }; |
|
982 } |
|
983 |
|
984 head.appendChild(link); |
|
985 }); |
|
986 }, |
|
987 |
|
988 /** |
|
989 * Adds a class to the specified element or elements. |
|
990 * |
|
991 * @method addClass |
|
992 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. |
|
993 * @param {String} cls Class name to add to each element. |
|
994 * @return {String/Array} String with new class value or array with new class values for all elements. |
|
995 * @example |
|
996 * // Adds a class to all paragraphs in the active editor |
|
997 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass'); |
|
998 * |
|
999 * // Adds a class to a specific element in the current page |
|
1000 * tinymce.DOM.addClass('mydiv', 'myclass'); |
|
1001 */ |
|
1002 addClass: function(elm, cls) { |
|
1003 this.$$(elm).addClass(cls); |
|
1004 }, |
|
1005 |
|
1006 /** |
|
1007 * Removes a class from the specified element or elements. |
|
1008 * |
|
1009 * @method removeClass |
|
1010 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. |
|
1011 * @param {String} cls Class name to remove from each element. |
|
1012 * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements |
|
1013 * were passed in. |
|
1014 * @example |
|
1015 * // Removes a class from all paragraphs in the active editor |
|
1016 * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass'); |
|
1017 * |
|
1018 * // Removes a class from a specific element in the current page |
|
1019 * tinymce.DOM.removeClass('mydiv', 'myclass'); |
|
1020 */ |
|
1021 removeClass: function(elm, cls) { |
|
1022 this.toggleClass(elm, cls, false); |
|
1023 }, |
|
1024 |
|
1025 /** |
|
1026 * Returns true if the specified element has the specified class. |
|
1027 * |
|
1028 * @method hasClass |
|
1029 * @param {String/Element} n HTML element or element id string to check CSS class on. |
|
1030 * @param {String} c CSS class to check for. |
|
1031 * @return {Boolean} true/false if the specified element has the specified class. |
|
1032 */ |
|
1033 hasClass: function(elm, cls) { |
|
1034 return this.$$(elm).hasClass(cls); |
|
1035 }, |
|
1036 |
|
1037 /** |
|
1038 * Toggles the specified class on/off. |
|
1039 * |
|
1040 * @method toggleClass |
|
1041 * @param {Element} elm Element to toggle class on. |
|
1042 * @param {[type]} cls Class to toggle on/off. |
|
1043 * @param {[type]} state Optional state to set. |
|
1044 */ |
|
1045 toggleClass: function(elm, cls, state) { |
|
1046 this.$$(elm).toggleClass(cls, state).each(function() { |
|
1047 if (this.className === '') { |
|
1048 $(this).attr('class', null); |
|
1049 } |
|
1050 }); |
|
1051 }, |
|
1052 |
|
1053 /** |
|
1054 * Shows the specified element(s) by ID by setting the "display" style. |
|
1055 * |
|
1056 * @method show |
|
1057 * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show. |
|
1058 */ |
|
1059 show: function(elm) { |
|
1060 this.$$(elm).show(); |
|
1061 }, |
|
1062 |
|
1063 /** |
|
1064 * Hides the specified element(s) by ID by setting the "display" style. |
|
1065 * |
|
1066 * @method hide |
|
1067 * @param {String/Element/Array} e ID of DOM element or DOM element or array with elements or IDs to hide. |
|
1068 * @example |
|
1069 * // Hides an element by id in the document |
|
1070 * tinymce.DOM.hide('myid'); |
|
1071 */ |
|
1072 hide: function(elm) { |
|
1073 this.$$(elm).hide(); |
|
1074 }, |
|
1075 |
|
1076 /** |
|
1077 * Returns true/false if the element is hidden or not by checking the "display" style. |
|
1078 * |
|
1079 * @method isHidden |
|
1080 * @param {String/Element} e Id or element to check display state on. |
|
1081 * @return {Boolean} true/false if the element is hidden or not. |
|
1082 */ |
|
1083 isHidden: function(elm) { |
|
1084 return this.$$(elm).css('display') == 'none'; |
|
1085 }, |
|
1086 |
|
1087 /** |
|
1088 * Returns a unique id. This can be useful when generating elements on the fly. |
|
1089 * This method will not check if the element already exists. |
|
1090 * |
|
1091 * @method uniqueId |
|
1092 * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_". |
|
1093 * @return {String} Unique id. |
|
1094 */ |
|
1095 uniqueId: function(prefix) { |
|
1096 return (!prefix ? 'mce_' : prefix) + (this.counter++); |
|
1097 }, |
|
1098 |
|
1099 /** |
|
1100 * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means |
|
1101 * URLs will get converted, hex color values fixed etc. Check processHTML for details. |
|
1102 * |
|
1103 * @method setHTML |
|
1104 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of. |
|
1105 * @param {String} h HTML content to set as inner HTML of the element. |
|
1106 * @example |
|
1107 * // Sets the inner HTML of all paragraphs in the active editor |
|
1108 * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html'); |
|
1109 * |
|
1110 * // Sets the inner HTML of an element by id in the document |
|
1111 * tinymce.DOM.setHTML('mydiv', 'some inner html'); |
|
1112 */ |
|
1113 setHTML: function(elm, html) { |
|
1114 elm = this.$$(elm); |
|
1115 |
|
1116 if (isIE) { |
|
1117 elm.each(function(i, target) { |
|
1118 if (target.canHaveHTML === false) { |
|
1119 return; |
|
1120 } |
|
1121 |
|
1122 // Remove all child nodes, IE keeps empty text nodes in DOM |
|
1123 while (target.firstChild) { |
|
1124 target.removeChild(target.firstChild); |
|
1125 } |
|
1126 |
|
1127 try { |
|
1128 // IE will remove comments from the beginning |
|
1129 // unless you padd the contents with something |
|
1130 target.innerHTML = '<br>' + html; |
|
1131 target.removeChild(target.firstChild); |
|
1132 } catch (ex) { |
|
1133 // IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p |
|
1134 $('<div>').html('<br>' + html).contents().slice(1).appendTo(target); |
|
1135 } |
|
1136 |
|
1137 return html; |
|
1138 }); |
|
1139 } else { |
|
1140 elm.html(html); |
|
1141 } |
|
1142 }, |
|
1143 |
|
1144 /** |
|
1145 * Returns the outer HTML of an element. |
|
1146 * |
|
1147 * @method getOuterHTML |
|
1148 * @param {String/Element} elm Element ID or element object to get outer HTML from. |
|
1149 * @return {String} Outer HTML string. |
|
1150 * @example |
|
1151 * tinymce.DOM.getOuterHTML(editorElement); |
|
1152 * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody()); |
|
1153 */ |
|
1154 getOuterHTML: function(elm) { |
|
1155 elm = this.get(elm); |
|
1156 |
|
1157 // Older FF doesn't have outerHTML 3.6 is still used by some orgaizations |
|
1158 return elm.nodeType == 1 && "outerHTML" in elm ? elm.outerHTML : $('<div>').append($(elm).clone()).html(); |
|
1159 }, |
|
1160 |
|
1161 /** |
|
1162 * Sets the specified outer HTML on an element or elements. |
|
1163 * |
|
1164 * @method setOuterHTML |
|
1165 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on. |
|
1166 * @param {Object} html HTML code to set as outer value for the element. |
|
1167 * @param {Document} doc Optional document scope to use in this process - defaults to the document of the DOM class. |
|
1168 * @example |
|
1169 * // Sets the outer HTML of all paragraphs in the active editor |
|
1170 * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>'); |
|
1171 * |
|
1172 * // Sets the outer HTML of an element by id in the document |
|
1173 * tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>'); |
|
1174 */ |
|
1175 setOuterHTML: function(elm, html) { |
|
1176 var self = this; |
|
1177 |
|
1178 self.$$(elm).each(function() { |
|
1179 try { |
|
1180 // Older FF doesn't have outerHTML 3.6 is still used by some orgaizations |
|
1181 if ("outerHTML" in this) { |
|
1182 this.outerHTML = html; |
|
1183 return; |
|
1184 } |
|
1185 } catch (ex) { |
|
1186 // Ignore |
|
1187 } |
|
1188 |
|
1189 // OuterHTML for IE it sometimes produces an "unknown runtime error" |
|
1190 self.remove($(this).html(html), true); |
|
1191 }); |
|
1192 }, |
|
1193 |
|
1194 /** |
|
1195 * Entity decodes a string. This method decodes any HTML entities, such as å. |
|
1196 * |
|
1197 * @method decode |
|
1198 * @param {String} s String to decode entities on. |
|
1199 * @return {String} Entity decoded string. |
|
1200 */ |
|
1201 decode: Entities.decode, |
|
1202 |
|
1203 /** |
|
1204 * Entity encodes a string. This method encodes the most common entities, such as <>"&. |
|
1205 * |
|
1206 * @method encode |
|
1207 * @param {String} text String to encode with entities. |
|
1208 * @return {String} Entity encoded string. |
|
1209 */ |
|
1210 encode: Entities.encodeAllRaw, |
|
1211 |
|
1212 /** |
|
1213 * Inserts an element after the reference element. |
|
1214 * |
|
1215 * @method insertAfter |
|
1216 * @param {Element} node Element to insert after the reference. |
|
1217 * @param {Element/String/Array} reference_node Reference element, element id or array of elements to insert after. |
|
1218 * @return {Element/Array} Element that got added or an array with elements. |
|
1219 */ |
|
1220 insertAfter: function(node, referenceNode) { |
|
1221 referenceNode = this.get(referenceNode); |
|
1222 |
|
1223 return this.run(node, function(node) { |
|
1224 var parent, nextSibling; |
|
1225 |
|
1226 parent = referenceNode.parentNode; |
|
1227 nextSibling = referenceNode.nextSibling; |
|
1228 |
|
1229 if (nextSibling) { |
|
1230 parent.insertBefore(node, nextSibling); |
|
1231 } else { |
|
1232 parent.appendChild(node); |
|
1233 } |
|
1234 |
|
1235 return node; |
|
1236 }); |
|
1237 }, |
|
1238 |
|
1239 /** |
|
1240 * Replaces the specified element or elements with the new element specified. The new element will |
|
1241 * be cloned if multiple input elements are passed in. |
|
1242 * |
|
1243 * @method replace |
|
1244 * @param {Element} newElm New element to replace old ones with. |
|
1245 * @param {Element/String/Array} oldELm Element DOM node, element id or array of elements or ids to replace. |
|
1246 * @param {Boolean} k Optional keep children state, if set to true child nodes from the old object will be added to new ones. |
|
1247 */ |
|
1248 replace: function(newElm, oldElm, keepChildren) { |
|
1249 var self = this; |
|
1250 |
|
1251 return self.run(oldElm, function(oldElm) { |
|
1252 if (is(oldElm, 'array')) { |
|
1253 newElm = newElm.cloneNode(true); |
|
1254 } |
|
1255 |
|
1256 if (keepChildren) { |
|
1257 each(grep(oldElm.childNodes), function(node) { |
|
1258 newElm.appendChild(node); |
|
1259 }); |
|
1260 } |
|
1261 |
|
1262 return oldElm.parentNode.replaceChild(newElm, oldElm); |
|
1263 }); |
|
1264 }, |
|
1265 |
|
1266 /** |
|
1267 * Renames the specified element and keeps its attributes and children. |
|
1268 * |
|
1269 * @method rename |
|
1270 * @param {Element} elm Element to rename. |
|
1271 * @param {String} name Name of the new element. |
|
1272 * @return {Element} New element or the old element if it needed renaming. |
|
1273 */ |
|
1274 rename: function(elm, name) { |
|
1275 var self = this, newElm; |
|
1276 |
|
1277 if (elm.nodeName != name.toUpperCase()) { |
|
1278 // Rename block element |
|
1279 newElm = self.create(name); |
|
1280 |
|
1281 // Copy attribs to new block |
|
1282 each(self.getAttribs(elm), function(attrNode) { |
|
1283 self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName)); |
|
1284 }); |
|
1285 |
|
1286 // Replace block |
|
1287 self.replace(newElm, elm, 1); |
|
1288 } |
|
1289 |
|
1290 return newElm || elm; |
|
1291 }, |
|
1292 |
|
1293 /** |
|
1294 * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic. |
|
1295 * |
|
1296 * @method findCommonAncestor |
|
1297 * @param {Element} a Element to find common ancestor of. |
|
1298 * @param {Element} b Element to find common ancestor of. |
|
1299 * @return {Element} Common ancestor element of the two input elements. |
|
1300 */ |
|
1301 findCommonAncestor: function(a, b) { |
|
1302 var ps = a, pe; |
|
1303 |
|
1304 while (ps) { |
|
1305 pe = b; |
|
1306 |
|
1307 while (pe && ps != pe) { |
|
1308 pe = pe.parentNode; |
|
1309 } |
|
1310 |
|
1311 if (ps == pe) { |
|
1312 break; |
|
1313 } |
|
1314 |
|
1315 ps = ps.parentNode; |
|
1316 } |
|
1317 |
|
1318 if (!ps && a.ownerDocument) { |
|
1319 return a.ownerDocument.documentElement; |
|
1320 } |
|
1321 |
|
1322 return ps; |
|
1323 }, |
|
1324 |
|
1325 /** |
|
1326 * Parses the specified RGB color value and returns a hex version of that color. |
|
1327 * |
|
1328 * @method toHex |
|
1329 * @param {String} rgbVal RGB string value like rgb(1,2,3) |
|
1330 * @return {String} Hex version of that RGB value like #FF00FF. |
|
1331 */ |
|
1332 toHex: function(rgbVal) { |
|
1333 return this.styles.toHex(Tools.trim(rgbVal)); |
|
1334 }, |
|
1335 |
|
1336 /** |
|
1337 * Executes the specified function on the element by id or dom element node or array of elements/id. |
|
1338 * |
|
1339 * @method run |
|
1340 * @param {String/Element/Array} Element ID or DOM element object or array with ids or elements. |
|
1341 * @param {function} f Function to execute for each item. |
|
1342 * @param {Object} s Optional scope to execute the function in. |
|
1343 * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in. |
|
1344 */ |
|
1345 run: function(elm, func, scope) { |
|
1346 var self = this, result; |
|
1347 |
|
1348 if (typeof elm === 'string') { |
|
1349 elm = self.get(elm); |
|
1350 } |
|
1351 |
|
1352 if (!elm) { |
|
1353 return false; |
|
1354 } |
|
1355 |
|
1356 scope = scope || this; |
|
1357 if (!elm.nodeType && (elm.length || elm.length === 0)) { |
|
1358 result = []; |
|
1359 |
|
1360 each(elm, function(elm, i) { |
|
1361 if (elm) { |
|
1362 if (typeof elm == 'string') { |
|
1363 elm = self.get(elm); |
|
1364 } |
|
1365 |
|
1366 result.push(func.call(scope, elm, i)); |
|
1367 } |
|
1368 }); |
|
1369 |
|
1370 return result; |
|
1371 } |
|
1372 |
|
1373 return func.call(scope, elm); |
|
1374 }, |
|
1375 |
|
1376 /** |
|
1377 * Returns a NodeList with attributes for the element. |
|
1378 * |
|
1379 * @method getAttribs |
|
1380 * @param {HTMLElement/string} elm Element node or string id to get attributes from. |
|
1381 * @return {NodeList} NodeList with attributes. |
|
1382 */ |
|
1383 getAttribs: function(elm) { |
|
1384 var attrs; |
|
1385 |
|
1386 elm = this.get(elm); |
|
1387 |
|
1388 if (!elm) { |
|
1389 return []; |
|
1390 } |
|
1391 |
|
1392 if (isIE) { |
|
1393 attrs = []; |
|
1394 |
|
1395 // Object will throw exception in IE |
|
1396 if (elm.nodeName == 'OBJECT') { |
|
1397 return elm.attributes; |
|
1398 } |
|
1399 |
|
1400 // IE doesn't keep the selected attribute if you clone option elements |
|
1401 if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) { |
|
1402 attrs.push({specified: 1, nodeName: 'selected'}); |
|
1403 } |
|
1404 |
|
1405 // It's crazy that this is faster in IE but it's because it returns all attributes all the time |
|
1406 var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi; |
|
1407 elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function(a) { |
|
1408 attrs.push({specified: 1, nodeName: a}); |
|
1409 }); |
|
1410 |
|
1411 return attrs; |
|
1412 } |
|
1413 |
|
1414 return elm.attributes; |
|
1415 }, |
|
1416 |
|
1417 /** |
|
1418 * Returns true/false if the specified node is to be considered empty or not. |
|
1419 * |
|
1420 * @example |
|
1421 * tinymce.DOM.isEmpty(node, {img: true}); |
|
1422 * @method isEmpty |
|
1423 * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements. |
|
1424 * @return {Boolean} true/false if the node is empty or not. |
|
1425 */ |
|
1426 isEmpty: function(node, elements) { |
|
1427 var self = this, i, attributes, type, walker, name, brCount = 0; |
|
1428 |
|
1429 node = node.firstChild; |
|
1430 if (node) { |
|
1431 walker = new TreeWalker(node, node.parentNode); |
|
1432 elements = elements || (self.schema ? self.schema.getNonEmptyElements() : null); |
|
1433 |
|
1434 do { |
|
1435 type = node.nodeType; |
|
1436 |
|
1437 if (type === 1) { |
|
1438 // Ignore bogus elements |
|
1439 if (node.getAttribute('data-mce-bogus')) { |
|
1440 continue; |
|
1441 } |
|
1442 |
|
1443 // Keep empty elements like <img /> |
|
1444 name = node.nodeName.toLowerCase(); |
|
1445 if (elements && elements[name]) { |
|
1446 // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p> |
|
1447 if (name === 'br') { |
|
1448 brCount++; |
|
1449 continue; |
|
1450 } |
|
1451 |
|
1452 return false; |
|
1453 } |
|
1454 |
|
1455 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a> |
|
1456 attributes = self.getAttribs(node); |
|
1457 i = attributes.length; |
|
1458 while (i--) { |
|
1459 name = attributes[i].nodeName; |
|
1460 if (name === "name" || name === 'data-mce-bookmark') { |
|
1461 return false; |
|
1462 } |
|
1463 } |
|
1464 } |
|
1465 |
|
1466 // Keep comment nodes |
|
1467 if (type == 8) { |
|
1468 return false; |
|
1469 } |
|
1470 |
|
1471 // Keep non whitespace text nodes |
|
1472 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) { |
|
1473 return false; |
|
1474 } |
|
1475 } while ((node = walker.next())); |
|
1476 } |
|
1477 |
|
1478 return brCount <= 1; |
|
1479 }, |
|
1480 |
|
1481 /** |
|
1482 * Creates a new DOM Range object. This will use the native DOM Range API if it's |
|
1483 * available. If it's not, it will fall back to the custom TinyMCE implementation. |
|
1484 * |
|
1485 * @method createRng |
|
1486 * @return {DOMRange} DOM Range object. |
|
1487 * @example |
|
1488 * var rng = tinymce.DOM.createRng(); |
|
1489 * alert(rng.startContainer + "," + rng.startOffset); |
|
1490 */ |
|
1491 createRng: function() { |
|
1492 var doc = this.doc; |
|
1493 |
|
1494 return doc.createRange ? doc.createRange() : new Range(this); |
|
1495 }, |
|
1496 |
|
1497 /** |
|
1498 * Returns the index of the specified node within its parent. |
|
1499 * |
|
1500 * @method nodeIndex |
|
1501 * @param {Node} node Node to look for. |
|
1502 * @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization. |
|
1503 * @return {Number} Index of the specified node. |
|
1504 */ |
|
1505 nodeIndex: function(node, normalized) { |
|
1506 var idx = 0, lastNodeType, nodeType; |
|
1507 |
|
1508 if (node) { |
|
1509 for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) { |
|
1510 nodeType = node.nodeType; |
|
1511 |
|
1512 // Normalize text nodes |
|
1513 if (normalized && nodeType == 3) { |
|
1514 if (nodeType == lastNodeType || !node.nodeValue.length) { |
|
1515 continue; |
|
1516 } |
|
1517 } |
|
1518 idx++; |
|
1519 lastNodeType = nodeType; |
|
1520 } |
|
1521 } |
|
1522 |
|
1523 return idx; |
|
1524 }, |
|
1525 |
|
1526 /** |
|
1527 * Splits an element into two new elements and places the specified split |
|
1528 * element or elements between the new ones. For example splitting the paragraph at the bold element in |
|
1529 * this example <p>abc<b>abc</b>123</p> would produce <p>abc</p><b>abc</b><p>123</p>. |
|
1530 * |
|
1531 * @method split |
|
1532 * @param {Element} parentElm Parent element to split. |
|
1533 * @param {Element} splitElm Element to split at. |
|
1534 * @param {Element} replacementElm Optional replacement element to replace the split element with. |
|
1535 * @return {Element} Returns the split element or the replacement element if that is specified. |
|
1536 */ |
|
1537 split: function(parentElm, splitElm, replacementElm) { |
|
1538 var self = this, r = self.createRng(), bef, aft, pa; |
|
1539 |
|
1540 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense |
|
1541 // but we don't want that in our code since it serves no purpose for the end user |
|
1542 // For example splitting this html at the bold element: |
|
1543 // <p>text 1<span><b>CHOP</b></span>text 2</p> |
|
1544 // would produce: |
|
1545 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p> |
|
1546 // this function will then trim off empty edges and produce: |
|
1547 // <p>text 1</p><b>CHOP</b><p>text 2</p> |
|
1548 function trimNode(node) { |
|
1549 var i, children = node.childNodes, type = node.nodeType; |
|
1550 |
|
1551 function surroundedBySpans(node) { |
|
1552 var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; |
|
1553 var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; |
|
1554 return previousIsSpan && nextIsSpan; |
|
1555 } |
|
1556 |
|
1557 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') { |
|
1558 return; |
|
1559 } |
|
1560 |
|
1561 for (i = children.length - 1; i >= 0; i--) { |
|
1562 trimNode(children[i]); |
|
1563 } |
|
1564 |
|
1565 if (type != 9) { |
|
1566 // Keep non whitespace text nodes |
|
1567 if (type == 3 && node.nodeValue.length > 0) { |
|
1568 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>" |
|
1569 // Also keep text nodes with only spaces if surrounded by spans. |
|
1570 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b |
|
1571 var trimmedLength = trim(node.nodeValue).length; |
|
1572 if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) { |
|
1573 return; |
|
1574 } |
|
1575 } else if (type == 1) { |
|
1576 // If the only child is a bookmark then move it up |
|
1577 children = node.childNodes; |
|
1578 |
|
1579 // TODO fix this complex if |
|
1580 if (children.length == 1 && children[0] && children[0].nodeType == 1 && |
|
1581 children[0].getAttribute('data-mce-type') == 'bookmark') { |
|
1582 node.parentNode.insertBefore(children[0], node); |
|
1583 } |
|
1584 |
|
1585 // Keep non empty elements or img, hr etc |
|
1586 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) { |
|
1587 return; |
|
1588 } |
|
1589 } |
|
1590 |
|
1591 self.remove(node); |
|
1592 } |
|
1593 |
|
1594 return node; |
|
1595 } |
|
1596 |
|
1597 if (parentElm && splitElm) { |
|
1598 // Get before chunk |
|
1599 r.setStart(parentElm.parentNode, self.nodeIndex(parentElm)); |
|
1600 r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm)); |
|
1601 bef = r.extractContents(); |
|
1602 |
|
1603 // Get after chunk |
|
1604 r = self.createRng(); |
|
1605 r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1); |
|
1606 r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1); |
|
1607 aft = r.extractContents(); |
|
1608 |
|
1609 // Insert before chunk |
|
1610 pa = parentElm.parentNode; |
|
1611 pa.insertBefore(trimNode(bef), parentElm); |
|
1612 |
|
1613 // Insert middle chunk |
|
1614 if (replacementElm) { |
|
1615 pa.replaceChild(replacementElm, splitElm); |
|
1616 } else { |
|
1617 pa.insertBefore(splitElm, parentElm); |
|
1618 } |
|
1619 |
|
1620 // Insert after chunk |
|
1621 pa.insertBefore(trimNode(aft), parentElm); |
|
1622 self.remove(parentElm); |
|
1623 |
|
1624 return replacementElm || splitElm; |
|
1625 } |
|
1626 }, |
|
1627 |
|
1628 /** |
|
1629 * Adds an event handler to the specified object. |
|
1630 * |
|
1631 * @method bind |
|
1632 * @param {Element/Document/Window/Array} target Target element to bind events to. |
|
1633 * handler to or an array of elements/ids/documents. |
|
1634 * @param {String} name Name of event handler to add, for example: click. |
|
1635 * @param {function} func Function to execute when the event occurs. |
|
1636 * @param {Object} scope Optional scope to execute the function in. |
|
1637 * @return {function} Function callback handler the same as the one passed in. |
|
1638 */ |
|
1639 bind: function(target, name, func, scope) { |
|
1640 var self = this; |
|
1641 |
|
1642 if (Tools.isArray(target)) { |
|
1643 var i = target.length; |
|
1644 |
|
1645 while (i--) { |
|
1646 target[i] = self.bind(target[i], name, func, scope); |
|
1647 } |
|
1648 |
|
1649 return target; |
|
1650 } |
|
1651 |
|
1652 // Collect all window/document events bound by editor instance |
|
1653 if (self.settings.collect && (target === self.doc || target === self.win)) { |
|
1654 self.boundEvents.push([target, name, func, scope]); |
|
1655 } |
|
1656 |
|
1657 return self.events.bind(target, name, func, scope || self); |
|
1658 }, |
|
1659 |
|
1660 /** |
|
1661 * Removes the specified event handler by name and function from an element or collection of elements. |
|
1662 * |
|
1663 * @method unbind |
|
1664 * @param {Element/Document/Window/Array} target Target element to unbind events on. |
|
1665 * @param {String} name Event handler name, for example: "click" |
|
1666 * @param {function} func Function to remove. |
|
1667 * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements |
|
1668 * were passed in. |
|
1669 */ |
|
1670 unbind: function(target, name, func) { |
|
1671 var self = this, i; |
|
1672 |
|
1673 if (Tools.isArray(target)) { |
|
1674 i = target.length; |
|
1675 |
|
1676 while (i--) { |
|
1677 target[i] = self.unbind(target[i], name, func); |
|
1678 } |
|
1679 |
|
1680 return target; |
|
1681 } |
|
1682 |
|
1683 // Remove any bound events matching the input |
|
1684 if (self.boundEvents && (target === self.doc || target === self.win)) { |
|
1685 i = self.boundEvents.length; |
|
1686 |
|
1687 while (i--) { |
|
1688 var item = self.boundEvents[i]; |
|
1689 |
|
1690 if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) { |
|
1691 this.events.unbind(item[0], item[1], item[2]); |
|
1692 } |
|
1693 } |
|
1694 } |
|
1695 |
|
1696 return this.events.unbind(target, name, func); |
|
1697 }, |
|
1698 |
|
1699 /** |
|
1700 * Fires the specified event name with object on target. |
|
1701 * |
|
1702 * @method fire |
|
1703 * @param {Node/Document/Window} target Target element or object to fire event on. |
|
1704 * @param {String} name Name of the event to fire. |
|
1705 * @param {Object} evt Event object to send. |
|
1706 * @return {Event} Event object. |
|
1707 */ |
|
1708 fire: function(target, name, evt) { |
|
1709 return this.events.fire(target, name, evt); |
|
1710 }, |
|
1711 |
|
1712 // Returns the content editable state of a node |
|
1713 getContentEditable: function(node) { |
|
1714 var contentEditable; |
|
1715 |
|
1716 // Check type |
|
1717 if (!node || node.nodeType != 1) { |
|
1718 return null; |
|
1719 } |
|
1720 |
|
1721 // Check for fake content editable |
|
1722 contentEditable = node.getAttribute("data-mce-contenteditable"); |
|
1723 if (contentEditable && contentEditable !== "inherit") { |
|
1724 return contentEditable; |
|
1725 } |
|
1726 |
|
1727 // Check for real content editable |
|
1728 return node.contentEditable !== "inherit" ? node.contentEditable : null; |
|
1729 }, |
|
1730 |
|
1731 getContentEditableParent: function(node) { |
|
1732 var root = this.getRoot(), state = null; |
|
1733 |
|
1734 for (; node && node !== root; node = node.parentNode) { |
|
1735 state = this.getContentEditable(node); |
|
1736 |
|
1737 if (state !== null) { |
|
1738 break; |
|
1739 } |
|
1740 } |
|
1741 |
|
1742 return state; |
|
1743 }, |
|
1744 |
|
1745 /** |
|
1746 * Destroys all internal references to the DOM to solve IE leak issues. |
|
1747 * |
|
1748 * @method destroy |
|
1749 */ |
|
1750 destroy: function() { |
|
1751 var self = this; |
|
1752 |
|
1753 // Unbind all events bound to window/document by editor instance |
|
1754 if (self.boundEvents) { |
|
1755 var i = self.boundEvents.length; |
|
1756 |
|
1757 while (i--) { |
|
1758 var item = self.boundEvents[i]; |
|
1759 this.events.unbind(item[0], item[1], item[2]); |
|
1760 } |
|
1761 |
|
1762 self.boundEvents = null; |
|
1763 } |
|
1764 |
|
1765 // Restore sizzle document to window.document |
|
1766 // Since the current document might be removed producing "Permission denied" on IE see #6325 |
|
1767 if (Sizzle.setDocument) { |
|
1768 Sizzle.setDocument(); |
|
1769 } |
|
1770 |
|
1771 self.win = self.doc = self.root = self.events = self.frag = null; |
|
1772 }, |
|
1773 |
|
1774 isChildOf: function(node, parent) { |
|
1775 while (node) { |
|
1776 if (parent === node) { |
|
1777 return true; |
|
1778 } |
|
1779 |
|
1780 node = node.parentNode; |
|
1781 } |
|
1782 |
|
1783 return false; |
|
1784 }, |
|
1785 |
|
1786 // #ifdef debug |
|
1787 |
|
1788 dumpRng: function(r) { |
|
1789 return ( |
|
1790 'startContainer: ' + r.startContainer.nodeName + |
|
1791 ', startOffset: ' + r.startOffset + |
|
1792 ', endContainer: ' + r.endContainer.nodeName + |
|
1793 ', endOffset: ' + r.endOffset |
|
1794 ); |
|
1795 }, |
|
1796 |
|
1797 // #endif |
|
1798 |
|
1799 _findSib: function(node, selector, name) { |
|
1800 var self = this, func = selector; |
|
1801 |
|
1802 if (node) { |
|
1803 // If expression make a function of it using is |
|
1804 if (typeof func == 'string') { |
|
1805 func = function(node) { |
|
1806 return self.is(node, selector); |
|
1807 }; |
|
1808 } |
|
1809 |
|
1810 // Loop all siblings |
|
1811 for (node = node[name]; node; node = node[name]) { |
|
1812 if (func(node)) { |
|
1813 return node; |
|
1814 } |
|
1815 } |
|
1816 } |
|
1817 |
|
1818 return null; |
|
1819 } |
|
1820 }; |
|
1821 |
|
1822 /** |
|
1823 * Instance of DOMUtils for the current document. |
|
1824 * |
|
1825 * @static |
|
1826 * @property DOM |
|
1827 * @type tinymce.dom.DOMUtils |
|
1828 * @example |
|
1829 * // Example of how to add a class to some element by id |
|
1830 * tinymce.DOM.addClass('someid', 'someclass'); |
|
1831 */ |
|
1832 DOMUtils.DOM = new DOMUtils(document); |
|
1833 |
|
1834 return DOMUtils; |
|
1835 }); |