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