mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-27 08:39:59 +08:00
Merge pull request #423 from webzard-io/refactor/mask-elementfrompoint
Use elementFromPoint api to refactor
This commit is contained in:
commit
23bcfd0ed5
@ -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')
|
|
||||||
})
|
|
@ -1,8 +1,7 @@
|
|||||||
import React, { CSSProperties, useEffect, useRef } from 'react';
|
import React, { CSSProperties, useEffect, useRef } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { DIALOG_CONTAINER_ID } from '@sunmao-ui/runtime';
|
|
||||||
import { Box, Text } from '@chakra-ui/react';
|
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 { DropSlotMask } from './DropSlotMask';
|
||||||
import { EditorMaskManager } from './EditorMaskManager';
|
import { EditorMaskManager } from './EditorMaskManager';
|
||||||
import { EditorServices } from '../../types';
|
import { EditorServices } from '../../types';
|
||||||
@ -55,23 +54,29 @@ export const EditorMask: React.FC<Props> = observer((props: Props) => {
|
|||||||
const { isDraggingNewComponent } = editorStore;
|
const { isDraggingNewComponent } = editorStore;
|
||||||
const maskContainerRef = useRef<HTMLDivElement>(null);
|
const maskContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const store = useLocalStore(
|
const manager = useLocalObservable(
|
||||||
() =>
|
() =>
|
||||||
new EditorMaskManager(services, wrapperRef, maskContainerRef, hoverComponentIdRef)
|
new EditorMaskManager(services, wrapperRef, maskContainerRef, hoverComponentIdRef)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { hoverComponentId, hoverMaskPosition, selectedMaskPosition } = store;
|
const { hoverComponentId, hoverMaskPosition, selectedMaskPosition } = manager;
|
||||||
useEffect(() => {
|
|
||||||
store.setMousePosition(mousePosition);
|
|
||||||
}, [mousePosition, store]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hoverComponentIdRef.current = hoverComponentId;
|
hoverComponentIdRef.current = hoverComponentId;
|
||||||
}, [hoverComponentId, hoverComponentIdRef]);
|
}, [hoverComponentId, hoverComponentIdRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
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 ? (
|
const hoverMask = hoverMaskPosition ? (
|
||||||
<HoverMask style={hoverMaskPosition.style} id={hoverMaskPosition.id} />
|
<HoverMask style={hoverMaskPosition.style} id={hoverMaskPosition.id} />
|
||||||
|
@ -1,40 +1,29 @@
|
|||||||
import { action, computed, makeObservable, observable } from 'mobx';
|
import { action, autorun, makeObservable, observable } from 'mobx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EditorServices } from '../../types';
|
import { EditorServices } from '../../types';
|
||||||
|
|
||||||
|
type MaskPosition = {
|
||||||
|
id: string;
|
||||||
|
style: {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
export class EditorMaskManager {
|
export class EditorMaskManager {
|
||||||
// observable: current mouse position
|
hoverComponentId = '';
|
||||||
|
hoverMaskPosition: MaskPosition | null = null;
|
||||||
|
selectedMaskPosition: MaskPosition | null = null;
|
||||||
mousePosition: [number, number] = [0, 0];
|
mousePosition: [number, number] = [0, 0];
|
||||||
// observable: rects of all foreground components
|
private elementIdMap = new Map<Element, string>();
|
||||||
rects: Record<string, DOMRect> = {};
|
// rect of mask container
|
||||||
// observable: rect of mask container
|
private maskContainerRect: DOMRect | null = null;
|
||||||
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;
|
|
||||||
// visible status of all components.
|
|
||||||
private visibleMap = new Map<Element, boolean>();
|
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
private visibleMap = new Map<Element, boolean>();
|
||||||
private intersectionObserver: IntersectionObserver;
|
private intersectionObserver: IntersectionObserver;
|
||||||
private MaskPadding = 4;
|
private MaskPadding = 4;
|
||||||
|
private lastHoverElement: Element | null = null;
|
||||||
get hoverComponentId() {
|
|
||||||
const where = whereIsMouse(
|
|
||||||
this.mousePosition[0] - this.systemOffset[0],
|
|
||||||
this.mousePosition[1] - this.systemOffset[1],
|
|
||||||
this.rects
|
|
||||||
);
|
|
||||||
return where;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hoverMaskPosition() {
|
|
||||||
return this.getMaskPosition(this.hoverComponentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedMaskPosition() {
|
|
||||||
return this.getMaskPosition(this.services.editorStore.selectedComponentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public services: EditorServices,
|
public services: EditorServices,
|
||||||
public wrapperRef: React.MutableRefObject<HTMLDivElement | null>,
|
public wrapperRef: React.MutableRefObject<HTMLDivElement | null>,
|
||||||
@ -42,33 +31,67 @@ export class EditorMaskManager {
|
|||||||
public hoverComponentIdRef: React.MutableRefObject<string>
|
public hoverComponentIdRef: React.MutableRefObject<string>
|
||||||
) {
|
) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
rects: observable.shallow,
|
mousePosition: observable,
|
||||||
mousePosition: observable.ref,
|
hoverMaskPosition: observable.ref,
|
||||||
maskContainerRect: observable.ref,
|
selectedMaskPosition: observable.ref,
|
||||||
systemOffset: observable.ref,
|
hoverComponentId: observable,
|
||||||
setRects: action,
|
|
||||||
setMousePosition: action,
|
setMousePosition: action,
|
||||||
setMaskContainerRect: action,
|
setHoverComponentId: action,
|
||||||
setSystemOffset: action,
|
setHoverMaskPosition: action,
|
||||||
hoverComponentId: computed,
|
setSelectedMaskPosition: action,
|
||||||
hoverMaskPosition: computed,
|
|
||||||
selectedMaskPosition: computed,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
this.refreshSystem();
|
this.refreshHoverElement();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.intersectionObserver = this.initIntersectionObserver();
|
this.intersectionObserver = this.initIntersectionObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.maskContainerRect =
|
||||||
|
this.maskContainerRef.current?.getBoundingClientRect() || null;
|
||||||
|
|
||||||
this.observeIntersection();
|
this.observeIntersection();
|
||||||
this.observeResize();
|
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);
|
||||||
|
|
||||||
|
// 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 {
|
private initIntersectionObserver(): IntersectionObserver {
|
||||||
const options = {
|
const options = {
|
||||||
root: document.getElementById('editor-mask-wrapper'),
|
root: null,
|
||||||
rootMargin: '0px',
|
rootMargin: '0px',
|
||||||
threshold: buildThresholdList(),
|
threshold: buildThresholdList(),
|
||||||
};
|
};
|
||||||
@ -79,8 +102,8 @@ export class EditorMaskManager {
|
|||||||
entries.forEach(e => {
|
entries.forEach(e => {
|
||||||
this.visibleMap.set(e.target, e.isIntersecting);
|
this.visibleMap.set(e.target, e.isIntersecting);
|
||||||
});
|
});
|
||||||
// the coordinate system need to be refresh
|
// Refresh the mask, because the component visibility changed,
|
||||||
this.refreshSystem();
|
this.refreshMaskPosition();
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,64 +122,28 @@ export class EditorMaskManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private observeEvents() {
|
private onHTMLElementsUpdated = () => {
|
||||||
// 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.observeIntersection();
|
||||||
this.observeResize();
|
this.observeResize();
|
||||||
|
|
||||||
|
// generate elementIdMap, this only aim to improving the performance of refreshHoverElement method
|
||||||
|
const elementIdMap = new Map<Element, string>();
|
||||||
|
this.eleMap.forEach((ele, id) => {
|
||||||
|
elementIdMap.set(ele, id);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh the whole coordinate system.
|
this.elementIdMap = elementIdMap;
|
||||||
// Coordinate system is made up of: reacts, maskContainerRect and systemOffset.
|
this.refreshHoverElement();
|
||||||
private refreshSystem() {
|
this.refreshMaskPosition();
|
||||||
this.updateEleRects();
|
};
|
||||||
if (this.maskContainerEle) {
|
|
||||||
this.setMaskContainerRect(this.maskContainerEle.getBoundingClientRect());
|
|
||||||
}
|
|
||||||
if (this.wrapperEle) {
|
|
||||||
this.setSystemOffset([this.wrapperEle.scrollLeft, this.wrapperEle.scrollTop]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateEleRects() {
|
private onScroll = () => {
|
||||||
const _rects: Record<string, DOMRect> = {};
|
this.refreshHoverElement();
|
||||||
const modalEleMap = new Map<string, HTMLElement>();
|
this.refreshMaskPosition();
|
||||||
// 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 getMaskPosition(id: string) {
|
private getMaskPosition(id: string) {
|
||||||
const rect = this.rects[id];
|
const rect = this.eleMap.get(id)?.getBoundingClientRect();
|
||||||
if (!this.maskContainerRect || !rect) return null;
|
if (!this.maskContainerRect || !rect) return null;
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@ -169,33 +156,56 @@ export class EditorMaskManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setMousePosition(val: [number, number]) {
|
private refreshHoverElement() {
|
||||||
this.mousePosition = val;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSystemOffset(systemOffset: [number, number]) {
|
this.lastHoverElement = hoverElement;
|
||||||
this.systemOffset = systemOffset;
|
this.setHoverComponentId(this.elementIdMap.get(curr) || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
setRects(rects: Record<string, DOMRect>) {
|
private refreshMaskPosition() {
|
||||||
this.rects = rects;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setMaskContainerRect(maskContainerRect: DOMRect) {
|
setMousePosition(mousePosition: [number, number]) {
|
||||||
this.maskContainerRect = maskContainerRect;
|
this.mousePosition = mousePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHoverComponentId(hoverComponentId: string) {
|
||||||
|
this.hoverComponentId = hoverComponentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHoverMaskPosition(hoverMaskPosition: MaskPosition | null) {
|
||||||
|
this.hoverMaskPosition = hoverMaskPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedMaskPosition(selectedMaskPosition: MaskPosition | null) {
|
||||||
|
this.selectedMaskPosition = selectedMaskPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get eleMap() {
|
private get eleMap() {
|
||||||
return this.services.editorStore.eleMap;
|
return this.services.editorStore.eleMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get wrapperEle() {
|
|
||||||
return this.wrapperRef.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get maskContainerEle() {
|
|
||||||
return this.maskContainerRef.current;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildThresholdList() {
|
function buildThresholdList() {
|
||||||
@ -210,30 +220,3 @@ function buildThresholdList() {
|
|||||||
thresholds.push(0);
|
thresholds.push(0);
|
||||||
return thresholds;
|
return thresholds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function whereIsMouse(
|
|
||||||
left: number,
|
|
||||||
top: number,
|
|
||||||
rects: Record<string, DOMRect>
|
|
||||||
): 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;
|
|
||||||
}
|
|
||||||
|
@ -30,6 +30,7 @@ export const EditorMaskWrapper: React.FC<Props> = observer(props => {
|
|||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
if (wrapperRef.current) {
|
if (wrapperRef.current) {
|
||||||
setScrollOffset([wrapperRef.current.scrollLeft, wrapperRef.current.scrollTop]);
|
setScrollOffset([wrapperRef.current.scrollLeft, wrapperRef.current.scrollTop]);
|
||||||
|
eventBus.send('MaskWrapperScrollCapture');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ export type EventNames = {
|
|||||||
// it is only used for some operations' side effect
|
// it is only used for some operations' side effect
|
||||||
selectComponent: string;
|
selectComponent: string;
|
||||||
HTMLElementsUpdated: undefined;
|
HTMLElementsUpdated: undefined;
|
||||||
|
MaskWrapperScrollCapture: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initEventBus = () => {
|
export const initEventBus = () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user