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