Source RapidContext.Util.js

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2024 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 * Converts a string to a title-cased string. All word boundaries are replaced
35 * with a single space and the subsequent character is capitalized.
36 *
37 * All underscore ("_"), hyphen ("-") and lower-upper character pairs are
38 * recognized as word boundaries. Note that this function does not change the
39 * capitalization of other characters in the string.
40 *
41 * @param {string} str the string to convert
42 *
43 * @return {string} the converted string
44 *
45 * @example
46 * RapidContext.Util.toTitleCase("a short heading")
47 * ==> "A Short Heading"
48 *
49 * @example
50 * RapidContext.Util.toTitleCase("camelCase")
51 * ==> "Camel Case"
52 *
53 * @example
54 * RapidContext.Util.toTitleCase("bounding-box")
55 * ==> "Bounding Box"
56 *
57 * @example
58 * RapidContext.Util.toTitleCase("UPPER_CASE_VALUE")
59 * ==> "UPPER CASE VALUE"
60 */
61RapidContext.Util.toTitleCase = function (str) {
62    str = str.replace(/[._-]+/g, " ").trim();
63    str = str.replace(/[a-z][A-Z]/g, function (match) {
64        return match.charAt(0) + " " + match.charAt(1);
65    });
66    str = str.replace(/(^|\s)[a-z]/g, function (match) {
67        return match.toUpperCase();
68    });
69    return str;
70};
71
72
73// DOM utility functions
74
75/**
76 * Blurs (unfocuses) a specified DOM node and all relevant child nodes. This
77 * function will recursively blur all `<a>`, `<button>`, `<input>`,
78 * `<textarea>` and `<select>` child nodes found.
79 *
80 * @param {Object} node the HTML DOM node
81 */
82RapidContext.Util.blurAll = function (node) {
83    node.blur();
84    var tags = ["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"];
85    for (var i = 0; i < tags.length; i++) {
86        var nodes = node.getElementsByTagName(tags[i]);
87        for (var j = 0; j < nodes.length; j++) {
88            nodes[j].blur();
89        }
90    }
91};
92
93/**
94 * Registers size constraints for the element width and/or height. The
95 * constraints may either be fixed numeric values or simple arithmetic (in a
96 * string). The formulas will be converted to CSS calc() expressions.
97 *
98 * Legacy constraint functions are still supported and must take two arguments
99 * (parent width and height) and should return a number. The returned number is
100 * set as the new element width or height (in pixels). Any returned value will
101 * also be bounded by the parent element size to avoid calculation errors.
102 *
103 * @param {Object} node the HTML DOM node
104 * @param {number|string|function} [width] the width constraint
105 * @param {number|string|function} [height] the height constraint
106 *
107 * @deprecated Use CSS width and height with calc() instead.
108 *
109 * @example
110 * RapidContext.Util.registerSizeConstraints(node, "50%-20", "100%");
111 * ==> Sets width to 50%-20 px and height to 100% of parent dimension
112 *
113 * @example
114 * RapidContext.Util.resizeElements(node, otherNode);
115 * ==> Evaluates the size constraints for both nodes
116 */
117RapidContext.Util.registerSizeConstraints = function (node, width, height) {
118    function toCSS(val) {
119        if (/[+-]/.test(val)) {
120            val = "calc( " + val.replace(/[+-]/g, " $& ") + " )";
121        }
122        val = val.replace(/(\d)( |$)/g, "$1px$2");
123        return val;
124    }
125    console.warn("deprecated: call to RapidContext.Util.registerSizeConstraints(), use CSS calc() instead");
126    node = MochiKit.DOM.getElement(node);
127    if (typeof(width) == "number" || typeof(width) == "string") {
128        node.style.width = toCSS(String(width));
129    } else if (typeof(width) == "function") {
130        console.info("registerSizeConstraints: width function support will be removed", node);
131        node.sizeConstraints = node.sizeConstraints || { w: null, h: null };
132        node.sizeConstraints.w = width;
133    }
134    if (typeof(height) == "number" || typeof(height) == "string") {
135        node.style.height = toCSS(String(height));
136    } else if (typeof(height) == "function") {
137        console.info("registerSizeConstraints: height function support will be removed", node);
138        node.sizeConstraints = node.sizeConstraints || { w: null, h: null };
139        node.sizeConstraints.h = height;
140    }
141};
142
143/**
144 * Resizes one or more DOM nodes using their registered size constraints and
145 * their parent element sizes. The resize operation will only modify those
146 * elements that have constraints, but will perform a depth-first recursion
147 * over all element child nodes as well.
148 *
149 * Partial constraints are accepted, in which case only the width or the height
150 * is modified. Aspect ratio constraints are applied after the width and height
151 * constraints. The result will always be bounded by the parent element width
152 * or height.
153 *
154 * The recursive descent of this function can be limited by adding a
155 * `resizeContent` function to a DOM node. Such a function will be called to
156 * handle all subnode resizing, making it possible to limit or omitting the
157 * DOM tree traversal.
158 *
159 * @param {...Node} node the HTML DOM nodes to resize
160 *
161 * @deprecated Use CSS width and height with calc() instead.
162 *
163 * @example
164 * RapidContext.Util.resizeElements(node);
165 * ==> Evaluates the size constraints for a node and all child nodes
166 *
167 * @example
168 * elem.resizeContent = () => {};
169 * ==> Assigns a no-op child resize handler to elem
170 */
171RapidContext.Util.resizeElements = function (/* ... */) {
172    console.warn("deprecated: call to RapidContext.Util.resizeElements(), use CSS calc() instead");
173    Array.from(arguments).forEach(function (arg) {
174        var node = MochiKit.DOM.getElement(arg);
175        if (node && node.nodeType === 1 && node.parentNode && node.sizeConstraints) {
176            var ref = { w: node.parentNode.w, h: node.parentNode.h };
177            if (ref.w == null && ref.h == null) {
178                ref = MochiKit.Style.getElementDimensions(node.parentNode, true);
179            }
180            var dim = RapidContext.Util._evalConstraints(node.sizeConstraints, ref);
181            MochiKit.Style.setElementDimensions(node, dim);
182            node.w = dim.w;
183            node.h = dim.h;
184        }
185        if (node && typeof(node.resizeContent) == "function") {
186            try {
187                node.resizeContent();
188            } catch (e) {
189                console.error("Error in resizeContent()", node, e);
190            }
191        } else if (node && node.childNodes) {
192            Array.from(node.childNodes).forEach(function (child) {
193                if (child.nodeType === 1) {
194                    RapidContext.Util.resizeElements(child);
195                }
196            });
197        }
198    });
199};
200
201/**
202 * Evaluates the size constraint functions with a refeence dimension
203 * object. This is an internal function used to encapsulate the
204 * function calls and provide logging on errors.
205 *
206 * @param {Object} sc the size constraints object
207 * @param {Object} ref the MochiKit.Style.Dimensions maximum
208 *            reference values
209 *
210 * @return {Object} the MochiKit.Style.Dimensions with evaluated size
211 *         constraint values (some may be null)
212 */
213RapidContext.Util._evalConstraints = function (sc, ref) {
214    var w, h;
215    if (typeof(sc.w) == "function") {
216        try {
217            w = Math.max(0, Math.min(ref.w, sc.w(ref.w, ref.h)));
218        } catch (e) {
219            console.error("Error evaluating width size constraint; " +
220                          "w: " + ref.w + ", h: " + ref.h, e);
221        }
222    }
223    if (typeof(sc.h) == "function") {
224        try {
225            h = Math.max(0, Math.min(ref.h, sc.h(ref.w, ref.h)));
226        } catch (e) {
227            console.error("Error evaluating height size constraint; " +
228                          "w: " + ref.w + ", h: " + ref.h, e);
229        }
230    }
231    if (w != null) {
232        w = Math.floor(w);
233    }
234    if (h != null) {
235        h = Math.floor(h);
236    }
237    return new MochiKit.Style.Dimensions(w, h);
238};
239