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