74a282a4189a214b622101ce2f9063dd543d7b8a
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / HTMLArea / DOM / DOM.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13
14 /**
15 * Module: TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM
16 * HTMLArea.DOM: Utility functions for dealing with the DOM tree *
17 */
18 define(['TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util'],
20 function (UserAgent, Util) {
21
22 /**
23 *
24 * @type {{ELEMENT_NODE: number, ATTRIBUTE_NODE: number, TEXT_NODE: number, CDATA_SECTION_NODE: number, ENTITY_REFERENCE_NODE: number, ENTITY_NODE: number, PROCESSING_INSTRUCTION_NODE: number, COMMENT_NODE: number, DOCUMENT_NODE: number, DOCUMENT_TYPE_NODE: number, DOCUMENT_FRAGMENT_NODE: number, NOTATION_NODE: number, RE_blockTags: RegExp, RE_noClosingTag: RegExp, RE_bodyTag: RegExp, isBlockElement: Function, needsClosingTag: Function, getClassNames: Function, hasClass: Function, addClass: Function, removeClass: Function, isRequiredClass: Function, getInnerText: Function, getBlockAncestors: Function, getFirstAncestorOfType: Function, getPositionWithinParent: Function, hasAllowedAttributes: Function, removeFromParent: Function, convertNode: Function, rangeIntersectsNode: Function, makeUrlsAbsolute: Function, makeImageSourceAbsolute: Function, makeLinkHrefAbsolute: Function, addBaseUrl: Function, getPosition: Function, getSize: Function, setSize: Function, setStyle: Function}}
25 * @exports TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM
26 */
27 var Dom = {
28
29 /***************************************************
30 * DOM NODES TYPES CONSTANTS
31 ***************************************************/
32 ELEMENT_NODE: 1,
33 ATTRIBUTE_NODE: 2,
34 TEXT_NODE: 3,
35 CDATA_SECTION_NODE: 4,
36 ENTITY_REFERENCE_NODE: 5,
37 ENTITY_NODE: 6,
38 PROCESSING_INSTRUCTION_NODE: 7,
39 COMMENT_NODE: 8,
40 DOCUMENT_NODE: 9,
41 DOCUMENT_TYPE_NODE: 10,
42 DOCUMENT_FRAGMENT_NODE: 11,
43 NOTATION_NODE: 12,
44
45 /***************************************************
46 * DOM-RELATED REGULAR EXPRESSIONS
47 ***************************************************/
48 RE_blockTags: /^(address|article|aside|body|blockquote|caption|dd|div|dl|dt|fieldset|footer|form|header|hr|h1|h2|h3|h4|h5|h6|iframe|li|ol|p|pre|nav|noscript|section|table|tbody|td|tfoot|th|thead|tr|ul)$/i,
49 RE_noClosingTag: /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i,
50 RE_bodyTag: new RegExp('<\/?(body)[^>]*>', 'gi'),
51
52 /***************************************************
53 * STATIC METHODS ON DOM NODE
54 ***************************************************/
55 /**
56 * Determine whether an element node is a block element
57 *
58 * @param object element: the element node
59 *
60 * @return boolean true, if the element node is a block element
61 */
62 isBlockElement: function (element) {
63 return element && element.nodeType === Dom.ELEMENT_NODE && Dom.RE_blockTags.test(element.nodeName);
64 },
65
66 /**
67 * Determine whether an element node needs a closing tag
68 *
69 * @param object element: the element node
70 *
71 * @return boolean true, if the element node needs a closing tag
72 */
73 needsClosingTag: function (element) {
74 return element && element.nodeType === Dom.ELEMENT_NODE && !Dom.RE_noClosingTag.test(element.nodeName);
75 },
76
77 /**
78 * Gets the class names assigned to a node, reserved classes removed
79 *
80 * @param object node: the node
81 * @return array array of class names on the node, reserved classes removed
82 */
83 getClassNames: function (node) {
84 var classNames = [];
85 if (node) {
86 if (node.className && /\S/.test(node.className)) {
87 classNames = node.className.trim().split(' ');
88 }
89 if (HTMLArea.reservedClassNames.test(node.className)) {
90 var cleanClassNames = [];
91 var j = -1;
92 for (var i = 0, n = classNames.length; i < n; ++i) {
93 if (!HTMLArea.reservedClassNames.test(classNames[i])) {
94 cleanClassNames[++j] = classNames[i];
95 }
96 }
97 classNames = cleanClassNames;
98 }
99 }
100 return classNames;
101 },
102
103 /**
104 * Check if a class name is in the class attribute of a node
105 *
106 * @param object node: the node
107 * @param string className: the class name to look for
108 * @param boolean substring: if true, look for a class name starting with the given string
109 * @return boolean true if the class name was found, false otherwise
110 */
111 hasClass: function (node, className, substring) {
112 var found = false;
113 if (node && node.className) {
114 var classes = node.className.trim().split(' ');
115 for (var i = classes.length; --i >= 0;) {
116 found = ((classes[i] == className) || (substring && classes[i].indexOf(className) == 0));
117 if (found) {
118 break;
119 }
120 }
121 }
122 return found;
123 },
124
125 /**
126 * Add a class name to the class attribute of a node
127 *
128 * @param object node: the node
129 * @param string className: the name of the class to be added
130 * @param integer recursionLevel: recursion level of current call
131 * @return void
132 */
133 addClass: function (node, className, recursionLevel) {
134 if (node) {
135 var classNames = Dom.getClassNames(node);
136 if (classNames.indexOf(className) === -1) {
137 // Remove classes configured to be incompatible with the class to be added
138 if (node.className && HTMLArea.classesXOR && HTMLArea.classesXOR[className] && typeof HTMLArea.classesXOR[className].test === 'function') {
139 for (var i = classNames.length; --i >= 0;) {
140 if (HTMLArea.classesXOR[className].test(classNames[i])) {
141 Dom.removeClass(node, classNames[i]);
142 }
143 }
144 }
145 // Check dependencies to add required classes recursively
146 if (typeof HTMLArea.classesRequires !== 'undefined' && typeof HTMLArea.classesRequires[className] !== 'undefined') {
147 if (typeof recursionLevel === 'undefined') {
148 var recursionLevel = 1;
149 } else {
150 recursionLevel++;
151 }
152 if (recursionLevel < 20) {
153 for (var i = 0, n = HTMLArea.classesRequires[className].length; i < n; i++) {
154 var classNames = Dom.getClassNames(node);
155 if (classNames.indexOf(HTMLArea.classesRequires[className][i]) === -1) {
156 Dom.addClass(node, HTMLArea.classesRequires[className][i], recursionLevel);
157 }
158 }
159 }
160 }
161 if (node.className) {
162 node.className += ' ' + className;
163 } else {
164 node.className = className;
165 }
166 }
167 }
168 },
169
170 /**
171 * Remove a class name from the class attribute of a node
172 *
173 * @param object node: the node
174 * @param string className: the class name to removed
175 * @param boolean substring: if true, remove the class names starting with the given string
176 * @return void
177 */
178 removeClass: function (node, className, substring) {
179 if (node && node.className) {
180 var classes = node.className.trim().split(' ');
181 var newClasses = [];
182 for (var i = classes.length; --i >= 0;) {
183 if ((!substring && classes[i] != className) || (substring && classes[i].indexOf(className) != 0)) {
184 newClasses[newClasses.length] = classes[i];
185 }
186 }
187 if (newClasses.length) {
188 node.className = newClasses.join(' ');
189 } else {
190 if (!UserAgent.isOpera) {
191 node.removeAttribute('class');
192 if (UserAgent.isIEBeforeIE9) {
193 node.removeAttribute('className');
194 }
195 } else {
196 node.className = '';
197 }
198 }
199 // Remove the first unselectable class that is no more required, the following ones being removed by recursive calls
200 if (node.className && typeof HTMLArea.classesSelectable !== 'undefined') {
201 classes = Dom.getClassNames(node);
202 for (var i = classes.length; --i >= 0;) {
203 if (typeof HTMLArea.classesSelectable[classes[i]] !== 'undefined' && !HTMLArea.classesSelectable[classes[i]] && !Dom.isRequiredClass(node, classes[i])) {
204 Dom.removeClass(node, classes[i]);
205 break;
206 }
207 }
208 }
209 }
210 },
211
212 /**
213 * Check if the class is required by another class assigned to the node
214 *
215 * @param object node: the node
216 * @param string className: the class name to check
217 * @return boolean
218 */
219 isRequiredClass: function (node, className) {
220 if (typeof HTMLArea.classesRequiredBy !== 'undefined') {
221 var classes = Dom.getClassNames(node);
222 for (var i = classes.length; --i >= 0;) {
223 if (typeof HTMLArea.classesRequiredBy[classes[i]] !== 'undefined' && HTMLArea.classesRequiredBy[classes[i]].indexOf(className) !== -1) {
224 return true;
225 }
226 }
227 }
228 return false;
229 },
230
231 /**
232 * Get the innerText of a given node
233 *
234 * @param object node: the node
235 *
236 * @return string the text inside the node
237 */
238 getInnerText: function (node) {
239 return UserAgent.isIEBeforeIE9 ? node.innerText : node.textContent;;
240 },
241
242 /**
243 * Get the block ancestors of a node within a given block
244 *
245 * @param object node: the given node
246 * @param object withinBlock: the containing node
247 *
248 * @return array array of block ancestors
249 */
250 getBlockAncestors: function (node, withinBlock) {
251 var ancestors = [];
252 var ancestor = node;
253 while (ancestor && (ancestor.nodeType === Dom.ELEMENT_NODE) && !/^(body)$/i.test(ancestor.nodeName) && ancestor != withinBlock) {
254 if (Dom.isBlockElement(ancestor)) {
255 ancestors.unshift(ancestor);
256 }
257 ancestor = ancestor.parentNode;
258 }
259 ancestors.unshift(ancestor);
260 return ancestors;
261 },
262
263 /**
264 * Get the deepest element ancestor of a given node that is of one of the specified types
265 *
266 * @param object node: the given node
267 * @param array types: an array of nodeNames
268 *
269 * @return object the found ancestor of one of the given types or null
270 */
271 getFirstAncestorOfType: function (node, types) {
272 var ancestor = null,
273 parent = node;
274 if (typeof types === 'string') {
275 var types = [types];
276 }
277 // Is types a non-empty array?
278 if (types && Object.prototype.toString.call(types) === '[object Array]' && types.length > 0) {
279 types = new RegExp( '^(' + types.join('|') + ')$', 'i');
280 while (parent && parent.nodeType === Dom.ELEMENT_NODE && !/^(body)$/i.test(parent.nodeName)) {
281 if (types.test(parent.nodeName)) {
282 ancestor = parent;
283 break;
284 }
285 parent = parent.parentNode;
286 }
287 }
288 return ancestor;
289 },
290
291 /**
292 * Get the position of the node within the children of its parent
293 * Adapted from FCKeditor
294 *
295 * @param object node: the DOM node
296 * @param boolean normalized: if true, a normalized position is calculated
297 *
298 * @return integer the position of the node
299 */
300 getPositionWithinParent: function (node, normalized) {
301 var current = node,
302 position = 0;
303 while (current = current.previousSibling) {
304 // For a normalized position, do not count any empty text node or any text node following another one
305 if (
306 normalized
307 && current.nodeType == Dom.TEXT_NODE
308 && (!current.nodeValue.length || (current.previousSibling && current.previousSibling.nodeType == Dom.TEXT_NODE))
309 ) {
310 continue;
311 }
312 position++;
313 }
314 return position;
315 },
316
317 /**
318 * Determine whether a given node has any allowed attributes
319 *
320 * @param object node: the DOM node
321 * @param array allowedAttributes: array of allowed attribute names
322 *
323 * @return boolean true if the node has one of the allowed attributes
324 */
325 hasAllowedAttributes: function (node, allowedAttributes) {
326 var value,
327 hasAllowedAttributes = false;
328 if (typeof allowedAttributes === 'string') {
329 var allowedAttributes = [allowedAttributes];
330 }
331 // Is allowedAttributes an array?
332 if (allowedAttributes && Object.prototype.toString.call(allowedAttributes) === '[object Array]') {
333 for (var i = allowedAttributes.length; --i >= 0;) {
334 value = node.getAttribute(allowedAttributes[i]);
335 if (value) {
336 if (allowedAttributes[i] === 'style') {
337 if (node.style.cssText) {
338 hasAllowedAttributes = true;
339 break;
340 }
341 } else {
342 hasAllowedAttributes = true;
343 break;
344 }
345 }
346 }
347 }
348 return hasAllowedAttributes;
349 },
350
351 /**
352 * Remove the given node from its parent
353 *
354 * @param object node: the DOM node
355 *
356 * @return void
357 */
358 removeFromParent: function (node) {
359 var parent = node.parentNode;
360 if (parent) {
361 parent.removeChild(node);
362 }
363 },
364
365 /**
366 * Change the nodeName of an element node
367 *
368 * @param object node: the node to convert (must belong to a document)
369 * @param string nodeName: the nodeName of the converted node
370 *
371 * @retrun object the converted node or the input node
372 */
373 convertNode: function (node, nodeName) {
374 var convertedNode = node,
375 ownerDocument = node.ownerDocument;
376 if (ownerDocument && node.nodeType === Dom.ELEMENT_NODE) {
377 var convertedNode = ownerDocument.createElement(nodeName),
378 parent = node.parentNode;
379 while (node.firstChild) {
380 convertedNode.appendChild(node.firstChild);
381 }
382 parent.insertBefore(convertedNode, node);
383 parent.removeChild(node);
384 }
385 return convertedNode;
386 },
387
388 /**
389 * Determine whether a given range intersects a given node
390 *
391 * @param object range: the range
392 * @param object node: the DOM node (must belong to a document)
393 *
394 * @return boolean true if the range intersects the node
395 */
396 rangeIntersectsNode: function (range, node) {
397 var rangeIntersectsNode = false,
398 ownerDocument = node.ownerDocument;
399 if (ownerDocument) {
400 if (UserAgent.isIEBeforeIE9) {
401 var nodeRange = ownerDocument.body.createTextRange();
402 nodeRange.moveToElementText(node);
403 rangeIntersectsNode = (range.compareEndPoints('EndToStart', nodeRange) == -1 && range.compareEndPoints('StartToEnd', nodeRange) == 1) ||
404 (range.compareEndPoints('EndToStart', nodeRange) == 1 && range.compareEndPoints('StartToEnd', nodeRange) == -1);
405 } else {
406 var nodeRange = ownerDocument.createRange();
407 try {
408 nodeRange.selectNode(node);
409 } catch (e) {
410 if (UserAgent.isWebKit) {
411 nodeRange.setStart(node, 0);
412 if (node.nodeType === Dom.TEXT_NODE || node.nodeType === Dom.COMMENT_NODE || node.nodeType === Dom.CDATA_SECTION_NODE) {
413 nodeRange.setEnd(node, node.textContent.length);
414 } else {
415 nodeRange.setEnd(node, node.childNodes.length);
416 }
417 } else {
418 nodeRange.selectNodeContents(node);
419 }
420 }
421 // Note: sometimes WebKit inverts the end points
422 rangeIntersectsNode = (range.compareBoundaryPoints(range.END_TO_START, nodeRange) == -1 && range.compareBoundaryPoints(range.START_TO_END, nodeRange) == 1) ||
423 (range.compareBoundaryPoints(range.END_TO_START, nodeRange) == 1 && range.compareBoundaryPoints(range.START_TO_END, nodeRange) == -1);
424 }
425 }
426 return rangeIntersectsNode;
427 },
428
429 /**
430 * Make url's absolute in the DOM tree under the root node
431 *
432 * @param object root: the root node
433 * @param string baseUrl: base url to use
434 * @param string walker: a HLMLArea.DOM.Walker object
435 * @return void
436 */
437 makeUrlsAbsolute: function (node, baseUrl, walker) {
438 walker.walk(node, true, 'args[0].makeImageSourceAbsolute(node, args[2]) || args[0].makeLinkHrefAbsolute(node, args[2])', 'args[1].emptyFunction', [Dom, Util, baseUrl]);
439 },
440
441 /**
442 * Make the src attribute of an image node absolute
443 *
444 * @param object node: the image node
445 * @param string baseUrl: base url to use
446 * @return void
447 */
448 makeImageSourceAbsolute: function (node, baseUrl) {
449 if (/^img$/i.test(node.nodeName)) {
450 var src = node.getAttribute('src');
451 if (src) {
452 node.setAttribute('src', Dom.addBaseUrl(src, baseUrl));
453 }
454 return true;
455 }
456 return false;
457 },
458
459 /**
460 * Make the href attribute of an a node absolute
461 *
462 * @param object node: the image node
463 * @param string baseUrl: base url to use
464 * @return void
465 */
466 makeLinkHrefAbsolute: function (node, baseUrl) {
467 if (/^a$/i.test(node.nodeName)) {
468 var href = node.getAttribute('href');
469 if (href) {
470 node.setAttribute('href', Dom.addBaseUrl(href, baseUrl));
471 }
472 return true;
473 }
474 return false;
475 },
476
477 /**
478 * Add base url
479 *
480 * @param string url: value of a href or src attribute
481 * @param string baseUrl: base url to add
482 * @return string absolute url
483 */
484 addBaseUrl: function (url, baseUrl) {
485 var absoluteUrl = url;
486 // If the url has no scheme...
487 if (!/^[a-z0-9_]{2,}\:/i.test(absoluteUrl)) {
488 var base = baseUrl;
489 while (absoluteUrl.match(/^\.\.\/(.*)/)) {
490 // Remove leading ../ from url
491 absoluteUrl = RegExp.$1;
492 base.match(/(.*\:\/\/.*\/)[^\/]+\/$/);
493 // Remove lowest directory level from base
494 base = RegExp.$1;
495 absoluteUrl = base + absoluteUrl;
496 }
497 // If the url is still not absolute...
498 if (!/^.*\:\/\//.test(absoluteUrl)) {
499 absoluteUrl = baseUrl + absoluteUrl;
500 }
501 }
502 return absoluteUrl;
503 },
504
505 /**
506 * Get the position of a node
507 *
508 * @param object node
509 * @return object left and top coordinates
510 */
511 getPosition: function (node) {
512 var x = 0, y = 0;
513 while (node && !isNaN(node.offsetLeft) && !isNaN(node.offsetTop)) {
514 x += node.offsetLeft - node.scrollLeft;
515 y += node.offsetTop - node.scrollTop;
516 node = node.offsetParent;
517 }
518 return { x: x, y: y };
519 },
520
521 /**
522 * Get the current size of a node
523 *
524 * @param object node
525 * @return object width and height
526 */
527 getSize: function (node) {
528 return {
529 width: Math.max(node.offsetWidth, node.clientWidth) || 0,
530 height: Math.max(node.offsetHeight, node.clientHeight) || 0
531 }
532 },
533
534 /**
535 * Set the size of a node
536 *
537 * @param object node
538 * @param object size: width and height
539 * @return void
540 */
541 setSize: function (node, size) {
542 if (typeof size.width !== 'undefined') {
543 node.style.width = size.width + 'px';
544 }
545 if (typeof size.height !== 'undefined') {
546 node.style.height = size.height + 'px';
547 }
548 },
549
550 /**
551 * Set the style of a node
552 *
553 * @param object node
554 * @param object style
555 * @return void
556 */
557 setStyle: function (node, style) {
558 for (var property in style) {
559 if (typeof style[property] !== 'undefined') {
560 node.style[property] = style[property];
561 }
562 }
563 }
564 };
565
566 return Dom;
567
568 });