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