interface Attribute {
name: string;
type: string;
value: string;
}
interface ClassProps {
position: string;
zIndex: number;
background?: string;
animation?: string;
}
interface Rect {
t: number;
l: number;
w: number;
h: number;
}
interface OriginStyle {
scrollTop: number;
bodyOverflow: string;
}
interface Header {
height: number;
background: string;
}
interface Options {
init?: Function;
includeElement?: Function;
background?: string;
animation?: string;
header?: Header | undefined;
offsetTop?: number;
}
interface DrawBlock {
width?: number;
height?: number;
top?: number;
left?: number;
zIndex?: number;
background?: string;
radius?: string;
subClas?: boolean;
}
const WIN_WIDTH = window.innerWidth;
const WIN_HEIGHT = window.innerHeight;
const ELEMENTS = [
"audio",
"button",
"canvas",
"code",
"img",
"input",
"pre",
"svg",
"textarea",
"video",
"xmp",
];
const DEFAULT_ANIMATION = "opacity 1.5s linear infinite";
const DEFAULT_BACKGROUND = "#ecf0f2;";
const DEFAULT_POSITION = "fixed";
const DEFAULT_ZINDEX = 999;
const _eval = eval;
const classProps: ClassProps = {
position: DEFAULT_POSITION,
zIndex: DEFAULT_ZINDEX,
};
/**
* 解析入参数组为对象格式
* @param attrs
* @returns
*/
function parseAgrs(attrs: Attribute[]) {
let params = {};
attrs.forEach(({ name, type, value }) => {
const v =
type === "function"
? _eval("(" + value + ")")
: type === "object"
? JSON.parse(value)
: value;
Reflect.set(params, name, v);
});
return params;
}
/**
* 计算百分比
* @param {*} total
* @param {*} x
* @returns
*/
function percent(total: number, x: number): number {
return Number(parseFloat(`${x / total * 100}`).toFixed(3));
}
/**
* 当前节点是否在给定列表中包含
* @param elements
* @param node
* @returns
*/
function includeElement(elements: string[], node: HTMLElement) {
return ~elements.indexOf((node.tagName || "").toLowerCase());
}
/**
* 获取节点的指定属性的值
* Node.nodeType ELEMENT_NODE=1
* @param {*} node
* @param {*} attr
* @returns
*/
function getStyle(node: Element, attr: string): string {
let value = "";
if (node.nodeType === 1) {
value = window.getComputedStyle(node).getPropertyValue(attr);
}
return value;
}
/**
* 是否隐藏节点
* @param {*} node
* @returns
*/
function isHideStyle(node: HTMLElement) {
const isNone = getStyle(node, "display") === "none";
const isHidden = getStyle(node, "visibility") === "hidden";
const isNoOpacity = Number(getStyle(node, "opacity")) === 0;
return isNone || isHidden || isNoOpacity || node.hidden;
}
/**
* 有背景色且有四边框或设置阴影的需要绘制
* @param node
* @returns
*/
function isCustomCardBlock(node: HTMLElement) {
// 设置rgba的时候alpha不为0
// 不存在渐变内容
const bgStyle = getStyle(node, "background");
const bgColorReg = /rgba\([\s\S]+?0\)/gi;
const hasBgColor = !bgColorReg.test(bgStyle) || ~bgStyle.indexOf("gradient");
// 检查border的四边是否有0像素或none的情况,四边都有值的时候绘制
const bdReg = /(0px)|(none)/;
const hasNoBorder = ["top", "left", "right", "bottom"].some((item) => {
return bdReg.test(getStyle(node, "border-" + item).toString());
});
// 存在阴影的情况需要绘制
const hasBoxShadow = getStyle(node, "box-shadow") != "none";
const { w, h } = getRect(node);
// !!: !=null && !=undefined && !=''
const customCardBlock = !!(hasBgColor && (!hasNoBorder || hasBoxShadow) &&
w > 0 &&
h > 0 &&
// 最大宽高设上限,避免尺寸过大绘制效果差
w < 0.95 * WIN_WIDTH &&
h < 0.3 * WIN_HEIGHT
);
// 返回true需要绘制
return customCardBlock;
}
/**
* 获取元素的上下距离和宽高
* @param {*} node
* @returns
*/
function getRect(node: HTMLElement): Rect {
if (!node) return { t: 0, l: 0, w: 0, h: 0 };
const { top: t, left: l, width: w, height: h } = node.getBoundingClientRect();
return { t, l, w, h };
}
/**
* 获取元素的padding
* @param {*} node
* @returns
*/
function getPadding(node: HTMLElement) {
return {
paddingTop: parseInt(getStyle(node, "padding-top")),
paddingLeft: parseInt(getStyle(node, "padding-left")),
paddingBottom: parseInt(getStyle(node, "padding-bottom")),
paddingRight: parseInt(getStyle(node, "padding-right")),
};
}
class DrawPageFrame {
rootNode = document.body;
blocks: string[] = [];
originStyle: OriginStyle = {
scrollTop: 0,
bodyOverflow: "",
};
background: string;
animation: string;
header: Header | undefined;
offsetTop: number;
init: Function;
includeElement: Function;
constructor(opts: Options) {
this.init = opts.init || function () { };
this.includeElement = opts.includeElement || function () { };
this.background = opts?.background || DEFAULT_BACKGROUND;
this.animation = opts?.animation || DEFAULT_ANIMATION;
this.header = opts?.header;
this.offsetTop = opts.offsetTop || 0;
this.initClassProps();
}
initClassProps() {
classProps.background = this.background;
classProps.animation = this.animation;
const inlineStyle = ["");
this.blocks.push(inlineStyle.join(""));
}
startDraw() {
this.resetDOM();
// body NodeList
const nodes = this.rootNode.childNodes;
const deepTraverseNode = (nodes: string | any[] | NodeListOf) => {
if (nodes.length) {
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
let childNodes = node.childNodes;
// 不进行绘制的元素
const isHideNode = isHideStyle(node);
// 自定义跳过的元素
const isCustomSkip = typeof this.includeElement === "function" && this.includeElement(node, this.drawBlock) == false;
if (isHideNode || isCustomSkip) continue;
// 需要绘制的元素
// 1. 元素设置了背景图,即使内容空也绘制
let background = getStyle(node, "background-image");
let backgroundHasurl = background.match(/url\(.+?\)/);
const _hasBackgroundHasurl = backgroundHasurl && backgroundHasurl.length;
// 2. 子元素遍历后有文本元素且有内容就绘制
let hasChildText = false;
for (let j = 0; j < childNodes.length; j++) {
// Node.nodeType TEXT_NODE=3 有文本内容存在就需要绘制
if (
childNodes[j].nodeType === 3 &&
childNodes[j].textContent.trim().length
) {
hasChildText = true;
break;
}
}
// 3. 本身元素为文本的且有内容
const hasText = node.nodeType === 3 && node.textContent.trim().length;
// 4. 当设置头时,元素被头覆盖了,那么就跳过不绘制了
const _inHeader = this.inHeader(node);
// 5. 在特殊元素列表指定的需要绘制
const _includeElement = includeElement(ELEMENTS, node);
// 存疑
const _isCustomCardBlock = isCustomCardBlock(node);
if (
(_includeElement || _hasBackgroundHasurl || hasText || hasChildText || _isCustomCardBlock) &&
!_inHeader
) {
// top left width high
const { t, l, w, h } = getRect(node);
if (
w > 0 &&
h > 0 &&
l >= 0 &&
t >= 0 &&
l < WIN_WIDTH &&
WIN_HEIGHT - t >= 20
) {
const { paddingTop, paddingLeft, paddingBottom, paddingRight } = getPadding(node);
const radius = getStyle(node, "border-radius");
// 绘制色块
this.drawBlock({
width: percent(WIN_WIDTH, w - paddingLeft - paddingRight),
height: percent(WIN_HEIGHT, h - paddingTop - paddingBottom),
top: percent(WIN_HEIGHT, t + paddingTop),
left: percent(WIN_WIDTH, l + paddingLeft),
radius: radius,
});
}
}
// 存在子节点的情况下要进行递归遍历,当前节点为文本类型的跳过
if (childNodes && childNodes.length) {
if (!hasChildText) {
deepTraverseNode(childNodes);
}
}
}
}
};
// 遍历Node节点进行处理
deepTraverseNode(nodes);
return this.showBlocks();
}
resetDOM() {
this.init && this.init();
this.originStyle = {
scrollTop: window.scrollY,
bodyOverflow: getStyle(document.body, "overflow"),
};
window.scrollTo(0, this.offsetTop);
document.body.style.cssText += "overflow:hidden!important;";
this.drawBlock({
height: 100,
zIndex: 990,
background: "#fff",
subClas: true,
});
if (this.header) {
this.withHeader();
}
}
inHeader(node: HTMLElement) {
if (this.header) {
const height = parseInt(`${this.header.height}`);
const { t } = getRect(node);
return t <= height;
}
}
withHeader() {
const { height, background } = this.header!;
const hHeight = parseInt(`${height}`);
const hBackground = background || this.background;
this.drawBlock({
height: percent(WIN_HEIGHT, hHeight),
zIndex: 999,
background: hBackground,
subClas: true,
});
}
/**
* 绘制元素块
* @param {*} param0
*/
drawBlock({
width,
height,
top,
left,
zIndex = 999,
background = this.background,
radius,
subClas = false,
}: DrawBlock) {
const styles = [`height:${height}%`];
if (!subClas) {
styles.push(`top:${top}%`, `left:${left}%`, `width:${width}%`);
}
if (classProps.zIndex !== zIndex) {
styles.push(`z-index:${zIndex}`);
}
if (classProps.background !== background) {
styles.push(`background:${background}`);
}
radius && radius != "0px" && styles.push(`border-radius:${radius}`);
this.blocks.push(
``
);
}
/**
* 将片段直接注入当前网页的body节点下进行预览
* @returns
*/
showBlocks() {
const { body } = document;
const blocksHTML = this.blocks.join("");
const div = document.createElement("div");
div.innerHTML = blocksHTML;
body.appendChild(div);
window.scrollTo(0, this.originStyle.scrollTop);
document.body.style.overflow = this.originStyle.bodyOverflow;
return blocksHTML;
}
}
/**
* 启动脚本函数,挂载到window便于调用
* @param args
* @returns
*/
// @ts-ignore
window.evalDOMScripts = (...args: any) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
const html = new DrawPageFrame(parseAgrs(args)).startDraw();
resolve(html);
} catch (e) {
reject(e);
}
}, 1000);
});
};