UNPKG

5.93 kBJavaScriptView Raw
1"use strict";
2
3module.exports = {
4
5 /**
6 * Wrapper for creating a new DOM element, optionally assigning it a hash of properties upon construction.
7 *
8 * @param {String} nodeType - Element type to create.
9 * @param {Object} obj - An optional hash of properties to assign the newly-created object.
10 * @return {Element}
11 */
12 New(nodeType, obj){
13 function absorb(a, b){
14 for(const i in b)
15 if(Object(a[i]) === a[i] && Object(b[i]) === b[i])
16 absorb(a[i], b[i]);
17 else a[i] = b[i];
18 };
19 const node = document.createElement(nodeType);
20 if(obj) absorb(node, obj);
21 return node;
22 },
23
24
25 /**
26 * Curried method to append multiple nodes at once.
27 *
28 * @example addTo(node)(el1, el2, …)
29 * @example node = addTo(node)(…)[0]
30 * @return {Function}
31 */
32 addTo(parent){
33 let count = 0;
34 let target = parent;
35
36 const fn = (...nodes) => {
37 let lastElement;
38
39 for(let node of nodes){
40 if("string" === typeof node)
41 node = document.createTextNode(node);
42 else if(node)
43 lastElement =
44 fn[++count] = node;
45 node && target.appendChild(node);
46 }
47
48 target = lastElement || target;
49 return fn;
50 };
51 fn[count] = target;
52 return fn;
53 },
54
55
56 /**
57 * Use a Symbol to store references between objects.
58 *
59 * Gives new meaning to the term "symbolic link".
60 *
61 * @param {Symbol} symbol
62 * @param {Object} objects
63 */
64 link(symbol, objects){
65 for(const key in objects){
66 const obj = objects[key];
67 if(obj == null) continue;
68 let copyTo = obj[symbol] || {};
69 obj[symbol] = Object.assign(copyTo, objects);
70 }
71 },
72
73
74 /**
75 * Return the containing element of a node that matches the given selector.
76 *
77 * If the node itself matches, it'll be returned unless ignoreSelf is set.
78 *
79 * @param {Node} node - A document node to inspect the hierarchy of
80 * @param {String} selector - A CSS selector string
81 * @param {Boolean} ignoreSelf - If given a truthy value, only the parents of a node will be queried
82 * @return {Element} The closest matching element, or NULL if none of the node's parents matched the selector.
83 */
84 nearest(node, selector, ignoreSelf){
85 let match;
86 let parent = ignoreSelf ? node.parentNode : node;
87 const matches = document.querySelectorAll(selector);
88 const numMatches = matches.length;
89
90 if(numMatches) while(parent){
91 for(match = 0; match < numMatches; ++match)
92 if(matches[match] === parent) return parent;
93 parent = parent.parentNode;
94 }
95 return null;
96 },
97
98
99 /**
100 * Locate the root directory shared by multiple paths.
101 *
102 * @param {Array} paths - A list of filesystem paths
103 * @return {String}
104 */
105 findBasePath(paths){
106 const POSIX = paths[0].indexOf("/") !== -1;
107 let matched = [];
108
109 // Spare ourselves the trouble if there's only one path
110 if(1 === paths.length){
111 matched = (paths[0].replace(/[\\/]+$/, "")).split(/[\\/]/g);
112 matched.pop();
113 }
114
115 // Otherwise, comb each array
116 else{
117 const rows = paths.map(d => d.split(/[\\/]/g));
118 const width = Math.max(...rows.map(d => d.length));
119 const height = rows.length;
120
121 let x, y;
122 X: for(x = 0; x < width; ++x){
123 const str = rows[0][x];
124 for(let y = 1; y < height; ++y)
125 if(str !== rows[y][x]) break X;
126 matched.push(str);
127 }
128 }
129
130 return matched.join(POSIX ? "/" : "\\");
131 },
132
133
134 /**
135 * Return the width of the scrollbars being displayed by this user's OS/device.
136 *
137 * @return {Number}
138 */
139 getScrollbarWidth(){
140 let el = document.createElement("div");
141 let style = el.style;
142 let size = 120;
143 style.width =
144 style.height = size+"px";
145 style.overflow = "auto";
146 el.innerHTML = Array(size*5).join(" W ");
147 (document.body || document.documentElement).appendChild(el);
148
149 let result = el.offsetWidth - el.scrollWidth;
150 el.parentNode.removeChild(el);
151 return result;
152 },
153
154
155 /**
156 * Generate a RegEx from its string-based representation.
157 *
158 * Useful for "deserialising" a regex from JSON. Optional flags can be given
159 * to override trailing modifiers found in the source, if any.
160 *
161 * @example "/\\S+/i" -> /\S+/i
162 * @example "\\d+\\.\\d+$" -> /\d+\.\d+$/
163 * @param {String} src
164 * @param {String} flags
165 * @return {RegExp}
166 */
167 regexFromString(src, flags){
168 src = (src || "").toString();
169 if(!src) return null;
170
171 const matchEnd = src.match(/\/([gimuy]*)$/);
172
173 // Input is a "complete" regular expression
174 if(matchEnd && /^\//.test(src))
175 return new RegExp(
176 src.replace(/^\/|\/([gimuy]*)$/gi, ""),
177 flags != null ? flags : matchEnd[1]
178 );
179
180 return new RegExp(src, flags);
181 },
182
183
184 /**
185 * Escape special regex characters within a string.
186 *
187 * @example "file.js" -> "file\\.js"
188 * @param {String} input
189 * @return {String}
190 */
191 escapeRegExp(input){
192 return input.replace(/([/\\^$*+?{}\[\]().|])/g, "\\$1");
193 },
194
195
196 /**
197 * Replace HTML metacharacters with numeric character references.
198 *
199 * Affected characters are: & < > "
200 *
201 * NOTE: Named entities are NOT checked, and will be double-escaped.
202 * Exceptions are made for `&quot;`, `&lt;` and `&gt;`, due to their
203 * abundant use. Numeric entities, even with invalid codepoints, are
204 * also safe from double-encoding.
205 *
206 * @example "name"<email> -> &#34;name&#34;&#60;email&#62;
207 * @param {String} input
208 * @return {String}
209 */
210 escapeHTML(input){
211 return input.replace(/["<>]|&(?!quot;|gt;|lt;|#x?[A-F\d]+;)/gi, s => "&#"+s.charCodeAt(0)+";");
212 },
213
214
215 /**
216 * Parse a list of keywords into an object of boolean "true" values.
217 *
218 * @example parseKeywords("top left") -> {top: true, left: true}
219 * @param {Mixed} keywords - A space-delimited string or an array of strings
220 * @return {Object}
221 */
222 parseKeywords(keywords){
223 if(!Array.isArray(keywords)){
224 if(!keywords) return null;
225 keywords = [keywords];
226 }
227
228 const output = {};
229 for(const k of keywords)
230 k.split(/\s+/g).filter(i => i).forEach(k => output[k] = true);
231 return output;
232 }
233};