1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2026 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/**
16 * Provides a browser compatibility and diagnostics information.
17 * @namespace RapidContext.Browser
18 */
19(function (window) {
20 /* eslint no-var: "off", prefer-template: "off" */
21
22 /**
23 * List of all required browser features (ie. Chrome 86+, Firefox 80+, Safari 14+).
24 *
25 * @name REQUIRED
26 * @memberof RapidContext.Browser
27 * @constant
28 */
29 var REQUIRED = [
30 "AbortController",
31 "AbortSignal",
32 "Array.isArray",
33 "Array.from",
34 "Array.of",
35 "Array.prototype.every",
36 "Array.prototype.filter",
37 "Array.prototype.find",
38 "Array.prototype.findIndex",
39 "Array.prototype.flat",
40 "Array.prototype.flatMap",
41 "Array.prototype.forEach",
42 "Array.prototype.includes",
43 "Array.prototype.map",
44 "Array.prototype.reduce",
45 "Array.prototype.some",
46 "BigInt",
47 "Blob",
48 "CSS.supports",
49 "CustomEvent",
50 "Date.now",
51 "Date.prototype.toISOString",
52 "Date.prototype.toLocaleString",
53 "DOMParser",
54 "DOMTokenList.prototype.add",
55 "DOMTokenList.prototype.contains",
56 "DOMTokenList.prototype.remove",
57 "DOMTokenList.prototype.toggle",
58 "Element.prototype.after",
59 "Element.prototype.append",
60 "Element.prototype.before",
61 "Element.prototype.closest",
62 "Element.prototype.matches",
63 "Element.prototype.prepend",
64 "Element.prototype.remove",
65 "Element.prototype.replaceChildren",
66 "Element.prototype.replaceWith",
67 "FileReader",
68 "FormData",
69 "Function.prototype.bind",
70 "HTMLTemplateElement",
71 "HTMLSlotElement",
72 "JSON.parse",
73 "JSON.stringify",
74 "Math.imul",
75 "Node.prototype.contains",
76 "NodeList.prototype.forEach",
77 "Number.prototype.toLocaleString",
78 "Object.assign",
79 "Object.create",
80 "Object.defineProperty",
81 "Object.entries",
82 "Object.fromEntries",
83 "Object.getOwnPropertyDescriptor",
84 "Object.getOwnPropertyNames",
85 "Object.getPrototypeOf",
86 "Object.is",
87 "Object.keys",
88 "Object.setPrototypeOf",
89 "Object.values",
90 "Promise",
91 "Promise.all",
92 "Promise.allSettled",
93 "Promise.any",
94 "Promise.race",
95 "Promise.reject",
96 "Promise.resolve",
97 "Promise.prototype.finally",
98 "ResizeObserver",
99 "Set",
100 "String.prototype.endsWith",
101 "String.prototype.includes",
102 "String.prototype.matchAll",
103 "String.prototype.padEnd",
104 "String.prototype.padStart",
105 "String.prototype.replaceAll",
106 "String.prototype.startsWith",
107 "String.prototype.trim",
108 "String.raw", // proxy for template literals
109 "TextDecoder",
110 "TextEncoder",
111 "URL",
112 "URL.createObjectURL",
113 "URL.revokeObjectURL",
114 "URLSearchParams",
115 "WeakRef",
116 "console.assert",
117 "console.debug",
118 "console.error",
119 "console.info",
120 "console.log",
121 "console.warn",
122 "document.querySelectorAll",
123 "fetch",
124 "localStorage.getItem",
125 "sessionStorage.setItem",
126 { test: "'head' in document", name: "document.head shortcut" },
127 { test: "'activeElement' in document", name: "document.activeElement" },
128 { test: "'classList' in Element.prototype", name: "Element.prototype.classList" },
129 { test: "'dataset' in HTMLElement.prototype", name: "HTMLElement.prototype.dataset" },
130 { test: "'onload' in HTMLLinkElement.prototype", name: "<link> element onload" },
131 {
132 test: "var el = document.createElement('div');el.style.cssText='width:calc(10px);';!!el.style.length",
133 name: "CSS calc()"
134 },
135 { test: "let a = 2; a === 2", name: "Let statements" },
136 { test: "const a = 3; a === 3", name: "Const statements" },
137 { test: "for (var el of []) el; true", name: "loops with for...of" },
138 { test: "var a = 42; ({ a }).a === 42", name: "shorthand property names" },
139 { test: "({ f(arg) { return arg; } }).f(true)", name: "shorthand method names" },
140 { test: "var k = 'a'; ({ [k]: 42 })['a'] === 42", name: "computed property names" },
141 { test: "((a, b, ...args) => true)()", name: "rest parameters" },
142 { test: "Math.max(...[1, 2, 3]) === 3", name: "spread parameters" },
143 { test: "[...[1, 2, 3]].length === 3", name: "spread array literals" },
144 { test: "({...{ a: 42}}).a == 42", name: "spread object literals" },
145 { test: "var [a, ...b] = [1, 2, 3]; a == 1 && b.length == 2", name: "array destructuring assignment" },
146 { test: "var {a, b} = { a: 1, b: 2, c: 3 }; a == 1 && b == 2", name: "object destructuring assignment" },
147 { test: "123_456.78_9_12 == 123456.78912", name: "numeric separators (_)" },
148 { test: "class Test {}; true", name: "Class declarations" },
149 { test: "(() => true)()", name: "Arrow functions" },
150 { test: "async () => await Promise.resolve(true)", name: "async/await functions" },
151 { test: "typeof(Symbol.iterator) === 'symbol'", name: "Symbol.iterator" },
152 { test: "globalThis === window", name: "globalThis" },
153 { test: "'key' in KeyboardEvent.prototype", name: "KeyboardEvent.key" },
154 { test: "/^\\p{L}+$/u.test('a\u00E5\u00C4')", name: "regexp Unicode character class escape" },
155 { test: "'noModule' in HTMLScriptElement.prototype", name: "ES modules (script element)" },
156 { test: "var a = null; a?.dummy; true", name: "optional chaining operator (?.)" },
157 { test: "null ?? true", name: "nullish coalescing operator (??)" },
158 { test: "var a = true; a &&= true; a", name: "logical and assignment operator (&&=)" },
159 { test: "var a = false; a ||= true; a", name: "logical or assignment operator (||=)" },
160 { test: "var a = null; a ??= true; a", name: "nullish assignment operator (??=)" },
161 { test: "class Test { a; b = 123; static c = true; }; Test.c", name: "public class fields" },
162 { test: "class Test { #a; #b = 123; static #c = 0; }; true", name: "private class fields" },
163 "--variable-name: #fff",
164 "display: flex",
165 "appearance: none",
166 "inset: 0",
167 ];
168
169 /**
170 * List of all optional (or recommended) browser features. These are
171 * not used in built-in libraries and apps, but will be in the future.
172 *
173 * @name OPTIONAL
174 * @memberof RapidContext.Browser
175 * @constant
176 */
177 var OPTIONAL = [
178 "Array.prototype.at",
179 "Array.prototype.findLast",
180 "Array.prototype.findLastIndex",
181 // "Array.prototype.toReversed",
182 // "Array.prototype.toSorted",
183 // "Array.prototype.toSpliced",
184 // "Array.prototype.with",
185 "Element.prototype.attachShadow",
186 // "Element.prototype.checkVisibility",
187 // "Element.prototype.scrollIntoView",
188 "HTMLDialogElement",
189 // "HTMLScriptElement.supports",
190 // "Map.groupBy",
191 // "Object.groupBy",
192 "Object.hasOwn",
193 // "Promise.withResolvers",
194 // unicodeSets
195 // "Set.prototype.difference",
196 // "Set.prototype.intersection",
197 // "Set.prototype.isDisjointFrom",
198 // "Set.prototype.isSubsetOf",
199 // "Set.prototype.isSupersetOf",
200 // "Set.prototype.union",
201 // "Temporal",
202 "customElements.define",
203 // { test: "HTMLScriptElement.supports('module')", name: "ES module (script element, modern test)", },
204 // { test: "HTMLScriptElement.supports('importmap')", name: "ES module import maps", },
205 { test: "new Error('test', { cause: true }).cause", name: "error cause property" },
206 { test: "class Test { static { 1; } }; true", name: "static class initialization block" },
207 // { test: "'unicodeSets' in RegExp.prototype", name: "regexp unicodeSets" },
208 "selector(:has(*))",
209 // "color: color-mix(in srgb, #000, #fff)",
210 // "line-clamp: 3",
211 // "user-select: none",
212 ];
213
214 /**
215 * Checks if the browser supports all required APIs.
216 *
217 * @return {boolean} true if the browser is supported, or false otherwise
218 *
219 * @memberof RapidContext.Browser
220 */
221 function isSupported() {
222 var hasRequired = supports(REQUIRED);
223 var hasOptional = supports(OPTIONAL);
224 if (!hasRequired) {
225 console.error("browser: required features are missing", info());
226 } else if (!hasOptional) {
227 console.warn("browser: some recommended features are missing", info());
228 }
229 return hasRequired;
230 }
231
232 /**
233 * Checks for browser support for one or more specified APIs. Supports
234 * checking JavaScript APIs, JavaScript syntax and CSS support.
235 *
236 * @param {...(string|Object|Array)} feature one or more features to check
237 * @return {boolean} `true` if supported, or `false` otherwise
238 *
239 * @example
240 * RapidContext.Browser.supports("Array.isArray") ==> true;
241 * RapidContext.Browser.supports({ test: "let a = 2; a === 2", name: "Let statements" }) ==> true;
242 * RapidContext.Browser.supports("display: flex") ==> false;
243 *
244 * @memberof RapidContext.Browser
245 */
246 function supports(feature/*, ...*/) {
247 function checkCSS(code) {
248 try {
249 return CSS.supports("(" + code + ")");
250 } catch (ignore) {
251 return false;
252 }
253 }
254 function checkPath(base, path) {
255 while (base && path.length > 0) {
256 base = base[path.shift()];
257 }
258 return typeof(base) === "function";
259 }
260 function checkEval(code) {
261 try {
262 /* eslint no-eval: "off" */
263 var val = eval(code);
264 return val !== undefined && val !== null;
265 } catch (ignore) {
266 return false;
267 }
268 }
269 function check(test) {
270 var isCSS = /^[a-z-]+:|selector\(/i.test(test);
271 var isPath = /^[a-z]+(\.[a-z]+)*$/i.test(test);
272 var isValid = (
273 (isCSS && checkCSS(test)) ||
274 (isPath && checkPath(window, test.split("."))) ||
275 (!isCSS && !isPath && checkEval(test))
276 );
277 return isValid;
278 }
279 // NOTE: Code below uses old school JavaScript for compatibility
280 var res = true;
281 var features = (feature instanceof Array) ? feature : Array.prototype.slice.call(arguments);
282 for (var i = 0; i < features.length; i++) {
283 var def = features[i].test ? features[i] : { test: features[i] };
284 if (!check(def.test)) {
285 var explain = [def.name, def.test].filter(Boolean).join(": ");
286 console.warn("browser: missing support for " + explain);
287 res = false;
288 }
289 }
290 return res;
291 }
292
293 /**
294 * Returns browser information version and platform information.
295 *
296 * @param {string} [userAgent] the agent string, or undefined for this browser
297 * @return {Object} a browser meta-data object
298 *
299 * @memberof RapidContext.Browser
300 */
301 function info(userAgent) {
302 function firstMatch(patterns, text) {
303 for (var k in patterns) {
304 var m = patterns[k].exec(text);
305 if (m) {
306 var extra = m[1] ? " " + m[1] : "";
307 if (/^[ \d_.]+$/.test(extra)) {
308 extra = extra.replace(/_/g, ".");
309 }
310 return k + extra;
311 }
312 }
313 return null;
314 }
315 function clone(obj, keys) {
316 var res = {};
317 for (var i = 0; i < keys.length; i++) {
318 var k = keys[i];
319 res[k] = obj[k];
320 }
321 return res;
322 }
323 var BROWSERS = {
324 "Bot": /Bot|Spider|PhantomJS|Headless|Electron|slimerjs|Python/i,
325 "Edge": /Edg(?:e|A|iOS|)\/([^\s;]+)/,
326 "Chrome": /Chrome\/([^\s;]+)/,
327 "Chrome iOS": /CriOS\/([^\s;]+)/,
328 "Firefox": /Firefox\/([^\s;]+)/,
329 "Firefox iOS": /FxiOS\/([^\s;]+)/,
330 "Safari": /Version\/(\S+).* Safari\/[^\s;]+/,
331 "MSIE": /(?:MSIE |Trident\/.*rv:)([^\s;)]+)/
332 };
333 var PLATFORMS = {
334 "Android": /Android ([^;)]+)/,
335 "Chrome OS": /CrOS [^\s]+ ([^;)]+)/,
336 "iOS": /(?:iPhone|CPU) OS ([\d_]+)/,
337 "macOS": /Mac OS X ([^;)]+)/,
338 "Linux": /Linux ([^;)]+)/,
339 "Windows": /Windows ([^;)]+)/
340 };
341 var DEVICES = {
342 "iPad": /iPad/,
343 "iPhone": /iPhone/,
344 "Tablet": /Tablet/,
345 "Mobile": /Mobile|Android/
346 };
347 var ua = userAgent || window.navigator.userAgent;
348 var browser = firstMatch(BROWSERS, ua);
349 var platform = firstMatch(PLATFORMS, ua);
350 var device = firstMatch(DEVICES, ua);
351 var res = {};
352 if (browser && platform) {
353 res["browser"] = browser;
354 res["platform"] = platform;
355 res["device"] = device || "Desktop/Other";
356 }
357 if (!userAgent) {
358 res["language"] = window.navigator.language;
359 res["screen"] = clone(window.screen, ["width", "height", "colorDepth"]);
360 res["window"] = clone(window, ["innerWidth", "innerHeight", "devicePixelRatio"]);
361 res["cookies"] = _cookies();
362 }
363 res["userAgent"] = String(ua);
364 return res;
365 }
366
367 function _cookies() {
368 var cookies = {};
369 var pairs = window.document.cookie.split(/\s*;\s*/g);
370 pairs.forEach(function (pair) {
371 var name = pair.split("=")[0];
372 var value = pair.substr(name.length + 1);
373 if (name && value) {
374 cookies[decodeURIComponent(name)] = decodeURIComponent(value);
375 }
376 });
377 return cookies;
378 }
379
380 /**
381 * Gets, sets or removes browser cookies.
382 *
383 * @param {string} [name] the cookie name to get/set
384 * @param {string} [value] the cookie value to set, or null to remove
385 * @return {Object|string} all cookie values or a single value
386 *
387 * @memberof RapidContext.Browser
388 */
389 function cookie(name, value) {
390 if (name === undefined || name === null) {
391 return _cookies();
392 } else if (value === undefined) {
393 return _cookies()[name];
394 } else if (value === null) {
395 var prefix = encodeURIComponent(name) + "=";
396 var suffix = ";path=/;expires=" + new Date(0).toUTCString();
397 var domain = window.location.hostname.split(".");
398 while (domain.length > 1) {
399 var domainPart = ";domain=" + domain.join(".");
400 window.document.cookie = prefix + domainPart + suffix;
401 domain.shift();
402 }
403 window.document.cookie = prefix + suffix;
404 return null;
405 } else {
406 window.document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + ";path=/";
407 return value;
408 }
409 }
410
411 // Create namespaces
412 var RapidContext = window.RapidContext || (window.RapidContext = {});
413 var module = RapidContext.Browser || (RapidContext.Browser = {});
414
415 // Export namespace symbols
416 module.REQUIRED = REQUIRED;
417 module.OPTIONAL = OPTIONAL;
418 module.isSupported = isSupported;
419 module.supports = supports;
420 module.info = info;
421 module.cookie = cookie;
422
423})(globalThis);
424
RapidContext
Access · Discovery · Insight
www.rapidcontext.com