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 { 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<Props> = observer((props: Props) => {
|
||||
const { isDraggingNewComponent } = editorStore;
|
||||
const maskContainerRef = useRef<HTMLDivElement>(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 ? (
|
||||
<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 { EditorServices } from '../../types';
|
||||
|
||||
type MaskPosition = {
|
||||
id: string;
|
||||
style: {
|
||||
top: number;
|
||||
left: number;
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
};
|
||||
export class EditorMaskManager {
|
||||
// observable: current mouse position
|
||||
hoverComponentId = '';
|
||||
hoverMaskPosition: MaskPosition | null = null;
|
||||
selectedMaskPosition: MaskPosition | null = null;
|
||||
mousePosition: [number, number] = [0, 0];
|
||||
// observable: rects of all foreground components
|
||||
rects: Record<string, DOMRect> = {};
|
||||
// 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;
|
||||
// visible status of all components.
|
||||
private visibleMap = new Map<Element, boolean>();
|
||||
private elementIdMap = new Map<Element, string>();
|
||||
// rect of mask container
|
||||
private maskContainerRect: DOMRect | null = null;
|
||||
private resizeObserver: ResizeObserver;
|
||||
private visibleMap = new Map<Element, boolean>();
|
||||
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;
|
||||
}
|
||||
|
||||
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<HTMLDivElement | null>,
|
||||
@ -42,33 +31,67 @@ export class EditorMaskManager {
|
||||
public hoverComponentIdRef: React.MutableRefObject<string>
|
||||
) {
|
||||
makeObservable(this, {
|
||||
rects: observable.shallow,
|
||||
mousePosition: observable.ref,
|
||||
maskContainerRect: observable.ref,
|
||||
systemOffset: observable.ref,
|
||||
setRects: action,
|
||||
mousePosition: observable,
|
||||
hoverMaskPosition: observable.ref,
|
||||
selectedMaskPosition: observable.ref,
|
||||
hoverComponentId: observable,
|
||||
setMousePosition: action,
|
||||
setMaskContainerRect: action,
|
||||
setSystemOffset: action,
|
||||
hoverComponentId: computed,
|
||||
hoverMaskPosition: computed,
|
||||
selectedMaskPosition: computed,
|
||||
setHoverComponentId: action,
|
||||
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);
|
||||
|
||||
// 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(),
|
||||
};
|
||||
@ -79,8 +102,8 @@ export class EditorMaskManager {
|
||||
entries.forEach(e => {
|
||||
this.visibleMap.set(e.target, e.isIntersecting);
|
||||
});
|
||||
// the coordinate system need to be refresh
|
||||
this.refreshSystem();
|
||||
// Refresh the mask, because the component visibility changed,
|
||||
this.refreshMaskPosition();
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -99,64 +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();
|
||||
private onHTMLElementsUpdated = () => {
|
||||
this.observeIntersection();
|
||||
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.
|
||||
// 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<string, DOMRect> = {};
|
||||
const modalEleMap = new Map<string, HTMLElement>();
|
||||
// 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,
|
||||
@ -169,33 +156,56 @@ export class EditorMaskManager {
|
||||
};
|
||||
}
|
||||
|
||||
setMousePosition(val: [number, number]) {
|
||||
this.mousePosition = val;
|
||||
private refreshHoverElement() {
|
||||
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.systemOffset = systemOffset;
|
||||
this.lastHoverElement = hoverElement;
|
||||
this.setHoverComponentId(this.elementIdMap.get(curr) || '');
|
||||
}
|
||||
|
||||
setRects(rects: Record<string, DOMRect>) {
|
||||
this.rects = rects;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
setMaskContainerRect(maskContainerRect: DOMRect) {
|
||||
this.maskContainerRect = maskContainerRect;
|
||||
setMousePosition(mousePosition: [number, number]) {
|
||||
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() {
|
||||
return this.services.editorStore.eleMap;
|
||||
}
|
||||
|
||||
private get wrapperEle() {
|
||||
return this.wrapperRef.current;
|
||||
}
|
||||
|
||||
private get maskContainerEle() {
|
||||
return this.maskContainerRef.current;
|
||||
}
|
||||
}
|
||||
|
||||
function buildThresholdList() {
|
||||
@ -210,30 +220,3 @@ function buildThresholdList() {
|
||||
thresholds.push(0);
|
||||
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 = () => {
|
||||
if (wrapperRef.current) {
|
||||
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
|
||||
selectComponent: string;
|
||||
HTMLElementsUpdated: undefined;
|
||||
MaskWrapperScrollCapture: undefined;
|
||||
};
|
||||
|
||||
export const initEventBus = () => {
|
||||
|
Loading…
Reference in New Issue
Block a user