其实整体思路 首先划分两种选择,圈选模式,普通模式
当用户触发圈选选择
- 首先会禁止掉原生事件,比如click,onchange, 冒泡等事件
- 其次当用户hover ,或者click 待圈选元素,首先会获取到对应唯一标识符,其次将当前组件各种信息,属性,全部进行聚合收集,并通过添加样式表的方式,给目标元素从而进行高亮区分.
- 最终通过posemessage的方式,给圈选平台侧进行通信上报
整体思路我通过策略模式进行处理
js
import resultArray from "@wind/wind-monitor-userConsider-markEngine/config/config";
import {
TOPIC, TOPIC_STATUS, wlsCircleSelectText, WLS_CIRCLE_SELECT, WLS_CIRCLE_SELECT_ID,
wlsCircleHighLightText, WLS_CIRCLE_HIGHLIGHT, WLS_CIRCLE_HIGHLIGHT_ID,
wlsCircleMarkLightText, WLS_CIRCLE_MARKLIGHT, WLS_CIRCLE_MARKLIGHT_ID
} from "./types/constant";
import { Options } from "./types/options";
import { appendWLSStyle, removeWLSStyle } from "./utils/appendStyle";
import { win, doc } from "./utils/bom";
import eventUtil from "./utils/eventUtil";
import getEvent from "./utils/getEvent";
import getLogData from "./utils/getLogData";
import { DATA_CONSIDER_ID, DATA_CONSIDER_CONTROL_TYPE } from "./utils/userConsiderDic";
class CircleMark {
options: Options;
isItemSelected: boolean = false;
topic!: TOPIC | null;
actions!: { topic: TOPIC, status: string, function: Function }[];
constructor (options: Options) {
this.options = options;
this.initActions();
this.postMessageToMainWindow({ topic: 'circleMarkApp', data: { appName: this.options.appName } });
this.addMessageListener();
}
initActions() {
this.actions = [
{
topic: TOPIC.CIRCLE_SELECT, status: TOPIC_STATUS.ON, function: (topic: any, controlData: any) => {
// @ts-ignore
window.Is_Circle_Mark = true;
this.topic = topic;
this.bindCircleCollection(true);
appendWLSStyle(WLS_CIRCLE_SELECT_ID, wlsCircleSelectText);
}
},
{
topic: TOPIC.CIRCLE_SELECT, status: TOPIC_STATUS.OFF, function: (topic: any, controlData: any) => {
// @ts-ignore
window.Is_Circle_Mark = false;
this.topic = null;
this.bindCircleCollection(false);
removeWLSStyle(WLS_CIRCLE_SELECT_ID);
}
},
{
topic: TOPIC.CIRCLE_HIGHLIGHT, status: TOPIC_STATUS.ON, function: (topic: any, controlData: any) => {
this.topic = topic;
appendWLSStyle(WLS_CIRCLE_HIGHLIGHT_ID, wlsCircleHighLightText);
this.highLightElements(true, controlData);
}
},
{
topic: TOPIC.CIRCLE_HIGHLIGHT, status: TOPIC_STATUS.OFF, function: (topic: any, controlData: any) => {
this.topic = null;
removeWLSStyle(WLS_CIRCLE_HIGHLIGHT_ID);
this.highLightElements(false, controlData);
}
},
{
topic: TOPIC.CIRCLE_NAVIGATION_CONTROLS, status: TOPIC_STATUS.ON, function: (topic: any, controlData: any) => {
this.getNavigationControls();
}
},
{
topic: TOPIC.CIRCLE_MARKLIGHT, status: TOPIC_STATUS.ON, function: (topic: any, controlData: any) => {
this.topic = topic;
appendWLSStyle(WLS_CIRCLE_MARKLIGHT_ID, wlsCircleMarkLightText);
this.highLightMarkElements(true, controlData);
}
},
{
topic: TOPIC.CIRCLE_MARKLIGHT, status: TOPIC_STATUS.OFF, function: (topic: any, controlData: any) => {
this.topic = null;
removeWLSStyle(WLS_CIRCLE_MARKLIGHT_ID);
this.highLightMarkElements(false, controlData);
}
}
];
}
addMessageListener() {
//监听外部控件控制开关功能
eventUtil.on(win, 'message', (event: any) => {
if (!event.data?.type?.includes('webpack')) {
try {
const data = event.data?.topic ? event.data : JSON.parse(event.data);
const { topic, status, controlData } = data;
const action = this.actions.find((item) => (item.topic === topic && item.status === status));
action?.function(topic, controlData);
} catch (e) {
console.log(e);
}
}
})
}
isIframeElementVisible(iframe, element) {
var rect = element.getBoundingClientRect();
var viewHeight = iframe.documentElement.clientHeight;
return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
}
highLightElements(highLight: any, controlData: any) {
if (highLight) {
controlData.forEach((item: any) => {
const items = document.querySelectorAll(`[${DATA_CONSIDER_ID}="${item.dataConsiderId}"]`);
items.forEach((item: any) => {
item && item.classList.add(WLS_CIRCLE_HIGHLIGHT);
});
if (items.length >= 1) {
const element = items[0];
if (!this.isIframeElementVisible(document, element)) {
// @ts-ignore
element.scrollIntoViewIfNeeded ? element.scrollIntoViewIfNeeded() : element.scrollIntoView();
}
}
});
} else {
controlData.forEach((item: any) => {
const items = document.querySelectorAll(`[${DATA_CONSIDER_ID}="${item.dataConsiderId}"]`);
items.forEach((item: any) => {
item && item.classList.remove(WLS_CIRCLE_HIGHLIGHT);
});
});
}
}
getNavigationControls() {
const items = [...document.querySelectorAll(`[${DATA_CONSIDER_ID}]`)];
let controls = items.filter(item => {
const type = item.attributes[DATA_CONSIDER_CONTROL_TYPE]?.value;
const controlConfig = resultArray.find(x => {
return x.controlType === type &&
(!x.eventName.includes("click")
&& !x.eventName.includes("scroll") ||
["dropdown", "checkbox", "checkboxgroup", "radio", "radiogroup", "rate", "switch"].includes(x.controlType))
});
return controlConfig !== undefined;
}).reduce((pre, cur) => {
const id = cur.attributes[DATA_CONSIDER_ID]?.value;
let control = {
controlId: id.split("_").reverse()[0],
controlType: cur.attributes[DATA_CONSIDER_CONTROL_TYPE]?.value
}
let existControl = pre.find(x => { return x.controlId === control.controlId });
if (control.controlId?.length > 0 && !existControl) {
return pre.concat(control);
}
else {
return pre;
}
}, []);
this.postMessageToMainWindow({ topic: 'navigationControls', data: controls });
}
highLightMarkElements(highLight: any, controlData: any) {
if (highLight) {
let Items = document.querySelectorAll(`[${DATA_CONSIDER_ID}]`);
// @ts-ignore
Items.forEach((item) => {
const controlId = item.getAttribute(DATA_CONSIDER_ID);
if (!controlData.find((x: { dataConsiderId: any; }) => { return x.dataConsiderId === controlId })) {
item && item.classList.add(WLS_CIRCLE_MARKLIGHT);
}
});
} else {
let Items = document.querySelectorAll(`[${DATA_CONSIDER_ID}]`);
// @ts-ignore
Items.forEach((item) => {
const controlId = item.getAttribute(DATA_CONSIDER_ID);
if (!controlData.find((x: any) => { return x.dataConsiderId === controlId })) {
item && item.classList.remove(WLS_CIRCLE_MARKLIGHT);
}
});
}
}
selectElement(event: any, targetElement: any) {
this.isItemSelected = true;
const elements = doc.getElementsByClassName(WLS_CIRCLE_SELECT);
for (let i = 0, len = elements.length; i < len; i++) {
elements[i].classList.remove(WLS_CIRCLE_SELECT);
}
eventUtil.stopDefault(event);
eventUtil.stopBubble(event);
targetElement.classList.add(WLS_CIRCLE_SELECT);
}
postMessageToMainWindow(message: any) {
const { postMsgOpts } = this.options;
postMsgOpts.forEach((opt) => {
const { targetWindow, targetOrigin } = opt;
targetWindow.postMessage(message, targetOrigin);
});
}
isMarkedAsConsider(targetElement: any) {
const { attributes } = targetElement;
let flag = false;
if (attributes[DATA_CONSIDER_ID]) {
flag = true;
}
return flag;
}
findElementByAttr(element: HTMLElement) {
let trueElement = element;
let i = 0;
while (!this.isMarkedAsConsider(trueElement) && i < 3) {
if (trueElement.parentElement) {
trueElement = trueElement.parentElement;
}
else {
break;
}
i++;
}
return trueElement;
}
circleHoverHandle(e: Event) {
let scope = this;
if (scope.isItemSelected) {
setTimeout(() => {
scope.isItemSelected = false;
}, 500);
return;
}
try {
let { event, targetElement } = getEvent(e);
let element = targetElement;
if (!this.isMarkedAsConsider(element)) {
element = this.findElementByAttr(targetElement.parentElement);
}
if (this.isMarkedAsConsider(element)) {
const assignData = {
et: 'mouseenter',
ed: 'auto_hover',
}
const logData = getLogData(event, element, assignData);
//@ts-ignore
if (window.Is_Circle_Mark) {
this.selectElement(event, element);
}
}
} catch (err) {
console.log(`circleHoverHandle`, err);
}
}
circleClickHandle(e: Event) {
try {
const { event, targetElement } = getEvent(e);
let element = targetElement;
if (!this.isMarkedAsConsider(element)) {
element = this.findElementByAttr(targetElement.parentElement);
}
if (this.isMarkedAsConsider(element)) {
const { appName } = this.options;
const logData = getLogData(event, element);
//@ts-ignore
if (window.Is_Circle_Mark) {
this.selectElement(event, element);
this.postMessageToMainWindow({ topic: 'circleMark', data: { ...logData, appName } });
} else {
console.log(logData);
}
}
} catch (err) {
console.log(`circleClickHandle`, err);
}
}
throttleLast(fn: Function, delay: number): Function {
let timer: any;
return function (...args: any) {
clearTimeout(timer);
timer = setTimeout(() => {
// @ts-ignore
fn.apply(this, args)
}, delay);
}
}
bindCircleCollection(switcher: boolean = false) {
const circleHoverThrottle = this.throttleLast(this.circleHoverHandle.bind(this), 500);
const circleClickHandleThrottle = this.circleClickHandle.bind(this);
if (switcher) {
eventUtil.on(doc.body, 'click', circleClickHandleThrottle);
eventUtil.on(doc.body, 'mouseenter', circleHoverThrottle);
}
else {
eventUtil.off(doc.body, 'click', circleClickHandleThrottle);
eventUtil.off(doc.body, 'mouseenter', circleHoverThrottle);
}
}
}
export default CircleMark;
const event = {
on: (target: any, type: any, handler: any) => {
if (target.addEventListener) {
target.addEventListener(type, handler, true);
} else {
target.attachEvent(
'on' + type,
(event: Event) => handler.call(target, event),
false,
);
}
},
off: (target: any, type: any, handler: any) => {
if (target.removeEventListener) {
target.removeEventListener(type, handler, true);
} else {
target.detachEvent(
'on' + type,
(event: Event) => handler.call(target, event),
false
);
}
},
stopDefault: (e: Event) => {
if (typeof e.preventDefault === 'function') {
e.preventDefault();
}
if (typeof e.returnValue === 'boolean') {
e.returnValue = false;
}
},
stopBubble: (e: Event) => {
if (typeof e.stopPropagation === 'function') {
e.stopPropagation();
}
if (typeof e.cancelBubble === 'boolean') {
e.cancelBubble = true;
}
},
};
export default event;
mport { ref, title } from "./bom";
import { DATA_CONSIDER_ID, DATA_CONSIDER_CONTROL_TYPE, DATA_CONSIDER_INDEX } from "./userConsiderDic";
//目的是为了能够获取当前准确的位置
const getBoundingClientRect = (elm: any) => {
const rect = elm?.getBoundingClientRect();
const { left, top, right, bottom } = rect;
const width = rect.width || right - left;
const height = rect.height || bottom - top;
return {
width,
height,
left,
top,
bottom,
right,
};
}
const sliceText = (text = '') => {
const limit = 15;
const len = text.length;
if (len > limit * 2) {
return `${text.substring(0, limit)}...${text.substring(len - limit, len)}`;
}
return text;
}
const buildLogData = (eventData: { dataConsiderId: any; dataConsiderControlType: any; dataConsiderEvent: string; dataConsiderEvents: string[]; dataConsiderIndex: any; left: any; top: any; width: any; height: any; }) => {
return {
eventData: {
...eventData,
rUrl: ref,
docTitle: title,
t: new Date().getTime(),
},
};
}
const getLogData = (event: { pageX: any; clientX: number; pageY: any; clientY: number; }, targetElement: { nodeName?: any; innerText?: any; value?: any; attributes?: any; getBoundingClientRect?: any }, assignData = {}) => {
const { attributes } = targetElement;
const dataConsiderId = attributes[DATA_CONSIDER_ID]?.value.toString();
const dataConsiderControlType = attributes[DATA_CONSIDER_CONTROL_TYPE]?.value.toString();
let dataConsiderIndex = [];
if (attributes[DATA_CONSIDER_INDEX]) {
dataConsiderIndex.push(attributes[DATA_CONSIDER_INDEX]?.value.toString());
}
if (attributes[`${DATA_CONSIDER_INDEX}-y`]) {
dataConsiderIndex.push(attributes[`${DATA_CONSIDER_INDEX}-y`]?.value.toString());
}
const nodeName = targetElement.nodeName && targetElement.nodeName.toLocaleLowerCase() || '';
const text = targetElement.innerText || targetElement.value;
const rect = getBoundingClientRect(targetElement);
const documentElement = document.documentElement || document.body.parentNode;
// @ts-ignore
const scrollX = (documentElement && typeof documentElement.scrollLeft == 'number' ? documentElement : document.body).scrollLeft;
// @ts-ignore
const scrollY = (documentElement && typeof documentElement.scrollTop == 'number' ? documentElement : document.body).scrollTop;
const pageX = event.pageX || event.clientX + scrollX;
const pageY = event.pageY || event.clientY + scrollY;
// @ts-ignore
let canSelectAttributes = Array.from(attributes).map((x: any) => { return x.name; });
if (nodeName === 'button') {
canSelectAttributes.push('button-name');
}
let multipleItemNoIndex = documentElement?.querySelectorAll ? documentElement.querySelectorAll(`[${DATA_CONSIDER_ID}="${dataConsiderId}"]`)?.length > 1 && dataConsiderIndex.length === 0 : false;
const eventData = {
dataConsiderId,
dataConsiderControlType,
dataConsiderEvent: '',
dataConsiderIndex,
nodeName,
canSelectAttributes,
multipleItemNoIndex,
text: sliceText(text),
offsetX: ((pageX - rect.left - scrollX) / rect.width).toFixed(6),
offsetY: ((pageY - rect.top - scrollY) / rect.height).toFixed(6),
pageX,
pageY,
scrollX,
scrollY,
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
};
const logData = buildLogData({ ...eventData, ...assignData });
return logData;
}
export default getLogData;