blob: 68ff085b92afc1d36e0d8acb7d9b369e5f330ff8 [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001/**
2 * Extend object a with the properties of object b.
3 * If there's a conflict, object b takes precedence.
4 *
5 * @param {object} a
6 * @param {object} b
7 */
8export const extend = ( a, b ) => {
9
10 for( let i in b ) {
11 a[ i ] = b[ i ];
12 }
13
14 return a;
15
16}
17
18/**
19 * querySelectorAll but returns an Array.
20 */
21export const queryAll = ( el, selector ) => {
22
23 return Array.from( el.querySelectorAll( selector ) );
24
25}
26
27/**
28 * classList.toggle() with cross browser support
29 */
30export const toggleClass = ( el, className, value ) => {
31 if( value ) {
32 el.classList.add( className );
33 }
34 else {
35 el.classList.remove( className );
36 }
37}
38
39/**
40 * Utility for deserializing a value.
41 *
42 * @param {*} value
43 * @return {*}
44 */
45export const deserialize = ( value ) => {
46
47 if( typeof value === 'string' ) {
48 if( value === 'null' ) return null;
49 else if( value === 'true' ) return true;
50 else if( value === 'false' ) return false;
51 else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value );
52 }
53
54 return value;
55
56}
57
58/**
59 * Measures the distance in pixels between point a
60 * and point b.
61 *
62 * @param {object} a point with x/y properties
63 * @param {object} b point with x/y properties
64 *
65 * @return {number}
66 */
67export const distanceBetween = ( a, b ) => {
68
69 let dx = a.x - b.x,
70 dy = a.y - b.y;
71
72 return Math.sqrt( dx*dx + dy*dy );
73
74}
75
76/**
77 * Applies a CSS transform to the target element.
78 *
79 * @param {HTMLElement} element
80 * @param {string} transform
81 */
82export const transformElement = ( element, transform ) => {
83
84 element.style.transform = transform;
85
86}
87
88/**
89 * Element.matches with IE support.
90 *
91 * @param {HTMLElement} target The element to match
92 * @param {String} selector The CSS selector to match
93 * the element against
94 *
95 * @return {Boolean}
96 */
97export const matches = ( target, selector ) => {
98
99 let matchesMethod = target.matches || target.matchesSelector || target.msMatchesSelector;
100
101 return !!( matchesMethod && matchesMethod.call( target, selector ) );
102
103}
104
105/**
106 * Find the closest parent that matches the given
107 * selector.
108 *
109 * @param {HTMLElement} target The child element
110 * @param {String} selector The CSS selector to match
111 * the parents against
112 *
113 * @return {HTMLElement} The matched parent or null
114 * if no matching parent was found
115 */
116export const closest = ( target, selector ) => {
117
118 // Native Element.closest
119 if( typeof target.closest === 'function' ) {
120 return target.closest( selector );
121 }
122
123 // Polyfill
124 while( target ) {
125 if( matches( target, selector ) ) {
126 return target;
127 }
128
129 // Keep searching
130 target = target.parentNode;
131 }
132
133 return null;
134
135}
136
137/**
138 * Handling the fullscreen functionality via the fullscreen API
139 *
140 * @see http://fullscreen.spec.whatwg.org/
141 * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
142 */
143export const enterFullscreen = element => {
144
145 element = element || document.documentElement;
146
147 // Check which implementation is available
148 let requestMethod = element.requestFullscreen ||
149 element.webkitRequestFullscreen ||
150 element.webkitRequestFullScreen ||
151 element.mozRequestFullScreen ||
152 element.msRequestFullscreen;
153
154 if( requestMethod ) {
155 requestMethod.apply( element );
156 }
157
158}
159
160/**
161 * Creates an HTML element and returns a reference to it.
162 * If the element already exists the existing instance will
163 * be returned.
164 *
165 * @param {HTMLElement} container
166 * @param {string} tagname
167 * @param {string} classname
168 * @param {string} innerHTML
169 *
170 * @return {HTMLElement}
171 */
172export const createSingletonNode = ( container, tagname, classname, innerHTML='' ) => {
173
174 // Find all nodes matching the description
175 let nodes = container.querySelectorAll( '.' + classname );
176
177 // Check all matches to find one which is a direct child of
178 // the specified container
179 for( let i = 0; i < nodes.length; i++ ) {
180 let testNode = nodes[i];
181 if( testNode.parentNode === container ) {
182 return testNode;
183 }
184 }
185
186 // If no node was found, create it now
187 let node = document.createElement( tagname );
188 node.className = classname;
189 node.innerHTML = innerHTML;
190 container.appendChild( node );
191
192 return node;
193
194}
195
196/**
197 * Injects the given CSS styles into the DOM.
198 *
199 * @param {string} value
200 */
201export const createStyleSheet = ( value ) => {
202
203 let tag = document.createElement( 'style' );
204 tag.type = 'text/css';
205
206 if( value && value.length > 0 ) {
207 if( tag.styleSheet ) {
208 tag.styleSheet.cssText = value;
209 }
210 else {
211 tag.appendChild( document.createTextNode( value ) );
212 }
213 }
214
215 document.head.appendChild( tag );
216
217 return tag;
218
219}
220
221/**
222 * Returns a key:value hash of all query params.
223 */
224export const getQueryHash = () => {
225
226 let query = {};
227
228 location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, a => {
229 query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
230 } );
231
232 // Basic deserialization
233 for( let i in query ) {
234 let value = query[ i ];
235
236 query[ i ] = deserialize( unescape( value ) );
237 }
238
239 // Do not accept new dependencies via query config to avoid
240 // the potential of malicious script injection
241 if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
242
243 return query;
244
245}
246
247/**
248 * Returns the remaining height within the parent of the
249 * target element.
250 *
251 * remaining height = [ configured parent height ] - [ current parent height ]
252 *
253 * @param {HTMLElement} element
254 * @param {number} [height]
255 */
256export const getRemainingHeight = ( element, height = 0 ) => {
257
258 if( element ) {
259 let newHeight, oldHeight = element.style.height;
260
261 // Change the .stretch element height to 0 in order find the height of all
262 // the other elements
263 element.style.height = '0px';
264
265 // In Overview mode, the parent (.slide) height is set of 700px.
266 // Restore it temporarily to its natural height.
267 element.parentNode.style.height = 'auto';
268
269 newHeight = height - element.parentNode.offsetHeight;
270
271 // Restore the old height, just in case
272 element.style.height = oldHeight + 'px';
273
274 // Clear the parent (.slide) height. .removeProperty works in IE9+
275 element.parentNode.style.removeProperty('height');
276
277 return newHeight;
278 }
279
280 return height;
281
282}