Source RapidContext.Util.js

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2023 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15// Create default RapidContext object
16if (typeof(RapidContext) == "undefined") {
17    RapidContext = {};
18}
19
20/**
21 * Provides utility functions for basic objects, arrays, DOM nodes and CSS.
22 * These functions are complementary to what is available in MochiKit and/or
23 * jQuery.
24 * @namespace RapidContext.Util
25 */
26if (typeof(RapidContext.Util) == "undefined") {
27    RapidContext.Util = {};
28}
29
30
31// General utility functions
32
33/**
34 * Creates a dictionary object from a list of keys and values. Optionally a
35 * list of key-value pairs can be provided instead. As a third option, a single
36 * (non-array) value can be assigned to all the keys.
37 *
38 * If a key is specified twice, only the last value will be used. Note that
39 * this function is the reverse of `MochiKit.Base.items()`,
40 * `MochiKit.Base.keys()` and `MochiKit.Base.values()`.
41 *
42 * @param {Array} itemsOrKeys the list of keys or items
43 * @param {Array} [values] the list of values (optional if key-value
44 *            pairs are specified in first argument)
45 *
46 * @return {Object} an object with properties for each key-value pair
47 *
48 * @deprecated Use RapidContext.Data.object() instead.
49 *
50 * @example
51 * RapidContext.Util.dict(['a','b'], [1, 2])
52 * ==> { a: 1, b: 2 }
53 *
54 * @example
55 * RapidContext.Util.dict([['a', 1], ['b', 2]])
56 * ==> { a: 1, b: 2 }
57 *
58 * @example
59 * RapidContext.Util.dict(['a','b'], true)
60 * ==> { a: true, b: true }
61 */
62RapidContext.Util.dict = function (itemsOrKeys, values) {
63    console.warn("deprecated: RapidContext.Util.dict() called, use RapidContext.Data.object() instead");
64    var o = {};
65    if (!MochiKit.Base.isArrayLike(itemsOrKeys)) {
66        throw new TypeError("First argument must be array-like");
67    }
68    if (MochiKit.Base.isArrayLike(values) && itemsOrKeys.length !== values.length) {
69        throw new TypeError("Both arrays must be of same length");
70    }
71    for (var i = 0; i < itemsOrKeys.length; i++) {
72        var k = itemsOrKeys[i];
73        if (k === null || k === undefined) {
74            throw new TypeError("Key at index " + i + " is null or undefined");
75        } else if (MochiKit.Base.isArrayLike(k)) {
76            o[k[0]] = k[1];
77        } else if (MochiKit.Base.isArrayLike(values)) {
78            o[k] = values[i];
79        } else {
80            o[k] = values;
81        }
82    }
83    return o;
84};
85
86/**
87 * Filters an object by removing a list of keys. A list of key names (or an
88 * object whose property names will be used as keys) must be specified as an
89 * argument. A new object containing the source object values for the specified
90 * keys will be returned. The source object will be modified by removing all
91 * the specified keys.
92 *
93 * @param {Object} src the source object to select and modify
94 * @param {Array|Object} keys the list of keys to remove, or an
95 *            object with the keys to remove
96 *
97 * @return {Object} a new object containing the matching keys and
98 *             values found in the source object
99 *
100 * @deprecated This function will be removed in the future.
101 *
102 * @example
103 * var o = { a: 1, b: 2 };
104 * RapidContext.Util.mask(o, ['a', 'c']);
105 * ==> { a: 1 } and modifies o to { b: 2 }
106 *
107 * @example
108 * var o = { a: 1, b: 2 };
109 * RapidContext.Util.mask(o, { a: null, c: null });
110 * ==> { a: 1 } and modifies o to { b: 2 }
111 */
112RapidContext.Util.mask = function (src, keys) {
113    console.warn("deprecated: RapidContext.Util.mask() called, use object destructuring assignment instead");
114    var res = {};
115    if (!MochiKit.Base.isArrayLike(keys)) {
116        keys = MochiKit.Base.keys(keys);
117    }
118    for (var i = 0; i < keys.length; i++) {
119        var k = keys[i];
120        if (k in src) {
121            res[k] = src[k];
122            delete src[k];
123        }
124    }
125    return res;
126};
127
128/**
129 * Converts a string to a title-cased string. All word boundaries are replaced
130 * with a single space and the subsequent character is capitalized.
131 *
132 * All underscore ("_"), hyphen ("-") and lower-upper character pairs are
133 * recognized as word boundaries. Note that this function does not change the
134 * capitalization of other characters in the string.
135 *
136 * @param {string} str the string to convert
137 *
138 * @return {string} the converted string
139 *
140 * @example
141 * RapidContext.Util.toTitleCase("a short heading")
142 * ==> "A Short Heading"
143 *
144 * @example
145 * RapidContext.Util.toTitleCase("camelCase")
146 * ==> "Camel Case"
147 *
148 * @example
149 * RapidContext.Util.toTitleCase("bounding-box")
150 * ==> "Bounding Box"
151 *
152 * @example
153 * RapidContext.Util.toTitleCase("UPPER_CASE_VALUE")
154 * ==> "UPPER CASE VALUE"
155 */
156RapidContext.Util.toTitleCase = function (str) {
157    str = str.replace(/[._-]+/g, " ").trim();
158    str = str.replace(/[a-z][A-Z]/g, function (match) {
159        return match.charAt(0) + " " + match.charAt(1);
160    });
161    str = str.replace(/(^|\s)[a-z]/g, function (match) {
162        return match.toUpperCase();
163    });
164    return str;
165};
166
167/**
168 * Resolves a relative URI to an absolute URI. This function will return
169 * absolute URI:s directly and traverse any "../" directory paths in the
170 * specified URI. The base URI provided must be absolute.
171 *
172 * @param {string} uri the relative URI to resolve
173 * @param {string} [base] the absolute base URI, defaults to the
174 *            the current document base URI
175 *
176 * @return {string} the resolved absolute URI
177 *
178 * @deprecated This function will be removed and/or renamed in the future.
179 *     Use `new URL(..., document.baseURI)` instead.
180 */
181RapidContext.Util.resolveURI = function (uri, base) {
182    console.warn("deprecated: resolveURI() called, use 'new URL(...)' instead");
183    var pos;
184    base = base || document.baseURI || document.getElementsByTagName("base")[0].href;
185    if (uri.includes(":")) {
186        return uri;
187    } else if (uri.startsWith("#")) {
188        pos = base.lastIndexOf("#");
189        if (pos >= 0) {
190            base = base.substring(0, pos);
191        }
192        return base + uri;
193    } else if (uri.startsWith("/")) {
194        pos = base.indexOf("/", base.indexOf("://") + 3);
195        base = base.substring(0, pos);
196        return base + uri;
197    } else if (uri.startsWith("../")) {
198        pos = base.lastIndexOf("/");
199        base = base.substring(0, pos);
200        uri = uri.substring(3);
201        return RapidContext.Util.resolveURI(uri, base);
202    } else {
203        pos = base.lastIndexOf("/");
204        base = base.substring(0, pos + 1);
205        return base + uri;
206    }
207};
208
209
210// DOM utility functions
211
212/**
213 * Blurs (unfocuses) a specified DOM node and all relevant child nodes. This
214 * function will recursively blur all `<a>`, `<button>`, `<input>`,
215 * `<textarea>` and `<select>` child nodes found.
216 *
217 * @param {Object} node the HTML DOM node
218 */
219RapidContext.Util.blurAll = function (node) {
220    node.blur();
221    var tags = ["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"];
222    for (var i = 0; i < tags.length; i++) {
223        var nodes = node.getElementsByTagName(tags[i]);
224        for (var j = 0; j < nodes.length; j++) {
225            nodes[j].blur();
226        }
227    }
228};
229
230/**
231 * Registers size constraints for the element width and/or height. The
232 * constraints may either be fixed numeric values or simple arithmetic (in a
233 * string). The formulas will be converted to CSS calc() expressions.
234 *
235 * Legacy constraint functions are still supported and must take two arguments
236 * (parent width and height) and should return a number. The returned number is
237 * set as the new element width or height (in pixels). Any returned value will
238 * also be bounded by the parent element size to avoid calculation errors.
239 *
240 * @param {Object} node the HTML DOM node
241 * @param {number|string|function} [width] the width constraint
242 * @param {number|string|function} [height] the height constraint
243 *
244 * @see RapidContext.Util.resizeElements
245 *
246 * @example
247 * RapidContext.Util.registerSizeConstraints(node, "50%-20", "100%");
248 * ==> Sets width to 50%-20 px and height to 100% of parent dimension
249 *
250 * @example
251 * RapidContext.Util.resizeElements(node, otherNode);
252 * ==> Evaluates the size constraints for both nodes
253 */
254RapidContext.Util.registerSizeConstraints = function (node, width, height) {
255    function toCSS(val) {
256        if (/[+-]/.test(val)) {
257            val = "calc( " + val.replace(/[+-]/g, " $& ") + " )";
258        }
259        val = val.replace(/(\d)( |$)/g, "$1px$2");
260        return val;
261    }
262    node = MochiKit.DOM.getElement(node);
263    if (typeof(width) == "number" || typeof(width) == "string") {
264        node.style.width = toCSS(String(width));
265    } else if (typeof(width) == "function") {
266        console.info("registerSizeConstraints: width function support will be removed", node);
267        node.sizeConstraints = node.sizeConstraints || { w: null, h: null };
268        node.sizeConstraints.w = width;
269    }
270    if (typeof(height) == "number" || typeof(height) == "string") {
271        node.style.height = toCSS(String(height));
272    } else if (typeof(height) == "function") {
273        console.info("registerSizeConstraints: height function support will be removed", node);
274        node.sizeConstraints = node.sizeConstraints || { w: null, h: null };
275        node.sizeConstraints.h = height;
276    }
277};
278
279/**
280 * Resizes one or more DOM nodes using their registered size constraints and
281 * their parent element sizes. The resize operation will only modify those
282 * elements that have constraints, but will perform a depth-first recursion
283 * over all element child nodes as well.
284 *
285 * Partial constraints are accepted, in which case only the width or the height
286 * is modified. Aspect ratio constraints are applied after the width and height
287 * constraints. The result will always be bounded by the parent element width
288 * or height.
289 *
290 * The recursive descent of this function can be limited by adding a
291 * `resizeContent` function to a DOM node. Such a function will be called to
292 * handle all subnode resizing, making it possible to limit or omitting the
293 * DOM tree traversal.
294 *
295 * @param {...Node} node the HTML DOM nodes to resize
296 *
297 * @see RapidContext.Util.registerSizeConstraints
298 *
299 * @example
300 * RapidContext.Util.resizeElements(node);
301 * ==> Evaluates the size constraints for a node and all child nodes
302 *
303 * @example
304 * elem.resizeContent = () => {};
305 * ==> Assigns a no-op child resize handler to elem
306 */
307RapidContext.Util.resizeElements = function (/* ... */) {
308    Array.from(arguments).forEach(function (arg) {
309        var node = MochiKit.DOM.getElement(arg);
310        if (node && node.nodeType === 1 && node.parentNode && node.sizeConstraints) {
311            var ref = { w: node.parentNode.w, h: node.parentNode.h };
312            if (ref.w == null && ref.h == null) {
313                ref = MochiKit.Style.getElementDimensions(node.parentNode, true);
314            }
315            var dim = RapidContext.Util._evalConstraints(node.sizeConstraints, ref);
316            MochiKit.Style.setElementDimensions(node, dim);
317            node.w = dim.w;
318            node.h = dim.h;
319        }
320        if (node && typeof(node.resizeContent) == "function") {
321            try {
322                node.resizeContent();
323            } catch (e) {
324                console.error("Error in resizeContent()", node, e);
325            }
326        } else if (node && node.childNodes) {
327            Array.from(node.childNodes).forEach(function (child) {
328                if (child.nodeType === 1) {
329                    RapidContext.Util.resizeElements(child);
330                }
331            });
332        }
333    });
334};
335
336/**
337 * Evaluates the size constraint functions with a refeence dimension
338 * object. This is an internal function used to encapsulate the
339 * function calls and provide logging on errors.
340 *
341 * @param {Object} sc the size constraints object
342 * @param {Object} ref the MochiKit.Style.Dimensions maximum
343 *            reference values
344 *
345 * @return {Object} the MochiKit.Style.Dimensions with evaluated size
346 *         constraint values (some may be null)
347 */
348RapidContext.Util._evalConstraints = function (sc, ref) {
349    var w, h;
350    if (typeof(sc.w) == "function") {
351        try {
352            w = Math.max(0, Math.min(ref.w, sc.w(ref.w, ref.h)));
353        } catch (e) {
354            console.error("Error evaluating width size constraint; " +
355                          "w: " + ref.w + ", h: " + ref.h, e);
356        }
357    }
358    if (typeof(sc.h) == "function") {
359        try {
360            h = Math.max(0, Math.min(ref.h, sc.h(ref.w, ref.h)));
361        } catch (e) {
362            console.error("Error evaluating height size constraint; " +
363                          "w: " + ref.w + ", h: " + ref.h, e);
364        }
365    }
366    if (w != null) {
367        w = Math.floor(w);
368    }
369    if (h != null) {
370        h = Math.floor(h);
371    }
372    return new MochiKit.Style.Dimensions(w, h);
373};
374