Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Dom / Util.js
CommitLineData
4bbf6ff1
AE
1/**
2 * Provides helper functions to work with DOM nodes.
16764d0d 3 *
4bbf6ff1 4 * @author Alexander Ebert
7b7b9764 5 * @copyright 2001-2019 WoltLab GmbH
4bbf6ff1 6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
b168f9c9 7 * @module Dom/Util (alias)
58d7e8f8 8 * @module WoltLabSuite/Core/Dom/Util
4bbf6ff1 9 */
9f7796e3 10define(['Environment', 'StringUtil'], function(Environment, StringUtil) {
565853e8
TD
11 "use strict";
12
16764d0d
AE
13 function _isBoundaryNode(element, ancestor, position) {
14 if (!ancestor.contains(element)) {
15 throw new Error("Ancestor element does not contain target element.");
16 }
17
18 var node, whichSibling = position + 'Sibling';
19 while (element !== null && element !== ancestor) {
20 if (element[position + 'ElementSibling'] !== null) {
21 return false;
22 }
23 else if (element[whichSibling]) {
24 node = element[whichSibling];
25 while (node) {
26 if (node.textContent.trim() !== '') {
27 return false;
28 }
29
30 node = node[whichSibling];
31 }
32 }
33
34 element = element.parentNode;
35 }
36
37 return true;
38 }
39
4bbf6ff1
AE
40 var _idCounter = 0;
41
42 /**
58d7e8f8 43 * @exports WoltLabSuite/Core/Dom/Util
4bbf6ff1 44 */
9a421cc7 45 var DomUtil = {
ab8ebbc4
AE
46 /**
47 * Returns a DocumentFragment containing the provided HTML string as DOM nodes.
48 *
49 * @param {string} html HTML string
50 * @return {DocumentFragment} fragment containing DOM nodes
51 */
52 createFragmentFromHtml: function(html) {
d0023381 53 var tmp = elCreate('div');
e41f8bc3 54 this.setInnerHtml(tmp, html);
ab8ebbc4
AE
55
56 var fragment = document.createDocumentFragment();
57 while (tmp.childNodes.length) {
58 fragment.appendChild(tmp.childNodes[0]);
59 }
60
61 return fragment;
62 },
63
4bbf6ff1
AE
64 /**
65 * Returns a unique element id.
66 *
67 * @return {string} unique id
68 */
69 getUniqueId: function() {
70 var elementId;
71
72 do {
73 elementId = 'wcf' + _idCounter++;
74 }
d0023381 75 while (elById(elementId) !== null);
4bbf6ff1
AE
76
77 return elementId;
78 },
79
80 /**
81 * Returns the element's id. If there is no id set, a unique id will be
82 * created and assigned.
83 *
84 * @param {Element} el element
85 * @return {string} element id
86 */
87 identify: function(el) {
431e4cb4
AE
88 if (!(el instanceof Element)) {
89 throw new TypeError("Expected a valid DOM element as argument.");
4bbf6ff1
AE
90 }
91
d0023381 92 var id = elAttr(el, 'id');
4bbf6ff1
AE
93 if (!id) {
94 id = this.getUniqueId();
d0023381 95 elAttr(el, 'id', id);
4bbf6ff1
AE
96 }
97
98 return id;
99 },
100
4bbf6ff1
AE
101 /**
102 * Returns the outer height of an element including margins.
103 *
104 * @param {Element} el element
105 * @param {CSSStyleDeclaration=} styles result of window.getComputedStyle()
b9f49efd 106 * @return {int} outer height in px
4bbf6ff1
AE
107 */
108 outerHeight: function(el, styles) {
109 styles = styles || window.getComputedStyle(el);
110
111 var height = el.offsetHeight;
112 height += ~~styles.marginTop + ~~styles.marginBottom;
113
114 return height;
115 },
116
117 /**
118 * Returns the outer width of an element including margins.
119 *
120 * @param {Element} el element
121 * @param {CSSStyleDeclaration=} styles result of window.getComputedStyle()
a20b01c7 122 * @return {int} outer width in px
4bbf6ff1
AE
123 */
124 outerWidth: function(el, styles) {
125 styles = styles || window.getComputedStyle(el);
126
127 var width = el.offsetWidth;
128 width += ~~styles.marginLeft + ~~styles.marginRight;
129
130 return width;
131 },
132
133 /**
134 * Returns the outer dimensions of an element including margins.
135 *
29758c29
AE
136 * @param {Element} el element
137 * @return {{height: int, width: int}} dimensions in px
4bbf6ff1
AE
138 */
139 outerDimensions: function(el) {
140 var styles = window.getComputedStyle(el);
141
142 return {
143 height: this.outerHeight(el, styles),
144 width: this.outerWidth(el, styles)
145 };
146 },
147
148 /**
149 * Returns the element's offset relative to the document's top left corner.
150 *
29758c29
AE
151 * @param {Element} el element
152 * @return {{left: int, top: int}} offset relative to top left corner
4bbf6ff1
AE
153 */
154 offset: function(el) {
155 var rect = el.getBoundingClientRect();
156
157 return {
3274a36a
AE
158 top: Math.round(rect.top + (window.scrollY || window.pageYOffset)),
159 left: Math.round(rect.left + (window.scrollX || window.pageXOffset))
c318ad3b 160 };
4bbf6ff1
AE
161 },
162
09f7100b
AE
163 /**
164 * Prepends an element to a parent element.
165 *
166 * @param {Element} el element to prepend
167 * @param {Element} parentEl future containing element
92edef79 168 * @deprecated 5.3 Use `parentEl.insertBefore(el, parentEl.firstChild)` instead.
09f7100b
AE
169 */
170 prepend: function(el, parentEl) {
d85b1843 171 if (parentEl.childNodes.length === 0) {
09f7100b
AE
172 parentEl.appendChild(el);
173 }
174 else {
d85b1843 175 parentEl.insertBefore(el, parentEl.childNodes[0]);
09f7100b
AE
176 }
177 },
178
dbd9f599
AE
179 /**
180 * Inserts an element after an existing element.
181 *
182 * @param {Element} newEl element to insert
183 * @param {Element} el reference element
92edef79 184 * @deprecated 5.3 Use `el.parentNode.insertBefore(newEl, el.nextSibling)` instead.
dbd9f599
AE
185 */
186 insertAfter: function(newEl, el) {
6d095884
AE
187 if (el.nextSibling !== null) {
188 el.parentNode.insertBefore(newEl, el.nextSibling);
dbd9f599
AE
189 }
190 else {
191 el.parentNode.appendChild(newEl);
192 }
193 },
194
4bbf6ff1
AE
195 /**
196 * Applies a list of CSS properties to an element.
197 *
198 * @param {Element} el element
9d118fa4 199 * @param {Object<string, *>} styles list of CSS styles
4bbf6ff1
AE
200 */
201 setStyles: function(el, styles) {
0d498c15 202 var important = false;
4bbf6ff1 203 for (var property in styles) {
3274a36a 204 if (styles.hasOwnProperty(property)) {
0d498c15
AE
205 if (/ !important$/.test(styles[property])) {
206 important = true;
207
208 styles[property] = styles[property].replace(/ !important$/, '');
209 }
210 else {
211 important = false;
212 }
213
3274a36a
AE
214 // for a set style property with priority = important, some browsers are
215 // not able to overwrite it with a property != important; removing the
216 // property first solves this issue
217 if (el.style.getPropertyPriority(property) === 'important' && !important) {
9f7796e3
MS
218 el.style.removeProperty(property);
219 }
220
0d498c15 221 el.style.setProperty(property, styles[property], (important ? 'important' : ''));
4bbf6ff1
AE
222 }
223 }
c318ad3b
AE
224 },
225
226 /**
227 * Returns a style property value as integer.
228 *
229 * The behavior of this method is undefined for properties that are not considered
230 * to have a "numeric" value, e.g. "background-image".
231 *
232 * @param {CSSStyleDeclaration} styles result of window.getComputedStyle()
233 * @param {string} propertyName property name
9d118fa4 234 * @return {int} property value as integer
c318ad3b
AE
235 */
236 styleAsInt: function(styles, propertyName) {
237 var value = styles.getPropertyValue(propertyName);
238 if (value === null) {
239 return 0;
240 }
241
242 return parseInt(value);
126b3f33
AE
243 },
244
245 /**
246 * Sets the inner HTML of given element and reinjects <script> elements to be properly executed.
247 *
248 * @see http://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0
249 * @param {Element} element target element
250 * @param {string} innerHtml HTML string
251 */
252 setInnerHtml: function(element, innerHtml) {
253 element.innerHTML = innerHtml;
254
255 var newScript, script, scripts = elBySelAll('script', element);
256 for (var i = 0, length = scripts.length; i < length; i++) {
257 script = scripts[i];
258 newScript = elCreate('script');
259 if (script.src) {
260 newScript.src = script.src;
261 }
262 else {
263 newScript.textContent = script.textContent;
264 }
265
266 element.appendChild(newScript);
f5336f4f 267 elRemove(script);
126b3f33 268 }
45433290
AE
269 },
270
9d118fa4
AE
271 /**
272 *
273 * @param html
274 * @param {Element} referenceElement
275 * @param insertMethod
276 */
277 insertHtml: function(html, referenceElement, insertMethod) {
278 var element = elCreate('div');
279 this.setInnerHtml(element, html);
280
69521cea
AE
281 if (!element.childNodes.length) {
282 return;
9d118fa4 283 }
69521cea
AE
284
285 var node = element.childNodes[0];
286 switch (insertMethod) {
287 case 'append':
288 referenceElement.appendChild(node);
289 break;
290
291 case 'after':
292 this.insertAfter(node, referenceElement);
293 break;
294
295 case 'prepend':
296 this.prepend(node, referenceElement);
297 break;
4dbd7203 298
69521cea
AE
299 case 'before':
300 referenceElement.parentNode.insertBefore(node, referenceElement);
301 break;
302
303 default:
304 throw new Error("Unknown insert method '" + insertMethod + "'.");
305 break;
9d118fa4 306 }
69521cea
AE
307
308 var tmp;
309 while (element.childNodes.length) {
310 tmp = element.childNodes[0];
311
312 this.insertAfter(tmp, node);
313 node = tmp;
9d118fa4
AE
314 }
315 },
316
45433290
AE
317 /**
318 * Returns true if `element` contains the `child` element.
319 *
320 * @param {Element} element container element
321 * @param {Element} child child element
322 * @returns {boolean} true if `child` is a (in-)direct child of `element`
323 */
324 contains: function(element, child) {
325 while (child !== null) {
326 child = child.parentNode;
327
328 if (element === child) {
329 return true;
330 }
331 }
332
333 return false;
9d118fa4
AE
334 },
335
336 /**
337 * Retrieves all data attributes from target element, optionally allowing for
338 * a custom prefix that serves two purposes: First it will restrict the results
339 * for items starting with it and second it will remove that prefix.
340 *
341 * @param {Element} element target element
342 * @param {string=} prefix attribute prefix
b2aa772d 343 * @param {boolean=} camelCaseName transform attribute names into camel case using dashes as separators
9d118fa4
AE
344 * @param {boolean=} idToUpperCase transform '-id' into 'ID'
345 * @returns {object<string, string>} list of data attributes
346 */
b2aa772d 347 getDataAttributes: function(element, prefix, camelCaseName, idToUpperCase) {
9d118fa4
AE
348 prefix = prefix || '';
349 if (!/^data-/.test(prefix)) prefix = 'data-' + prefix;
b2aa772d 350 camelCaseName = (camelCaseName === true);
9d118fa4
AE
351 idToUpperCase = (idToUpperCase === true);
352
353 var attribute, attributes = {}, name, tmp;
354 for (var i = 0, length = element.attributes.length; i < length; i++) {
355 attribute = element.attributes[i];
356
357 if (attribute.name.indexOf(prefix) === 0) {
358 name = attribute.name.replace(new RegExp('^' + prefix), '');
b2aa772d 359 if (camelCaseName) {
9d118fa4
AE
360 tmp = name.split('-');
361 name = '';
362 for (var j = 0, innerLength = tmp.length; j < innerLength; j++) {
363 if (name.length) {
364 if (idToUpperCase && tmp[j] === 'id') {
365 tmp[j] = 'ID';
366 }
367 else {
368 tmp[j] = StringUtil.ucfirst(tmp[j]);
369 }
370 }
371
372 name += tmp[j];
373 }
374 }
375
376 attributes[name] = attribute.value;
377 }
378 }
379
380 return attributes;
16764d0d
AE
381 },
382
383 /**
384 * Unwraps contained nodes by moving them out of `element` while
385 * preserving their previous order. Target element will be removed
386 * at the end of the operation.
387 *
388 * @param {Element} element target element
389 */
390 unwrapChildNodes: function(element) {
391 var parent = element.parentNode;
392 while (element.childNodes.length) {
393 parent.insertBefore(element.childNodes[0], element);
394 }
395
396 elRemove(element);
397 },
398
399 /**
400 * Replaces an element by moving all child nodes into the new element
401 * while preserving their previous order. The old element will be removed
402 * at the end of the operation.
403 *
404 * @param {Element} oldElement old element
405 * @param {Element} newElement old element
406 */
407 replaceElement: function(oldElement, newElement) {
408 while (oldElement.childNodes.length) {
409 newElement.appendChild(oldElement.childNodes[0]);
410 }
411
412 oldElement.parentNode.insertBefore(newElement, oldElement);
413 elRemove(oldElement);
414 },
415
416 /**
417 * Returns true if given element is the most left node of the ancestor, that is
418 * a node without any content nor elements before it or its parent nodes.
419 *
420 * @param {Element} element target element
421 * @param {Element} ancestor ancestor element, must contain the target element
422 * @returns {boolean} true if target element is the most left node
423 */
424 isAtNodeStart: function(element, ancestor) {
425 return _isBoundaryNode(element, ancestor, 'previous');
426 },
427
428 /**
429 * Returns true if given element is the most right node of the ancestor, that is
430 * a node without any content nor elements after it or its parent nodes.
431 *
432 * @param {Element} element target element
433 * @param {Element} ancestor ancestor element, must contain the target element
434 * @returns {boolean} true if target element is the most right node
435 */
436 isAtNodeEnd: function(element, ancestor) {
437 return _isBoundaryNode(element, ancestor, 'next');
e1a048df
AE
438 },
439
440 /**
441 * Returns the first ancestor element with position fixed or null.
442 *
443 * @param {Element} element target element
444 * @returns {(Element|null)} first ancestor with position fixed or null
445 */
446 getFixedParent: function (element) {
447 while (element && element !== document.body) {
448 if (window.getComputedStyle(element).getPropertyValue('position') === 'fixed') {
449 return element;
450 }
451
452 element = element.offsetParent;
453 }
454
455 return null;
4bbf6ff1
AE
456 }
457 };
458
d788c437 459 // expose on window object for backward compatibility
9a421cc7 460 window.bc_wcfDomUtil = DomUtil;
d788c437 461
9a421cc7 462 return DomUtil;
4bbf6ff1 463});