浏览器类型检测
export const inBrowser = typeof window !== 'undefined';
export const UA = inBrowser && window.navigator.userAgent.toLowerCase();
export const isIE = UA && /msie|trident/.test(UA);
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0;
export const isEdge = UA && UA.indexOf('edge/') > 0;
export const isChrome = UA && /chrome/\d+/.test(UA) && !isEdge;
export const isPhantomJS = UA && /phantomjs/.test(UA);
export const isFF = UA && UA.match(/firefox/(\d+)/);
检测是否可以使用document对象
function canUseDom() {
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
}
debouncedWatch-监听防抖
// 传入监听函数与监听属性,watchWithFilter实现监听,eventFilter包装的防抖函数
export default function debouncedWatch<Immediate extends Readonly<boolean> = false>(
source: any,
cb: any,
options: DebouncedWatchOptions<Immediate> = {},
): WatchStopHandle {
const { debounce = 0, ...watchOptions } = options;
return watchWithFilter(source, cb, {
...watchOptions,
eventFilter: debounceFilter(debounce),
});
}
// 调用vue监听,createFilterWrapper将监听函数的执行进行包装,eventFilter防抖函数
export function watchWithFilter<Immediate extends Readonly<boolean> = false>(
source: any,
cb: any,
options: WatchWithFilterOptions<Immediate> = {},
): WatchStopHandle {
const { eventFilter = bypassFilter, ...watchOptions } = options;
return watch(source, createFilterWrapper(eventFilter, cb), watchOptions);
}
// 接受eventFilter的传入,采用装饰模式将原监听函数封装为防抖函数
function createFilterWrapper<T extends FunctionArgs>(filter: EventFilter, fn: T) {
function wrapper(this: any, ...args: any[]) {
filter(() => fn.apply(this, args), { fn, thisArg: this, args });
}
return wrapper as any as T;
}
// 防抖实现,采用闭包
export function debounceFilter(ms: MaybeRef<number>) {
let timer: ReturnType<typeof setTimeout> | undefined;
const filter: EventFilter = invoke => {
const duration = unref(ms);
if (timer) clearTimeout(timer);
if (duration <= 0) return invoke();
timer = setTimeout(invoke, duration);
};
return filter;
}
requestAnimationFramePolyfill-requestAnimationFrame的兼容性
// 不同浏览器私有方法
const availablePrefixs = ['moz', 'ms', 'webkit'];
// 采用setTimeout模拟requestAnimationFrame行为
function requestAnimationFramePolyfill() {
let lastTime = 0;
return function (callback) {
const currTime = new Date().getTime();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = window.setTimeout(function () {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
export default function getRequestAnimationFrame() {
if (typeof window === 'undefined') {
return () => {};
}
if (window.requestAnimationFrame) {
return window.requestAnimationFrame.bind(window);
}
const prefix = availablePrefixs.filter(key => `${key}RequestAnimationFrame` in window)[0];
return prefix ? window[`${prefix}RequestAnimationFrame`] : requestAnimationFramePolyfill();
}
// cancelAnimationFrame的浏览器兼容性处理
export function cancelRequestAnimationFrame(id) {
if (typeof window === 'undefined') {
return null;
}
if (window.cancelAnimationFrame) {
return window.cancelAnimationFrame(id);
}
const prefix = availablePrefixs.filter(
key => `${key}CancelAnimationFrame` in window || `${key}CancelRequestAnimationFrame` in window,
)[0];
return prefix
? (
window[`${prefix}CancelAnimationFrame`] || window[`${prefix}CancelRequestAnimationFrame`]
).call(this, id)
: clearTimeout(id);
}
getScroll-获取滚动的距离
// 通过window.window来判断当前环境是否处理浏览器中
export function isWindow(obj: any) {
return obj !== null && obj !== undefined && obj === obj.window;
}
// el instanceof Document判断当前节点是否为文档对象,documentElement会返回文档对象(document)的根元素(如<html>元素)
export default function getScroll(
target: HTMLElement | Window | Document | null,
top: boolean,
): number {
if (typeof window === 'undefined') {
return 0;
}
const method = top ? 'scrollTop' : 'scrollLeft';
let result = 0;
if (isWindow(target)) {
result = (target as Window)[top ? 'pageYOffset' : 'pageXOffset'];
} else if (target instanceof Document) {
result = target.documentElement[method];
} else if (target) {
result = (target as HTMLElement)[method];
}
if (target && !isWindow(target) && typeof result !== 'number') {
result = ((target as HTMLElement).ownerDocument || (target as Document)).documentElement?.[
method
];
}
return result;
}
getScrollBarSize-获取滚动条大小
// 首先通过getComputedStyle获取scrollbar大小,如果不支持手动创建dom获取大小
// 手动创建dom时,通过overflow:hidden时的offsetWidth与overflow:scroll时的offsetWidth相减得到大小
// getComputedStyle,方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性的值
let cached: number;
export default function getScrollBarSize(fresh?: boolean) {
if (typeof document === 'undefined') {
return 0;
}
if (fresh || cached === undefined) {
const inner = document.createElement('div');
inner.style.width = '100%';
inner.style.height = '200px';
const outer = document.createElement('div');
const outerStyle = outer.style;
outerStyle.position = 'absolute';
outerStyle.top = '0';
outerStyle.left = '0';
outerStyle.pointerEvents = 'none';
outerStyle.visibility = 'hidden';
outerStyle.width = '200px';
outerStyle.height = '150px';
outerStyle.overflow = 'hidden';
outer.appendChild(inner);
document.body.appendChild(outer);
const widthContained = inner.offsetWidth;
outer.style.overflow = 'scroll';
let widthScroll = inner.offsetWidth;
if (widthContained === widthScroll) {
widthScroll = outer.clientWidth;
}
document.body.removeChild(outer);
cached = widthContained - widthScroll;
}
return cached;
}
function ensureSize(str: string) {
const match = str.match(/^(.*)px$/);
const value = Number(match?.[1]);
return Number.isNaN(value) ? getScrollBarSize() : value;
}
export function getTargetScrollBarSize(target: HTMLElement) {
if (typeof document === 'undefined' || !target || !(target instanceof Element)) {
return { width: 0, height: 0 };
}
const { width, height } = getComputedStyle(target, '::-webkit-scrollbar');
return {
width: ensureSize(width),
height: ensureSize(height),
};
}
isCssAnimationSupported-判断是否支持animation
// 根据浏览器私有属性前缀,依次判断是否支持animation
function isCssAnimationSupported() {
if (animation !== undefined) {
return animation;
}
const domPrefixes = 'Webkit Moz O ms Khtml'.split(' ');
const elm = document.createElement('div');
if (elm.style.animationName !== undefined) {
animation = true;
}
if (animation !== undefined) {
for (let i = 0; i < domPrefixes.length; i++) {
if (elm.style[`${domPrefixes[i]}AnimationName`] !== undefined) {
animation = true;
break;
}
}
}
animation = animation || false;
return animation;
}
isMobile-判断是否为手机端及手机类型
// 根据navigator.userAgent来正则匹配手机类型
const applePhone = /iPhone/i;
const appleIpod = /iPod/i;
const appleTablet = /iPad/i;
// (?:x)匹配 'x' 但是不记住匹配项 .匹配除换行符之外的任何单个字符 +匹配前面一个表达式 1 次或者多次。等价于 {1,} \b匹配一个词的边界
const androidPhone = /\bAndroid(?:.+)Mobile\b/i; // Match 'Android' AND 'Mobile'
const androidTablet = /Android/i;
const amazonPhone = /\bAndroid(?:.+)SD4930UR\b/i;
const amazonTablet = /\bAndroid(?:.+)(?:KF[A-Z]{2,4})\b/i;
const windowsPhone = /Windows Phone/i;
const windowsTablet = /\bWindows(?:.+)ARM\b/i; // Match 'Windows' AND 'ARM'
const otherBlackberry = /BlackBerry/i;
const otherBlackberry10 = /BB10/i;
const otherOpera = /Opera Mini/i;
const otherChrome = /\b(CriOS|Chrome)(?:.+)Mobile/i;
const otherFirefox = /Mobile(?:.+)Firefox\b/i; // Match 'Mobile' AND 'Firefox'
function match(regex, userAgent) {
return regex.test(userAgent);
}
function isMobile(userAgent) {
let ua = userAgent || (typeof navigator !== 'undefined' ? navigator.userAgent : '');
// Facebook mobile app's integrated browser adds a bunch of strings that
// match everything. Strip it out if it exists.
let tmp = ua.split('[FBAN');
if (typeof tmp[1] !== 'undefined') {
[ua] = tmp;
}
// Twitter mobile app's integrated browser on iPad adds a "Twitter for
// iPhone" string. Same probably happens on other tablet platforms.
// This will confuse detection so strip it out if it exists.
tmp = ua.split('Twitter');
if (typeof tmp[1] !== 'undefined') {
[ua] = tmp;
}
const result = {
apple: {
phone: match(applePhone, ua) && !match(windowsPhone, ua),
ipod: match(appleIpod, ua),
tablet: !match(applePhone, ua) && match(appleTablet, ua) && !match(windowsPhone, ua),
device:
(match(applePhone, ua) || match(appleIpod, ua) || match(appleTablet, ua)) &&
!match(windowsPhone, ua),
},
amazon: {
phone: match(amazonPhone, ua),
tablet: !match(amazonPhone, ua) && match(amazonTablet, ua),
device: match(amazonPhone, ua) || match(amazonTablet, ua),
},
android: {
phone:
(!match(windowsPhone, ua) && match(amazonPhone, ua)) ||
(!match(windowsPhone, ua) && match(androidPhone, ua)),
tablet:
!match(windowsPhone, ua) &&
!match(amazonPhone, ua) &&
!match(androidPhone, ua) &&
(match(amazonTablet, ua) || match(androidTablet, ua)),
device:
(!match(windowsPhone, ua) &&
(match(amazonPhone, ua) ||
match(amazonTablet, ua) ||
match(androidPhone, ua) ||
match(androidTablet, ua))) ||
match(/\bokhttp\b/i, ua),
},
windows: {
phone: match(windowsPhone, ua),
tablet: match(windowsTablet, ua),
device: match(windowsPhone, ua) || match(windowsTablet, ua),
},
other: {
blackberry: match(otherBlackberry, ua),
blackberry10: match(otherBlackberry10, ua),
opera: match(otherOpera, ua),
firefox: match(otherFirefox, ua),
chrome: match(otherChrome, ua),
device:
match(otherBlackberry, ua) ||
match(otherBlackberry10, ua) ||
match(otherOpera, ua) ||
match(otherFirefox, ua) ||
match(otherChrome, ua),
},
// Additional
any: null,
phone: null,
tablet: null,
};
result.any =
result.apple.device || result.android.device || result.windows.device || result.other.device;
// excludes 'other' devices and ipods, targeting touchscreen phones
result.phone = result.apple.phone || result.android.phone || result.windows.phone;
result.tablet = result.apple.tablet || result.android.tablet || result.windows.tablet;
return result;
}
const defaultResult = {
...isMobile(),
isMobile,
};
export default defaultResult;
obj2mq-用于从JSON或javascript对象生成媒体查询字符串
// json2mq({minWidth: 100, maxWidth: 200});
// -> '(min-width: 100px) and (max-width: 200px)'
// 驼峰式转短横线
const camel2hyphen = function (str) {
return str
.replace(/[A-Z]/g, function (match) {
return '-' + match.toLowerCase();
})
.toLowerCase();
};
const isDimension = function (feature) {
const re = /[height|width]$/;
return re.test(feature);
};
const obj2mq = function (obj) {
let mq = '';
const features = Object.keys(obj);
features.forEach(function (feature, index) {
let value = obj[feature];
feature = camel2hyphen(feature);
// Add px to dimension features
if (isDimension(feature) && typeof value === 'number') {
value = value + 'px';
}
if (value === true) {
mq += feature;
} else if (value === false) {
mq += 'not ' + feature;
} else {
mq += '(' + feature + ': ' + value + ')';
}
if (index < features.length - 1) {
mq += ' and ';
}
});
return mq;
};
export default function (query) {
let mq = '';
if (typeof query === 'string') {
return query;
}
// Handling array of media queries
if (query instanceof Array) {
query.forEach(function (q, index) {
mq += obj2mq(q);
if (index < query.length - 1) {
mq += ', ';
}
});
return mq;
}
// Handling single media query
return obj2mq(query);
}
setStyle-设置element的style
// 接受三个参数,一个更改的style,一个options包括了需要更改的element
// 返回element中旧的style
export interface SetStyleOptions {
element?: HTMLElement;
}
function setStyle(style: CSSProperties, options: SetStyleOptions = {}): CSSProperties {
const { element = document.body } = options;
const oldStyle: CSSProperties = {};
const styleKeys = Object.keys(style);
// IE browser compatible
styleKeys.forEach(key => {
oldStyle[key] = element.style[key];
});
styleKeys.forEach(key => {
element.style[key] = style[key];
});
return oldStyle;
}
isStyleNameSupport-检测是否支持style属性
const isStyleNameSupport = (styleName: string | string[]): boolean => {
if (canUseDom() && window.document.documentElement) {
const styleNameList = Array.isArray(styleName) ? styleName : [styleName];
const { documentElement } = window.document;
return styleNameList.some(name => name in documentElement.style);
}
return false;
};
copy-复制到粘贴板
// 先用document.execCommand('copy')判断支持性
// 再用e.clipboardData.setData判断支持,动态创建一个span标签并设置标签文本,设置标签样式,让标签文本可以选中,注册copy事件,复制数据
// 再使用(window as any).clipboardData.setData来设置粘贴板数据
function copy(text: string, options?: Options): boolean {
let message,
reselectPrevious,
range,
selection,
mark,
success = false;
if (!options) {
options = {};
}
const debug = options.debug || false;
try {
reselectPrevious = deselectCurrent();
range = document.createRange();
selection = document.getSelection();
mark = document.createElement('span');
mark.textContent = text;
// reset user styles for span element
mark.style.all = 'unset';
// prevents scrolling to the end of the page
mark.style.position = 'fixed';
mark.style.top = 0;
mark.style.clip = 'rect(0, 0, 0, 0)';
// used to preserve spaces and line breaks
mark.style.whiteSpace = 'pre';
// do not inherit user-select (it may be `none`)
mark.style.webkitUserSelect = 'text';
mark.style.MozUserSelect = 'text';
mark.style.msUserSelect = 'text';
mark.style.userSelect = 'text';
mark.addEventListener('copy', function (e) {
e.stopPropagation();
if (options.format) {
e.preventDefault();
if (typeof e.clipboardData === 'undefined') {
// IE 11
debug && console.warn('unable to use e.clipboardData');
debug && console.warn('trying IE specific stuff');
(window as any).clipboardData.clearData();
const format =
clipboardToIE11Formatting[options.format] || clipboardToIE11Formatting['default'];
(window as any).clipboardData.setData(format, text);
} else {
// all other browsers
e.clipboardData.clearData();
e.clipboardData.setData(options.format, text);
}
}
if (options.onCopy) {
e.preventDefault();
options.onCopy(e.clipboardData);
}
});
document.body.appendChild(mark);
range.selectNodeContents(mark);
selection.addRange(range);
const successful = document.execCommand('copy');
if (!successful) {
throw new Error('copy command was unsuccessful');
}
success = true;
} catch (err) {
debug && console.error('unable to copy using execCommand: ', err);
debug && console.warn('trying IE specific stuff');
try {
(window as any).clipboardData.setData(options.format || 'text', text);
options.onCopy && options.onCopy((window as any).clipboardData);
success = true;
} catch (err) {
debug && console.error('unable to copy using clipboardData: ', err);
debug && console.error('falling back to prompt');
message = format('message' in options ? options.message : defaultMessage);
window.prompt(message, text);
}
} finally {
if (selection) {
if (typeof selection.removeRange == 'function') {
selection.removeRange(range);
} else {
selection.removeAllRanges();
}
}
if (mark) {
document.body.removeChild(mark);
}
reselectPrevious();
}
return success;
}