@os-design/editor 1.0.204 → 1.0.205

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +16 -8
  2. package/src/@types/emotion.d.ts +7 -0
  3. package/src/Editor/BlockToolbar.tsx +49 -0
  4. package/src/Editor/StyleToolbar.tsx +80 -0
  5. package/src/Editor/Toolbar.tsx +18 -0
  6. package/src/Editor/ToolbarButton.tsx +65 -0
  7. package/src/Editor/blocks/Figure.tsx +10 -0
  8. package/src/Editor/blocks/FigureCaption.tsx +19 -0
  9. package/src/Editor/blocks/imageBlock.tsx +162 -0
  10. package/src/Editor/blocks/types.ts +21 -0
  11. package/src/Editor/blocks/videoBlock.tsx +83 -0
  12. package/src/Editor/decorators/linkDecorator.tsx +23 -0
  13. package/src/Editor/hooks/useBlockToolbarProps.ts +72 -0
  14. package/src/Editor/hooks/usePastedTextHandler.ts +47 -0
  15. package/src/Editor/hooks/useReturnHandler.ts +37 -0
  16. package/src/Editor/hooks/useStyleToolbarProps.ts +53 -0
  17. package/src/Editor/index.tsx +203 -0
  18. package/src/Editor/styles/defaultDraftJsStyles.ts +179 -0
  19. package/src/Editor/styles/overrideDraftJsStyles.ts +20 -0
  20. package/src/Editor/utils/addNewBlockAt.ts +59 -0
  21. package/src/Editor/utils/changeBlock.ts +20 -0
  22. package/src/Editor/utils/createContentEditorState.ts +7 -0
  23. package/src/Editor/utils/createDecorator.ts +7 -0
  24. package/src/Editor/utils/createEmptyEditorState.ts +7 -0
  25. package/src/Editor/utils/defaultStyleToolbarItems.tsx +30 -0
  26. package/src/Editor/utils/getCurrentBlock.ts +9 -0
  27. package/src/Editor/utils/getSelectedBlockElement.ts +17 -0
  28. package/src/Editor/utils/getSelectionRange.ts +7 -0
  29. package/src/Editor/utils/setLink.ts +22 -0
  30. package/src/Editor/utils/transformers.ts +20 -0
  31. package/src/Editor/utils/unsetLink.ts +11 -0
  32. package/src/EditorSkeleton/index.tsx +21 -0
  33. package/src/index.ts +16 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-design/editor",
3
- "version": "1.0.204",
3
+ "version": "1.0.205",
4
4
  "license": "UNLICENSED",
5
5
  "repository": "git@gitlab.com:os-team/libs/os-design.git",
6
6
  "main": "dist/cjs/index.js",
@@ -14,7 +14,15 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "src",
19
+ "!**/*.test.ts",
20
+ "!**/*.test.tsx",
21
+ "!**/__tests__",
22
+ "!**/*.stories.tsx",
23
+ "!**/*.stories.mdx",
24
+ "!**/*.example.tsx",
25
+ "!**/*.emotion.d.ts"
18
26
  ],
19
27
  "sideEffects": false,
20
28
  "scripts": {
@@ -29,11 +37,11 @@
29
37
  "access": "public"
30
38
  },
31
39
  "dependencies": {
32
- "@os-design/core": "^1.0.199",
33
- "@os-design/icons": "^1.0.47",
34
- "@os-design/styles": "^1.0.44",
35
- "@os-design/theming": "^1.0.42",
36
- "@os-design/utils": "^1.0.61",
40
+ "@os-design/core": "^1.0.200",
41
+ "@os-design/icons": "^1.0.48",
42
+ "@os-design/styles": "^1.0.45",
43
+ "@os-design/theming": "^1.0.43",
44
+ "@os-design/utils": "^1.0.62",
37
45
  "draft-js": "^0.11.7"
38
46
  },
39
47
  "devDependencies": {
@@ -49,5 +57,5 @@
49
57
  "react": ">=18",
50
58
  "react-dom": ">=18"
51
59
  },
52
- "gitHead": "bbd193f118a3128033d4d99ffcb4e96fe06f0dba"
60
+ "gitHead": "3d6b264027712ef81a75379fe3fde3c76c3079af"
53
61
  }
@@ -0,0 +1,7 @@
1
+ import '@emotion/react';
2
+ import { Theme as BaseTheme } from '@os-design/theming';
3
+
4
+ declare module '@emotion/react' {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface Theme extends BaseTheme {}
7
+ }
@@ -0,0 +1,49 @@
1
+ import { PopoverProps } from '@os-design/core';
2
+ import { EditorState } from 'draft-js';
3
+ import React, { useCallback } from 'react';
4
+
5
+ import Toolbar from './Toolbar';
6
+ import ToolbarButton from './ToolbarButton';
7
+ import { BlockToolbarItem } from './blocks/types';
8
+ import getCurrentBlock from './utils/getCurrentBlock';
9
+
10
+ interface BlockToolbarProps extends Omit<PopoverProps, 'onChange'> {
11
+ items: BlockToolbarItem[];
12
+ value: EditorState;
13
+ onChange: (value: EditorState) => void;
14
+ setReadOnly: (readOnly: boolean) => void;
15
+ }
16
+
17
+ const BlockToolbar: React.FC<BlockToolbarProps> = ({
18
+ items,
19
+ value,
20
+ onChange,
21
+ setReadOnly,
22
+ ...rest
23
+ }) => {
24
+ const clickHandler = useCallback<(item: BlockToolbarItem) => void>(
25
+ ({ onClick }) => {
26
+ const currentBlock = getCurrentBlock(value);
27
+ if (currentBlock.getType() !== 'unstyled' || currentBlock.getLength() > 0)
28
+ return;
29
+ onClick({
30
+ value,
31
+ onChange,
32
+ setReadOnly,
33
+ });
34
+ },
35
+ [value, onChange, setReadOnly]
36
+ );
37
+
38
+ return (
39
+ <Toolbar {...rest}>
40
+ {items.map((item) => (
41
+ <ToolbarButton key={item.type} onClick={() => clickHandler(item)}>
42
+ {item.icon}
43
+ </ToolbarButton>
44
+ ))}
45
+ </Toolbar>
46
+ );
47
+ };
48
+
49
+ export default BlockToolbar;
@@ -0,0 +1,80 @@
1
+ import { PopoverProps } from '@os-design/core';
2
+ import { EditorState, RichUtils } from 'draft-js';
3
+
4
+ import React, { useCallback, useMemo } from 'react';
5
+ import Toolbar from './Toolbar';
6
+ import ToolbarButton from './ToolbarButton';
7
+
8
+ import { StyleToolbarItem } from './utils/defaultStyleToolbarItems';
9
+ import setLink from './utils/setLink';
10
+ import unsetLink from './utils/unsetLink';
11
+
12
+ interface StyleToolbarProps extends Omit<PopoverProps, 'onChange'> {
13
+ items: StyleToolbarItem[];
14
+ value: EditorState;
15
+ onChange: (value: EditorState) => void;
16
+ }
17
+
18
+ const StyleToolbar: React.FC<StyleToolbarProps> = ({
19
+ items,
20
+ value,
21
+ onChange = () => {},
22
+ ...rest
23
+ }) => {
24
+ const currentBlockType = useMemo(
25
+ () => RichUtils.getCurrentBlockType(value),
26
+ [value]
27
+ );
28
+ const currentBlockContainsLink = useMemo(
29
+ () => RichUtils.currentBlockContainsLink(value),
30
+ [value]
31
+ );
32
+
33
+ const toggleLink = useCallback(() => {
34
+ if (currentBlockContainsLink) {
35
+ onChange(unsetLink(value));
36
+ return;
37
+ }
38
+ // eslint-disable-next-line no-alert
39
+ const url = prompt('Paste or type a link');
40
+ if (!url) return;
41
+ onChange(setLink(value, url));
42
+ }, [currentBlockContainsLink, value, onChange]);
43
+
44
+ const clickHandler = useCallback<(item: StyleToolbarItem) => void>(
45
+ (item) => {
46
+ if (item.type === 'inline') {
47
+ onChange(RichUtils.toggleInlineStyle(value, item.name));
48
+ } else if (item.type === 'block') {
49
+ onChange(RichUtils.toggleBlockType(value, item.name));
50
+ }
51
+ },
52
+ [value, onChange]
53
+ );
54
+
55
+ if (items.length === 0) return null;
56
+
57
+ return (
58
+ <Toolbar {...rest}>
59
+ {items.map((item) => {
60
+ const isLink = item.name === 'LINK' && item.type === 'inline';
61
+
62
+ const active = isLink
63
+ ? currentBlockContainsLink
64
+ : value.getCurrentInlineStyle().has(item.name) ||
65
+ currentBlockType === item.name;
66
+ const onClick = isLink ? toggleLink : () => clickHandler(item);
67
+
68
+ return (
69
+ <ToolbarButton key={item.name} active={active} onClick={onClick}>
70
+ {item.icon}
71
+ </ToolbarButton>
72
+ );
73
+ })}
74
+ </Toolbar>
75
+ );
76
+ };
77
+
78
+ StyleToolbar.displayName = 'StyleToolbar';
79
+
80
+ export default StyleToolbar;
@@ -0,0 +1,18 @@
1
+ import styled from '@emotion/styled';
2
+ import { Popover } from '@os-design/core';
3
+ import { clr } from '@os-design/theming';
4
+
5
+ const Toolbar = styled(Popover)`
6
+ // Reset popover styles
7
+ border: 0;
8
+
9
+ display: flex;
10
+ flex-direction: row;
11
+ overflow: hidden; // For border-radius
12
+
13
+ background-color: ${(p) => clr(p.theme.editorToolbarButtonColorBg)};
14
+ `;
15
+
16
+ Toolbar.displayName = 'Toolbar';
17
+
18
+ export default Toolbar;
@@ -0,0 +1,65 @@
1
+ import { css } from '@emotion/react';
2
+
3
+ import styled from '@emotion/styled';
4
+ import { resetButtonStyles, transitionStyles } from '@os-design/styles';
5
+ import { clr } from '@os-design/theming';
6
+
7
+ import { omitEmotionProps } from '@os-design/utils';
8
+ import React from 'react';
9
+
10
+ type JsxButtonProps = JSX.IntrinsicElements['button'];
11
+ interface ToolbarButtonProps extends JsxButtonProps {
12
+ active?: boolean;
13
+ }
14
+
15
+ const activeStyles = (p) =>
16
+ p.active &&
17
+ css`
18
+ background-color: ${clr(p.theme.editorToolbarButtonColorBgActive)};
19
+ `;
20
+
21
+ type StyledToolbarButtonProps = Pick<ToolbarButtonProps, 'active'>;
22
+ const StyledToolbarButton = styled(
23
+ 'button',
24
+ omitEmotionProps('active')
25
+ )<StyledToolbarButtonProps>`
26
+ ${resetButtonStyles};
27
+ cursor: pointer;
28
+ font-size: 1.3em;
29
+
30
+ display: flex;
31
+ justify-content: center;
32
+ align-items: center;
33
+
34
+ width: ${(p) => p.theme.editorToolbarButtonSize}em;
35
+ height: ${(p) => p.theme.editorToolbarButtonSize}em;
36
+ background-color: ${(p) => clr(p.theme.editorToolbarButtonColorBg)};
37
+ color: ${(p) => clr(p.theme.editorToolbarButtonColorText)};
38
+
39
+ @media (hover: hover) {
40
+ &:hover,
41
+ &:focus {
42
+ background-color: ${(p) => clr(p.theme.editorToolbarButtonColorBgHover)};
43
+ }
44
+ }
45
+
46
+ ${activeStyles};
47
+ ${transitionStyles('background-color')};
48
+ `;
49
+
50
+ const ToolbarButton: React.FC<ToolbarButtonProps> = ({
51
+ onMouseDown = () => {},
52
+ ...rest
53
+ }) => (
54
+ <StyledToolbarButton
55
+ onMouseDown={(e) => {
56
+ onMouseDown(e);
57
+ e.preventDefault();
58
+ }}
59
+ {...rest}
60
+ />
61
+ );
62
+
63
+ ToolbarButton.displayName = 'ToolbarButton';
64
+
65
+ export default ToolbarButton;
@@ -0,0 +1,10 @@
1
+ import styled from '@emotion/styled';
2
+
3
+ const Figure = styled.figure`
4
+ border-radius: ${(p) => p.theme.borderRadius}em;
5
+ overflow: hidden;
6
+ `;
7
+
8
+ Figure.displayName = 'Figure';
9
+
10
+ export default Figure;
@@ -0,0 +1,19 @@
1
+ import styled from '@emotion/styled';
2
+ import { clr } from '@os-design/theming';
3
+
4
+ const FigureCaption = styled.figcaption`
5
+ font-size: ${(p) => p.theme.sizes.small}em;
6
+ padding: 0.4em 0.8em;
7
+
8
+ background-color: ${(p) => clr(p.theme.editorFigureCaptionColorBg)};
9
+ color: ${(p) => clr(p.theme.editorFigureCaptionColorText)};
10
+
11
+ & > div {
12
+ text-align: center !important;
13
+ margin: 0 !important;
14
+ }
15
+ `;
16
+
17
+ FigureCaption.displayName = 'FigureCaption';
18
+
19
+ export default FigureCaption;
@@ -0,0 +1,162 @@
1
+ import { css } from '@emotion/react';
2
+
3
+ import styled from '@emotion/styled';
4
+ import { message } from '@os-design/core';
5
+
6
+ import { Loading, Picture } from '@os-design/icons';
7
+
8
+ import { omitEmotionProps, useEvent } from '@os-design/utils';
9
+ import { EditorBlock } from 'draft-js';
10
+ import React, { useCallback, useRef, useState } from 'react';
11
+
12
+ import changeBlock from '../utils/changeBlock';
13
+ import getCurrentBlock from '../utils/getCurrentBlock';
14
+ import Figure from './Figure';
15
+ import FigureCaption from './FigureCaption';
16
+
17
+ import { BlockProps, BlockToolbarItem } from './types';
18
+
19
+ const widthStyles = (p) =>
20
+ p.width &&
21
+ css`
22
+ width: ${p.width}px;
23
+ `;
24
+
25
+ interface ImageFigureProps {
26
+ width: number;
27
+ }
28
+ const ImageFigure = styled(Figure, omitEmotionProps('width'))<ImageFigureProps>`
29
+ position: relative;
30
+ display: inline-block;
31
+ max-width: 100%;
32
+ ${widthStyles};
33
+ `;
34
+
35
+ const Mask = styled.div`
36
+ position: absolute;
37
+ top: 0;
38
+ right: 0;
39
+ bottom: 0;
40
+ left: 0;
41
+
42
+ display: flex;
43
+ justify-content: center;
44
+ align-items: center;
45
+
46
+ background-color: hsla(
47
+ 0,
48
+ 0%,
49
+ 0%,
50
+ ${(p) => p.theme.editorBlockImageMaskOpacity}
51
+ );
52
+ color: hsl(0, 0%, 100%);
53
+ `;
54
+
55
+ const LoadingIcon = styled(Loading)`
56
+ font-size: ${(p) => p.theme.editorBlockImageLoadingFontSize}em;
57
+ `;
58
+
59
+ const Image = styled.img`
60
+ max-width: 100%;
61
+ max-height: ${(p) => p.theme.editorBlockImageMaxHeight}em;
62
+ vertical-align: bottom;
63
+ `;
64
+
65
+ const ImageBlock: React.FC<BlockProps> = (props) => {
66
+ const { block } = props;
67
+ const data = block.getData();
68
+ const src = data.get('src') as string;
69
+ const loading = data.get('loading') as boolean;
70
+
71
+ // Update the width of the image
72
+ const imageRef = useRef<HTMLImageElement>(null);
73
+ const [width, setWidth] = useState(0);
74
+ const updateWidth = useCallback(() => {
75
+ if (!imageRef.current) return;
76
+ setWidth(imageRef.current.width);
77
+ }, []);
78
+ useEvent(
79
+ (typeof window !== 'undefined' ? window : undefined) as EventTarget,
80
+ 'resize',
81
+ updateWidth
82
+ );
83
+
84
+ if (!src) return null;
85
+
86
+ return (
87
+ <ImageFigure width={width}>
88
+ {loading && (
89
+ <Mask>
90
+ <LoadingIcon />
91
+ </Mask>
92
+ )}
93
+ <Image
94
+ src={src.startsWith('blob:') ? src : `${src}-1024`}
95
+ alt={block.getText()}
96
+ onLoad={updateWidth}
97
+ ref={imageRef}
98
+ />
99
+ {!loading && (
100
+ <FigureCaption>
101
+ <EditorBlock {...props} />
102
+ </FigureCaption>
103
+ )}
104
+ </ImageFigure>
105
+ );
106
+ };
107
+
108
+ export const IMAGE_BLOCK = 'atomic:image';
109
+
110
+ const imageBlock = (
111
+ onImageUpload: (file: File) => Promise<string>
112
+ ): BlockToolbarItem => ({
113
+ type: IMAGE_BLOCK,
114
+ component: ImageBlock,
115
+ icon: <Picture />,
116
+ onClick: ({ value, onChange, setReadOnly }) => {
117
+ if (!onImageUpload) throw new Error('Specify the onImageUpload method');
118
+
119
+ // Not working in mobile Safari.
120
+ // The input must be actually appended to the DOM.
121
+ const input = document.createElement('input');
122
+ input.type = 'file';
123
+ input.accept = 'image/jpeg,image/png,image/webp';
124
+ input.onchange = async (e) => {
125
+ const target = e.target as HTMLInputElement | null;
126
+ if (!target) return;
127
+ const { files } = target;
128
+ if (!files) return;
129
+
130
+ setReadOnly(true);
131
+ const file = files[0];
132
+ let src = URL.createObjectURL(file);
133
+
134
+ // Add the local image
135
+ const currentBlock = getCurrentBlock(value);
136
+ let nextEditorState = changeBlock(value, currentBlock, IMAGE_BLOCK, {
137
+ src,
138
+ loading: true,
139
+ });
140
+ onChange(nextEditorState);
141
+
142
+ // Replace the local image with the remote one
143
+ try {
144
+ src = await onImageUpload(file);
145
+ nextEditorState = changeBlock(value, currentBlock, IMAGE_BLOCK, {
146
+ src,
147
+ });
148
+ } catch (err) {
149
+ if (err instanceof Error) {
150
+ message.error(err.message);
151
+ }
152
+ nextEditorState = changeBlock(value, currentBlock, 'unstyled');
153
+ }
154
+
155
+ setReadOnly(false);
156
+ onChange(nextEditorState);
157
+ };
158
+ input.click();
159
+ },
160
+ });
161
+
162
+ export default imageBlock;
@@ -0,0 +1,21 @@
1
+ import { ContentBlock, EditorState } from 'draft-js';
2
+ import React from 'react';
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ export interface BlockProps extends Record<string, any> {
6
+ block: ContentBlock;
7
+ }
8
+
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export interface BlockToolbarItemOnClickProps extends Record<string, any> {
11
+ value: EditorState;
12
+ onChange: (value: EditorState) => void;
13
+ setReadOnly: (readOnly: boolean) => void;
14
+ }
15
+
16
+ export interface BlockToolbarItem {
17
+ type: string;
18
+ component: React.FC<BlockProps>;
19
+ icon: React.ReactElement;
20
+ onClick: (props: BlockToolbarItemOnClickProps) => void;
21
+ }
@@ -0,0 +1,83 @@
1
+ import { Video } from '@os-design/core';
2
+ import { Video as VideoIcon } from '@os-design/icons';
3
+ import { EditorBlock } from 'draft-js';
4
+ import React from 'react';
5
+
6
+ import changeBlock from '../utils/changeBlock';
7
+ import getCurrentBlock from '../utils/getCurrentBlock';
8
+ import Figure from './Figure';
9
+ import FigureCaption from './FigureCaption';
10
+ import { BlockProps, BlockToolbarItem } from './types';
11
+
12
+ const VideoBlock: React.FC<BlockProps> = (props) => {
13
+ const { block } = props;
14
+ const data = block.getData();
15
+ const src = data.get('src');
16
+
17
+ if (!src) return null;
18
+
19
+ return (
20
+ <Figure>
21
+ <Video src={src} title={block.getText()} />
22
+ <FigureCaption>
23
+ <EditorBlock {...props} />
24
+ </FigureCaption>
25
+ </Figure>
26
+ );
27
+ };
28
+
29
+ const videoTypes = [
30
+ /**
31
+ * YouTube. Supported formats:
32
+ * https://www.youtube.com/watch?v=FJIhWbUt600&ab_channel=IlyaOrdin
33
+ * https://www.youtube.com/embed/FJIhWbUt600
34
+ * https://youtu.be/FJIhWbUt600
35
+ */
36
+ {
37
+ re: /^https:\/\/(?:www\.youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([A-z0-9-_]*).*$/,
38
+ getUrl: (id: string) => `https://www.youtube.com/embed/${id}`,
39
+ },
40
+ /**
41
+ * RuTube. Supported formats:
42
+ * https://rutube.ru/video/d00526135b2b96d272f6d89b486036c1/
43
+ * https://rutube.ru/play/embed/d00526135b2b96d272f6d89b486036c1
44
+ */
45
+ {
46
+ re: /^https:\/\/rutube\.ru\/(?:video|play\/embed)\/([a-z0-9]*)\/?$/,
47
+ getUrl: (id: string) => `https://rutube.ru/play/embed/${id}`,
48
+ },
49
+ ];
50
+
51
+ const detectVideo = (url: string) => {
52
+ // eslint-disable-next-line no-restricted-syntax
53
+ for (const { re, getUrl } of videoTypes) {
54
+ const groups = url.match(re);
55
+ if (groups && groups[1]) return getUrl(groups[1]);
56
+ }
57
+ return null;
58
+ };
59
+
60
+ export const VIDEO_BLOCK = 'atomic:video';
61
+
62
+ const videoBlock: BlockToolbarItem = {
63
+ type: VIDEO_BLOCK,
64
+ component: VideoBlock,
65
+ icon: <VideoIcon />,
66
+ onClick: ({ value, onChange }) => {
67
+ // eslint-disable-next-line no-alert
68
+ const url = prompt('Insert a link to YouTube or RuTube');
69
+ if (!url) return;
70
+
71
+ const src = detectVideo(url);
72
+ if (!src) return;
73
+
74
+ const currentBlock = getCurrentBlock(value);
75
+ const nextEditorState = changeBlock(value, currentBlock, VIDEO_BLOCK, {
76
+ src,
77
+ });
78
+
79
+ onChange(nextEditorState);
80
+ },
81
+ };
82
+
83
+ export default videoBlock;
@@ -0,0 +1,23 @@
1
+ import { Link } from '@os-design/core';
2
+
3
+ import { DraftDecorator } from 'draft-js';
4
+
5
+ import React from 'react';
6
+
7
+ const linkDecorator: DraftDecorator = {
8
+ strategy: (contentBlock, callback, contentState) =>
9
+ contentBlock.findEntityRanges((character) => {
10
+ const entityKey = character.getEntity();
11
+ return (
12
+ entityKey !== null &&
13
+ contentState.getEntity(entityKey).getType() === 'LINK'
14
+ );
15
+ }, callback),
16
+
17
+ component: ({ contentState, entityKey, children }) => {
18
+ const { url } = contentState.getEntity(entityKey).getData();
19
+ return <Link href={url}>{children}</Link>;
20
+ },
21
+ };
22
+
23
+ export default linkDecorator;
@@ -0,0 +1,72 @@
1
+ import { EditorState } from 'draft-js';
2
+ import { useEffect, useState } from 'react';
3
+ import getSelectedBlockElement from '../utils/getSelectedBlockElement';
4
+
5
+ interface Rect {
6
+ top: number;
7
+ left: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+
12
+ interface UseBlockToolbarPropsRes {
13
+ trigger: Rect;
14
+ visible: boolean;
15
+ }
16
+
17
+ /**
18
+ * Updates the visibility of the block toolbar.
19
+ */
20
+ const useBlockToolbarProps = (
21
+ value: EditorState,
22
+ show: boolean
23
+ ): UseBlockToolbarPropsRes => {
24
+ const [trigger, setTrigger] = useState({
25
+ top: 0,
26
+ left: 0,
27
+ width: 0,
28
+ height: 0,
29
+ });
30
+ const [visible, setVisible] = useState(false);
31
+
32
+ useEffect(() => {
33
+ if (!show || !value) return;
34
+
35
+ const selectedBlockElement = getSelectedBlockElement();
36
+ if (!selectedBlockElement) {
37
+ setVisible(false);
38
+ return;
39
+ }
40
+
41
+ const selectionState = value.getSelection();
42
+ const currentBlockKey = selectionState.getStartKey();
43
+ const contentState = value.getCurrentContent();
44
+ const currentBlock = contentState.getBlockForKey(currentBlockKey);
45
+ const lineNumber = contentState
46
+ .getBlockMap()
47
+ .keySeq()
48
+ .findIndex((k) => k === currentBlockKey);
49
+
50
+ if (
51
+ currentBlock.getType() !== 'unstyled' ||
52
+ currentBlock.getLength() > 0 ||
53
+ lineNumber === 0
54
+ ) {
55
+ setVisible(false);
56
+ return;
57
+ }
58
+
59
+ const { top, left, height } = selectedBlockElement.getBoundingClientRect();
60
+ setTrigger({
61
+ top,
62
+ left,
63
+ width: 0,
64
+ height,
65
+ });
66
+ setVisible(true);
67
+ }, [show, value]);
68
+
69
+ return { trigger, visible };
70
+ };
71
+
72
+ export default useBlockToolbarProps;