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