From 14e07008a70b65d8c53c0ee162472f8d8a2c4a1f Mon Sep 17 00:00:00 2001 From: Bowen Tan Date: Fri, 27 May 2022 17:32:15 +0800 Subject: [PATCH 1/2] use elementFromPoint to locate mouse --- .../EditorMaskWrapper/EditorMask.tsx | 2 +- .../EditorMaskWrapper/EditorMaskManager.ts | 81 ++++++++++++++++--- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/components/EditorMaskWrapper/EditorMask.tsx b/packages/editor/src/components/EditorMaskWrapper/EditorMask.tsx index adf62f49..e8025a17 100644 --- a/packages/editor/src/components/EditorMaskWrapper/EditorMask.tsx +++ b/packages/editor/src/components/EditorMaskWrapper/EditorMask.tsx @@ -62,7 +62,7 @@ export const EditorMask: React.FC = observer((props: Props) => { const { hoverComponentId, hoverMaskPosition, selectedMaskPosition } = store; useEffect(() => { - store.setMousePosition(mousePosition); + // store.setMousePosition(mousePosition); }, [mousePosition, store]); useEffect(() => { diff --git a/packages/editor/src/components/EditorMaskWrapper/EditorMaskManager.ts b/packages/editor/src/components/EditorMaskWrapper/EditorMaskManager.ts index 7bfa7b46..34159592 100644 --- a/packages/editor/src/components/EditorMaskWrapper/EditorMaskManager.ts +++ b/packages/editor/src/components/EditorMaskWrapper/EditorMaskManager.ts @@ -1,5 +1,7 @@ -import { action, computed, makeObservable, observable } from 'mobx'; +import { forEach } from 'lodash-es'; +import { action, autorun, computed, makeObservable, observable } from 'mobx'; import React from 'react'; +import { consoleError } from '../../../../shared/lib'; import { EditorServices } from '../../types'; export class EditorMaskManager { @@ -12,20 +14,15 @@ export class EditorMaskManager { // observable: the coordinate system offset, it is almost equal to the scroll value of maskWrapper systemOffset: [number, number] = [0, 0]; modalContainerEle: HTMLElement | null = null; + elementIdMap = new Map(); + hoverElement: Element | null = null; + hoverComponentId = ''; // visible status of all components. private visibleMap = new Map(); private resizeObserver: ResizeObserver; private intersectionObserver: IntersectionObserver; private MaskPadding = 4; - - get hoverComponentId() { - const where = whereIsMouse( - this.mousePosition[0] - this.systemOffset[0], - this.mousePosition[1] - this.systemOffset[1], - this.rects - ); - return where; - } + private hoverElementCache: Element | null = null; get hoverMaskPosition() { return this.getMaskPosition(this.hoverComponentId); @@ -46,17 +43,23 @@ export class EditorMaskManager { mousePosition: observable.ref, maskContainerRect: observable.ref, systemOffset: observable.ref, + elementIdMap: observable.ref, + hoverElement: observable.ref, + hoverComponentId: observable, setRects: action, setMousePosition: action, setMaskContainerRect: action, setSystemOffset: action, - hoverComponentId: computed, + setElementIdMap: action, + setHoverElement: action, + setHoverComponentId: action, hoverMaskPosition: computed, selectedMaskPosition: computed, }); this.resizeObserver = new ResizeObserver(() => { this.refreshSystem(); + this.refreshHoverElement(); }); this.intersectionObserver = this.initIntersectionObserver(); @@ -64,6 +67,36 @@ export class EditorMaskManager { this.observeIntersection(); this.observeResize(); this.observeEvents(); + + setTimeout(() => { + document.addEventListener('mousemove', e => { + this.setMousePosition([e.x, e.y]); + this.refreshHoverElement(); + }); + }); + + autorun(() => { + if (!this.hoverElement) return; + if (this.hoverElement === this.hoverElementCache) return; + console.log('hoverComponentId 计算'); + const root = document.getElementById('editor-mask-wrapper'); + + let curr = this.hoverElement; + while (!this.elementIdMap.has(curr)) { + if (curr !== root && curr.parentElement) { + curr = curr.parentElement; + } else { + break; + } + } + console.log(this.elementIdMap.get(curr) || ''); + this.hoverElementCache = this.hoverElement; + this.setHoverComponentId(this.elementIdMap.get(curr) || ''); + }); + + autorun(() => { + this.hoverComponentIdRef.current = this.hoverComponentId; + }); } private initIntersectionObserver(): IntersectionObserver { @@ -81,6 +114,7 @@ export class EditorMaskManager { }); // the coordinate system need to be refresh this.refreshSystem(); + this.refreshHoverElement(); }, options); } @@ -107,6 +141,13 @@ export class EditorMaskManager { this.refreshSystem(); this.observeIntersection(); this.observeResize(); + + const eleIdMap = new Map(); + this.eleMap.forEach((ele, id) => { + eleIdMap.set(ele, id); + }); + this.setElementIdMap(eleIdMap); + this.refreshHoverElement(); }); } @@ -169,6 +210,10 @@ export class EditorMaskManager { }; } + private refreshHoverElement() { + this.setHoverElement(document.elementFromPoint(...this.mousePosition)); + } + setMousePosition(val: [number, number]) { this.mousePosition = val; } @@ -185,6 +230,20 @@ export class EditorMaskManager { this.maskContainerRect = maskContainerRect; } + setElementIdMap(elementIdMap: Map) { + this.elementIdMap = elementIdMap; + } + + setHoverElement(hoverElement: Element | null) { + console.log('setHoverElement'); + this.hoverElement = hoverElement; + } + + setHoverComponentId(hoverComponentId: string) { + console.log('sethoverComponentId'); + this.hoverComponentId = hoverComponentId; + } + private get eleMap() { return this.services.editorStore.eleMap; } From 44044d2d8af6ace1012deacb908f5e6bca506ad1 Mon Sep 17 00:00:00 2001 From: Bowen Tan Date: Mon, 30 May 2022 15:11:41 +0800 Subject: [PATCH 2/2] refactor(editorMask): remove coordinates system --- packages/editor/__tests__/editorMask.spec.ts | 13 - .../EditorMaskWrapper/EditorMask.tsx | 23 +- .../EditorMaskWrapper/EditorMaskManager.ts | 298 +++++++----------- .../EditorMaskWrapper/EditorMaskWrapper.tsx | 1 + packages/editor/src/services/eventBus.ts | 1 + 5 files changed, 127 insertions(+), 209 deletions(-) delete mode 100644 packages/editor/__tests__/editorMask.spec.ts diff --git a/packages/editor/__tests__/editorMask.spec.ts b/packages/editor/__tests__/editorMask.spec.ts deleted file mode 100644 index 30fdc8a6..00000000 --- a/packages/editor/__tests__/editorMask.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {whereIsMouse} from '../src/components/EditorMaskWrapper/EditorMaskManager' - -const mockRects = JSON.parse( - '{"button19":{"x":307,"y":85,"width":45.046875,"height":40,"top":85,"right":352.046875,"bottom":125,"left":307},"button15":{"x":393.046875,"y":102,"width":40,"height":40,"top":102,"right":433.046875,"bottom":142,"left":393.046875},"input18":{"x":457.046875,"y":102,"width":34,"height":40,"top":102,"right":491.046875,"bottom":142,"left":457.046875},"vstack20":{"x":515.046875,"y":102,"width":55.359375,"height":40,"top":102,"right":570.40625,"bottom":142,"left":515.046875},"hstack14":{"x":376.046875,"y":85,"width":211.359375,"height":74,"top":85,"right":587.40625,"bottom":159,"left":376.046875},"input16":{"x":376.046875,"y":199,"width":211.359375,"height":40,"top":199,"right":587.40625,"bottom":239,"left":376.046875},"stack13":{"x":376.046875,"y":85,"width":211.359375,"height":154,"top":85,"right":587.40625,"bottom":239,"left":376.046875},"button17":{"x":611.40625,"y":85,"width":45.046875,"height":40,"top":85,"right":656.453125,"bottom":125,"left":611.40625},"button23":{"x":714.453125,"y":119,"width":61.359375,"height":40,"top":119,"right":775.8125,"bottom":159,"left":714.453125},"button24":{"x":799.8125,"y":119,"width":61.359375,"height":40,"top":119,"right":861.171875,"bottom":159,"left":799.8125},"hstack22":{"x":697.453125,"y":102,"width":298.546875,"height":74,"top":102,"right":996,"bottom":176,"left":697.453125},"vstack21":{"x":680.453125,"y":85,"width":332.546875,"height":108,"top":85,"right":1013,"bottom":193,"left":680.453125},"hstack12":{"x":290,"y":68,"width":740,"height":188,"top":68,"right":1030,"bottom":256,"left":290}}' -); - -it('detect mouse position', () => { - expect(whereIsMouse(-1, -1, mockRects)).toBe('') - expect(whereIsMouse(10000, 10000, mockRects)).toBe('') - expect(whereIsMouse(668, 166, mockRects)).toBe('hstack12') - expect(whereIsMouse(957, 127, mockRects)).toBe('hstack22') - expect(whereIsMouse(409, 126, mockRects)).toBe('button15') -}) diff --git a/packages/editor/src/components/EditorMaskWrapper/EditorMask.tsx b/packages/editor/src/components/EditorMaskWrapper/EditorMask.tsx index e8025a17..f3cb82ff 100644 --- a/packages/editor/src/components/EditorMaskWrapper/EditorMask.tsx +++ b/packages/editor/src/components/EditorMaskWrapper/EditorMask.tsx @@ -1,8 +1,7 @@ import React, { CSSProperties, useEffect, useRef } from 'react'; import { css } from '@emotion/css'; -import { DIALOG_CONTAINER_ID } from '@sunmao-ui/runtime'; import { Box, Text } from '@chakra-ui/react'; -import { observer, useLocalStore } from 'mobx-react-lite'; +import { observer, useLocalObservable } from 'mobx-react-lite'; import { DropSlotMask } from './DropSlotMask'; import { EditorMaskManager } from './EditorMaskManager'; import { EditorServices } from '../../types'; @@ -55,23 +54,29 @@ export const EditorMask: React.FC = observer((props: Props) => { const { isDraggingNewComponent } = editorStore; const maskContainerRef = useRef(null); - const store = useLocalStore( + const manager = useLocalObservable( () => new EditorMaskManager(services, wrapperRef, maskContainerRef, hoverComponentIdRef) ); - const { hoverComponentId, hoverMaskPosition, selectedMaskPosition } = store; - useEffect(() => { - // store.setMousePosition(mousePosition); - }, [mousePosition, store]); + const { hoverComponentId, hoverMaskPosition, selectedMaskPosition } = manager; useEffect(() => { hoverComponentIdRef.current = hoverComponentId; }, [hoverComponentId, hoverComponentIdRef]); useEffect(() => { - store.modalContainerEle = document.getElementById(DIALOG_CONTAINER_ID); - }); + manager.init(); + return () => { + manager.destroy(); + }; + }, [manager]); + + useEffect(() => { + if (mousePosition[0] > 0 && mousePosition[1] > 0) { + manager.setMousePosition(mousePosition); + } + }, [manager, mousePosition]); const hoverMask = hoverMaskPosition ? ( diff --git a/packages/editor/src/components/EditorMaskWrapper/EditorMaskManager.ts b/packages/editor/src/components/EditorMaskWrapper/EditorMaskManager.ts index 34159592..4f32b8ef 100644 --- a/packages/editor/src/components/EditorMaskWrapper/EditorMaskManager.ts +++ b/packages/editor/src/components/EditorMaskWrapper/EditorMaskManager.ts @@ -1,37 +1,29 @@ -import { forEach } from 'lodash-es'; -import { action, autorun, computed, makeObservable, observable } from 'mobx'; +import { action, autorun, makeObservable, observable } from 'mobx'; import React from 'react'; -import { consoleError } from '../../../../shared/lib'; import { EditorServices } from '../../types'; +type MaskPosition = { + id: string; + style: { + top: number; + left: number; + height: number; + width: number; + }; +}; export class EditorMaskManager { - // observable: current mouse position - mousePosition: [number, number] = [0, 0]; - // observable: rects of all foreground components - rects: Record = {}; - // observable: rect of mask container - maskContainerRect: DOMRect | null = null; - // observable: the coordinate system offset, it is almost equal to the scroll value of maskWrapper - systemOffset: [number, number] = [0, 0]; - modalContainerEle: HTMLElement | null = null; - elementIdMap = new Map(); - hoverElement: Element | null = null; hoverComponentId = ''; - // visible status of all components. - private visibleMap = new Map(); + hoverMaskPosition: MaskPosition | null = null; + selectedMaskPosition: MaskPosition | null = null; + mousePosition: [number, number] = [0, 0]; + private elementIdMap = new Map(); + // rect of mask container + private maskContainerRect: DOMRect | null = null; private resizeObserver: ResizeObserver; + private visibleMap = new Map(); private intersectionObserver: IntersectionObserver; private MaskPadding = 4; - private hoverElementCache: Element | null = null; - - get hoverMaskPosition() { - return this.getMaskPosition(this.hoverComponentId); - } - - get selectedMaskPosition() { - return this.getMaskPosition(this.services.editorStore.selectedComponentId); - } - + private lastHoverElement: Element | null = null; constructor( public services: EditorServices, public wrapperRef: React.MutableRefObject, @@ -39,69 +31,67 @@ export class EditorMaskManager { public hoverComponentIdRef: React.MutableRefObject ) { makeObservable(this, { - rects: observable.shallow, - mousePosition: observable.ref, - maskContainerRect: observable.ref, - systemOffset: observable.ref, - elementIdMap: observable.ref, - hoverElement: observable.ref, + mousePosition: observable, + hoverMaskPosition: observable.ref, + selectedMaskPosition: observable.ref, hoverComponentId: observable, - setRects: action, setMousePosition: action, - setMaskContainerRect: action, - setSystemOffset: action, - setElementIdMap: action, - setHoverElement: action, setHoverComponentId: action, - hoverMaskPosition: computed, - selectedMaskPosition: computed, + setHoverMaskPosition: action, + setSelectedMaskPosition: action, }); this.resizeObserver = new ResizeObserver(() => { - this.refreshSystem(); this.refreshHoverElement(); }); this.intersectionObserver = this.initIntersectionObserver(); + } + + init() { + this.maskContainerRect = + this.maskContainerRef.current?.getBoundingClientRect() || null; this.observeIntersection(); this.observeResize(); - this.observeEvents(); + // listen to the DOM elements' mount and unmount events + // TODO: This is not very accurate, because sunmao runtime 'didDOMUpdate' hook is not accurate. + // We will refactor the 'didDOMUpdate' hook with components' life cycle in the future. + this.services.eventBus.on('HTMLElementsUpdated', this.onHTMLElementsUpdated); - setTimeout(() => { - document.addEventListener('mousemove', e => { - this.setMousePosition([e.x, e.y]); - this.refreshHoverElement(); - }); - }); - - autorun(() => { - if (!this.hoverElement) return; - if (this.hoverElement === this.hoverElementCache) return; - console.log('hoverComponentId 计算'); - const root = document.getElementById('editor-mask-wrapper'); - - let curr = this.hoverElement; - while (!this.elementIdMap.has(curr)) { - if (curr !== root && curr.parentElement) { - curr = curr.parentElement; - } else { - break; - } - } - console.log(this.elementIdMap.get(curr) || ''); - this.hoverElementCache = this.hoverElement; - this.setHoverComponentId(this.elementIdMap.get(curr) || ''); - }); + // listen scroll events + // scroll events' timing is similar to intersection events, but they are different. Both of them are necessary. + this.services.eventBus.on('MaskWrapperScrollCapture', this.onScroll); + // expose hoverComponentId autorun(() => { this.hoverComponentIdRef.current = this.hoverComponentId; }); + + // when hoverComponentId & selectedComponentId change, refresh mask position + autorun(() => { + this.refreshMaskPosition(); + }); + + // listen mousePosition change to refreshHoverElement + autorun(() => { + this.refreshHoverElement(); + }); + } + + destroy() { + this.intersectionObserver.disconnect(); + this.resizeObserver.disconnect(); + this.services.eventBus.off('HTMLElementsUpdated', this.onHTMLElementsUpdated); + + // listen scroll events + // scroll events' timing is similar to intersection events, but they are different. Both of them are necessary. + this.services.eventBus.off('MaskWrapperScrollCapture', this.onScroll); } private initIntersectionObserver(): IntersectionObserver { const options = { - root: document.getElementById('editor-mask-wrapper'), + root: null, rootMargin: '0px', threshold: buildThresholdList(), }; @@ -112,9 +102,8 @@ export class EditorMaskManager { entries.forEach(e => { this.visibleMap.set(e.target, e.isIntersecting); }); - // the coordinate system need to be refresh - this.refreshSystem(); - this.refreshHoverElement(); + // Refresh the mask, because the component visibility changed, + this.refreshMaskPosition(); }, options); } @@ -133,71 +122,28 @@ export class EditorMaskManager { }); } - private observeEvents() { - // listen to the DOM elements' mount and unmount events - // TODO: This is not very accurate, because sunmao runtime 'didDOMUpdate' hook is not accurate. - // We will refactor the 'didDOMUpdate' hook with components' life cycle in the future. - this.services.eventBus.on('HTMLElementsUpdated', () => { - this.refreshSystem(); - this.observeIntersection(); - this.observeResize(); + private onHTMLElementsUpdated = () => { + this.observeIntersection(); + this.observeResize(); - const eleIdMap = new Map(); - this.eleMap.forEach((ele, id) => { - eleIdMap.set(ele, id); - }); - this.setElementIdMap(eleIdMap); - this.refreshHoverElement(); + // generate elementIdMap, this only aim to improving the performance of refreshHoverElement method + const elementIdMap = new Map(); + this.eleMap.forEach((ele, id) => { + elementIdMap.set(ele, id); }); - } - // Refresh the whole coordinate system. - // Coordinate system is made up of: reacts, maskContainerRect and systemOffset. - private refreshSystem() { - this.updateEleRects(); - if (this.maskContainerEle) { - this.setMaskContainerRect(this.maskContainerEle.getBoundingClientRect()); - } - if (this.wrapperEle) { - this.setSystemOffset([this.wrapperEle.scrollLeft, this.wrapperEle.scrollTop]); - } - } + this.elementIdMap = elementIdMap; + this.refreshHoverElement(); + this.refreshMaskPosition(); + }; - private updateEleRects() { - const _rects: Record = {}; - const modalEleMap = new Map(); - // detect if there are components in modal - for (const id of this.eleMap.keys()) { - const ele = this.eleMap.get(id)!; - if (this.isChild(ele, this.modalContainerEle!)) { - modalEleMap.set(id, ele); - } - } - - const foregroundEleMap = modalEleMap.size > 0 ? modalEleMap : this.eleMap; - - foregroundEleMap.forEach((ele, id) => { - if (this.visibleMap.get(ele)) { - const rect = ele.getBoundingClientRect(); - _rects[id] = rect; - } - }); - this.setRects(_rects); - } - - private isChild(child: HTMLElement, parent: HTMLElement) { - let curr = child; - while (curr.parentElement && !curr.parentElement.isSameNode(this.wrapperEle)) { - if (curr.parentElement.isSameNode(parent)) { - return true; - } - curr = curr.parentElement; - } - return false; - } + private onScroll = () => { + this.refreshHoverElement(); + this.refreshMaskPosition(); + }; private getMaskPosition(id: string) { - const rect = this.rects[id]; + const rect = this.eleMap.get(id)?.getBoundingClientRect(); if (!this.maskContainerRect || !rect) return null; return { id, @@ -211,50 +157,55 @@ export class EditorMaskManager { } private refreshHoverElement() { - this.setHoverElement(document.elementFromPoint(...this.mousePosition)); + const hoverElement = document.elementFromPoint(...this.mousePosition); + if (!hoverElement) return; + if (hoverElement === this.lastHoverElement) return; + const root = this.wrapperRef.current; + + // check all the parents of hoverElement, until find a sunmao component's element + let curr = hoverElement; + while (!this.elementIdMap.has(curr)) { + if (curr !== root && curr.parentElement) { + curr = curr.parentElement; + } else { + break; + } + } + + this.lastHoverElement = hoverElement; + this.setHoverComponentId(this.elementIdMap.get(curr) || ''); } - setMousePosition(val: [number, number]) { - this.mousePosition = val; + private refreshMaskPosition() { + this.setHoverMaskPosition(this.getMaskPosition(this.hoverComponentId)); + const selectedComponentId = this.services.editorStore.selectedComponentId; + const selectedComponentEle = this.eleMap.get(selectedComponentId); + if (selectedComponentEle && this.visibleMap.get(selectedComponentEle)) { + this.setSelectedMaskPosition(this.getMaskPosition(selectedComponentId)); + } else { + this.setSelectedMaskPosition(null); + } } - setSystemOffset(systemOffset: [number, number]) { - this.systemOffset = systemOffset; - } - - setRects(rects: Record) { - this.rects = rects; - } - - setMaskContainerRect(maskContainerRect: DOMRect) { - this.maskContainerRect = maskContainerRect; - } - - setElementIdMap(elementIdMap: Map) { - this.elementIdMap = elementIdMap; - } - - setHoverElement(hoverElement: Element | null) { - console.log('setHoverElement'); - this.hoverElement = hoverElement; + setMousePosition(mousePosition: [number, number]) { + this.mousePosition = mousePosition; } setHoverComponentId(hoverComponentId: string) { - console.log('sethoverComponentId'); this.hoverComponentId = hoverComponentId; } + setHoverMaskPosition(hoverMaskPosition: MaskPosition | null) { + this.hoverMaskPosition = hoverMaskPosition; + } + + setSelectedMaskPosition(selectedMaskPosition: MaskPosition | null) { + this.selectedMaskPosition = selectedMaskPosition; + } + private get eleMap() { return this.services.editorStore.eleMap; } - - private get wrapperEle() { - return this.wrapperRef.current; - } - - private get maskContainerEle() { - return this.maskContainerRef.current; - } } function buildThresholdList() { @@ -269,30 +220,3 @@ function buildThresholdList() { thresholds.push(0); return thresholds; } - -export function whereIsMouse( - left: number, - top: number, - rects: Record -): string { - let nearest = { - id: '', - sum: Infinity, - }; - for (const id in rects) { - const rect = rects[id]; - if ( - top < rect.top || - left < rect.left || - top > rect.top + rect.height || - left > rect.left + rect.width - ) { - continue; - } - const sum = top - rect.top + (left - rect.left); - if (sum < nearest.sum) { - nearest = { id, sum }; - } - } - return nearest.id; -} diff --git a/packages/editor/src/components/EditorMaskWrapper/EditorMaskWrapper.tsx b/packages/editor/src/components/EditorMaskWrapper/EditorMaskWrapper.tsx index 6b70ee98..40489dd9 100644 --- a/packages/editor/src/components/EditorMaskWrapper/EditorMaskWrapper.tsx +++ b/packages/editor/src/components/EditorMaskWrapper/EditorMaskWrapper.tsx @@ -30,6 +30,7 @@ export const EditorMaskWrapper: React.FC = observer(props => { const onScroll = () => { if (wrapperRef.current) { setScrollOffset([wrapperRef.current.scrollLeft, wrapperRef.current.scrollTop]); + eventBus.send('MaskWrapperScrollCapture'); } }; diff --git a/packages/editor/src/services/eventBus.ts b/packages/editor/src/services/eventBus.ts index 22273d81..3e7c1da4 100644 --- a/packages/editor/src/services/eventBus.ts +++ b/packages/editor/src/services/eventBus.ts @@ -13,6 +13,7 @@ export type EventNames = { // it is only used for some operations' side effect selectComponent: string; HTMLElementsUpdated: undefined; + MaskWrapperScrollCapture: undefined; }; export const initEventBus = () => {