(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.pdr = factory());
}(this, (function () { 'use strict';
// 全局参数
const globalOptions = {
loading: true, // 是否添加 loading
loadingText: "", // 请求时的文字提示
// opacity: 1, // 请求时设置的不透明度
selectors: ["button"], // 需要全局统一处理请求的选择器列表
eventTypes: ["click"], // 需要全局统一处理请求的事件列表
onRequest(target, options) {}, // 请求发起的钩子
onResponse(target, options) {}, // 请求结束的钩子
};
// dom 相关的数据存储
const elementDataStore = {
elements: [],
set(el, key, data) {
const index = this.getIndex(el);
if (index > -1) {
const obj = this.elements[index].data;
obj[key] = data;
} else {
const obj = {
el,
data: {
[key]: data,
},
};
this.elements.push(obj);
}
},
remove(el, key) {
const index = this.getIndex(el);
if (index === -1) return;
if (!key) {
this.elements.splice(index, 1);
} else {
const obj = this.elements[index].data;
delete obj[key];
const keysLength = Object.keys(obj).length;
if (keysLength === 0) {
this.elements.splice(index, 1);
}
}
},
getIndex(el, key) {
const index = this.elements.findIndex((item) => item.el === el);
if (!key) {
return index;
} else if (index > -1) {
return this.getData(el, key) ? index : -1;
}
return -1;
},
getData(el, key) {
const index = this.getIndex(el);
if (index === -1) {
return null;
}
const data = this.elements[index].data;
return key ? data[key] : data;
},
has(el, key) {
return this.getIndex(el, key) > -1;
},
};
// 管理指令指定的dom
const directiveElStore = {
key: "directiveOptions",
add(el, options) {
elementDataStore.set(el, this.key, options);
if (options.fetching) {
const requestOptions = normalizeRequestOptions(el);
onRequest(el, requestOptions);
}
},
remove(el) {
return elementDataStore.remove(el, this.key);
},
update(el, options) {
if (!this.has(el)) return;
const oldOptions = this.getOptions(el);
// 对比新旧值,判断是请求开始还是请求结束
if (oldOptions.fetching && !options.fetching) {
onResponse(el, normalizeRequestOptions(el));
} else if (!oldOptions.fetching && options.fetching) {
onRequest(el, normalizeRequestOptions(el));
}
this.setOptions(el, options);
},
getIndex(el) {
return elementDataStore.getIndex(el, this.key);
},
getOptions(el) {
return elementDataStore.getData(el, this.key);
},
setOptions(el, options) {
elementDataStore.set(el, this.key, options);
},
has(el) {
return this.getIndex(el) > -1;
},
};
// 转换指令的 binding 参数
function normalizeDirectiveOptions(binding) {
const options = {
manual: false, // 是否手动
fetching: false, // 是否在请求中,仅当 manual 为 true 时有效
};
if (typeof binding.value === "boolean") {
options.fetching = binding.value;
options.manual = true;
} else if (typeof binding.value === "object") {
const bindingValue = binding.value;
const hasOwn = (prop) =>
Object.prototype.hasOwnProperty.call(bindingValue, prop);
if (hasOwn("fetching")) {
options.fetching = bindingValue.fetching;
options.manual = true;
}
if (hasOwn("loading")) {
options.loading = Boolean(bindingValue.loading);
}
if (hasOwn("loadingText")) {
options.loadingText = String(bindingValue.loadingText);
}
if (hasOwn("opacity")) {
options.opacity = String(bindingValue.opacity);
}
}
return options;
}
// vue 指令
const directive = {
inserted(el, binding) {
directiveElStore.add(el, normalizeDirectiveOptions(binding));
},
update(el, binding) {
directiveElStore.update(el, normalizeDirectiveOptions(binding));
},
unbind(el, binding) {
directiveElStore.update(el, normalizeDirectiveOptions(binding));
directiveElStore.remove(el);
},
};
// 获取元素某个样式
function getStyle(el, prop) {
return document.defaultView.getComputedStyle(el)[prop];
}
// 管理 loading
const loadingManager = {
key: "loadingMask",
createLoadingMask() {
const mask = document.createElement("div");
mask.className = "pdr-loading-mask";
mask.innerHTML = `
`;
return mask;
},
getMaskStyle(parent) {
const rect = parent.getBoundingClientRect();
const maskStyle = {};
["top", "left"].forEach((property) => {
const scroll = property === "top" ? "scrollTop" : "scrollLeft";
const margin = property === "top" ? "marginTop" : "marginLeft";
maskStyle[property] =
rect[property] +
document.body[scroll] +
document.documentElement[scroll] -
parseInt(getStyle(document.body, margin), 10) +
"px";
});
["height", "width"].forEach((property) => {
maskStyle[property] = rect[property] + "px";
});
return maskStyle;
},
setCircularSize(parent, mask) {
const rect = parent.getBoundingClientRect();
const { width, height } = rect;
const minSize = Math.min(width, height);
if (minSize < 42) {
const circular = mask.querySelector(".pdr-circular");
circular.style.width = minSize + "px";
circular.style.height = minSize + "px";
}
},
addLoading(el, loadingText) {
const position = getStyle(el, "position");
if (position !== "absolute" && position !== "fixed") {
el.classList.add("pdr-loading-parent-relative");
}
const mask = this.createLoadingMask();
const maskStyle = this.getMaskStyle(el);
Object.keys(maskStyle).forEach((property) => {
mask.style[property] = maskStyle[property];
});
this.setCircularSize(el, mask);
el.appendChild(mask);
if (loadingText) {
const circular = mask.querySelector(".pdr-circular");
// 如果空间不足就不显示文字
if (mask.clientHeight - circular.clientHeight < 26) return;
this.addLoadingText(mask, loadingText);
}
elementDataStore.set(el, this.key, mask);
},
addLoadingText(mask, loadingText) {
const text = document.createElement("div");
text.className = "pdr-loading-text";
text.textContent = loadingText;
mask.querySelector(".pdr-loading-spinner").appendChild(text);
},
removeLoading(el) {
el.classList.remove("pdr-loading-parent-relative");
const mask = elementDataStore.getData(el, this.key);
if (mask) {
el.removeChild(mask);
elementDataStore.remove(el, this.key);
}
},
};
// 管理不透明度
const opacityManager = {
key: "originalOpacity",
setOpacity(el, opacity) {
// 将原来内联样式中的 opacity 属性值和优先级存起来
const inlineOpacity = el.style.opacity;
if (inlineOpacity) {
const originalOpacity = {
value: inlineOpacity,
priority: el.style.getPropertyPriority("opacity"),
};
elementDataStore.set(el, this.key, originalOpacity);
}
el.style.setProperty("opacity", opacity, "important");
},
restoreOpacity(el) {
el.style.removeProperty("opacity");
const originalOpacity = elementDataStore.getData(el, this.key);
elementDataStore.remove(el, this.key);
// 恢复原来内联样式中的 opacity 属性值和优先级
if (originalOpacity) {
el.style.setProperty(
"opacity",
originalOpacity.value,
originalOpacity.priority
);
}
},
};
// 获取元素 data-pdr-* 属性中的值
function getDataOptions(el) {
const options = {};
const dataset = el.dataset;
if (dataset.pdrOpacity) {
options.opacity = Number(dataset.pdrOpacity);
}
if (dataset.pdrLoading) {
options.loading = dataset.pdrLoading === "true";
}
if (dataset.pdrLoadingText) {
options.loadingText = dataset.pdrLoadingText;
}
return options;
}
// 转换 onRequest 的参数
function normalizeRequestOptions(el, options) {
const defaults = {
loading: globalOptions.loading,
loadingText: globalOptions.loadingText,
opacity: globalOptions.opacity,
type: "manual",
xhr: null,
};
const requestOptions = Object.assign(defaults, options);
if (directiveElStore.has(el)) {
const directiveOptions = directiveElStore.getOptions(el);
Object.assign(requestOptions, directiveOptions);
} else {
const dataOptions = getDataOptions(el);
Object.assign(requestOptions, dataOptions);
}
return requestOptions;
}
// 发起请求时的回调函数
function onRequest(target, options) {
target.classList.add("pdr-no-pointer-events");
target.classList.add("pdr-fetching");
if (options.loading) {
loadingManager.addLoading(target, options.loadingText);
}
if (options.opacity) {
opacityManager.setOpacity(target, options.opacity);
}
globalOptions.onRequest.call(null, target, options);
}
// 请求结束时的回调函数
function onResponse(target, options) {
target.classList.remove("pdr-no-pointer-events");
target.classList.remove("pdr-fetching");
loadingManager.removeLoading(target);
opacityManager.restoreOpacity(target);
globalOptions.onResponse.call(null, target, options);
}
// 手动调用方法,表示元素开始请求
function start(el, options) {
onRequest(el, normalizeRequestOptions(el, options));
}
// 手动调用方法,表示元素请求完成
function stop(el, options) {
onResponse(el, options);
}
// 获取发起请求的dom
function getTarget(target) {
if (!target) return null;
const isMatchedSelector = (selector) => target.matches(selector);
const isMatched =
globalOptions.selectors.some(isMatchedSelector) ||
directiveElStore.has(target);
if (isMatched) {
return target;
} else {
return getTarget(target.parentElement);
}
}
// 重写 XMLHttpRequest.prototype.send 方法以获取点击目标
function resetXMLHttpRequest() {
const xhrSend = XMLHttpRequest.prototype.send;
if (xhrSend._isReset) return;
XMLHttpRequest.prototype.send = function (...args) {
if (
!window.event ||
globalOptions.eventTypes.indexOf(window.event.type) === -1
) {
return xhrSend.apply(this, args);
}
const target = getTarget(window.event.target);
const isDirectiveManualEl = () => {
return (
directiveElStore.has(target) &&
directiveElStore.getOptions(target).manual
);
};
// 当不存在目标或目标由指令指定且是手动设置 fetching 值时不做处理
if (!target || isDirectiveManualEl()) {
return xhrSend.apply(this, args);
}
const type = window.event.type;
const options = normalizeRequestOptions(target, {
type,
xhr: this,
});
onRequest(target, options);
this.addEventListener("loadend", function (event) {
onResponse(target, options);
});
return xhrSend.apply(this, args);
};
XMLHttpRequest.prototype.send._isReset = true;
}
// 添加相关样式
function addStyle() {
const primaryColor = "#11A560";
const STYLE_TEXT = `
.pdr-no-pointer-events {
pointer-events: none !important;
}
.pdr-loading-parent-relative {
position: relative !important;
}
.pdr-loading-mask {
position: absolute;
z-index: 2000;
background-color: rgba(255, 255, 255, .9);
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
transition: opacity 0.3s;
}
.pdr-loading-spinner {
top: 50%;
transform: translateY(-50%);
width: 100%;
text-align: center;
position: absolute;
}
.pdr-circular {
height: 42px;
width: 42px;
animation: pdr-loading-rotate 2s linear infinite;
}
.pdr-path {
animation: pdr-loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: ${primaryColor};
stroke-linecap: round;
}
.pdr-loading-text {
color: ${primaryColor};
margin: 3px 0;
font-size: 14px;
}
@keyframes pdr-loading-rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes pdr-loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
`;
const style = document.createElement("style");
style.appendChild(document.createTextNode(STYLE_TEXT));
const head = document.getElementsByTagName("head")[0];
head.appendChild(style);
}
// 暴露给外部使用的对象
const preventDuplicatedRequest = {
// 是否已通过 Vue.use 注册该工具
_installed: false,
// 设置参数
setOptions(userOptions) {
userOptions = userOptions || {};
Object.assign(globalOptions, userOptions);
},
// 手动调用方法,表示元素开始请求
start: start,
// 手动调用方法,表示元素请求完成
stop: stop,
// 指令
directive,
// 通过 Vue.use 使用该工具时调用
install(Vue) {
if (this._installed) return;
Vue.directive("pdr", directive);
this._installed = true;
},
};
// 初始化
function init() {
resetXMLHttpRequest();
addStyle();
}
init();
return preventDuplicatedRequest;
})));