Skip to content

Commit e446a54

Browse files
committed
feat: improve link-popup state
1 parent a7dd6d4 commit e446a54

File tree

6 files changed

+166
-86
lines changed

6 files changed

+166
-86
lines changed

packages/drawnix/src/components/popup/link-popup/link-popup.tsx

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { LinkState, useDrawnix } from '../../../hooks/use-drawnix';
99
import { FeltTipPenIcon, TrashIcon } from '../../icons';
1010
import { Transforms } from 'slate';
1111
import { ReactEditor } from 'slate-react';
12+
import { LinkEditor } from '@plait/text-plugins';
13+
import { LinkElement } from '@plait/common';
1214

1315
export const LinkPopup = () => {
1416
const [isEditing, setIsEditing] = useState(false);
@@ -26,6 +28,19 @@ export const LinkPopup = () => {
2628

2729
const target = appState.linkState?.targetDom || null;
2830

31+
const closeHandle = () => {
32+
setIsOpening(false);
33+
setLinkState(null);
34+
setIsEditing(false);
35+
setUrl('');
36+
if (target) {
37+
setAppState({
38+
...appState,
39+
linkState: null,
40+
});
41+
}
42+
};
43+
2944
useEffect(() => {
3045
if (target) {
3146
setIsOpening(true);
@@ -40,29 +55,30 @@ export const LinkPopup = () => {
4055
if (appState.linkState?.isEditing) {
4156
setIsEditing(true);
4257
}
43-
} else if (!isHovering && !isEditing) {
44-
setIsOpening(false);
45-
setLinkState(null);
46-
setUrl('');
58+
} else if (!isHovering) {
59+
closeHandle();
60+
}
61+
}, [target]);
62+
63+
useEffect(() => {
64+
if (!isHovering && !isEditing && !target) {
65+
closeHandle();
4766
}
48-
}, [target, isHovering]);
67+
}, [isHovering]);
4968

5069
useEffect(() => {
5170
const handleClickOutside = (event: MouseEvent) => {
5271
if (
5372
refs.floating.current &&
5473
!refs.floating.current.contains(event.target as Node)
5574
) {
56-
setIsOpening(false);
57-
setIsEditing(false);
58-
setLinkState(null);
59-
setUrl('');
60-
if (linkState && url !== linkState.targetElement.url) {
61-
const editor = linkState.editor;
62-
const node = linkState.targetElement;
63-
const path = ReactEditor.findPath(editor, node);
64-
Transforms.setNodes(editor, { url: url }, { at: path });
75+
if (linkState) {
76+
const linkElement = LinkEditor.getLinkElement(linkState.editor);
77+
if (linkElement && !(linkElement[0] as LinkElement).url.trim()) {
78+
LinkEditor.unwrapLink(linkState.editor);
79+
}
6580
}
81+
closeHandle();
6682
}
6783
};
6884

@@ -72,6 +88,36 @@ export const LinkPopup = () => {
7288
};
7389
}, []);
7490

91+
useEffect(() => {
92+
if (appState.linkState) {
93+
if (isEditing && !appState.linkState.isEditing) {
94+
setAppState({
95+
...appState,
96+
linkState: {
97+
...appState.linkState,
98+
isEditing: true,
99+
},
100+
});
101+
} else if (!isEditing && appState.linkState.isEditing) {
102+
setAppState({
103+
...appState,
104+
linkState: {
105+
...appState.linkState,
106+
isEditing: false,
107+
},
108+
});
109+
}
110+
} else if (isEditing) {
111+
setAppState({
112+
...appState,
113+
linkState: {
114+
...linkState!,
115+
isEditing: true,
116+
},
117+
});
118+
}
119+
}, [isEditing]);
120+
75121
let timeoutId: any | null = null;
76122

77123
const saveUrl = () => {
@@ -104,19 +150,19 @@ export const LinkPopup = () => {
104150
<Stack.Row gap={1} align="center">
105151
{isEditing ? (
106152
<input
107-
type="text"
108-
value={url}
109-
onChange={(e) => {
110-
setUrl(e.target.value);
111-
}}
112-
onKeyDown={(e) => {
113-
if (e.key === 'Enter') {
114-
saveUrl();
115-
}
116-
}}
117-
className="link-popup__input"
118-
autoFocus
119-
/>
153+
type="text"
154+
value={url}
155+
onChange={(e) => {
156+
setUrl(e.target.value);
157+
}}
158+
onKeyDown={(e) => {
159+
if (e.key === 'Enter') {
160+
saveUrl();
161+
}
162+
}}
163+
className="link-popup__input"
164+
autoFocus
165+
/>
120166
) : (
121167
<>
122168
<a

packages/drawnix/src/drawnix.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ import { buildDrawnixHotkeyPlugin } from './plugins/with-hotkey';
2727
import { withFreehand } from './plugins/freehand/with-freehand';
2828
import { ThemeToolbar } from './components/toolbar/theme-toolbar';
2929
import { buildPencilPlugin } from './plugins/with-pencil';
30-
import { DrawnixContext, DrawnixState } from './hooks/use-drawnix';
30+
import {
31+
DrawnixBoard,
32+
DrawnixContext,
33+
DrawnixState,
34+
} from './hooks/use-drawnix';
3135
import { ClosePencilToolbar } from './components/toolbar/pencil-mode-toolbar';
3236
import { TTDDialog } from './components/ttd-dialog/ttd-dialog';
3337
import { CleanConfirm } from './components/clean-confirm/clean-confirm';
@@ -63,6 +67,7 @@ export const Drawnix: React.FC<DrawnixProps> = ({
6367
disabledScrollOnNonFocus: false,
6468
themeColors: MindThemeColors,
6569
};
70+
6671
const [appState, setAppState] = useState<DrawnixState>(() => {
6772
// TODO: need to consider how to maintenance the pointer state in future
6873
const md = new MobileDetect(window.navigator.userAgent);
@@ -75,16 +80,29 @@ export const Drawnix: React.FC<DrawnixProps> = ({
7580
};
7681
});
7782

83+
const [board, setBoard] = useState<DrawnixBoard | null>(null);
84+
85+
if (board) {
86+
board.appState = appState;
87+
}
88+
89+
const updateAppState = (newAppState: Partial<DrawnixState>) => {
90+
setAppState({
91+
...appState,
92+
...newAppState,
93+
});
94+
};
95+
7896
const plugins: PlaitPlugin[] = [
7997
withDraw,
8098
withGroup,
8199
withMind,
82100
withMindExtend,
83101
withCommonPlugin,
84-
buildDrawnixHotkeyPlugin(appState, setAppState),
102+
buildDrawnixHotkeyPlugin(updateAppState),
85103
withFreehand,
86-
buildPencilPlugin(appState, setAppState),
87-
buildTextLinkPlugin(appState, setAppState),
104+
buildPencilPlugin(updateAppState),
105+
buildTextLinkPlugin(updateAppState),
88106
];
89107

90108
const containerRef = useRef<HTMLDivElement>(null);
@@ -111,7 +129,12 @@ export const Drawnix: React.FC<DrawnixProps> = ({
111129
onThemeChange={onThemeChange}
112130
onValueChange={onValueChange}
113131
>
114-
<Board afterInit={afterInit}></Board>
132+
<Board
133+
afterInit={(board) => {
134+
setBoard(board as DrawnixBoard);
135+
afterInit && afterInit(board);
136+
}}
137+
></Board>
115138
<AppToolbar></AppToolbar>
116139
<CreationToolbar></CreationToolbar>
117140
<ZoomToolbar></ZoomToolbar>

packages/drawnix/src/hooks/use-drawnix.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
* A React context for sharing the board object, in a way that re-renders the
33
* context whenever changes occur.
44
*/
5-
6-
import { PlaitPointerType } from '@plait/core';
5+
import { PlaitBoard, PlaitPointerType } from '@plait/core';
76
import { createContext, useContext } from 'react';
87
import { MindPointerType } from '@plait/mind';
98
import { DrawPointerType } from '@plait/draw';
109
import { FreehandShape } from '../plugins/freehand/type';
11-
import { Editor, Element } from 'slate';
12-
import { LinkElement } from '@plait-board/react-text';
10+
import { Editor } from 'slate';
11+
import { LinkElement } from '@plait/common';
1312

1413
export enum DialogType {
1514
mermaidToDrawnix = 'mermaidToDrawnix',
@@ -22,11 +21,15 @@ export type DrawnixPointerType =
2221
| DrawPointerType
2322
| FreehandShape;
2423

24+
export interface DrawnixBoard extends PlaitBoard {
25+
appState: DrawnixState;
26+
}
27+
2528
export type LinkState = {
2629
targetDom: HTMLElement;
2730
editor: Editor;
2831
targetElement: LinkElement;
29-
isEditing?: boolean;
32+
isEditing: boolean;
3033
};
3134

3235
export type DrawnixState = {

packages/drawnix/src/plugins/with-hotkey.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { saveAsJSON } from '../data/json';
55
import { DrawnixState } from '../hooks/use-drawnix';
66

77
export const buildDrawnixHotkeyPlugin = (
8-
appState: DrawnixState,
9-
setAppState: (appState: DrawnixState) => void
8+
updateAppState: (appState: Partial<DrawnixState>) => void
109
) => {
1110
const withDrawnixHotkey = (board: PlaitBoard) => {
1211
const { globalKeyDown } = board;
@@ -29,8 +28,7 @@ export const buildDrawnixHotkeyPlugin = (
2928
isHotkey(['mod+backspace'])(event) ||
3029
isHotkey(['mod+delete'])(event)
3130
) {
32-
setAppState({
33-
...appState,
31+
updateAppState({
3432
openCleanConfirm: true,
3533
});
3634
event.preventDefault();

packages/drawnix/src/plugins/with-pencil.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@ export const setIsPencilMode = (board: PlaitBoard, isPencilMode: boolean) => {
1212
};
1313

1414
export const buildPencilPlugin = (
15-
appState: DrawnixState,
16-
setAppState: (appState: DrawnixState) => void
15+
updateAppState: (appState: Partial<DrawnixState>) => void
1716
) => {
1817
const withPencil = (board: PlaitBoard) => {
1918
const { pointerDown } = board;
2019

2120
board.pointerDown = (event: PointerEvent) => {
2221
if (isPencilEvent(event) && !isPencilMode(board)) {
2322
setIsPencilMode(board, true);
24-
setAppState({ ...appState, isPencilMode: true });
23+
updateAppState({ isPencilMode: true });
2524
}
2625
if (isPencilMode(board) && !isPencilEvent(event)) {
2726
return;

packages/drawnix/src/plugins/with-text-link.tsx

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { PlaitBoard, PlaitPointerType } from '@plait/core';
2-
import { DrawnixState } from '../hooks/use-drawnix';
1+
import {
2+
isMovingElements,
3+
PlaitBoard,
4+
PlaitPointerType,
5+
throttleRAF,
6+
} from '@plait/core';
7+
import { DrawnixBoard, DrawnixState } from '../hooks/use-drawnix';
38
import { ReactEditor } from 'slate-react';
49
import { Editor } from 'slate';
5-
import { LinkElement } from '@plait-board/react-text';
10+
import { isResizing, LinkElement } from '@plait/common';
611

712
export const buildTextLinkPlugin = (
8-
appState: DrawnixState,
9-
setAppState: (appState: DrawnixState) => void
13+
updateAppState: (appState: Partial<DrawnixState>) => void
1014
) => {
1115
const withTextLink = (board: PlaitBoard) => {
1216
const { pointerMove } = board;
@@ -16,46 +20,53 @@ export const buildTextLinkPlugin = (
1620
let timeoutId: any | null = null;
1721

1822
board.pointerMove = (event: PointerEvent) => {
23+
const { appState } = board as DrawnixBoard;
24+
const isEditing =
25+
appState && appState.linkState && appState.linkState.isEditing;
1926
if (
20-
PlaitBoard.isPointer(board, PlaitPointerType.selection) ||
21-
PlaitBoard.isPointer(board, PlaitPointerType.hand)
27+
(PlaitBoard.isPointer(board, PlaitPointerType.selection) ||
28+
PlaitBoard.isPointer(board, PlaitPointerType.hand)) &&
29+
!isMovingElements(board) &&
30+
!isResizing(board) &&
31+
!isEditing
2232
) {
23-
const textLinkDom = (event.target as HTMLElement).closest(
24-
'.plait-board-link'
25-
) as HTMLElement | null;
26-
if (textLinkDom && textLinkDom !== target) {
27-
const editable = textLinkDom.closest(
28-
'.plait-text-container'
29-
) as HTMLElement;
30-
const editor = ReactEditor.toSlateNode(
31-
undefined as unknown as Editor,
32-
editable
33-
) as Editor;
34-
const node = ReactEditor.toSlateNode(
35-
undefined as unknown as Editor,
36-
textLinkDom
37-
) as LinkElement;
38-
target = textLinkDom;
39-
setAppState({
40-
...appState,
41-
linkState: {
42-
targetDom: textLinkDom,
43-
targetElement: node,
44-
editor,
45-
},
46-
});
47-
clearTimeout(timeoutId);
48-
} else {
49-
if (!textLinkDom && target) {
50-
timeoutId = setTimeout(() => {
51-
setAppState({
52-
...appState,
53-
linkState: null,
54-
});
55-
}, 300);
56-
target = null;
33+
throttleRAF(board, 'with-text-link', () => {
34+
const textLinkDom = (event.target as HTMLElement).closest(
35+
'.plait-board-link'
36+
) as HTMLElement | null;
37+
if (textLinkDom && textLinkDom !== target) {
38+
const editable = textLinkDom.closest(
39+
'.plait-text-container'
40+
) as HTMLElement;
41+
const editor = ReactEditor.toSlateNode(
42+
undefined as unknown as Editor,
43+
editable
44+
) as Editor;
45+
const node = ReactEditor.toSlateNode(
46+
undefined as unknown as Editor,
47+
textLinkDom
48+
) as LinkElement;
49+
target = textLinkDom;
50+
updateAppState({
51+
linkState: {
52+
targetDom: textLinkDom,
53+
targetElement: node,
54+
editor,
55+
isEditing: false,
56+
},
57+
});
58+
clearTimeout(timeoutId);
59+
} else {
60+
if (!textLinkDom && target) {
61+
timeoutId = setTimeout(() => {
62+
updateAppState({
63+
linkState: null,
64+
});
65+
}, 300);
66+
target = null;
67+
}
5768
}
58-
}
69+
});
5970
}
6071
pointerMove(event);
6172
};

0 commit comments

Comments
 (0)