Skip to content

Commit a63b121

Browse files
committed
feat(YfmHtmlBlock): use shared state to manage editing state
1 parent 8568fb9 commit a63b121

File tree

7 files changed

+91
-39
lines changed

7 files changed

+91
-39
lines changed

src/extensions/additional/YfmHtmlBlock/YfmHtmlBlock.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {BaseNode, BaseSchemaSpecs} from '../../specs';
77
import {YfmHtmlBlockSpecs} from './YfmHtmlBlockSpecs';
88
import {YfmHtmlBlockAttrs, yfmHtmlBlockNodeName} from './const';
99

10+
jest.mock<{v4: () => string}>('uuid', () => ({
11+
v4: jest.fn().mockReturnValue('8bca-mocked-7abc'),
12+
}));
13+
1014
const {
1115
schema,
1216
markupParser: parser,
@@ -29,6 +33,7 @@ describe('YfmHtmlBlock extension', () => {
2933
doc(
3034
yfmHtmlBlock({
3135
[YfmHtmlBlockAttrs.srcdoc]: 'content\n',
36+
[YfmHtmlBlockAttrs.EntityId]: 'yfm_html_block-8bca-mocked-7abc',
3237
}),
3338
),
3439
));

src/extensions/additional/YfmHtmlBlock/YfmHtmlBlockNodeView/NodeView.tsx

+25-7
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import {Portal} from '@gravity-ui/uikit';
22
import type {Node} from 'prosemirror-model';
33
import type {EditorView, NodeView} from 'prosemirror-view';
44

5-
import {getReactRendererFromState} from '../../../behavior';
6-
import {YfmHtmlBlockConsts} from '../YfmHtmlBlockSpecs/const';
5+
import {getReactRendererFromState} from 'src/extensions/behavior/ReactRenderer';
6+
import {generateEntityId, isInvalidEntityId} from 'src/utils/entity-id';
7+
8+
import {YfmHtmlBlockConsts, defaultYfmHtmlBlockEntityId} from '../YfmHtmlBlockSpecs/const';
79
import type {YfmHtmlBlockOptions} from '../index';
810

911
import {STOP_EVENT_CLASSNAME, YfmHtmlBlockView} from './YfmHtmlBlockView';
@@ -39,15 +41,12 @@ export class WYfmHtmlBlockNodeView implements NodeView {
3941
'yfmHtmlBlock-view',
4042
this.renderYfmHtmlBlock.bind(this),
4143
);
44+
45+
this.validateEntityId();
4246
}
4347

4448
update(node: Node) {
4549
if (node.type !== this.node.type) return false;
46-
if (
47-
node.attrs[YfmHtmlBlockConsts.NodeAttrs.newCreated] !==
48-
this.node.attrs[YfmHtmlBlockConsts.NodeAttrs.newCreated]
49-
)
50-
return false;
5150
this.node = node;
5251
this.renderItem.rerender();
5352
return true;
@@ -66,6 +65,25 @@ export class WYfmHtmlBlockNodeView implements NodeView {
6665
return target.classList.contains(STOP_EVENT_CLASSNAME);
6766
}
6867

68+
private validateEntityId() {
69+
if (
70+
isInvalidEntityId({
71+
node: this.node,
72+
doc: this.view.state.doc,
73+
defaultId: defaultYfmHtmlBlockEntityId,
74+
})
75+
) {
76+
const newId = generateEntityId(YfmHtmlBlockConsts.NodeName);
77+
this.view.dispatch(
78+
this.view.state.tr.setNodeAttribute(
79+
this.getPos()!,
80+
YfmHtmlBlockConsts.NodeAttrs.EntityId,
81+
newId,
82+
),
83+
);
84+
}
85+
}
86+
6987
private onChange(attrs: {[YfmHtmlBlockConsts.NodeAttrs.srcdoc]: string}) {
7088
const pos = this.getPos();
7189
if (pos === undefined) return;

src/extensions/additional/YfmHtmlBlock/YfmHtmlBlockNodeView/YfmHtmlBlockView.tsx

+21-18
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
1-
import {useEffect, useRef, useState} from 'react';
1+
import {useEffect, useMemo, useRef, useState} from 'react';
22

33
import {getStyles} from '@diplodoc/html-extension';
44
import type {IHTMLIFrameElementConfig} from '@diplodoc/html-extension/runtime';
55
import {Ellipsis as DotsIcon, Eye} from '@gravity-ui/icons';
66
import {Button, Icon, Label, Menu, Popup} from '@gravity-ui/uikit';
7-
import debounce from 'lodash/debounce';
87
import type {Node} from 'prosemirror-model';
98
import type {EditorView} from 'prosemirror-view';
109

11-
import {cn} from '../../../../classname';
12-
import {TextAreaFixed as TextArea} from '../../../../forms/TextInput';
13-
import {i18n} from '../../../../i18n/common';
14-
import {useBooleanState, useElementState} from '../../../../react-utils/hooks';
15-
import {removeNode} from '../../../../utils/remove-node';
10+
import {cn} from 'src/classname';
11+
import {SharedStateKey} from 'src/extensions/behavior/SharedState';
12+
import {TextAreaFixed as TextArea} from 'src/forms/TextInput';
13+
import {i18n} from 'src/i18n/common';
14+
import {debounce} from 'src/lodash';
15+
import {useBooleanState, useElementState} from 'src/react-utils/hooks';
16+
import {useSharedEditingState} from 'src/react-utils/useSharedEditingState';
17+
import {removeNode} from 'src/utils/remove-node';
18+
1619
import {YfmHtmlBlockConsts} from '../YfmHtmlBlockSpecs/const';
1720
import type {YfmHtmlBlockOptions} from '../index';
21+
import type {YfmHtmlBlockEntitySharedState} from '../types';
1822

1923
import './YfmHtmlBlock.scss';
2024

@@ -25,7 +29,7 @@ const b = cnYfmHtmlBlock;
2529

2630
interface YfmHtmlBlockViewProps {
2731
html: string;
28-
onСlick: () => void;
32+
onClick: () => void;
2933
config?: IHTMLIFrameElementConfig;
3034
}
3135

@@ -48,7 +52,7 @@ const createLinkCLickHandler = (value: Element, document: Document) => (event: E
4852
}
4953
};
5054

51-
const YfmHtmlBlockPreview: React.FC<YfmHtmlBlockViewProps> = ({html, onСlick, config}) => {
55+
const YfmHtmlBlockPreview: React.FC<YfmHtmlBlockViewProps> = ({html, onClick, config}) => {
5256
const ref = useRef<HTMLIFrameElement>(null);
5357
const styles = useRef<Record<string, string>>({});
5458
const classNames = useRef<string[]>([]);
@@ -69,7 +73,7 @@ const YfmHtmlBlockPreview: React.FC<YfmHtmlBlockViewProps> = ({html, onСlick, c
6973
if (contentWindow) {
7074
const frameDocument = contentWindow.document;
7175
frameDocument.addEventListener('dblclick', () => {
72-
onСlick();
76+
onClick();
7377
});
7478
}
7579
};
@@ -235,19 +239,18 @@ export const YfmHtmlBlockView: React.FC<{
235239
view,
236240
options: {useConfig, sanitize, styles, baseTarget = '_parent', head: headContent = ''},
237241
}) => {
238-
const [editing, setEditing, unsetEditing, toggleEditing] = useBooleanState(
239-
Boolean(node.attrs[YfmHtmlBlockConsts.NodeAttrs.newCreated]),
242+
const entityId: string = node.attrs[YfmHtmlBlockConsts.NodeAttrs.EntityId];
243+
const entityKey = useMemo(
244+
() => SharedStateKey.define<YfmHtmlBlockEntitySharedState>({name: entityId}),
245+
[entityId],
240246
);
241247

242248
const config = useConfig?.();
243249

250+
const [editing, setEditing, unsetEditing] = useSharedEditingState(view, entityKey);
244251
const [menuOpen, _openMenu, closeMenu, toggleMenuOpen] = useBooleanState(false);
245252
const [anchorElement, setAnchorElement] = useElementState();
246253

247-
const handleClick = () => {
248-
setEditing();
249-
};
250-
251254
if (editing) {
252255
return (
253256
<CodeEditMode
@@ -283,7 +286,7 @@ export const YfmHtmlBlockView: React.FC<{
283286
<Label className={b('label')} icon={<Icon size={16} data={Eye} />}>
284287
{i18n('preview')}
285288
</Label>
286-
<YfmHtmlBlockPreview html={resultHtml} onСlick={handleClick} config={config} />
289+
<YfmHtmlBlockPreview html={resultHtml} onClick={setEditing} config={config} />
287290

288291
<div className={b('menu')}>
289292
<Button
@@ -303,7 +306,7 @@ export const YfmHtmlBlockView: React.FC<{
303306
<Menu>
304307
<Menu.Item
305308
onClick={() => {
306-
toggleEditing();
309+
setEditing();
307310
closeMenu();
308311
}}
309312
>

src/extensions/additional/YfmHtmlBlock/YfmHtmlBlockSpecs/const.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import {nodeTypeFactory} from '../../../../utils/schema';
1+
import {entityIdAttr} from 'src/utils/entity-id';
2+
import {nodeTypeFactory} from 'src/utils/schema';
23

34
export enum YfmHtmlBlockAttrs {
5+
// @ts-expect-error error TS18055
6+
EntityId = entityIdAttr,
47
class = 'class',
58
frameborder = 'frameborder',
9+
// MAJOR: remove before next major
10+
/** @deprecated This is no longer used. Removed in next major version */
611
newCreated = 'newCreated',
712
srcdoc = 'srcdoc',
813
style = 'style',
@@ -18,3 +23,5 @@ export const YfmHtmlBlockConsts = {
1823
NodeAttrs: YfmHtmlBlockAttrs,
1924
nodeType: yfmHtmlBlockNodeType,
2025
} as const;
26+
27+
export const defaultYfmHtmlBlockEntityId = yfmHtmlBlockNodeName + '#0';

src/extensions/additional/YfmHtmlBlock/YfmHtmlBlockSpecs/index.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {type PluginOptions, transform} from '@diplodoc/html-extension';
22

3-
import type {ExtensionAuto, ExtensionNodeSpec} from '../../../../core';
3+
import type {ExtensionAuto, ExtensionNodeSpec} from '#core';
4+
import {generateEntityId} from 'src/utils/entity-id';
45

5-
import {YfmHtmlBlockConsts} from './const';
6+
import {YfmHtmlBlockConsts, defaultYfmHtmlBlockEntityId} from './const';
67

7-
export {yfmHtmlBlockNodeName} from './const';
8+
export {yfmHtmlBlockNodeName, YfmHtmlBlockConsts} from './const';
89

910
export interface YfmHtmlBlockSpecsOptions
1011
extends Omit<PluginOptions, 'runtimeJsPath' | 'containerClasses' | 'bundle' | 'embeddingMode'> {
@@ -32,7 +33,12 @@ const YfmHtmlBlockSpecsExtension: ExtensionAuto<YfmHtmlBlockSpecsOptions> = (
3233
name: YfmHtmlBlockConsts.NodeName,
3334
type: 'node',
3435
noCloseToken: true,
35-
getAttrs: ({content}) => ({srcdoc: content}),
36+
getAttrs: ({content}) => ({
37+
[YfmHtmlBlockConsts.NodeAttrs.srcdoc]: content,
38+
[YfmHtmlBlockConsts.NodeAttrs.EntityId]: generateEntityId(
39+
YfmHtmlBlockConsts.NodeName,
40+
),
41+
}),
3642
},
3743
},
3844
spec: {
@@ -43,6 +49,7 @@ const YfmHtmlBlockSpecsExtension: ExtensionAuto<YfmHtmlBlockSpecsOptions> = (
4349
[YfmHtmlBlockConsts.NodeAttrs.srcdoc]: {default: ''},
4450
[YfmHtmlBlockConsts.NodeAttrs.style]: {default: null},
4551
[YfmHtmlBlockConsts.NodeAttrs.newCreated]: {default: null},
52+
[YfmHtmlBlockConsts.NodeAttrs.EntityId]: {default: defaultYfmHtmlBlockEntityId},
4653
},
4754
toDOM: (node) => ['iframe', node.attrs],
4855
},
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
import type {ActionSpec} from '../../../core';
1+
import type {ActionSpec} from '#core';
2+
import {SharedStateKey} from 'src/extensions/behavior/SharedState';
3+
import {generateEntityId} from 'src/utils/entity-id';
24

35
import {YfmHtmlBlockConsts, yfmHtmlBlockNodeType} from './YfmHtmlBlockSpecs/const';
6+
import type {YfmHtmlBlockEntitySharedState} from './types';
47

58
export const addYfmHtmlBlock: ActionSpec = {
69
isEnable(state) {
710
return state.selection.empty;
811
},
912
run(state, dispatch, _view) {
10-
dispatch(
11-
state.tr.insert(
12-
state.selection.from,
13-
yfmHtmlBlockNodeType(state.schema).create({
14-
[YfmHtmlBlockConsts.NodeAttrs.srcdoc]: '\n',
15-
[YfmHtmlBlockConsts.NodeAttrs.newCreated]: true,
16-
}),
17-
),
13+
const entityId = generateEntityId(YfmHtmlBlockConsts.NodeName);
14+
const sharedKey = SharedStateKey.define<YfmHtmlBlockEntitySharedState>({name: entityId});
15+
16+
const tr = state.tr.insert(
17+
state.selection.from,
18+
yfmHtmlBlockNodeType(state.schema).create({
19+
[YfmHtmlBlockConsts.NodeAttrs.srcdoc]: '\n',
20+
[YfmHtmlBlockConsts.NodeAttrs.newCreated]: true,
21+
[YfmHtmlBlockConsts.NodeAttrs.EntityId]: entityId,
22+
}),
1823
);
24+
25+
sharedKey.appendTransaction.set(tr, {editing: true});
26+
27+
dispatch(tr);
1928
},
2029
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type YfmHtmlBlockEntitySharedState = {
2+
editing: boolean;
3+
};

0 commit comments

Comments
 (0)