Updating minified JavaScript files
[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
AE
4 * @author Alexander Ebert
5 * @copyright 2001-2015 WoltLab GmbH
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');
ab8ebbc4
AE
53 tmp.innerHTML = html;
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) {
184 if (el.nextElementSibling !== null) {
185 el.parentNode.insertBefore(newEl, el.nextElementSibling);
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
278 if (insertMethod === 'append' || insertMethod === 'after') {
279 while (element.childNodes.length) {
280 if (insertMethod === 'append') {
281 referenceElement.appendChild(element.childNodes[0]);
282 }
283 else {
284 this.insertAfter(element.childNodes[0], referenceElement);
285 }
286 }
287 }
288 else if (insertMethod === 'prepend' || insertMethod === 'before') {
289 for (var i = element.childNodes.length - 1; i >= 0; i--) {
290 if (insertMethod === 'prepend') {
291 this.prepend(element.childNodes[i], referenceElement);
292 }
293 else {
294 referenceElement.parentNode.insertBefore(element.childNodes[i], referenceElement);
295 }
296 }
297 }
298 else {
299 throw new Error("Unknown insert method '" + insertMethod + "'.");
300 }
301 },
302
45433290
AE
303 /**
304 * Returns true if `element` contains the `child` element.
305 *
306 * @param {Element} element container element
307 * @param {Element} child child element
308 * @returns {boolean} true if `child` is a (in-)direct child of `element`
309 */
310 contains: function(element, child) {
311 while (child !== null) {
312 child = child.parentNode;
313
314 if (element === child) {
315 return true;
316 }
317 }
318
319 return false;
9d118fa4
AE
320 },
321
322 /**
323 * Retrieves all data attributes from target element, optionally allowing for
324 * a custom prefix that serves two purposes: First it will restrict the results
325 * for items starting with it and second it will remove that prefix.
326 *
327 * @param {Element} element target element
328 * @param {string=} prefix attribute prefix
b2aa772d 329 * @param {boolean=} camelCaseName transform attribute names into camel case using dashes as separators
9d118fa4
AE
330 * @param {boolean=} idToUpperCase transform '-id' into 'ID'
331 * @returns {object<string, string>} list of data attributes
332 */
b2aa772d 333 getDataAttributes: function(element, prefix, camelCaseName, idToUpperCase) {
9d118fa4
AE
334 prefix = prefix || '';
335 if (!/^data-/.test(prefix)) prefix = 'data-' + prefix;
b2aa772d 336 camelCaseName = (camelCaseName === true);
9d118fa4
AE
337 idToUpperCase = (idToUpperCase === true);
338
339 var attribute, attributes = {}, name, tmp;
340 for (var i = 0, length = element.attributes.length; i < length; i++) {
341 attribute = element.attributes[i];
342
343 if (attribute.name.indexOf(prefix) === 0) {
344 name = attribute.name.replace(new RegExp('^' + prefix), '');
b2aa772d 345 if (camelCaseName) {
9d118fa4
AE
346 tmp = name.split('-');
347 name = '';
348 for (var j = 0, innerLength = tmp.length; j < innerLength; j++) {
349 if (name.length) {
350 if (idToUpperCase && tmp[j] === 'id') {
351 tmp[j] = 'ID';
352 }
353 else {
354 tmp[j] = StringUtil.ucfirst(tmp[j]);
355 }
356 }
357
358 name += tmp[j];
359 }
360 }
361
362 attributes[name] = attribute.value;
363 }
364 }
365
366 return attributes;
16764d0d
AE
367 },
368
369 /**
370 * Unwraps contained nodes by moving them out of `element` while
371 * preserving their previous order. Target element will be removed
372 * at the end of the operation.
373 *
374 * @param {Element} element target element
375 */
376 unwrapChildNodes: function(element) {
377 var parent = element.parentNode;
378 while (element.childNodes.length) {
379 parent.insertBefore(element.childNodes[0], element);
380 }
381
382 elRemove(element);
383 },
384
385 /**
386 * Replaces an element by moving all child nodes into the new element
387 * while preserving their previous order. The old element will be removed
388 * at the end of the operation.
389 *
390 * @param {Element} oldElement old element
391 * @param {Element} newElement old element
392 */
393 replaceElement: function(oldElement, newElement) {
394 while (oldElement.childNodes.length) {
395 newElement.appendChild(oldElement.childNodes[0]);
396 }
397
398 oldElement.parentNode.insertBefore(newElement, oldElement);
399 elRemove(oldElement);
400 },
401
402 /**
403 * Returns true if given element is the most left node of the ancestor, that is
404 * a node without any content nor elements before it or its parent nodes.
405 *
406 * @param {Element} element target element
407 * @param {Element} ancestor ancestor element, must contain the target element
408 * @returns {boolean} true if target element is the most left node
409 */
410 isAtNodeStart: function(element, ancestor) {
411 return _isBoundaryNode(element, ancestor, 'previous');
412 },
413
414 /**
415 * Returns true if given element is the most right node of the ancestor, that is
416 * a node without any content nor elements after it or its parent nodes.
417 *
418 * @param {Element} element target element
419 * @param {Element} ancestor ancestor element, must contain the target element
420 * @returns {boolean} true if target element is the most right node
421 */
422 isAtNodeEnd: function(element, ancestor) {
423 return _isBoundaryNode(element, ancestor, 'next');
4bbf6ff1
AE
424 }
425 };
426
d788c437 427 // expose on window object for backward compatibility
9a421cc7 428 window.bc_wcfDomUtil = DomUtil;
d788c437 429
9a421cc7 430 return DomUtil;
4bbf6ff1 431});