Merge pull request #8054 from keymanapp/change/web/device-detector-modularization

change(web): device-detector subpackage modularization 🧩
This commit is contained in:
Joshua Horton 2023-02-03 08:45:31 +07:00 committed by GitHub
commit f4ea4acfff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 209 additions and 223 deletions

View file

@ -7,8 +7,6 @@
# Exit on command failure and when using unset variables:
set -eu
# Include some helper functions from resources
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")"

2
package-lock.json generated
View file

@ -7934,7 +7934,7 @@
"@keymanapp/web-utils": "*",
"@types/node": "^10.17.21",
"chai": "^4.3.4",
"combine-source-map": "*",
"combine-source-map": "^0.8.0",
"es6-shim": "^0.35.5",
"karma": "^6.4.1",
"karma-browserstack-launcher": "^1.6.0",

View file

@ -39,6 +39,10 @@ builder_parse "$@"
####
TIER=`cat ../TIER.md`
BUILD_NUMBER=`cat ../VERSION.md`
S_KEYMAN_COM=../../s.keyman.com
if builder_start_action build; then
# Build step: since CI builds start (and should start) from scratch, run the following
# three actions:

View file

@ -1,203 +1,201 @@
// Includes version-related functionality
///<reference path="utils/styleConstants.ts" />
import StyleConstants from 'utils/styleConstants.js';
import { DeviceSpec, Version } from "@keymanapp/web-utils/build/obj/index.js";
// The Device object definition -------------------------------------------------
namespace com.keyman {
export class Device {
touchable: boolean;
OS: string;
formFactor: string;
dyPortrait: number;
dyLandscape: number;
version: string;
orientation: string|number;
browser: string;
colorScheme: 'light' | 'dark';
export class Device {
touchable: boolean;
OS: string;
formFactor: string;
dyPortrait: number;
dyLandscape: number;
version: string;
orientation: string|number;
browser: string;
colorScheme: 'light' | 'dark';
private detected: boolean = false;
private _styles: utils.StyleConstants;
private detected: boolean = false;
private _styles: StyleConstants;
// Generates a default Device value.
constructor() {
this.touchable = !!('ontouchstart' in window);
this.OS = '';
this.formFactor='desktop';
this.dyPortrait=0;
this.dyLandscape=0;
this.version='0';
this.orientation=window.orientation;
this.browser='';
}
// Generates a default Device value.
constructor() {
this.touchable = !!('ontouchstart' in window);
this.OS = '';
this.formFactor='desktop';
this.dyPortrait=0;
this.dyLandscape=0;
this.version='0';
this.orientation=window.orientation;
this.browser='';
}
/**
* Get device horizontal DPI for touch devices, to set actual size of active regions
* Note that the actual physical DPI may be somewhat different.
*
* @return {number}
*/
getDPI(): number {
var t=document.createElement('DIV') ,s=t.style,dpi=96;
if(document.readyState !== 'complete') {
return dpi;
}
t.id='calculateDPI';
s.position='absolute'; s.display='block';s.visibility='hidden';
s.left='10px'; s.top='10px'; s.width='1in'; s.height='10px';
document.body.appendChild(t);
dpi=(typeof window.devicePixelRatio == 'undefined') ? t.offsetWidth : t.offsetWidth * window.devicePixelRatio;
document.body.removeChild(t);
/**
* Get device horizontal DPI for touch devices, to set actual size of active regions
* Note that the actual physical DPI may be somewhat different.
*
* @return {number}
*/
getDPI(): number {
var t=document.createElement('DIV') ,s=t.style,dpi=96;
if(document.readyState !== 'complete') {
return dpi;
}
detect() : void {
var possMacSpoof = false;
t.id='calculateDPI';
s.position='absolute'; s.display='block';s.visibility='hidden';
s.left='10px'; s.top='10px'; s.width='1in'; s.height='10px';
document.body.appendChild(t);
dpi=(typeof window.devicePixelRatio == 'undefined') ? t.offsetWidth : t.offsetWidth * window.devicePixelRatio;
document.body.removeChild(t);
return dpi;
}
if(navigator && navigator.userAgent) {
var agent=navigator.userAgent;
detect() : void {
var possMacSpoof = false;
if(agent.indexOf('iPad') >= 0) {
this.OS='iOS';
this.formFactor='tablet';
this.dyPortrait=this.dyLandscape=0;
} else if(agent.indexOf('iPhone') >= 0) {
this.OS='iOS';
this.formFactor='phone';
this.dyPortrait=this.dyLandscape=25;
} else if(agent.indexOf('Android') >= 0) {
this.OS='Android';
this.formFactor='phone'; // form factor may be redefined on initialization
this.dyPortrait=75;
this.dyLandscape=25;
try {
var rx=new RegExp("(?:Android\\s+)(\\d+\\.\\d+\\.\\d+)");
this.version=agent.match(rx)[1];
} catch(ex) {}
} else if(agent.indexOf('Linux') >= 0) {
this.OS='Linux';
} else if(agent.indexOf('Macintosh') >= 0) {
// Starting with 13.1, "Macintosh" can reflect iPads (by default) or iPhones
// (by user setting); a new "Request Desktop Website" setting for Safari will
// change the user agent string to match a desktop Mac.
//
// Firefox uses '.' between version components, while Chrome and Safari use
// '_' instead. So, we have to check for both. Yay.
let regex = /Intel Mac OS X (\d+(?:[_\.]\d+)+)/i;
let results = regex.exec(agent);
if(navigator && navigator.userAgent) {
var agent=navigator.userAgent;
// Match result: a version string with components separated by underscores.
if(!results) {
console.warn("KMW could not properly parse the user agent string."
+ "A suboptimal keyboard layout may result.");
this.OS='MacOSX';
} else if(results.length > 1 && results[1]) {
// Convert version string into a usable form.
let versionString = results[1].replace('_', '.');
let version = new utils.Version(versionString);
if(agent.indexOf('iPad') >= 0) {
this.OS='iOS';
this.formFactor='tablet';
this.dyPortrait=this.dyLandscape=0;
} else if(agent.indexOf('iPhone') >= 0) {
this.OS='iOS';
this.formFactor='phone';
this.dyPortrait=this.dyLandscape=25;
} else if(agent.indexOf('Android') >= 0) {
this.OS='Android';
this.formFactor='phone'; // form factor may be redefined on initialization
this.dyPortrait=75;
this.dyLandscape=25;
try {
var rx=new RegExp("(?:Android\\s+)(\\d+\\.\\d+\\.\\d+)");
this.version=agent.match(rx)[1];
} catch(ex) {}
} else if(agent.indexOf('Linux') >= 0) {
this.OS='Linux';
} else if(agent.indexOf('Macintosh') >= 0) {
// Starting with 13.1, "Macintosh" can reflect iPads (by default) or iPhones
// (by user setting); a new "Request Desktop Website" setting for Safari will
// change the user agent string to match a desktop Mac.
//
// Firefox uses '.' between version components, while Chrome and Safari use
// '_' instead. So, we have to check for both. Yay.
let regex = /Intel Mac OS X (\d+(?:[_\.]\d+)+)/i;
let results = regex.exec(agent);
possMacSpoof = utils.Version.MAC_POSSIBLE_IPAD_ALIAS.compareTo(version) <= 0;
this.OS='MacOSX';
}
} else if(agent.indexOf('Windows NT') >= 0) {
this.OS='Windows';
if(agent.indexOf('Touch') >= 0) {
this.formFactor='phone'; // will be redefined as tablet if resolution high enough
}
// Match result: a version string with components separated by underscores.
if(!results) {
console.warn("KMW could not properly parse the user agent string."
+ "A suboptimal keyboard layout may result.");
this.OS='MacOSX';
} else if(results.length > 1 && results[1]) {
// Convert version string into a usable form.
let versionString = results[1].replace('_', '.');
let version = new Version(versionString);
// Windows Phone and Tablet PC
if(typeof (<any>navigator).msMaxTouchPoints == 'number' && (<any>navigator).msMaxTouchPoints > 0) {
this.touchable=true;
}
possMacSpoof = Version.MAC_POSSIBLE_IPAD_ALIAS.compareTo(version) <= 0;
this.OS='MacOSX';
}
} else if(agent.indexOf('Windows NT') >= 0) {
this.OS='Windows';
if(agent.indexOf('Touch') >= 0) {
this.formFactor='phone'; // will be redefined as tablet if resolution high enough
}
// Windows Phone and Tablet PC
if(typeof (<any>navigator).msMaxTouchPoints == 'number' && (<any>navigator).msMaxTouchPoints > 0) {
this.touchable=true;
}
}
}
// We look at the screen resolution for Android, because we can't tell from
// the user agent string whether or not this is supposed to be a tablet.
// It seems that there are a handful of older phones out there that report a
// higher resolution than 700px*___px, but it is proving hard to test these,
// and the majority have an aspect ratio <= 0.5625 anyway.
// But we trust what iOS tells us for phone vs tablet.
// We look at the screen resolution for Android, because we can't tell from
// the user agent string whether or not this is supposed to be a tablet.
// It seems that there are a handful of older phones out there that report a
// higher resolution than 700px*___px, but it is proving hard to test these,
// and the majority have an aspect ratio <= 0.5625 anyway.
// But we trust what iOS tells us for phone vs tablet.
const dimMin = Math.min(screen.width,screen.height), dimMax = Math.max(screen.width,screen.height);
const aspect = dimMin / dimMax;
const dimMin = Math.min(screen.width,screen.height), dimMax = Math.max(screen.width,screen.height);
const aspect = dimMin / dimMax;
if(this.OS != 'iOS' &&
this.formFactor == 'phone' &&
((dimMin >= 600 && aspect > 0.5625) || // 0.5625 -> 1920x1080 is common phone res
(aspect >= 0.625)) // all reported devices with aspect >= 0.625 are tablets per https://screensiz.es/
) {
this.formFactor='tablet';
}
if(this.OS != 'iOS' &&
this.formFactor == 'phone' &&
((dimMin >= 600 && aspect > 0.5625) || // 0.5625 -> 1920x1080 is common phone res
(aspect >= 0.625)) // all reported devices with aspect >= 0.625 are tablets per https://screensiz.es/
) {
this.formFactor='tablet';
}
// Test for potential Chrome emulation on Windows or macOS X (used only in next if-check)
let possibleChromeEmulation = navigator.platform == 'Win32' || navigator.platform == 'MacIntel'
// Test for potential Chrome emulation on Windows or macOS X (used only in next if-check)
let possibleChromeEmulation = navigator.platform == 'Win32' || navigator.platform == 'MacIntel'
// alert(sxx+'->'+device.formFactor);
// Check for phony iOS devices (but don't undo for Chrome emulation used during development)
if(this.OS == 'iOS' && !('ongesturestart' in window) && !possibleChromeEmulation) {
this.OS='Android';
}
// alert(sxx+'->'+device.formFactor);
// Check for phony iOS devices (but don't undo for Chrome emulation used during development)
if(this.OS == 'iOS' && !('ongesturestart' in window) && !possibleChromeEmulation) {
this.OS='Android';
}
// Determine application or browser
this.browser='web';
if(this.OS == 'iOS' || this.OS.toLowerCase() == 'macosx') {
// Determine application or browser
this.browser='web';
if(this.OS == 'iOS' || this.OS.toLowerCase() == 'macosx') {
this.browser='safari';
}
var bMatch=/Firefox|Chrome|OPR|Safari|Edge/;
if(bMatch.test(navigator.userAgent)) {
if((navigator.userAgent.indexOf('Firefox') >= 0) && ('onmozorientationchange' in screen)) {
this.browser='firefox';
} else if(navigator.userAgent.indexOf('OPR') >= 0) {
this.browser='opera';
} else if(navigator.userAgent.indexOf(' Edge/') >= 0) {
// Edge is too common a word, so test for Edge/ :)
// Must come before Chrome and Safari test because
// Edge pretends to be both
this.browser='edge';
} else if(navigator.userAgent.indexOf('Chrome') >= 0) {
// This test must come before Safari test because on macOS,
// Chrome also reports "Safari"
this.browser='chrome';
} else if(navigator.userAgent.indexOf('Safari') >= 0) {
this.browser='safari';
}
var bMatch=/Firefox|Chrome|OPR|Safari|Edge/;
if(bMatch.test(navigator.userAgent)) {
if((navigator.userAgent.indexOf('Firefox') >= 0) && ('onmozorientationchange' in screen)) {
this.browser='firefox';
} else if(navigator.userAgent.indexOf('OPR') >= 0) {
this.browser='opera';
} else if(navigator.userAgent.indexOf(' Edge/') >= 0) {
// Edge is too common a word, so test for Edge/ :)
// Must come before Chrome and Safari test because
// Edge pretends to be both
this.browser='edge';
} else if(navigator.userAgent.indexOf('Chrome') >= 0) {
// This test must come before Safari test because on macOS,
// Chrome also reports "Safari"
this.browser='chrome';
} else if(navigator.userAgent.indexOf('Safari') >= 0) {
this.browser='safari';
}
}
if(possMacSpoof && this.browser == 'safari') {
// Indistinguishable user agent string! We need a different test; fortunately, true macOS
// Safari doesn't support TouchEvents. (Chrome does, though! Hence the filter above.)
if(window['TouchEvent']) {
this.OS='iOS';
this.formFactor='tablet';
this.dyPortrait=this.dyLandscape=0;
// It's currently impossible to differentiate between iPhone and iPad here
// except for by screen dimensions.
let aspectRatio = screen.height / screen.width;
if(aspectRatio < 1) {
aspectRatio = 1 / aspectRatio;
}
// iPhones usually have a ratio of 16:9 (or 1.778) or higher, while iPads use 4:3 (or 1.333)
if(aspectRatio > 1.6) {
// Override - we'll treat this device as an iPhone.
this.formFactor = 'phone';
this.dyPortrait=this.dyLandscape=25;
}
}
}
this.colorScheme = utils.StyleConstants.prefersDarkMode() ? 'dark' : 'light';
this.detected = true;
}
/**
* Returns a slimmer, web-core compatible version of this object.
*/
public get coreSpec(): utils.DeviceSpec {
return new utils.DeviceSpec(this.browser, this.formFactor, this.OS, this.touchable);
if(possMacSpoof && this.browser == 'safari') {
// Indistinguishable user agent string! We need a different test; fortunately, true macOS
// Safari doesn't support TouchEvents. (Chrome does, though! Hence the filter above.)
if(window['TouchEvent']) {
this.OS='iOS';
this.formFactor='tablet';
this.dyPortrait=this.dyLandscape=0;
// It's currently impossible to differentiate between iPhone and iPad here
// except for by screen dimensions.
let aspectRatio = screen.height / screen.width;
if(aspectRatio < 1) {
aspectRatio = 1 / aspectRatio;
}
// iPhones usually have a ratio of 16:9 (or 1.778) or higher, while iPads use 4:3 (or 1.333)
if(aspectRatio > 1.6) {
// Override - we'll treat this device as an iPhone.
this.formFactor = 'phone';
this.dyPortrait=this.dyLandscape=25;
}
}
}
this.colorScheme = StyleConstants.prefersDarkMode() ? 'dark' : 'light';
this.detected = true;
}
/**
* Returns a slimmer, web-core compatible version of this object.
*/
public get coreSpec(): DeviceSpec {
return new DeviceSpec(this.browser, this.formFactor, this.OS, this.touchable);
}
}

View file

@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outFile": "../../../build/engine/device-detect/obj/index.bundled.js",
},
"references": [
{ "path": "../../../../common/web/keyman-version", "prepend": true },
{ "path": "../../../../common/web/utils", "prepend": true }
]
}

View file

@ -2,18 +2,19 @@
"extends": "../../../../tsconfig-base.json",
"compilerOptions": {
"allowJs": true,
"allowJs": false,
"inlineSources": true,
"module": "none",
"outFile": "../../../build/engine/device-detect/obj/index.js",
"module": "es6",
"moduleResolution": "Node",
"sourceMap": true,
"target": "es5"
"target": "es5",
"baseUrl": "./",
"outDir": "../../../build/engine/device-detect/obj/",
"tsBuildInfoFile": "../../../build/engine/device-detect/obj/tsconfig.tsbuildinfo",
"rootDir": "."
},
"files": [
"kmwdevice.ts",
"utils/styleConstants.ts", // goes with kmwdevice.ts
],
"include": [ "**/*.ts" ],
"references": [
{ "path": "../../../../common/web/keyman-version" },

View file

@ -1,34 +1,31 @@
// Includes Device definitions, which may play a role in constant logic.
///<reference path="../kmwdevice.ts" />
import { DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js";
/*
* This file is intended for CSS-styling constants that see use with the OSK.
*/
namespace com.keyman.utils {
/**
* Defines device-level constants used for CSS styling.
*/
export class StyleConstants {
constructor(device: com.keyman.utils.DeviceSpec) {
// popupCanvasBackgroundColor
if(device.OS == utils.OperatingSystem.Android) {
this.popupCanvasBackgroundColor = '#999';
} else {
this.popupCanvasBackgroundColor = StyleConstants.prefersDarkMode() ? '#0f1319' : '#ffffff';
}
/**
* Defines device-level constants used for CSS styling.
*/
export default class StyleConstants {
constructor(device: DeviceSpec) {
// popupCanvasBackgroundColor
if(device.OS == DeviceSpec.OperatingSystem.Android) {
this.popupCanvasBackgroundColor = '#999';
} else {
this.popupCanvasBackgroundColor = StyleConstants.prefersDarkMode() ? '#0f1319' : '#ffffff';
}
/**
* Checks is a user's browser is in dark mode, if the feature is supported. Returns false otherwise.
*
* Thanks to https://stackoverflow.com/a/57795518 for this code.
*/
static prefersDarkMode(): boolean {
// Ensure the detector exists (otherwise, returns false)
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
public readonly popupCanvasBackgroundColor: string;
}
/**
* Checks is a user's browser is in dark mode, if the feature is supported. Returns false otherwise.
*
* Thanks to https://stackoverflow.com/a/57795518 for this code.
*/
static prefersDarkMode(): boolean {
// Ensure the detector exists (otherwise, returns false)
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
public readonly popupCanvasBackgroundColor: string;
}