d5ee21374400fb3b4149dd6d726ffa2cb88b9720
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / DOM / HTMLArea.DOM.js
1 /*****************************************************************
2 * HTMLArea.DOM: Utility functions for dealing with the DOM tree *
3 *****************************************************************/
4 HTMLArea.DOM = function () {
5 return {
6 /***************************************************
7 * DOM-RELATED CONSTANTS
8 ***************************************************/
9 // DOM node types
10 ELEMENT_NODE: 1,
11 ATTRIBUTE_NODE: 2,
12 TEXT_NODE: 3,
13 CDATA_SECTION_NODE: 4,
14 ENTITY_REFERENCE_NODE: 5,
15 ENTITY_NODE: 6,
16 PROCESSING_INSTRUCTION_NODE: 7,
17 COMMENT_NODE: 8,
18 DOCUMENT_NODE: 9,
19 DOCUMENT_TYPE_NODE: 10,
20 DOCUMENT_FRAGMENT_NODE: 11,
21 NOTATION_NODE: 12,
22 /***************************************************
23 * DOM-RELATED REGULAR EXPRESSIONS
24 ***************************************************/
25 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,
26 RE_noClosingTag: /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i,
27 RE_bodyTag: new RegExp('<\/?(body)[^>]*>', 'gi'),
28 /***************************************************
29 * STATIC METHODS ON DOM NODE
30 ***************************************************/
31 /*
32 * Determine whether an element node is a block element
33 *
34 * @param object element: the element node
35 *
36 * @return boolean true, if the element node is a block element
37 */
38 isBlockElement: function (element) {
39 return element && element.nodeType === HTMLArea.DOM.ELEMENT_NODE && HTMLArea.DOM.RE_blockTags.test(element.nodeName);
40 },
41 /*
42 * Determine whether an element node needs a closing tag
43 *
44 * @param object element: the element node
45 *
46 * @return boolean true, if the element node needs a closing tag
47 */
48 needsClosingTag: function (element) {
49 return element && element.nodeType === HTMLArea.DOM.ELEMENT_NODE && !HTMLArea.DOM.RE_noClosingTag.test(element.nodeName);
50 },
51 /*
52 * Gets the class names assigned to a node, reserved classes removed
53 *
54 * @param object node: the node
55 * @return array array of class names on the node, reserved classes removed
56 */
57 getClassNames: function (node) {
58 var classNames = [];
59 if (node) {
60 if (node.className && /\S/.test(node.className)) {
61 classNames = node.className.trim().split(' ');
62 }
63 if (HTMLArea.reservedClassNames.test(node.className)) {
64 var cleanClassNames = [];
65 var j = -1;
66 for (var i = 0, n = classNames.length; i < n; ++i) {
67 if (!HTMLArea.reservedClassNames.test(classNames[i])) {
68 cleanClassNames[++j] = classNames[i];
69 }
70 }
71 classNames = cleanClassNames;
72 }
73 }
74 return classNames;
75 },
76 /*
77 * Check if a class name is in the class attribute of a node
78 *
79 * @param object node: the node
80 * @param string className: the class name to look for
81 * @param boolean substring: if true, look for a class name starting with the given string
82 * @return boolean true if the class name was found, false otherwise
83 */
84 hasClass: function (node, className, substring) {
85 var found = false;
86 if (node && node.className) {
87 var classes = node.className.trim().split(' ');
88 for (var i = classes.length; --i >= 0;) {
89 found = ((classes[i] == className) || (substring && classes[i].indexOf(className) == 0));
90 if (found) {
91 break;
92 }
93 }
94 }
95 return found;
96 },
97 /*
98 * Add a class name to the class attribute of a node
99 *
100 * @param object node: the node
101 * @param string className: the name of the class to be added
102 * @return void
103 */
104 addClass: function (node, className) {
105 if (node) {
106 HTMLArea.DOM.removeClass(node, className);
107 // Remove classes configured to be incompatible with the class to be added
108 if (node.className && HTMLArea.classesXOR && HTMLArea.classesXOR[className] && Ext.isFunction(HTMLArea.classesXOR[className].test)) {
109 var classNames = node.className.trim().split(' ');
110 for (var i = classNames.length; --i >= 0;) {
111 if (HTMLArea.classesXOR[className].test(classNames[i])) {
112 HTMLArea.DOM.removeClass(node, classNames[i]);
113 }
114 }
115 }
116 if (node.className) {
117 node.className += ' ' + className;
118 } else {
119 node.className = className;
120 }
121 }
122 },
123 /*
124 * Remove a class name from the class attribute of a node
125 *
126 * @param object node: the node
127 * @param string className: the class name to removed
128 * @param boolean substring: if true, remove the class names starting with the given string
129 * @return void
130 */
131 removeClass: function (node, className, substring) {
132 if (node && node.className) {
133 var classes = node.className.trim().split(' ');
134 var newClasses = [];
135 for (var i = classes.length; --i >= 0;) {
136 if ((!substring && classes[i] != className) || (substring && classes[i].indexOf(className) != 0)) {
137 newClasses[newClasses.length] = classes[i];
138 }
139 }
140 if (newClasses.length) {
141 node.className = newClasses.join(' ');
142 } else {
143 if (!Ext.isOpera) {
144 node.removeAttribute('class');
145 if (HTMLArea.isIEBeforeIE9) {
146 node.removeAttribute('className');
147 }
148 } else {
149 node.className = '';
150 }
151 }
152 }
153 },
154 /*
155 * Get the innerText of a given node
156 *
157 * @param object node: the node
158 *
159 * @return string the text inside the node
160 */
161 getInnerText: function (node) {
162 return HTMLArea.isIEBeforeIE9 ? node.innerText : node.textContent;;
163 },
164 /*
165 * Get the block ancestors of a node within a given block
166 *
167 * @param object node: the given node
168 * @param object withinBlock: the containing node
169 *
170 * @return array array of block ancestors
171 */
172 getBlockAncestors: function (node, withinBlock) {
173 var ancestors = [];
174 var ancestor = node;
175 while (ancestor && (ancestor.nodeType === HTMLArea.DOM.ELEMENT_NODE) && !/^(body)$/i.test(ancestor.nodeName) && ancestor != withinBlock) {
176 if (HTMLArea.DOM.isBlockElement(ancestor)) {
177 ancestors.unshift(ancestor);
178 }
179 ancestor = ancestor.parentNode;
180 }
181 ancestors.unshift(ancestor);
182 return ancestors;
183 },
184 /*
185 * Get the deepest element ancestor of a given node that is of one of the specified types
186 *
187 * @param object node: the given node
188 * @param array types: an array of nodeNames
189 *
190 * @return object the found ancestor of one of the given types or null
191 */
192 getFirstAncestorOfType: function (node, types) {
193 var ancestor = null,
194 parent = node;
195 if (!Ext.isEmpty(types)) {
196 if (Ext.isString(types)) {
197 var types = [types];
198 }
199 types = new RegExp( '^(' + types.join('|') + ')$', 'i');
200 while (parent && parent.nodeType === HTMLArea.DOM.ELEMENT_NODE && !/^(body)$/i.test(parent.nodeName)) {
201 if (types.test(parent.nodeName)) {
202 ancestor = parent;
203 break;
204 }
205 parent = parent.parentNode;
206 }
207 }
208 return ancestor;
209 },
210 /*
211 * Get the position of the node within the children of its parent
212 * Adapted from FCKeditor
213 *
214 * @param object node: the DOM node
215 * @param boolean normalized: if true, a normalized position is calculated
216 *
217 * @return integer the position of the node
218 */
219 getPositionWithinParent: function (node, normalized) {
220 var current = node,
221 position = 0;
222 while (current = current.previousSibling) {
223 // For a normalized position, do not count any empty text node or any text node following another one
224 if (
225 normalized
226 && current.nodeType == HTMLArea.DOM.TEXT_NODE
227 && (!current.nodeValue.length || (current.previousSibling && current.previousSibling.nodeType == HTMLArea.DOM.TEXT_NODE))
228 ) {
229 continue;
230 }
231 position++;
232 }
233 return position;
234 },
235 /*
236 * Determine whether a given node has any allowed attributes
237 *
238 * @param object node: the DOM node
239 * @param array allowedAttributes: array of allowed attribute names
240 *
241 * @return boolean true if the node has one of the allowed attributes
242 */
243 hasAllowedAttributes: function (node, allowedAttributes) {
244 var value,
245 hasAllowedAttributes = false;
246 if (Ext.isString(allowedAttributes)) {
247 allowedAttributes = [allowedAttributes];
248 }
249 allowedAttributes = allowedAttributes || [];
250 for (var i = allowedAttributes.length; --i >= 0;) {
251 value = node.getAttribute(allowedAttributes[i]);
252 if (value) {
253 if (allowedAttributes[i] === 'style') {
254 if (node.style.cssText) {
255 hasAllowedAttributes = true;
256 break;
257 }
258 } else {
259 hasAllowedAttributes = true;
260 break;
261 }
262 }
263 }
264 return hasAllowedAttributes;
265 },
266 /*
267 * Remove the given node from its parent
268 *
269 * @param object node: the DOM node
270 *
271 * @return void
272 */
273 removeFromParent: function (node) {
274 var parent = node.parentNode;
275 if (parent) {
276 parent.removeChild(node);
277 }
278 },
279 /*
280 * Change the nodeName of an element node
281 *
282 * @param object node: the node to convert (must belong to a document)
283 * @param string nodeName: the nodeName of the converted node
284 *
285 * @retrun object the converted node or the input node
286 */
287 convertNode: function (node, nodeName) {
288 var convertedNode = node,
289 ownerDocument = node.ownerDocument;
290 if (ownerDocument && node.nodeType === HTMLArea.DOM.ELEMENT_NODE) {
291 var convertedNode = ownerDocument.createElement(nodeName),
292 parent = node.parentNode;
293 while (node.firstChild) {
294 convertedNode.appendChild(node.firstChild);
295 }
296 parent.insertBefore(convertedNode, node);
297 parent.removeChild(node);
298 }
299 return convertedNode;
300 },
301 /*
302 * Determine whether a given range intersects a given node
303 *
304 * @param object range: the range
305 * @param object node: the DOM node (must belong to a document)
306 *
307 * @return boolean true if the range intersects the node
308 */
309 rangeIntersectsNode: function (range, node) {
310 var rangeIntersectsNode = false,
311 ownerDocument = node.ownerDocument;
312 if (ownerDocument) {
313 if (HTMLArea.isIEBeforeIE9) {
314 var nodeRange = ownerDocument.body.createTextRange();
315 nodeRange.moveToElementText(node);
316 rangeIntersectsNode = (range.compareEndPoints('EndToStart', nodeRange) == -1 && range.compareEndPoints('StartToEnd', nodeRange) == 1) ||
317 (range.compareEndPoints('EndToStart', nodeRange) == 1 && range.compareEndPoints('StartToEnd', nodeRange) == -1);
318 } else {
319 var nodeRange = ownerDocument.createRange();
320 try {
321 nodeRange.selectNode(node);
322 } catch (e) {
323 if (Ext.isWebKit) {
324 nodeRange.setStart(node, 0);
325 if (node.nodeType === HTMLArea.DOM.TEXT_NODE || node.nodeType === HTMLArea.DOM.COMMENT_NODE || node.nodeType === HTMLArea.DOM.CDATA_SECTION_NODE) {
326 nodeRange.setEnd(node, node.textContent.length);
327 } else {
328 nodeRange.setEnd(node, node.childNodes.length);
329 }
330 } else {
331 nodeRange.selectNodeContents(node);
332 }
333 }
334 // Note: sometimes WebKit inverts the end points
335 rangeIntersectsNode = (range.compareBoundaryPoints(range.END_TO_START, nodeRange) == -1 && range.compareBoundaryPoints(range.START_TO_END, nodeRange) == 1) ||
336 (range.compareBoundaryPoints(range.END_TO_START, nodeRange) == 1 && range.compareBoundaryPoints(range.START_TO_END, nodeRange) == -1);
337 }
338 }
339 return rangeIntersectsNode;
340 },
341 /*
342 * Make url's absolute in the DOM tree under the root node
343 *
344 * @param object root: the root node
345 * @param string baseUrl: base url to use
346 * @param string walker: a HLMLArea.DOM.Walker object
347 * @return void
348 */
349 makeUrlsAbsolute: function (node, baseUrl, walker) {
350 walker.walk(node, true, 'HTMLArea.DOM.makeImageSourceAbsolute(node, args[0]) || HTMLArea.DOM.makeLinkHrefAbsolute(node, args[0])', 'Ext.emptyFn', [baseUrl]);
351 },
352 /*
353 * Make the src attribute of an image node absolute
354 *
355 * @param object node: the image node
356 * @param string baseUrl: base url to use
357 * @return void
358 */
359 makeImageSourceAbsolute: function (node, baseUrl) {
360 if (/^img$/i.test(node.nodeName)) {
361 var src = node.getAttribute('src');
362 if (src) {
363 node.setAttribute('src', HTMLArea.DOM.addBaseUrl(src, baseUrl));
364 }
365 return true;
366 }
367 return false;
368 },
369 /*
370 * Make the href attribute of an a node absolute
371 *
372 * @param object node: the image node
373 * @param string baseUrl: base url to use
374 * @return void
375 */
376 makeLinkHrefAbsolute: function (node, baseUrl) {
377 if (/^a$/i.test(node.nodeName)) {
378 var href = node.getAttribute('href');
379 if (href) {
380 node.setAttribute('href', HTMLArea.DOM.addBaseUrl(href, baseUrl));
381 }
382 return true;
383 }
384 return false;
385 },
386 /*
387 * Add base url
388 *
389 * @param string url: value of a href or src attribute
390 * @param string baseUrl: base url to add
391 * @return string absolute url
392 */
393 addBaseUrl: function (url, baseUrl) {
394 var absoluteUrl = url;
395 // If the url has no scheme...
396 if (!/^[a-z0-9_]{2,}\:/i.test(absoluteUrl)) {
397 var base = baseUrl;
398 while (absoluteUrl.match(/^\.\.\/(.*)/)) {
399 // Remove leading ../ from url
400 absoluteUrl = RegExp.$1;
401 base.match(/(.*\:\/\/.*\/)[^\/]+\/$/);
402 // Remove lowest directory level from base
403 base = RegExp.$1;
404 absoluteUrl = base + absoluteUrl;
405 }
406 // If the url is still not absolute...
407 if (!/^.*\:\/\//.test(absoluteUrl)) {
408 absoluteUrl = baseUrl + absoluteUrl;
409 }
410 }
411 return absoluteUrl;
412 }
413 };
414 }();