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
RapidContext
Access · Discovery · Insight
www.rapidcontext.com