From 8f33376722b2ddabbb2784aadf471966d1147230 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 9 Nov 2023 15:37:51 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[inula-dev-tools]=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=A0=91=E9=80=89=E4=B8=AD=E9=AB=98=E4=BA=AE=E5=90=88?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/highlight/index.ts | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 packages/inula-dev-tools/src/highlight/index.ts diff --git a/packages/inula-dev-tools/src/highlight/index.ts b/packages/inula-dev-tools/src/highlight/index.ts new file mode 100644 index 00000000..78ae260b --- /dev/null +++ b/packages/inula-dev-tools/src/highlight/index.ts @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import assign from 'object-assign'; +import { VNode } from '../../../inula/src/renderer/vnode/VNode'; + +const overlayStyles = { + background: 'rgba(120, 170, 210, 0.7)', + padding: 'rgba(77, 200, 0, 0.3)', + margin: 'rgba(255, 155, 0, 0.3)', + border: 'rgba(255, 200, 50, 0.3)' +}; + +type Rect = { + bottom: number; + height: number; + left: number; + right: number; + top: number; + width: number; +}; + +function setBoxStyle(eleStyle, boxArea, node) { + assign(node.style, { + borderTopWidth: eleStyle[boxArea + 'Top'] + 'px', + borderLeftWidth: eleStyle[boxArea + 'Left'] + 'px', + borderRightWidth: eleStyle[boxArea + 'Right'] + 'px', + borderBottomWidth: eleStyle[boxArea + 'Bottom'] + 'px', + }); +} + +function getOwnerWindow(node: Element): typeof window | null { + if (!node.ownerDocument) { + return null; + } + return node.ownerDocument.defaultView; +} + +function getOwnerIframe(node: Element): Element | null { + const nodeWindow = getOwnerWindow(node); + if (nodeWindow) { + return nodeWindow.frameElement; + } + return null; +} + +function getElementStyle(domElement: Element) { + const style = window.getComputedStyle(domElement); + return{ + marginLeft: parseInt(style.marginLeft, 10), + marginRight: parseInt(style.marginRight, 10), + marginTop: parseInt(style.marginTop, 10), + marginBottom: parseInt(style.marginBottom, 10), + borderLeft: parseInt(style.borderLeftWidth, 10), + borderRight: parseInt(style.borderRightWidth, 10), + borderTop: parseInt(style.borderTopWidth, 10), + borderBottom: parseInt(style.borderBottomWidth, 10), + paddingLeft: parseInt(style.paddingLeft, 10), + paddingRight: parseInt(style.paddingRight, 10), + paddingTop: parseInt(style.paddingTop, 10), + paddingBottom: parseInt(style.paddingBottom, 10) + }; +} + +function mergeRectOffsets(rects: Array): Rect { + return rects.reduce((previousRect, rect) => { + if (previousRect == null) { + return rect; + } + + return { + top: previousRect.top + rect.top, + left: previousRect.left + rect.left, + width: previousRect.width + rect.width, + height: previousRect.height + rect.height, + bottom: previousRect.bottom + rect.bottom, + right: previousRect.right + rect.right + }; + }); +} + +function getBoundingClientRectWithBorderOffset(node: Element) { + const dimensions = getElementStyle(node); + return mergeRectOffsets([ + node.getBoundingClientRect(), + { + top: dimensions.borderTop, + left: dimensions.borderLeft, + bottom: dimensions.borderBottom, + right:dimensions.borderRight, + // 高度和宽度不会被使用 + width: 0, + height: 0 + } + ]); +} + +function getNestedBoundingClientRect( + node: HTMLElement, + boundaryWindow +): Rect { + const ownerIframe = getOwnerIframe(node); + if (ownerIframe && ownerIframe !== boundaryWindow) { + const rects = [node.getBoundingClientRect()] as Rect[]; + let currentIframe = ownerIframe; + let onlyOneMore = false; + while (currentIframe) { + const rect = getBoundingClientRectWithBorderOffset(currentIframe); + rects.push(rect); + currentIframe = getOwnerIframe(currentIframe); + + if (onlyOneMore) { + break; + } + + if (currentIframe &&getOwnerWindow(currentIframe) === boundaryWindow) { + onlyOneMore = true; + } + } + + return mergeRectOffsets(rects); + } else { + return node.getBoundingClientRect(); + } +} + +// 用来遮罩 +class OverlayRect { + node: HTMLElement; + border: HTMLElement; + padding: HTMLElement; + content: HTMLElement; + + constructor(doc: Document, container: HTMLElement) { + this.node = doc.createElement('div'); + this.border = doc.createElement('div'); + this.padding = doc.createElement('div'); + this.content = doc.createElement('div'); + + this.border.style.borderColor = overlayStyles.border; + this.padding.style.borderColor = overlayStyles.padding; + this.content.style.backgroundColor = overlayStyles.background; + + assign(this.node.style, { + borderColor: overlayStyles.margin, + pointerEvents: 'none', + position: 'fixed' + }); + + this.node.style.zIndex = '10000000'; + + this.node.appendChild(this.border); + this.border.appendChild(this.padding); + this.padding.appendChild(this.content); + container.appendChild(this.node); + } + + remove() { + if (this.node.parentNode) { + this.node.parentNode.removeChild(this.node); + } + } + + update(boxRect: Rect, eleStyle: any) { + setBoxStyle(eleStyle, 'margin', this.node); + setBoxStyle(eleStyle, 'border', this.border); + setBoxStyle(eleStyle, 'padding', this.padding); + + assign(this.content.style, { + height: boxRect.height - eleStyle.borderTop - eleStyle.borderBottom - eleStyle.paddingTop - eleStyle.paddingBottom + 'px', + width: boxRect.width - eleStyle.borderLeft - eleStyle.borderRight - eleStyle.paddingLeft - eleStyle.paddingRight + 'px' + }); + + assign(this.node.style, { + top: boxRect.top - eleStyle.marginTop + 'px', + left: boxRect.left - eleStyle.marginLeft + 'px' + }); + } +} + +class ElementOverlay { + window: typeof window; + container: HTMLElement; + rects: Array; + + constructor() { + this.window = window; + const doc = window.document; + this.container = doc.createElement('div'); + this.container.style.zIndex = '10000000'; + this.rects = []; + + doc.body.appendChild(this.container); + } + + remove() { + this.rects.forEach(rect => { + rect.remove(); + }); + this.rects.length = 0; + if (this.container.parentNode) { + this.container.parentNode.removeChild(this.container); + } + } + + execute(nodes: Array) { + const elements = nodes.filter(node => node.tag === 'DomComponent'); + + // 有几个 element 就添加几个 OverlayRect + while (this.rects.length > elements.length) { + const rect = this.rects.pop(); + rect.remove(); + } + if (elements.length === 0) { + return; + } + + while (this.rects.length < elements.length) { + this.rects.push(new OverlayRect(this.window.document, this.container)); + } + + const outerBox = { + top: Number.POSITIVE_INFINITY, + right: Number.NEGATIVE_INFINITY, + bottom: Number.NEGATIVE_INFINITY, + left: Number.POSITIVE_INFINITY + }; + + elements.forEach((element, index) => { + const eleStyle = getElementStyle(element.realNode); + const boxRect = getNestedBoundingClientRect(element.realNode, this.window); + + outerBox.top = Math.min(outerBox.top, boxRect.top - eleStyle.marginTop); + outerBox.right = Math.max(outerBox.right, boxRect.left + boxRect.width + eleStyle.marginRight); + outerBox.bottom = Math.max(outerBox.bottom, boxRect.top + boxRect.height + eleStyle.marginBottom); + outerBox.left = Math.min(outerBox.left, boxRect.left - eleStyle.marginLeft); + + const rect = this.rects[index]; + rect.update(boxRect, eleStyle); + }); + } +} + +let elementOverlay: ElementOverlay | null = null; +export function hideHighlight() { + if (elementOverlay !== null) { + elementOverlay.remove(); + elementOverlay = null; + } +} + +export function showHighlight(elements: Array | null) { + if (window.document == null || elements == null) { + return; + } + + if (elementOverlay === null) { + elementOverlay = new ElementOverlay(); + } + + elementOverlay.execute(elements); +} From 559a6fc3aaddd2cdbb9bee330760970004abe3f9 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Mon, 13 Nov 2023 16:52:51 +0800 Subject: [PATCH 2/2] =?UTF-8?q?[inula-dev-tools]=20injector=20?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/injector/index.ts | 485 ++++++++++++++++++ .../src/injector/pickElement.ts | 96 ++++ 2 files changed, 581 insertions(+) create mode 100644 packages/inula-dev-tools/src/injector/index.ts create mode 100644 packages/inula-dev-tools/src/injector/pickElement.ts diff --git a/packages/inula-dev-tools/src/injector/index.ts b/packages/inula-dev-tools/src/injector/index.ts new file mode 100644 index 00000000..3cf3cf2d --- /dev/null +++ b/packages/inula-dev-tools/src/injector/index.ts @@ -0,0 +1,485 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import parseTreeRoot, { clearVNode, queryVNode, VNodeToIdMap } from '../parser/parseVNode'; +import { packagePayload, checkMessage } from '../utils/transferUtils'; +import { + RequestAllVNodeTreeInfos, + AllVNodeTreeInfos, + RequestComponentAttrs, + ComponentAttrs, + DevToolHook, + DevToolContentScript, + ModifyAttrs, + ModifyHooks, + ModifyState, + ModifyProps, + InspectDom, + LogComponentData, + Highlight, + RemoveHighlight, + ViewSource, + PickElement, + StopPickElement, + CopyToConsole, + StorageValue, +} from '../utils/constants'; +import { VNode } from '../../../inula/src/renderer/vnode/VNode'; +import { parseVNodeAttrs } from '../parser/parseAttr'; +import { showHighlight, hideHighlight } from '../highlight'; +import { + FunctionComponent, + ClassComponent, + IncompleteClassComponent, + ForwardRef, + MemoComponent +} from '../../../inula/src/renderer/vnode/VNodeTags'; +import { pickElement } from './pickElement'; + +const roots = []; +let storeDataCount = 0; + +function addIfNotInclude(treeRoot: VNode) { + if (!roots.includes(treeRoot)) { + roots.push(treeRoot); + } +} + +function send() { + const result = roots.reduce((pre, current) => { + const info = parseTreeRoot(helper.travelVNodeTree, current); + pre.push(info); + return pre; + }, []); + postMessage(AllVNodeTreeInfos, result); +} + +function deleteVNode(vNode: VNode) { + // 开发工具中保存了 vNode 的引用,在清理 vNode 的时候需要一并删除 + clearVNode(vNode); + const index = roots.indexOf(vNode); + if (index !== -1) { + roots.splice(index, 1); + } +} + +export function postMessage(type: string, data) { + window.postMessage( + packagePayload( + { + type: type, + data: data, + }, + DevToolHook + ), + '*' + ); +} + +function parseCompAttrs(id: number) { + const vNode = queryVNode(id); + if (!vNode) { + console.error('Do not find match vNode, this is a bug, please report us.'); + return; + } + const parsedAttrs = parseVNodeAttrs(vNode, helper.getHookInfo); + postMessage(ComponentAttrs, parsedAttrs); +} + +function calculateNextValue(editValue, value, attrPath) { + let nextState; + const editValueType = typeof editValue; + if ( + editValueType === 'string' || + editValueType === 'undefined' || + editValueType === 'boolean' + ) { + nextState = value; + } else if (editValueType === 'number') { + const numValue = Number(value); + nextState = isNaN(numValue) ? value : numValue; // 如果能转为数字,转数字,不能转数字旧用原值 + } else if (editValueType === 'object') { + if (editValue === null) { + nextState = value; + } else { + const newValue = Array.isArray(editValue) ? [...editValue] : { ...editValue }; + // 遍历读取到直接指向需要修改值的对象 + let attr = newValue; + for (let i = 0; i < attrPath.length - 1; i++) { + attr = attr[attrPath[i]]; + } + // 修改对象上的值 + attr[attrPath[attrPath.length - 1]] = value; + nextState = newValue; + } + } else { + console.error( + 'The dev tools tried to edit a non-editable value, this is a bug, please report.', + editValue + ); + } + return nextState; +} + +function modifyVNodeAttrs(data) { + const { type, id, value, path } = data; + const vNode = queryVNode(id); + if (!vNode) { + console.error('Do not find match vNode, this is a bug, please report us.'); + return; + } + if (type === ModifyProps) { + const nextProps = calculateNextValue(vNode.props, value, path); + helper.updateProps(vNode, nextProps); + } else if (type === ModifyHooks) { + const hooks = vNode.hooks; + const editHook = hooks[path[0]]; + const hookInfo = helper.getHookInfo(editHook); + if (hookInfo) { + const editValue = hookInfo.value; + // path 的第一个指向 hIndex,从第二个值才开始指向具体属性访问路径 + const nextState = calculateNextValue(editValue, value, path.slice(1)); + helper.updateHooks(vNode, path[0], nextState); + } else { + console.error( + 'The dev tools tried to edit a non-editable hook, this is a bug, please report.', + hooks + ); + } + } else if (type === ModifyState) { + const oldState = vNode.state || {}; + const nextState = { ...oldState }; + let accessRef = nextState; + for (let i = 0; i < path.length - 1; i++) { + accessRef = accessRef[path[i]]; + } + accessRef[path[path.length - 1]] = value; + helper.updateState(vNode, nextState); + } +} + +function logComponentData(id: number) { + const vNode = queryVNode(id); + if (vNode == null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + if (vNode) { + const info = helper.getComponentInfo(vNode); + console.log('vNode: ', vNode); + console.log('Component Info: ', info); + } +} + +/** + * 通过 path 在 vNode 拿到对应的值 + * + * @param {VNode} vNode dom 节点 + * @param {Array} path 路径 + * @param {string} attrsName 值的类型(props 或者 hooks) + */ +const getValueByPath = ( + vNode: VNode, + path: Array, + attrsName: string +) => { + if (attrsName === 'Props') { + return path.reduce((previousValue, currentValue) => { + return previousValue[currentValue]; + }, vNode.props); + } else { + // attrsName 为 Hooks + if (path.length > 1) { + return path.reduce((previousValue, currentValue) => { + return previousValue[currentValue]; + }, vNode.hooks); + } + return vNode.hooks[path[0]]; + } +}; + +/** + * 通过 path 在 vNode 拿到对应的值,并且在控制台打印出来 + * + * @param {number} id idToVNodeMap 的 key 值,通过 id 拿到 VNode + * @param {string} itemName 打印出来值的名称 + * @param {Array} path 值的路径 + * @param {string} attrsName 值的类型 + */ +function logDataWithPath( + id: number, + itemName: string, + path: Array, + attrsName: string +) { + const vNode = queryVNode(id); + if (vNode === null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + if (vNode) { + const value = getValueByPath(vNode, path, attrsName); + if (attrsName === 'Hooks') { + console.log(itemName, value); + } else { + console.log(`${path[path.length - 1]}`, value); + } + } +} + +/** + * 通过 path 在 vNode 拿到对应的值,并且存为全局变量 + * + * @param {number} id idToVNodeMap 的 key 值,通过 id 拿到 VNode + * @param {Array} path 值的路径 + * @param {string} attrsName 值的类型 + */ +function storeDataWithPath( + id: number, + path: Array, + attrsName: string +) { + const vNode = queryVNode(id); + if (vNode === null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + if (vNode) { + const value = getValueByPath(vNode, path, attrsName); + const key = `$InulaTemp${storeDataCount++}`; + + window[key] = value; + console.log(key); + console.log(value); + } +} + +export let helper; + +function init(inulaHelper) { + helper = inulaHelper; + (window as any).__INULA_DEV_HOOK__.isInit = true; +} + +export function getElement(travelVNodeTree, treeRoot: VNode) { + const result: any[] = []; + travelVNodeTree( + treeRoot, + (node: VNode) => { + if (node.realNode) { + if (Object.keys(node.realNode).length > 0 || node.realNode.size > 0) { + result.push(node); + } + } + }, + (node: VNode) => + node.realNode != null && + (Object.keys(node.realNode).length > 0 || node.realNode.size > 0) + ); + return result; +} + +// dev tools 点击眼睛图标功能 +const inspectDom = data => { + const { id } = data; + const vNode = queryVNode(id); + if (vNode == null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + const info = getElement(helper.travelVNodeTree, vNode); + if (info) { + showHighlight(info); + (window as any).__INULA_DEV_HOOK__.$0 = info[0]; + } +}; + +const picker = pickElement(window); + +const actions = new Map([ + // 请求左树所有数据 + [ + RequestAllVNodeTreeInfos, + () => { + send(); + }, + ], + // 请求某个节点的 props,hooks + [ + RequestComponentAttrs, + data => { + parseCompAttrs(data); + }, + ], + // 修改 props,hooks + [ + ModifyAttrs, + data => { + modifyVNodeAttrs(data); + }, + ], + // 找到节点对应 element + [ + InspectDom, + data => { + inspectDom(data); + }, + ], + // 打印节点数据 + [ + LogComponentData, + data => { + logComponentData(data); + }, + ], + // 高亮 + [ + Highlight, + data => { + const node = queryVNode(data.id); + if (node == null) { + console.warn(`Could not find vNode with id "${data.id}"`); + return null; + } + const info = getElement(helper.travelVNodeTree, node); + showHighlight(info); + }, + ], + // 移出高亮 + [ + RemoveHighlight, + () => { + hideHighlight(); + }, + ], + // 查看节点源代码位置 + [ + ViewSource, + data => { + const node = queryVNode(data.id); + if (node == null) { + console.warn(`Could not find vNode with id "${data.id}"`); + return null; + } + showSource(node); + }, + ], + // 选中页面元素对应 dev tools 节点 + [ + PickElement, + () => { + picker.startPick(); + }, + ], + [ + StopPickElement, + () => { + picker.stopPick(); + }, + ], + // 在控制台打印 Props Hooks State 值 + [ + CopyToConsole, + data => { + const node = queryVNode(data.id); + if (node == null) { + console.warn(`Could not find vNode with id "${data.id}"`); + return null; + } + logDataWithPath(data.id, data.itemName, data.path, data.attrsName); + }, + ], + // 把 Props Hooks State 值存为全局变量 + [ + StorageValue, + data => { + const node = queryVNode(data.id); + if (node == null) { + console.warn(`Could not find vNode with id "${data.id}"`); + return null; + } + storeDataWithPath(data.id, data.path, data.attrsName); + }, + ], +]); + +const showSource = (node: VNode) => { + switch (node.tag) { + case ClassComponent: + case IncompleteClassComponent: + case FunctionComponent: + global.$type = node.type; + break; + case ForwardRef: + global.$type = node.type.render; + break; + case MemoComponent: + global.$type = node.type.type; + break; + default: + global.$type = null; + break; + } +}; + +const handleRequest = (type: string, data) => { + const action = actions.get(type); + if (action) { + action.call(this, data); + return null; + } + console.warn('unknown command', type); +}; + +function injectHook() { + if ((window as any).__INULA_DEV_HOOK__) { + return; + } + Object.defineProperty(window, '__INULA_DEV_HOOK__', { + enumerable: false, + value: { + $0: null, + init, + isInit: false, + addIfNotInclude, + send, + deleteVNode, + // inulaX 使用 + getVNodeId: vNode => { + return VNodeToIdMap.get(vNode); + }, + }, + }); + + window.addEventListener('message', function (event) { + // 只接收我们自己的消息 + if (event.source !== window) { + return; + } + const request = event.data; + if (checkMessage(request, DevToolContentScript)) { + const { payload } = request; + const { type, data } = payload; + + // 忽略 inulaX 的 actions + if (type.startsWith('inulax')) { + return; + } + handleRequest(type, data); + } + }); +} + +injectHook(); diff --git a/packages/inula-dev-tools/src/injector/pickElement.ts b/packages/inula-dev-tools/src/injector/pickElement.ts new file mode 100644 index 00000000..b1116faf --- /dev/null +++ b/packages/inula-dev-tools/src/injector/pickElement.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { PickElement, StopPickElement } from '../utils/constants'; +import { getElement, helper, postMessage } from './index'; +import { queryVNode, VNodeToIdMap } from '../parser/parseVNode'; +import { isUserComponent } from '../parser/parseVNode'; +import { throttle } from 'lodash'; +import { hideHighlight, showHighlight } from '../highlight'; + +// 判断鼠标移入节点是否为 dev tools 上的节点,如果不是则找父节点 +function getUserComponent(target) { + if (target.tag && isUserComponent(target.tag)) { + return target; + } + while (target.tag && !isUserComponent(target.tag)) { + if (target.parent) { + target = target.parent; + } + } + return target; +} + +function onMouseEvent(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); +} + +function onMouseMove(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + + const target = (event.target as any)._inula_VNode; + if (target) { + const id = VNodeToIdMap.get(getUserComponent(target)); + const vNode = queryVNode(id); + if (vNode == null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + const info = getElement(helper.travelVNodeTree, vNode); + if (info) { + showHighlight(info); + } + + // 0.5 秒内在节流结束后只触发一次 + throttle( + () => { + postMessage(PickElement, id); + }, + 500, + { leading: false, trailing: true } + )(); + } +} + +export function pickElement(window: Window) { + function onClick(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + + stopPick(); + postMessage(StopPickElement, null); + } + + const startPick = () => { + if (window && typeof window.addEventListener === 'function') { + window.addEventListener('click', onClick, true); + window.addEventListener('mousedown', onMouseEvent, true); + window.addEventListener('mousemove', onMouseMove, true); + window.addEventListener('mouseup', onMouseEvent, true); + } + }; + + const stopPick = () => { + hideHighlight(); + window.removeEventListener('click', onClick, true); + window.removeEventListener('mousedown', onMouseEvent, true); + window.removeEventListener('mousemove', onMouseMove, true); + window.removeEventListener('mouseup', onMouseEvent, true); + }; + + return { startPick, stopPick }; +}