import { StringEncoder, Utils as BaseUtils, MathUtils } from '@uino/base-thing';
import { System } from './System';
const _defaultOptions = {};
// Default threshold is 2, which matches the original logic: distance <= 2 triggers click
let _pointerMoveThreshold = 2;
// #region Internal Functions
let _codeMap = {};
let _codeId = 1;
window.__loadCodeComplete = function (key) {
_codeMap[key] = { result: true };
}
window.__catchError = function (key, error) {
_codeMap[key] = { result: false, error };
}
function _getDurationByMetric(measureName) {
const performanceEntries = window.performance.getEntriesByName(measureName);
const entry = performanceEntries[performanceEntries.length - 1];
if (entry && entry.entryType === 'measure') {
return entry.duration;
}
return -1;
}
function _performanceMeasure(measureName) {
const startMark = `mark_${measureName}_start`;
const endMark = `mark_${measureName}_end`;
window.performance.measure(measureName, startMark, endMark);
return _getDurationByMetric(measureName);
}
function _getPath(string) {
let startIndex = (string.indexOf('\\') >= 0 ? string.lastIndexOf('\\') : string.lastIndexOf('/'));
return string.substring(0, startIndex);
}
const isPerformanceSupported = window.performance && !!window.performance.getEntriesByType && !!window.performance.now && !!window.performance.mark;
function _appendTimestamp(name) {
if (name.lastIndexOf("?") == -1) {
return name + "?time=" + new Date().getTime();
}
else {
return name + "&time=" + new Date().getTime();
}
}
function _removeElementsByURL(tagName, attributeName, url) {
// Remove existing CSS items
let items = document.getElementsByTagName(tagName);
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (!item) {
continue;
}
let src = item.getAttribute(attributeName);
if (!src) {
continue;
}
// Compare without '?' search name
let name = src._removeSearchName();
if (name != url) {
continue;
}
item.parentNode.removeChild(item);
i--;
}
}
function _JSLoaderFromFile(url, cache, load, error) {
// Remove existing script items
_removeElementsByURL("script", "src", url);
// If we do not use cache then append the timestamp to make sure download it
if (!cache) {
url = _appendTimestamp(url);
}
// Create JS element
let script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function () { // chrome ff
if (load) {
load({ script });
}
};
script.onreadystatechange = function () { // ie
if (this.readyState == 'loaded' || this.readyState == 'complete') {
if (load) {
load({ script });
}
}
};
script.onerror = function () {
if (error) {
error();
}
}
document.getElementsByTagName("head")[0].appendChild(script);
}
function _CSSLoaderFromFile(url, cache, load, error) {
// Remove existing CSS items
_removeElementsByURL("link", "href", url);
// If we do not use cache then append the timestamp to make sure download it
if (!cache) {
url = _appendTimestamp(url);
}
// Create CSS element
let head = document.getElementsByTagName('head')[0];
let link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = url;
head.appendChild(link);
link.onreadystatechange = function () { // ie
if (css.readyState == 'complete') {
if (load) {
load();
}
}
};
link.onload = function () { // chrome ff
if (load) {
load();
}
};
link.onerror = function () {
if (error) {
error();
}
}
}
function _asyncLoadFile(url, cache, load, error) {
if (!url) {
return;
}
url = BaseUtils.resolveURL(url);
// Get the loader
let loader;
let extension = url._getExtension();
if (extension == "js") {
loader = _JSLoaderFromFile;
}
else if (extension == "css") {
loader = _CSSLoaderFromFile;
}
if (loader) {
loader(url, cache, load, error);
}
}
// #endregion
/**
* @class Utils
* Useful functions.
* @memberof THING
* @extends THING.UTILS.Utils
*/
class Utils extends BaseUtils {
static mainScriptUrl = Utils.getCurrentScriptUrl();
static hasPointerEvent = false;
static #options = {};
static system = new System();
/**
* @typedef {object} DomWorkerInitOptions
* @property {string} rootPath The working root path.
* @property {boolean} basis True indicates to enable basis compressed texture encoder and loader.
*/
/**
* @typedef {object} DomInitOptions
* @property {DomWorkerInitOptions} worker The worker options.
*/
/**
* Initialize.
* @param {DomInitOptions} options The options.
* @example
* THING.DOM.Utils.init({
* worker: {
* rootPath: './js/thing.dom.dist/workers',
* basis: true
* }
* });
*/
static init(options = {}) {
Utils.#options = Utils.cloneObject(options, false);
}
static getOptions() {
return Utils.#options;
}
/**
* 设置鼠标移动判断的阈值距离。
* @param {number} threshold 阈值距离(像素),默认值为2。
* @example
* THING.DOM.Utils.setPointerMoveThreshold(5);
* @public
*/
static setPointerMoveThreshold(threshold) {
if (Utils.isNumber(threshold) && threshold >= 0) {
_pointerMoveThreshold = threshold;
}
}
/**
* 获取鼠标移动判断的阈值距离。
* @returns {number} 阈值距离(像素)。
* @example
* let threshold = THING.DOM.Utils.getPointerMoveThreshold();
* @public
*/
static getPointerMoveThreshold() {
return _pointerMoveThreshold;
}
static parseCSSColor(value, defaultValue) {
if (Utils.isNull(value)) {
return defaultValue;
}
else if (Utils.isString(value) && value._startsWith('#')) {
return value;
}
else {
let color = Utils.parseColor(value, defaultValue);
let hex = (color[0] * 255) << 16 ^ (color[1] * 255) << 8 ^ (color[2] * 255) << 0;
return '#' + (('000000' + hex.toString(16)).slice(-6));
}
}
static parseMouseEventType(ev, type) {
let eventType = type || ev.type;
switch (eventType) {
case 'pointerdown':
eventType = 'mousedown';
break;
case 'pointermove':
eventType = 'mousemove';
break;
case 'pointerup':
eventType = 'mouseup';
break;
default:
break;
}
return eventType;
}
static buildKeyboardEvent(ev) {
// Because the keydown/up event only return uppercase code, so we need to fix it
let keyCode;
if (ev.key >= 'a' && ev.key <= 'z') {
keyCode = Utils.atoi(ev.key);
}
else {
keyCode = ev.keyCode || ev.which || ev.charCode;
}
return {
"type": ev.type,
"keyCode": keyCode, // We can use THING.KeyType.XXX to check it
"code": ev.code,
"location": ev.location,
"altKey": ev.altKey,
"ctrlKey": ev.ctrlKey,
"shiftKey": ev.shiftKey,
};
}
static buildMouseEvent(domElement, ev, type, mousePosition) {
let _mousePosition = mousePosition || Utils.getMousePosition(domElement, ev);
let mouseEvent = {
type: Utils.parseMouseEventType(ev, type),
x: _mousePosition.x,
y: _mousePosition.y,
button: ev.button || 0,
buttons: ev.buttons || 0,
altKey: ev.altKey,
ctrlKey: ev.ctrlKey,
shiftKey: ev.shiftKey,
};
if (Utils.isValid(ev.pointerId)) {
mouseEvent.pointerId = ev.pointerId;
}
if (Utils.isValid(ev.pointerType)) {
mouseEvent.pointerType = ev.pointerType;
}
return mouseEvent;
}
static buildTouchEvent(domElement, ev) {
let createTouchEvent = function (domElement, touch) {
let mousePosition = Utils.getMousePosition(domElement, touch);
return {
identifier: touch.identifier,
force: touch.force,
x: mousePosition.x,
y: mousePosition.y,
altKey: ev.altKey,
ctrlKey: ev.ctrlKey,
shiftKey: ev.shiftKey,
};
}
let touchEvent = {
type: ev.type,
altKey: ev.altKey,
ctrlKey: ev.ctrlKey,
shiftKey: ev.shiftKey,
};
touchEvent['changedTouches'] = [];
for (let i = 0; i < ev.changedTouches.length; i++) {
let touch = ev.changedTouches[i];
touchEvent['changedTouches'].push(createTouchEvent(domElement, touch));
}
touchEvent['targetTouches'] = [];
for (let i = 0; i < ev.targetTouches.length; i++) {
let touch = ev.targetTouches[i];
touchEvent['targetTouches'].push(createTouchEvent(domElement, touch));
}
touchEvent['touches'] = [];
for (let i = 0; i < ev.touches.length; i++) {
let touch = ev.touches[i];
touchEvent['touches'].push(createTouchEvent(domElement, touch));
}
return touchEvent;
}
static getMousePosition(domElement, ev, offset) {
let x, y;
// Thanks for sharing code: http://www.jacklmoore.com/notes/mouse-position/
if (domElement) {
let style = domElement.currentStyle || window.getComputedStyle(domElement, null);
let borderLeftWidth = offset ? offset[0] : parseInt(style['borderLeftWidth'], 10);
let borderTopWidth = offset ? offset[1] : parseInt(style['borderTopWidth'], 10);
let rect = domElement.getBoundingClientRect();
x = ev.clientX - borderLeftWidth - rect.left;
y = ev.clientY - borderTopWidth - rect.top;
}
else {
if (ev.layerX || ev.layerX == 0) {
x = ev.layerX;
y = ev.layerY;
}
else if (ev.offsetX || ev.offsetX == 0) { // Opera
x = ev.offsetX;
y = ev.offsetY;
}
else if (ev.clientX || ev.clientX == 0) {
x = ev.clientX;
y = ev.clientY;
}
}
return { x: Math.round(x), y: Math.round(y) };
}
/**
* Start performance measurement.
* @param {string} name The measure name.
* @example
* THING.DOM.Utils.startMark('exampleMeasure');
*/
static startMark(name) {
if (!isPerformanceSupported) {
return;
}
window.performance.mark(`mark_${name}_start`);
}
/**
* End performance measurement.
* @param {string} name The measure name.
* @example
* THING.DOM.Utils.endMark('exampleMeasure');
*/
static endMark(name) {
if (!isPerformanceSupported) {
return;
}
// End Performance Mark
window.performance.mark(`mark_${name}_end`);
return _performanceMeasure(name);
}
static normalizeURL(url) {
// 使用 BaseUtils.resolveURL 处理 protocol-relative URL
url = BaseUtils.resolveURL(url);
if (url === undefined) {
url = '';
}
if (url._startsWith('http')) {
return url;
}
if (url._startsWith('file://')) {
return url;
}
if (url._startsWith('blob')) {
return url;
}
if (url._startsWith('/')) {
return window.location.origin + url;
}
if (url._startsWith('./')) {
url = url.substring(2);
}
let origin = window.location.origin;
let pathname = window.location.pathname;
return _getPath(origin + pathname) + '/' + url;
}
static getUserAngent() {
if (!window["navigator"]) {
return '';
}
let ua = navigator.userAgent.toLowerCase();
return ua;
}
/**
* Check whether the given value is a DOM element.
* @param {*} value The value to check.
* @return {boolean} True if the value is a DOM element, false otherwise.
* @example
* const div = document.createElement('div');
* console.log(THING.DOM.Utils.isDom(div)); // Output: true
* console.log(THING.DOM.Utils.isDom('div')); // Output: false
*/
static isDom(value) {
return typeof value === 'object'
&& typeof value.nodeType === 'number'
&& typeof value.ownerDocument === 'object';
}
static isMobileDevice() {
let ua = this.getUserAngent();
return (ua.indexOf('mobile') != -1 || ua.indexOf('android') != -1 || ua.indexOf('iphone') != -1);
}
static loadImage(url, onLoad, onError) {
let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
image.crossOrigin = 'anonymous';
function onImageLoad() {
image.removeEventListener('load', onImageLoad, false);
image.removeEventListener('error', onImageError, false);
if (onLoad) {
onLoad(this);
}
}
function onImageError(event) {
image.removeEventListener('load', onImageLoad, false);
image.removeEventListener('error', onImageError, false);
if (onError) {
onError(event);
}
}
image.addEventListener('load', onImageLoad, false);
image.addEventListener('error', onImageError, false);
image.src = url;
return image;
}
// Code taken from MatthewCrumley (https://stackoverflow.com/a/934925/298479)
static imageToBase64(img) {
// Create an empty canvas element
let canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
// Copy the image contents to the canvas
let ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// Get the data-URL formatted image
// Firefox supports PNG and JPEG. You could check img.src to guess the
// original format, but be aware the using "image/jpg" will re-encode the image.
let dataURL = canvas.toDataURL("image/png");
return dataURL;
}
static base64ToImage(src) {
let image = new Image();
image.src = src;
return image;
}
static loadBitmapImageFromBase64Async(src) {
function dataURItoBlob(dataURI) {
let mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
let byteString = atob(dataURI.split(',')[1]);
let arrayBuffer = new ArrayBuffer(byteString.length);
let intArray = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
intArray[i] = byteString.charCodeAt(i);
}
return new Blob([intArray], { type: mimeString });
}
let blob = dataURItoBlob(src);
return createImageBitmap(blob, { imageOrientation: 'flipY' });
}
static loadBitmapImageFromUrlAsync(url, options = _defaultOptions) {
return new Promise(async (resolve, reject) => {
try {
url = Utils.normalizeURL(url);
const res = await fetch(url, { mode: 'cors' });
const blob = await res.blob();
if (!res.ok) {
reject('Image loading failed: ' + url);
return;
}
let imageOptions = {
premultiplyAlpha: 'none',
colorSpaceConversion: 'none'
};
if (options['flipY']) {
imageOptions.imageOrientation = 'flipY';
}
if (options['premultiplyAlpha']) {
imageOptions.premultiplyAlpha = 'premultiply';
}
const bitmap = await createImageBitmap(blob, imageOptions);
resolve(bitmap);
}
catch (ex) {
reject(ex);
}
});
}
static canvasToDataURL(canvas, format, quality) {
return canvas.toDataURL(format || 'image/jpeg', quality || 1.0);
}
static dataURLToCanvas(dataurl, cb) {
let canvas = document.createElement('CANVAS');
let ctx = canvas.getContext('2d');
let img = new Image();
img.onload = function () {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
cb(canvas);
};
img.src = dataurl;
}
static imageToCanvas(src, cb) {
let canvas = document.createElement('CANVAS');
let ctx = canvas.getContext('2d');
let img = new Image();
img.src = src;
img.onload = function () {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
cb(canvas);
};
}
static canvasToImage(canvas) {
let img = new Image();
img.src = canvas.toDataURL('image/jpeg', 1.0);
return img;
}
static fileOrBlobToDataURL(obj, cb) {
let a = new FileReader();
a.readAsDataURL(obj);
a.onload = function (e) {
cb(e.target.result);
};
}
static dataURLToBlob(dataurl) {
let arr = dataurl.split(',');
let mime = arr[0].match(/:(.*?);/)[1];
let bstr = atob(arr[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
static blobToImage(blob, cb) {
Utils.fileOrBlobToDataURL(blob, function (dataurl) {
let img = new Image();
img.src = dataurl;
cb(img);
});
}
static imageToBlob(src, cb) {
Utils.imageToCanvas(src, function (canvas) {
cb(Utils.dataURLToBlob(Utils.canvasToDataURL(canvas)));
});
}
static blobToCanvas(blob, cb) {
Utils.fileOrBlobToDataURL(blob, function (dataurl) {
Utils.dataURLToCanvas(dataurl, cb);
});
}
static canvasToBlob(canvas, cb) {
cb(Utils.dataURLToBlob(Utils.canvasToDataURL(canvas)));
}
static imageToDataURL(src, cb) {
Utils.imageToCanvas(src, function (canvas) {
cb(Utils.canvasToDataURL(canvas));
});
}
static dataURLToImage(dataurl) {
let img = new Image();
img.src = dataurl;
return img;
}
static dataToImage(type, data) {
let image = new Image();
image.src = URL.createObjectURL(
new Blob([data.buffer], { type: `image/${type}` })
);
return image;
}
/**
* Dynamically evaluate JavaScript-as-string in the browser.
* @param {string} js The JS code.
* @param {object | Array<string>} options The options.
* @param {boolean} options.es6Mode True indicates load code in es6 mode.
* @param {string} options.sourceURL The source URL in sync mode.
* @param {Function} options.onLoad The load finished callback function.
* @param {Function} options.onError The error occurred callback function.
* @example
* // Example usage in es6 mode
* THING.DOM.Utils.loadJSCode('console.log("Hello World");', {
* es6Mode: true,
* sourceURL: 'example.js',
* onLoad: function() {
* console.log('Code loaded successfully.');
* },
* onError: function(error) {
* console.error('Error loading code:', error);
* }
* });
* // Example usage in common mode
* THING.DOM.Utils.loadJSCode('console.log("Hello World");', {
* onLoad: function() {
* console.log('Code loaded successfully.');
* },
* onError: function() {
* console.error('Error loading code.');
* }
* });
*/
static loadJSCode(js, options = {}) {
try {
let es6Mode = options['es6Mode'];
let load = options['onLoad'] || options['load'];
let error = options['onError'] || options['error'];
let script = document.createElement('script');
// Load in es6 mode
if (es6Mode) {
// Add file key in order to find much easiler
let sourceURL = options['sourceURL'] || MathUtils.generateUUID();
sourceURL += '_' + _codeId++;
let code = `//# sourceURL=${sourceURL}\n`;
code += `try { \n`;
code += js;
// Link load function in order to invoke load callback function
code += `\n\n__loadCodeComplete('${sourceURL}');\n`;
code += `} \n catch (ex) { \n __catchError('${sourceURL}', ex); }`;
// Use es6 mode to load script
script.type = 'module';
script.text = code;
document.head.appendChild(script);
// Wait for code loaded, check every 5ms
let timer = setInterval(() => {
let codeInfo = _codeMap[sourceURL];
if (codeInfo) {
clearInterval(timer);
if (codeInfo.result) {
if (load) {
load();
}
}
else {
if (error) {
error(codeInfo.error);
}
}
delete _codeMap[sourceURL];
}
}, 5);
}
// Load in common mode
else {
script.onload = function () { // chrome ff
if (load) {
load();
}
};
script.onreadystatechange = function () { // ie
if (this.readyState == 'loaded' || this.readyState == 'complete') {
if (load) {
load();
}
}
};
script.onerror = function () {
if (error) {
error();
}
}
script.type = 'text/javascript';
script.src = 'data:text/javascript;charset=utf-8,' + escape(js);
document.head.appendChild(script);
}
}
catch (ex) {
console.error(ex);
}
}
/**
* Loads a file or an array of files.
*
* @param {(string | Array<string>)} resource The URL(s) of the file(s) to load.
* @param {(object | Array<string>)} options The options for loading the file(s).
* @param {Function} options.onLoad The callback function to call when the file(s) have finished loading.
* @param {Function} options.onError The callback function to call if an error occurs during loading.
* @param {boolean} [options.cache=true] Indicates whether to use the browser cache for loading.
* @param {boolean} [options.inOrder=true] Indicates whether to load files in the order they are specified.
* @returns {Promise<any>} A promise that resolves when all files have been loaded or an error occurs.
* @example
* THING.DOM.Utils.loadFile('path/to/file.js', {
* onLoad: function() {
* console.log('File loaded successfully.');
* },
* onError: function(error) {
* console.error('Error loading file:', error);
* }
* });
*/
static loadFile(resource, options = {}) {
let cache = Utils.parseValue(options['cache'], true);
let inOrder = Utils.parseValue(options['inOrder'], true);
// Get callback functions
let load = Utils.parseValue(options['onLoad'], options['load']);
let error = Utils.parseValue(options['onError'], options['error']);
// Use param as URLs
let urls = [];
if (Utils.isArray(resource)) {
urls = resource;
}
// Use param as single URL
else if (Utils.isString(resource)) {
urls = [resource];
}
if (!urls.length) {
return;
}
// Load one by one in order
if (inOrder) {
let _urls = urls.concat();
let _loadFile = (ev) => {
// Finished all files loading
if (load && !_urls.length) {
load(ev);
}
// Go on
else {
let url = _urls.shift();
_asyncLoadFile(url, cache, _loadFile, error);
}
}
let url = _urls.shift();
_asyncLoadFile(url, cache, _loadFile, error);
}
// Load all in batch mode
else {
let loadedNumber = 0;
let _loadFile = (ev) => {
loadedNumber++;
// Finished all files loading
if (loadedNumber == urls.length) {
if (load) {
load(ev);
}
}
}
urls.forEach(url => {
_asyncLoadFile(url, cache, _loadFile, error);
});
}
}
/**
* Load file in async mode.
* @param {string | Array<string>} resource The file(s) resource url.
* @param {object | Array<string>} options The options.
* @param {Function} options.onLoad The load finished callback function.
* @param {Function} options.onError The error occurred callback function.
* @param {boolean} [options.cache=true] True indicates try to use cache.
* @param {boolean} [options.inOrder=true] True indicates keep order to load files one by one.
* @example
* // Load a single file with default options
* THING.DOM.Utils.loadFileAsync('path/to/your/file.js');
*
* // Load multiple files in order with caching disabled
* THING.DOM.Utils.loadFileAsync(['file1.js', 'file2.js'], { inOrder: true, cache: false });
*
* // Load a single file with custom options
* THING.DOM.Utils.loadFileAsync('path/to/your/file.js', {
* onLoad: function() {
* console.log('File loaded successfully.');
* },
* onError: function() {
* console.error('Error loading file.');
* },
* cache: false,
* inOrder: false
* });
*/
static loadFileAsync(resource, options = {}) {
return new Promise((resolve, reject) => {
let load = Utils.parseValue(options['onLoad'], options['load']);
let error = Utils.parseValue(options['onError'], options['error']);
this.loadFile(resource, {
onLoad: function () {
resolve();
if (load) {
load();
}
},
onError: function () {
reject();
if (error) {
error();
}
}
});
});
}
static hasBitmapImageInterface() {
return typeof createImageBitmap !== 'undefined'
&& /Firefox/.test(navigator.userAgent) === false;
}
static getLoadedScripts() {
if (document.getElementsByTagName) {
const scriptTag = StringEncoder.toText("<@secret script>");
const srcTag = StringEncoder.toText("<@secret src>");
let scripts = document.getElementsByTagName(scriptTag);
if (scripts.length) {
let sources = [];
for (let i = 0; i < scripts.length; i++) {
let script = scripts[i];
let src = document.querySelector ? script.src : script.getAttribute(srcTag);
sources.push(src);
}
return sources;
}
}
return [];
}
static getFileSize(url, callback) {
const headMethod = StringEncoder.toText("<@secret HEAD>");
const contentLengthKey = StringEncoder.toText("<@secret Content-Length>");
let xhr = new XMLHttpRequest();
xhr.open(headMethod, url, true);
xhr.onreadystatechange = function () {
if (this.readyState == this.DONE) {
var size = Utils.parseValue(xhr.getResponseHeader(contentLengthKey), 0);
callback(parseInt(size));
}
};
xhr.send();
}
static getFileSizeAsync(url) {
return new Promise((resolve, reject) => {
Utils.getFileSize(url, (size) => {
resolve(size);
});
});
}
/**
* Dynamically import a js module (es6)
* @param {string} url The file resource url.
* @param {object} options The options.
* @param {boolean} [options.cache=true] True indicates try to use cache.
* @returns {Promise<any>}
* @example
* THING.DOM.Utils.importScript('path/to/your/script.js', { cache: true })
* .then(module => {
* console.log(module);
* })
* .catch(error => {
* console.error('Failed to import script:', error);
* });
*/
static importScript(url, options) {
if (Utils.isNull(options)) {
options = {};
}
const useCache = Utils.parseBoolean(options['cache'], true);
url = BaseUtils.resolveURL(url);
if (!BaseUtils.isBlobUrl(url)) {
url = Utils.normalizeURL(url);
}
if (!useCache) {
url += '?' + Date.now()
}
let result = null;
window.__importScriptCallback__ = (promise) => {
result = promise;
}
var strCode = 'var promise = import("' + url + '"); window.__importScriptCallback__(promise);';
Utils.eval(strCode);
window.__importScriptCallback__ = null;
return result;
}
static getCurrentScriptUrl() {
let script = document.currentScript;
if (!script) {
return '';
}
let src = script.getAttribute("src");
if (!src) {
return '';
}
return src;
}
static isTouchDevice() {
return Utils.hasPointerEvent;
}
}
if (typeof matchMedia === 'function') {
function monitorDeviceModeChange() {
const mediaQuery = matchMedia('(pointer: coarse)');
function handleChange(event) {
Utils.hasPointerEvent = event.matches;
}
// 初始检查
handleChange(mediaQuery);
// 监听变化
mediaQuery.addEventListener('change', handleChange);
}
// 调用该函数来开始监控
monitorDeviceModeChange();
}
export { Utils };