feat(editor): auto detect no ref component dom element

This commit is contained in:
Bowen Tan 2022-11-09 14:26:31 +08:00
parent 0b1e2f0931
commit b9bc122bce
5 changed files with 170 additions and 24 deletions

View File

@ -59,6 +59,7 @@
"react": "^17.0.2",
"react-codemirror2": "^7.2.1",
"react-dom": "^17.0.2",
"react-fiber-traverse": "^0.0.8",
"react-json-tree": "^0.16.1",
"scroll-into-view": "^1.16.2"
},
@ -86,9 +87,9 @@
"vite": "^3.0.8"
},
"peerDependencies": {
"@emotion/react": "^11.8.1",
"react": "16.x || 17.x",
"react-dom": "16.x || 17.x",
"@emotion/react": "^11.8.1"
"react-dom": "16.x || 17.x"
},
"husky": {
"hooks": {

View File

@ -59,7 +59,9 @@ export const EditorMask: React.FC<Props> = observer((props: Props) => {
const { hoverMaskPosition, selectedMaskPosition } = manager;
useEffect(() => {
manager.init();
setTimeout(() => {
manager.init();
}, 0);
return () => {
manager.destroy();
};

View File

@ -1,5 +1,7 @@
import { debounce } from 'lodash';
import { action, autorun, makeObservable, observable } from 'mobx';
import React from 'react';
import { traverse as traverseFiber } from 'react-fiber-traverse';
import { EditorServices } from '../../types';
type MaskPosition = {
@ -15,6 +17,8 @@ export class EditorMaskManager {
hoverMaskPosition: MaskPosition | null = null;
selectedMaskPosition: MaskPosition | null = null;
mousePosition: [number, number] = [0, 0];
// the elements get by fiber
private fiberEleMap = new Map<string, Element>();
private elementIdMap = new Map<Element, string>();
// rect of mask container
private maskContainerRect: DOMRect | null = null;
@ -59,10 +63,11 @@ export class EditorMaskManager {
}
init() {
this.unsafeAutoFindNoRefElement();
this.updateElementIdMap();
this.observeContainerResize();
this.observeIntersection();
this.observeResize();
this.refreshElementIdMap();
// when hoverComponentId & selectedComponentId change, refresh mask position
autorun(() => {
@ -110,6 +115,9 @@ export class EditorMaskManager {
this.eleMap.forEach(ele => {
this.resizeObserver.observe(ele);
});
this.fiberEleMap.forEach(ele => {
this.intersectionObserver.observe(ele);
});
}
private observeContainerResize() {
@ -126,23 +134,32 @@ export class EditorMaskManager {
this.eleMap.forEach(ele => {
this.intersectionObserver.observe(ele);
});
this.fiberEleMap.forEach(ele => {
this.intersectionObserver.observe(ele);
});
}
private onHTMLElementsUpdated = () => {
private onHTMLElementsUpdated = debounce(() => {
this.unsafeAutoFindNoRefElement();
this.updateElementIdMap();
this.observeIntersection();
this.observeResize();
this.refreshElementIdMap();
this.refreshHoverElement();
this.refreshMaskPosition();
};
}, 16);
private onScroll = () => {
this.refreshHoverElement();
this.refreshMaskPosition();
};
private getHTMLElement(id: string) {
return this.eleMap.get(id) || this.fiberEleMap.get(id);
}
private getMaskPosition(id: string) {
const rect = this.eleMap.get(id)?.getBoundingClientRect();
if (!id) return null;
const rect = this.getHTMLElement(id)?.getBoundingClientRect();
if (!this.maskContainerRect || !rect) return null;
return {
id,
@ -163,14 +180,63 @@ export class EditorMaskManager {
};
}
private refreshElementIdMap() {
private updateElementIdMap() {
// 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);
this.elementIdMap.set(ele, id);
});
this.fiberEleMap.forEach((ele, id) => {
this.elementIdMap.set(ele, id);
});
}
this.elementIdMap = elementIdMap;
private getElementsByFiber(ids: string[]): Record<string, HTMLElement | undefined> {
const map: Record<string, HTMLElement | undefined> = {};
const rootFiberNode = (document.getElementById('root') as any)._reactRootContainer
._internalRoot.current;
traverseFiber(rootFiberNode, (fiber: any) => {
if (!fiber.memoizedProps) {
return;
}
const componentId = fiber.memoizedProps['data-sunmao-id'];
const i = ids.indexOf(componentId);
if (i === -1) {
return;
}
ids.splice(i, 1);
// find the nearest child HTMLElement
let curr = fiber;
while (!curr.stateNode && curr.child) {
curr = curr.child;
}
if (curr.stateNode instanceof HTMLElement) {
map[componentId] = curr.stateNode;
}
});
return map;
}
// Auto find the components that does not have ref by React Fiber
// This function is a fallback.
// And because it uses React Fiber, it is not stable.
private unsafeAutoFindNoRefElement() {
try {
const noEleComponentIds: string[] = [];
this.services.appModelManager.appModel.allComponents.forEach(c => {
if (!this.eleMap.has(c.id)) {
const eleFromFiber = this.fiberEleMap.get(c.id);
if (!eleFromFiber || !eleFromFiber.isConnected) {
noEleComponentIds.push(c.id);
}
}
});
if (noEleComponentIds.length === 0) return;
const fiberElements = this.getElementsByFiber(noEleComponentIds);
for (const key in fiberElements) {
const ele = fiberElements[key];
if (ele) this.fiberEleMap.set(key, ele);
}
} catch {}
}
private refreshHoverElement() {
@ -199,7 +265,7 @@ export class EditorMaskManager {
private refreshMaskPosition() {
this.setHoverMaskPosition(this.getMaskPosition(this.hoverComponentId));
const selectedComponentId = this.services.editorStore.selectedComponentId;
const selectedComponentEle = this.eleMap.get(selectedComponentId);
const selectedComponentEle = this.getHTMLElement(selectedComponentId);
if (selectedComponentEle && this.visibleMap.get(selectedComponentEle)) {
this.setSelectedMaskPosition(this.getMaskPosition(selectedComponentId));
} else {

View File

@ -163,6 +163,7 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
const C = unmount ? null : (
<Impl
data-sunmao-id={c.id}
ref={ref}
key={c.id}
{...omit(props, ['slotContext'])}

View File

@ -1242,6 +1242,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.5.4":
version "7.20.1"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9"
integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==
dependencies:
regenerator-runtime "^0.13.10"
"@babel/template@^7.16.0", "@babel/template@^7.3.3":
version "7.16.0"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz"
@ -2661,6 +2668,24 @@
source-map "^0.5.7"
stylis "4.0.13"
"@emotion/babel-plugin@^11.10.5":
version "11.10.5"
resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c"
integrity sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==
dependencies:
"@babel/helper-module-imports" "^7.16.7"
"@babel/plugin-syntax-jsx" "^7.17.12"
"@babel/runtime" "^7.18.3"
"@emotion/hash" "^0.9.0"
"@emotion/memoize" "^0.8.0"
"@emotion/serialize" "^1.1.1"
babel-plugin-macros "^3.1.0"
convert-source-map "^1.5.0"
escape-string-regexp "^4.0.0"
find-root "^1.1.0"
source-map "^0.5.7"
stylis "4.1.3"
"@emotion/cache@^11.10.0":
version "11.10.3"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.3.tgz#c4f67904fad10c945fea5165c3a5a0583c164b87"
@ -2672,6 +2697,17 @@
"@emotion/weak-memoize" "^0.3.0"
stylis "4.0.13"
"@emotion/cache@^11.10.5":
version "11.10.5"
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12"
integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==
dependencies:
"@emotion/memoize" "^0.8.0"
"@emotion/sheet" "^1.2.1"
"@emotion/utils" "^1.2.0"
"@emotion/weak-memoize" "^0.3.0"
stylis "4.1.3"
"@emotion/cache@^11.4.0":
version "11.5.0"
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.5.0.tgz"
@ -2734,14 +2770,14 @@
integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
"@emotion/react@^11.0.0", "@emotion/react@^11.1.1", "@emotion/react@^11.8.1":
version "11.10.4"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.4.tgz#9dc6bccbda5d70ff68fdb204746c0e8b13a79199"
integrity sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==
version "11.10.5"
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d"
integrity sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A==
dependencies:
"@babel/runtime" "^7.18.3"
"@emotion/babel-plugin" "^11.10.0"
"@emotion/cache" "^11.10.0"
"@emotion/serialize" "^1.1.0"
"@emotion/babel-plugin" "^11.10.5"
"@emotion/cache" "^11.10.5"
"@emotion/serialize" "^1.1.1"
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
"@emotion/utils" "^1.2.0"
"@emotion/weak-memoize" "^0.3.0"
@ -2769,6 +2805,17 @@
"@emotion/utils" "^1.2.0"
csstype "^3.0.2"
"@emotion/serialize@^1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0"
integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==
dependencies:
"@emotion/hash" "^0.9.0"
"@emotion/memoize" "^0.8.0"
"@emotion/unitless" "^0.8.0"
"@emotion/utils" "^1.2.0"
csstype "^3.0.2"
"@emotion/sheet@^1.0.3":
version "1.0.3"
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.3.tgz"
@ -2779,15 +2826,20 @@
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.0.tgz#771b1987855839e214fc1741bde43089397f7be5"
integrity sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==
"@emotion/sheet@^1.2.1":
version "1.2.1"
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c"
integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==
"@emotion/styled@^11.0.0", "@emotion/styled@^11.8.1":
version "11.10.4"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.4.tgz#e93f84a4d54003c2acbde178c3f97b421fce1cd4"
integrity sha512-pRl4R8Ez3UXvOPfc2bzIoV8u9P97UedgHS4FPX594ntwEuAMA114wlaHvOK24HB48uqfXiGlYIZYCxVJ1R1ttQ==
version "11.10.5"
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79"
integrity sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw==
dependencies:
"@babel/runtime" "^7.18.3"
"@emotion/babel-plugin" "^11.10.0"
"@emotion/babel-plugin" "^11.10.5"
"@emotion/is-prop-valid" "^1.2.0"
"@emotion/serialize" "^1.1.0"
"@emotion/serialize" "^1.1.1"
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
"@emotion/utils" "^1.2.0"
@ -5925,6 +5977,11 @@ css-box-model@1.2.1:
dependencies:
tiny-invariant "^1.0.6"
css-what@^3.2.0:
version "3.4.2"
resolved "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==
css.escape@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
@ -10717,6 +10774,15 @@ react-fast-compare@^2.0.1:
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-fiber-traverse@^0.0.8:
version "0.0.8"
resolved "https://registry.npmjs.org/react-fiber-traverse/-/react-fiber-traverse-0.0.8.tgz#08987681bdeaba2c964593468fe22470e01ab486"
integrity sha512-UdCfB6inAFmB1/wT1NdMwqbZIuC2hh+i2vo8L9xy9ULKd2UjqTgxJiZBrF4RXmVmbP0XAFJSFWEz9axzXwKgPw==
dependencies:
"@babel/runtime" "^7.5.4"
css-what "^3.2.0"
prop-types "^15.7.2"
react-focus-lock@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.5.0.tgz#12e3a3940e897c26e2c2a0408cd25ea3c99b3709"
@ -11057,6 +11123,11 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.10:
version "0.13.10"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee"
integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==
regenerator-runtime@^0.13.4:
version "0.13.9"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
@ -11841,6 +11912,11 @@ stylis@4.0.13:
resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz"
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
stylis@4.1.3:
version "4.1.3"
resolved "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
stylis@^4.0.10:
version "4.0.10"
resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz"