@inshapardaz/likhari-react 0.1.6-dev.70 → 0.1.6
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.
- package/dist/EditorRoot.d.ts +1 -25
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +5 -24
- package/dist/components/LinkDialog.d.ts.map +1 -1
- package/dist/components/LinkDialog.js +3 -7
- package/dist/components/Toolbar.d.ts +1 -4
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +33 -246
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +0 -2
- package/dist/theme/EditorThemeProvider.d.ts +1 -17
- package/dist/theme/EditorThemeProvider.d.ts.map +1 -1
- package/dist/theme/EditorThemeProvider.js +2 -24
- package/dist/theme/editor.css +0 -125
- package/dist/theme/editorTheme.d.ts.map +1 -1
- package/dist/theme/editorTheme.js +0 -1
- package/package.json +1 -2
- package/dist/PortalTargetContext.d.ts +0 -16
- package/dist/PortalTargetContext.d.ts.map +0 -1
- package/dist/PortalTargetContext.js +0 -18
- package/dist/i18n/index.d.ts +0 -4
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/i18n/index.js +0 -2
- package/dist/i18n/strings.d.ts +0 -158
- package/dist/i18n/strings.d.ts.map +0 -1
- package/dist/i18n/strings.js +0 -425
- package/dist/i18n/useStrings.d.ts +0 -10
- package/dist/i18n/useStrings.d.ts.map +0 -1
- package/dist/i18n/useStrings.js +0 -14
- package/dist/image/ImageComponent.d.ts +0 -23
- package/dist/image/ImageComponent.d.ts.map +0 -1
- package/dist/image/ImageComponent.js +0 -171
- package/dist/image/ImageCropDialog.d.ts +0 -22
- package/dist/image/ImageCropDialog.d.ts.map +0 -1
- package/dist/image/ImageCropDialog.js +0 -120
- package/dist/image/ImageCropper.d.ts +0 -15
- package/dist/image/ImageCropper.d.ts.map +0 -1
- package/dist/image/ImageCropper.js +0 -120
- package/dist/image/ImageDialog.d.ts +0 -37
- package/dist/image/ImageDialog.d.ts.map +0 -1
- package/dist/image/ImageDialog.js +0 -176
- package/dist/image/ImageNode.d.ts +0 -64
- package/dist/image/ImageNode.d.ts.map +0 -1
- package/dist/image/ImageNode.js +0 -133
- package/dist/image/ImageOptionsContext.d.ts +0 -24
- package/dist/image/ImageOptionsContext.d.ts.map +0 -1
- package/dist/image/ImageOptionsContext.js +0 -10
- package/dist/image/cropMath.d.ts +0 -35
- package/dist/image/cropMath.d.ts.map +0 -1
- package/dist/image/cropMath.js +0 -92
- package/dist/image/imageEdit.d.ts +0 -49
- package/dist/image/imageEdit.d.ts.map +0 -1
- package/dist/image/imageEdit.js +0 -132
- package/dist/image/imageUrl.d.ts +0 -14
- package/dist/image/imageUrl.d.ts.map +0 -1
- package/dist/image/imageUrl.js +0 -27
- package/dist/theme/accentColor.d.ts +0 -43
- package/dist/theme/accentColor.d.ts.map +0 -1
- package/dist/theme/accentColor.js +0 -84
package/dist/EditorRoot.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { SerializedEditorState } from 'lexical';
|
|
|
3
3
|
import { type FormatId } from '@inshapardaz/likhari-converters';
|
|
4
4
|
import { type EditorFeatureConfig, type FeatureConfigPresetName } from '@inshapardaz/likhari-core';
|
|
5
5
|
import { type FontOption } from './fonts/index.js';
|
|
6
|
-
import { type Locale } from './i18n/index.js';
|
|
7
6
|
export interface EditorInitialContent {
|
|
8
7
|
format: Extract<FormatId, 'lexical-json' | 'plain-text'>;
|
|
9
8
|
value: string;
|
|
@@ -15,15 +14,7 @@ export interface EditorRootProps {
|
|
|
15
14
|
featurePreset?: FeatureConfigPresetName;
|
|
16
15
|
theme?: MantineThemeOverride;
|
|
17
16
|
colorScheme?: 'light' | 'dark';
|
|
18
|
-
|
|
19
|
-
* A CSS color (e.g. `'#2B6E6E'`) overriding the editor's default accent —
|
|
20
|
-
* both Mantine's own primary color (buttons, portaled dropdowns' active-item
|
|
21
|
-
* highlight, etc.) and the `--editor-accent` / `--editor-accent-soft` CSS
|
|
22
|
-
* variables the toolbar/canvas use directly. Omit to use the built-in
|
|
23
|
-
* default tokens (packages/core/src/theme/tokens.ts).
|
|
24
|
-
*/
|
|
25
|
-
accentColor?: string;
|
|
26
|
-
locale?: Locale;
|
|
17
|
+
locale?: 'en' | 'ur' | 'pa-shahmukhi';
|
|
27
18
|
placeholder?: string;
|
|
28
19
|
/**
|
|
29
20
|
* CSS height for the editor box (toolbar + canvas together) — accepts any
|
|
@@ -47,21 +38,6 @@ export interface EditorRootProps {
|
|
|
47
38
|
* the unsaved-changes state) or to hide it while keeping `onSave`.
|
|
48
39
|
*/
|
|
49
40
|
showSave?: boolean;
|
|
50
|
-
/**
|
|
51
|
-
* Stores an uploaded image and returns the URL to reference it by. Without
|
|
52
|
-
* it, uploaded images are embedded in the document as base64 data URIs —
|
|
53
|
-
* simple, but they bloat the document (see `images.maxSizeMB`).
|
|
54
|
-
*/
|
|
55
|
-
onImageUpload?: (file: File) => Promise<string>;
|
|
56
|
-
/**
|
|
57
|
-
* Downloads a linked image's bytes when converting it to an embedded copy
|
|
58
|
-
* or applying a crop/rotate/flip. Without it, the browser fetches the URL
|
|
59
|
-
* directly, which fails for any image server that doesn't send CORS
|
|
60
|
-
* headers — that restriction can't be worked around from the browser
|
|
61
|
-
* alone. Provide this to route the request through your own backend (or a
|
|
62
|
-
* CORS proxy) instead.
|
|
63
|
-
*/
|
|
64
|
-
fetchImage?: (url: string) => Promise<Blob>;
|
|
65
41
|
/**
|
|
66
42
|
* Entries for the toolbar's font-family dropdown. Defaults to
|
|
67
43
|
* `DEFAULT_FONT_OPTIONS` (generic Latin faces plus the Urdu/Arabic-script
|
package/dist/EditorRoot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAS1D,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAKzH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAGjE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGA6HrB,CAAC"}
|
package/dist/EditorRoot.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useEffect,
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
4
4
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
5
5
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
6
6
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
7
7
|
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
|
8
|
-
import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
|
|
9
8
|
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
|
10
9
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
11
10
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
@@ -17,33 +16,20 @@ import { EditorThemeProvider } from './theme/EditorThemeProvider.js';
|
|
|
17
16
|
import { Toolbar } from './components/Toolbar.js';
|
|
18
17
|
import { injectUrduWebFontsCss } from './fonts/index.js';
|
|
19
18
|
import { LinkPastePlugin } from './plugins/LinkPastePlugin.js';
|
|
20
|
-
import { ImageOptionsContext } from './image/ImageOptionsContext.js';
|
|
21
|
-
import { PortalTargetContext } from './PortalTargetContext.js';
|
|
22
|
-
import { UiStringsContext, getStrings } from './i18n/index.js';
|
|
23
19
|
function initialEditorStateJson(initialContent) {
|
|
24
20
|
if (!initialContent)
|
|
25
21
|
return undefined;
|
|
26
22
|
const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
|
|
27
23
|
return JSON.stringify(state);
|
|
28
24
|
}
|
|
29
|
-
export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme,
|
|
25
|
+
export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder = 'Start writing…', height = '480px', onChange, onSave, showSave = Boolean(onSave), fontOptions, }, ref) {
|
|
30
26
|
const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
|
|
31
|
-
const strings = useMemo(() => getStrings(locale), [locale]);
|
|
32
|
-
const resolvedPlaceholder = placeholder ?? strings.editor.placeholder;
|
|
33
27
|
// The urdu-web-fonts stylesheets are needed for the font dropdown and for
|
|
34
28
|
// RTL content, whose canvas font (editor.css) is one of those families.
|
|
35
29
|
useEffect(() => {
|
|
36
30
|
if (config.font.family || locale !== 'en')
|
|
37
31
|
injectUrduWebFontsCss();
|
|
38
32
|
}, [config.font.family, locale]);
|
|
39
|
-
const imageOptions = useMemo(() => ({
|
|
40
|
-
allowLinked: config.images.linked,
|
|
41
|
-
allowEmbedded: config.images.embedded,
|
|
42
|
-
allowCaption: config.images.caption,
|
|
43
|
-
maxSizeMB: config.images.maxSizeMB,
|
|
44
|
-
onImageUpload,
|
|
45
|
-
fetchImage,
|
|
46
|
-
}), [config.images.linked, config.images.embedded, config.images.caption, config.images.maxSizeMB, onImageUpload, fetchImage]);
|
|
47
33
|
const editorStateRef = useRef(null);
|
|
48
34
|
const lastSavedJsonRef = useRef(null);
|
|
49
35
|
const [isDirty, setIsDirty] = useState(false);
|
|
@@ -82,12 +68,12 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
82
68
|
return true;
|
|
83
69
|
if (typeof window === 'undefined')
|
|
84
70
|
return true;
|
|
85
|
-
return window.confirm(
|
|
71
|
+
return window.confirm('You have unsaved changes. Discard them?');
|
|
86
72
|
},
|
|
87
73
|
focus() {
|
|
88
74
|
rootElementRef.current?.focus();
|
|
89
75
|
},
|
|
90
|
-
}), [isDirty
|
|
76
|
+
}), [isDirty]);
|
|
91
77
|
const rootElementRef = useRef(null);
|
|
92
78
|
const handleChange = (state) => {
|
|
93
79
|
editorStateRef.current = state;
|
|
@@ -106,10 +92,5 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
106
92
|
onSave?.(content, format);
|
|
107
93
|
};
|
|
108
94
|
const dir = locale === 'en' ? 'ltr' : 'rtl';
|
|
109
|
-
|
|
110
|
-
// EditorRoot instances on one page each get their own portal target
|
|
111
|
-
// rather than colliding on a shared one.
|
|
112
|
-
const portalTargetId = useId();
|
|
113
|
-
const portalTargetSelector = `#${CSS.escape(portalTargetId)}`;
|
|
114
|
-
return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, accentColor: accentColor, scopeElementId: portalTargetId, children: _jsxs("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: [_jsx("div", { id: portalTargetId, className: "likhari-portal-target" }), _jsx(PortalTargetContext.Provider, { value: portalTargetSelector, children: _jsx(UiStringsContext.Provider, { value: strings, children: _jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir, locale: locale }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": strings.editor.contentLabel }), placeholder: _jsx("div", { className: "likhari-placeholder", children: resolvedPlaceholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.lists.check && _jsx(CheckListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }) })] }) }));
|
|
95
|
+
return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, children: _jsx("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": "Editor content" }), placeholder: _jsx("div", { className: "likhari-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }));
|
|
115
96
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LinkDialog.d.ts","sourceRoot":"","sources":["../../src/components/LinkDialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LinkDialog.d.ts","sourceRoot":"","sources":["../../src/components/LinkDialog.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxD,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;gDACgD;AAChD,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,+BAgE7G"}
|
|
@@ -2,13 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { Button, Group, Modal, TextInput } from '@mantine/core';
|
|
4
4
|
import { normalizeLinkUrl } from '../utils/linkUrl.js';
|
|
5
|
-
import { useUiStrings } from '../i18n/useStrings.js';
|
|
6
|
-
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
7
5
|
/** Insert/edit-link dialog (UI spec §6). Mantine Modal supplies focus
|
|
8
6
|
* trapping, Escape-to-close and the overlay. */
|
|
9
7
|
export function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemove, onClose }) {
|
|
10
|
-
const strings = useUiStrings();
|
|
11
|
-
const portalTarget = usePortalTarget();
|
|
12
8
|
const [url, setUrl] = useState(initialUrl);
|
|
13
9
|
const [text, setText] = useState('');
|
|
14
10
|
const [error, setError] = useState(null);
|
|
@@ -23,13 +19,13 @@ export function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemo
|
|
|
23
19
|
e.preventDefault();
|
|
24
20
|
const normalized = normalizeLinkUrl(url);
|
|
25
21
|
if (!normalized) {
|
|
26
|
-
setError(
|
|
22
|
+
setError('Enter a valid http(s), mailto, tel or relative URL');
|
|
27
23
|
return;
|
|
28
24
|
}
|
|
29
25
|
onSubmit({ url: normalized, text: text.trim() || normalized });
|
|
30
26
|
};
|
|
31
|
-
return (_jsx(Modal, { opened: opened, onClose: onClose, title: onRemove ?
|
|
27
|
+
return (_jsx(Modal, { opened: opened, onClose: onClose, title: onRemove ? 'Edit link' : 'Insert link', centered: true, size: "sm", children: _jsxs("form", { onSubmit: handleSubmit, children: [_jsx(TextInput, { label: "URL", placeholder: "https://example.com", value: url, onChange: (e) => {
|
|
32
28
|
setUrl(e.currentTarget.value);
|
|
33
29
|
setError(null);
|
|
34
|
-
}, error: error, "data-autofocus": true }), showTextField && (_jsx(TextInput, { mt: "sm", label:
|
|
30
|
+
}, error: error, "data-autofocus": true }), showTextField && (_jsx(TextInput, { mt: "sm", label: "Text", placeholder: "Link text (defaults to the URL)", value: text, onChange: (e) => setText(e.currentTarget.value) })), _jsxs(Group, { justify: "space-between", mt: "md", children: [onRemove ? (_jsx(Button, { variant: "subtle", color: "red", onClick: onRemove, children: "Remove link" })) : (_jsx("span", {})), _jsxs(Group, { gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", children: "Apply" })] })] })] }) }));
|
|
35
31
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ResolvedEditorFeatureConfig } from '@inshapardaz/likhari-core';
|
|
2
2
|
import { type FontOption } from '../fonts/index.js';
|
|
3
|
-
import type { Locale } from '../i18n/strings.js';
|
|
4
3
|
export interface ToolbarProps {
|
|
5
4
|
config: ResolvedEditorFeatureConfig;
|
|
6
5
|
onSave?: () => void;
|
|
@@ -10,8 +9,6 @@ export interface ToolbarProps {
|
|
|
10
9
|
fontOptions?: FontOption[];
|
|
11
10
|
/** Text direction of the canvas; picks which default font and size are pre-selected. */
|
|
12
11
|
direction?: 'ltr' | 'rtl';
|
|
13
|
-
/** UI locale — picks the translated strings for labels, tooltips and menu items. */
|
|
14
|
-
locale?: Locale;
|
|
15
12
|
}
|
|
16
|
-
export declare function Toolbar({ config, onSave, isDirty, showSave, fontOptions, direction
|
|
13
|
+
export declare function Toolbar({ config, onSave, isDirty, showSave, fontOptions, direction }: ToolbarProps): import("react").JSX.Element;
|
|
17
14
|
//# sourceMappingURL=Toolbar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAmPtG,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CAC3B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,EAAE,YAAY,+BAqmBjI"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
2
|
-
import { useCallback, useEffect,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Menu, Select } from '@mantine/core';
|
|
4
4
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
5
5
|
import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, $getRoot, $getNearestNodeFromDOMNode, $createTextNode, KEY_MODIFIER_COMMAND, COMMAND_PRIORITY_NORMAL, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND, UNDO_COMMAND, REDO_COMMAND, SELECTION_CHANGE_COMMAND, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, COMMAND_PRIORITY_LOW, } from 'lexical';
|
|
@@ -7,17 +7,13 @@ import { $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType } f
|
|
|
7
7
|
import { $createParagraphNode } from 'lexical';
|
|
8
8
|
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
|
|
9
9
|
import { $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, } from '@lexical/list';
|
|
10
|
-
import { $findMatchingParent
|
|
11
|
-
import { ImageDialog } from '../image/ImageDialog.js';
|
|
12
|
-
import { $createImageNode } from '../image/ImageNode.js';
|
|
10
|
+
import { $findMatchingParent } from '@lexical/utils';
|
|
13
11
|
import { $createLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
|
14
12
|
import { LinkDialog } from './LinkDialog.js';
|
|
15
13
|
import { normalizeLinkUrl } from '../utils/linkUrl.js';
|
|
16
14
|
import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fonts/index.js';
|
|
17
15
|
import { setStyleProperty } from '../utils/style.js';
|
|
18
|
-
import {
|
|
19
|
-
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
20
|
-
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconExternalLink, IconFeather, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconList, IconListCheck, IconListNumbers, IconPencil, IconPhoto, IconPilcrow, IconQuote, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
|
|
16
|
+
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconExternalLink, IconFeather, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconPencil, IconPhoto, IconPilcrow, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
|
|
21
17
|
/** Sentinel for "no inline style": Lexical returns the default it is given when
|
|
22
18
|
* the selection has none, so this tells "unstyled" (show the canvas default)
|
|
23
19
|
* apart from "styled with a value the dropdown doesn't list" (show nothing). */
|
|
@@ -44,35 +40,6 @@ const ALIGN_ICONS = {
|
|
|
44
40
|
right: IconAlignRight,
|
|
45
41
|
justify: IconAlignJustified,
|
|
46
42
|
};
|
|
47
|
-
const HEADING_ICONS = {
|
|
48
|
-
1: IconH1,
|
|
49
|
-
2: IconH2,
|
|
50
|
-
3: IconH3,
|
|
51
|
-
4: IconH4,
|
|
52
|
-
5: IconH5,
|
|
53
|
-
6: IconH6,
|
|
54
|
-
};
|
|
55
|
-
const FORMATTING_ICONS = {
|
|
56
|
-
paragraph: IconPilcrow,
|
|
57
|
-
h1: HEADING_ICONS[1],
|
|
58
|
-
h2: HEADING_ICONS[2],
|
|
59
|
-
h3: HEADING_ICONS[3],
|
|
60
|
-
h4: HEADING_ICONS[4],
|
|
61
|
-
h5: HEADING_ICONS[5],
|
|
62
|
-
h6: HEADING_ICONS[6],
|
|
63
|
-
number: IconListNumbers,
|
|
64
|
-
bullet: IconList,
|
|
65
|
-
check: IconListCheck,
|
|
66
|
-
quote: IconQuote,
|
|
67
|
-
};
|
|
68
|
-
/** Shared option renderer for the formatting/alignment dropdowns: an icon
|
|
69
|
-
* ahead of the label, looked up by the option's own value. */
|
|
70
|
-
function iconOptionRenderer(icons, fallback) {
|
|
71
|
-
return ({ option }) => {
|
|
72
|
-
const Icon = icons[option.value] ?? fallback;
|
|
73
|
-
return (_jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx(Icon, { size: 16, stroke: ICON_STROKE }), option.label] }));
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
43
|
function ToolbarButton({ icon: Icon, title, active, dirty, disabled, onClick, }) {
|
|
77
44
|
return (_jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": active ? 'true' : 'false', "data-dirty": dirty === undefined ? undefined : dirty ? 'true' : 'false', disabled: disabled, "aria-pressed": active, "aria-label": title, title: title, onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }) }));
|
|
78
45
|
}
|
|
@@ -80,30 +47,15 @@ function ToolbarButton({ icon: Icon, title, active, dirty, disabled, onClick, })
|
|
|
80
47
|
* isn't implemented yet (link/image/poetry/font/language-tooling — see
|
|
81
48
|
* docs/lexical-editor-spec.md §13's phasing). Renders so the toolbar's
|
|
82
49
|
* layout is final now and only needs its onClick wired up later. */
|
|
83
|
-
function StubButton({ icon, title
|
|
84
|
-
return _jsx(ToolbarButton, { icon: icon, title:
|
|
50
|
+
function StubButton({ icon, title }) {
|
|
51
|
+
return _jsx(ToolbarButton, { icon: icon, title: `${title} (coming soon)`, disabled: true, onClick: undefined });
|
|
85
52
|
}
|
|
86
53
|
/** Mantine Select (combobox) for the toolbar's dropdown controls. The icon
|
|
87
54
|
* labels the control itself, since a dropdown can't show one per option in
|
|
88
55
|
* its closed state. The dropdown portals to <body>, so it is not clipped by
|
|
89
56
|
* the editor's `overflow: hidden` frame. */
|
|
90
|
-
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, searchable, renderOption, onChange
|
|
91
|
-
|
|
92
|
-
return (_jsx(Select, { size: "xs", w: width, "aria-label": label, title: disabled && comingSoon ? comingSoon(label) : label, data: data, value: value, placeholder: placeholder, disabled: disabled, searchable: searchable, renderOption: renderOption, nothingFoundMessage: searchable ? noMatchMessage : undefined, allowDeselect: false, leftSection: _jsx(Icon, { size: 15, stroke: ICON_STROKE }), comboboxProps: { withinPortal: true, position: 'bottom-start', middlewares: { flip: true, shift: true }, portalProps: { target: portalTarget } }, classNames: { input: 'likhari-toolbar-select-input', dropdown: 'likhari-toolbar-select-dropdown' }, onChange: (v) => v && onChange?.(v) }));
|
|
93
|
-
}
|
|
94
|
-
const EMPTY_OVERFLOW = new Set();
|
|
95
|
-
function setsEqual(a, b) {
|
|
96
|
-
if (a.size !== b.size)
|
|
97
|
-
return false;
|
|
98
|
-
for (const v of a)
|
|
99
|
-
if (!b.has(v))
|
|
100
|
-
return false;
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
/** Renders a movable group's items as normal inline toolbar buttons (used
|
|
104
|
-
* both in the real toolbar and in the hidden width-measuring clone). */
|
|
105
|
-
function MovableGroupButtons({ group }) {
|
|
106
|
-
return (_jsx("div", { className: "likhari-toolbar-group", "data-group-key": group.key, children: group.items.map((item) => (_jsx(ToolbarButton, { icon: item.icon, title: item.label, active: item.active, onClick: item.onClick }, item.key))) }));
|
|
57
|
+
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, searchable, renderOption, onChange }) {
|
|
58
|
+
return (_jsx(Select, { size: "xs", w: width, "aria-label": label, title: disabled ? `${label} (coming soon)` : label, data: data, value: value, placeholder: placeholder, disabled: disabled, searchable: searchable, renderOption: renderOption, nothingFoundMessage: searchable ? 'No match' : undefined, allowDeselect: false, leftSection: _jsx(Icon, { size: 15, stroke: ICON_STROKE }), comboboxProps: { withinPortal: true, position: 'bottom-start', middlewares: { flip: true, shift: true } }, classNames: { input: 'likhari-toolbar-select-input', dropdown: 'likhari-toolbar-select-dropdown' }, onChange: (v) => v && onChange?.(v) }));
|
|
107
59
|
}
|
|
108
60
|
function OverflowItem({ icon: Icon, label, active, onClick, }) {
|
|
109
61
|
return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', className: "likhari-overflow-item",
|
|
@@ -112,27 +64,16 @@ function OverflowItem({ icon: Icon, label, active, onClick, }) {
|
|
|
112
64
|
}
|
|
113
65
|
/** The link menu's contents — shared by the toolbar button and the right-click
|
|
114
66
|
* menu: the URL (opens in a new tab), Edit link, Remove link. */
|
|
115
|
-
function LinkMenuItems({ url, onEdit, onRemove
|
|
67
|
+
function LinkMenuItems({ url, onEdit, onRemove }) {
|
|
116
68
|
// Only offer the link as clickable if it's a URL the editor would have accepted.
|
|
117
69
|
const safeUrl = normalizeLinkUrl(url);
|
|
118
|
-
return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children:
|
|
70
|
+
return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: "Link" }), safeUrl ? (_jsx(Menu.Item, { component: "a", href: safeUrl, target: "_blank", rel: "noopener noreferrer", leftSection: _jsx(IconExternalLink, { size: ICON_SIZE, stroke: ICON_STROKE }), title: url, className: "likhari-link-menu-url", children: url })) : (
|
|
119
71
|
// Not a URL the editor would accept (e.g. loaded from a document): show it, don't make it clickable.
|
|
120
|
-
_jsx(Menu.Item, { disabled: true, title: url, className: "likhari-link-menu-url", children: url ||
|
|
72
|
+
_jsx(Menu.Item, { disabled: true, title: url, className: "likhari-link-menu-url", children: url || '(no URL)' })), _jsx(Menu.Item, { leftSection: _jsx(IconPencil, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onEdit, children: "Edit link" }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconLinkOff, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onRemove, children: "Remove link" })] }));
|
|
121
73
|
}
|
|
122
|
-
export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr'
|
|
74
|
+
export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr' }) {
|
|
123
75
|
const [editor] = useLexicalComposerContext();
|
|
124
76
|
const [state, setState] = useState(INITIAL_STATE);
|
|
125
|
-
const strings = useStrings(locale);
|
|
126
|
-
const portalTarget = usePortalTarget();
|
|
127
|
-
// Responsive "priority+" overflow: the toolbar's outer container and a
|
|
128
|
-
// hidden nowrap clone (rendered with every movable group forced inline)
|
|
129
|
-
// are both measured; whenever the clone's natural width exceeds the
|
|
130
|
-
// container's available width, the lowest-priority movable groups are
|
|
131
|
-
// moved into the "..." menu, lowest-priority first, until what remains
|
|
132
|
-
// inline fits (or there's nothing left to move).
|
|
133
|
-
const toolbarContainerRef = useRef(null);
|
|
134
|
-
const toolbarMeasureRef = useRef(null);
|
|
135
|
-
const [overflowGroupKeys, setOverflowGroupKeys] = useState(EMPTY_OVERFLOW);
|
|
136
77
|
const updateToolbar = useCallback(() => {
|
|
137
78
|
editor.getEditorState().read(() => {
|
|
138
79
|
const selection = $getSelection();
|
|
@@ -254,25 +195,6 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
254
195
|
action();
|
|
255
196
|
editor.focus();
|
|
256
197
|
};
|
|
257
|
-
const [imageDialogOpen, setImageDialogOpen] = useState(false);
|
|
258
|
-
const openImageDialog = () => {
|
|
259
|
-
// Snapshot first: the dialog's focus trap makes Lexical drop the selection.
|
|
260
|
-
snapshotSelection();
|
|
261
|
-
setImageDialogOpen(true);
|
|
262
|
-
};
|
|
263
|
-
const closeImageDialog = () => {
|
|
264
|
-
setImageDialogOpen(false);
|
|
265
|
-
editor.focus();
|
|
266
|
-
};
|
|
267
|
-
const insertImage = (image) => {
|
|
268
|
-
const saved = menuSelectionRef.current;
|
|
269
|
-
if (saved)
|
|
270
|
-
editor.update(() => $setSelection(saved.clone()), { discrete: true });
|
|
271
|
-
editor.update(() => {
|
|
272
|
-
$insertNodeToNearestRoot($createImageNode(image));
|
|
273
|
-
});
|
|
274
|
-
closeImageDialog();
|
|
275
|
-
};
|
|
276
198
|
/** Applies a font property per `config.font.scope`: to the selection, to
|
|
277
199
|
* every text node in the document, or (for 'both') to the selection when
|
|
278
200
|
* there is a range and to the whole document when there isn't. */
|
|
@@ -428,112 +350,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
428
350
|
const showAlignGroup = config.alignment.start || config.alignment.center || config.alignment.justify || config.alignment.left || config.alignment.right;
|
|
429
351
|
const showStubInsertGroup = config.images.linked || config.images.embedded || config.poetry.enabled;
|
|
430
352
|
const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
|
|
431
|
-
|
|
432
|
-
// as normal inline toolbar groups whenever there's room, and only move
|
|
433
|
-
// into the "..." overflow menu when the measured toolbar content doesn't
|
|
434
|
-
// fit the available width (see the ResizeObserver effect below) — this
|
|
435
|
-
// replaces the old always-on overflow menu, which was driven purely by
|
|
436
|
-
// these config flags regardless of actual space.
|
|
437
|
-
const scriptCleanupItems = [];
|
|
438
|
-
if (fmt.strikethrough) {
|
|
439
|
-
scriptCleanupItems.push({
|
|
440
|
-
key: 'strikethrough',
|
|
441
|
-
icon: IconStrikethrough,
|
|
442
|
-
label: strings.toolbar.strikethrough,
|
|
443
|
-
active: state.activeFormats.has('strikethrough'),
|
|
444
|
-
onClick: () => formatText('strikethrough'),
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
if (fmt.superscript) {
|
|
448
|
-
scriptCleanupItems.push({
|
|
449
|
-
key: 'superscript',
|
|
450
|
-
icon: IconSuperscript,
|
|
451
|
-
label: strings.toolbar.superscript,
|
|
452
|
-
active: state.activeFormats.has('superscript'),
|
|
453
|
-
onClick: () => formatText('superscript'),
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
if (fmt.subscript) {
|
|
457
|
-
scriptCleanupItems.push({
|
|
458
|
-
key: 'subscript',
|
|
459
|
-
icon: IconSubscript,
|
|
460
|
-
label: strings.toolbar.subscript,
|
|
461
|
-
active: state.activeFormats.has('subscript'),
|
|
462
|
-
onClick: () => formatText('subscript'),
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
if (fmt.caseTransforms) {
|
|
466
|
-
scriptCleanupItems.push({ key: 'upper', icon: IconLetterCaseUpper, label: strings.toolbar.uppercase, onClick: () => applyCaseTransform('upper') }, { key: 'lower', icon: IconLetterCaseLower, label: strings.toolbar.lowercase, onClick: () => applyCaseTransform('lower') }, { key: 'capitalize', icon: IconLetterCase, label: strings.toolbar.capitalize, onClick: () => applyCaseTransform('capitalize') });
|
|
467
|
-
}
|
|
468
|
-
if (fmt.clearFormatting) {
|
|
469
|
-
scriptCleanupItems.push({ key: 'clear', icon: IconClearFormatting, label: strings.toolbar.clearFormatting, onClick: clearFormatting });
|
|
470
|
-
}
|
|
471
|
-
const indentItems = config.indent
|
|
472
|
-
? [
|
|
473
|
-
{
|
|
474
|
-
key: 'outdent',
|
|
475
|
-
icon: IconIndentDecrease,
|
|
476
|
-
label: strings.toolbar.outdent,
|
|
477
|
-
onClick: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
|
|
478
|
-
},
|
|
479
|
-
{
|
|
480
|
-
key: 'indent',
|
|
481
|
-
icon: IconIndentIncrease,
|
|
482
|
-
label: strings.toolbar.indent,
|
|
483
|
-
onClick: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
|
|
484
|
-
},
|
|
485
|
-
]
|
|
486
|
-
: [];
|
|
487
|
-
// Low to high priority: the first group here is the first one pushed into
|
|
488
|
-
// overflow once content stops fitting (UI spec §3.3).
|
|
489
|
-
const movableGroups = [
|
|
490
|
-
{ key: 'scriptCleanup', items: scriptCleanupItems },
|
|
491
|
-
{ key: 'indentOutdent', items: indentItems },
|
|
492
|
-
].filter((g) => g.items.length > 0);
|
|
493
|
-
const movableGroupKeys = useMemo(() => movableGroups.map((g) => g.key).join(','), [movableGroups]);
|
|
494
|
-
// Re-measure whenever the container is resized, or whenever the set of
|
|
495
|
-
// movable groups (and so the hidden clone's content) changes.
|
|
496
|
-
useLayoutEffect(() => {
|
|
497
|
-
const container = toolbarContainerRef.current;
|
|
498
|
-
const measure = toolbarMeasureRef.current;
|
|
499
|
-
if (!container || !measure)
|
|
500
|
-
return;
|
|
501
|
-
// Rough width of the "..." button itself (icon button + its gap) —
|
|
502
|
-
// only charged against the budget once something has actually moved
|
|
503
|
-
// into it, since it isn't rendered at all otherwise.
|
|
504
|
-
const OVERFLOW_BUTTON_WIDTH = 38;
|
|
505
|
-
const recompute = () => {
|
|
506
|
-
const available = container.clientWidth;
|
|
507
|
-
const total = measure.scrollWidth;
|
|
508
|
-
if (total <= available) {
|
|
509
|
-
setOverflowGroupKeys((prev) => (prev.size === 0 ? prev : EMPTY_OVERFLOW));
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
const widths = new Map();
|
|
513
|
-
measure.querySelectorAll('[data-group-key]').forEach((el) => {
|
|
514
|
-
widths.set(el.dataset.groupKey, el.getBoundingClientRect().width);
|
|
515
|
-
});
|
|
516
|
-
let remaining = total;
|
|
517
|
-
const next = new Set();
|
|
518
|
-
for (const key of movableGroupKeys.split(',').filter(Boolean)) {
|
|
519
|
-
if (remaining <= available)
|
|
520
|
-
break;
|
|
521
|
-
const w = widths.get(key);
|
|
522
|
-
if (w === undefined)
|
|
523
|
-
continue;
|
|
524
|
-
remaining -= w;
|
|
525
|
-
next.add(key);
|
|
526
|
-
}
|
|
527
|
-
if (next.size > 0)
|
|
528
|
-
remaining += OVERFLOW_BUTTON_WIDTH;
|
|
529
|
-
setOverflowGroupKeys((prev) => (setsEqual(prev, next) ? prev : next));
|
|
530
|
-
};
|
|
531
|
-
recompute();
|
|
532
|
-
const ro = new ResizeObserver(recompute);
|
|
533
|
-
ro.observe(container);
|
|
534
|
-
ro.observe(measure);
|
|
535
|
-
return () => ro.disconnect();
|
|
536
|
-
});
|
|
353
|
+
const showOverflowMenu = fmt.strikethrough || fmt.superscript || fmt.subscript || fmt.caseTransforms || fmt.clearFormatting || config.indent;
|
|
537
354
|
// Grouped for the dropdown when options declare groups (Latin / Urdu).
|
|
538
355
|
const fontData = (() => {
|
|
539
356
|
const data = [];
|
|
@@ -559,63 +376,33 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
559
376
|
const shownFontFamily = state.fontFamily === UNSET_STYLE ? canvasDefaults.family : state.fontFamily;
|
|
560
377
|
const shownFontSize = state.fontSize === UNSET_STYLE ? canvasDefaults.size : state.fontSize;
|
|
561
378
|
const formattingOptions = [
|
|
562
|
-
{ value: 'paragraph', label:
|
|
563
|
-
...headingLevels.map((level) => ({ value: `h${level}`, label:
|
|
564
|
-
...(config.lists.numbered ? [{ value: 'number', label:
|
|
565
|
-
...(config.lists.bullet ? [{ value: 'bullet', label:
|
|
566
|
-
...(config.lists.check ? [{ value: 'check', label:
|
|
567
|
-
...(config.blocks.quote ? [{ value: 'quote', label:
|
|
379
|
+
{ value: 'paragraph', label: 'Paragraph' },
|
|
380
|
+
...headingLevels.map((level) => ({ value: `h${level}`, label: `Heading ${level}` })),
|
|
381
|
+
...(config.lists.numbered ? [{ value: 'number', label: 'Numbered list' }] : []),
|
|
382
|
+
...(config.lists.bullet ? [{ value: 'bullet', label: 'Bullet list' }] : []),
|
|
383
|
+
...(config.lists.check ? [{ value: 'check', label: 'Task list' }] : []),
|
|
384
|
+
...(config.blocks.quote ? [{ value: 'quote', label: 'Quote' }] : []),
|
|
568
385
|
];
|
|
569
386
|
const alignOptions = [
|
|
570
|
-
...(config.alignment.start ? [{ value: 'start', label:
|
|
571
|
-
...(config.alignment.center ? [{ value: 'center', label:
|
|
572
|
-
...(config.alignment.start ? [{ value: 'end', label:
|
|
573
|
-
...(config.alignment.justify ? [{ value: 'justify', label:
|
|
574
|
-
...(config.alignment.left ? [{ value: 'left', label:
|
|
575
|
-
...(config.alignment.right ? [{ value: 'right', label:
|
|
387
|
+
...(config.alignment.start ? [{ value: 'start', label: 'Align start' }] : []),
|
|
388
|
+
...(config.alignment.center ? [{ value: 'center', label: 'Align center' }] : []),
|
|
389
|
+
...(config.alignment.start ? [{ value: 'end', label: 'Align end' }] : []),
|
|
390
|
+
...(config.alignment.justify ? [{ value: 'justify', label: 'Justify' }] : []),
|
|
391
|
+
...(config.alignment.left ? [{ value: 'left', label: 'Align left' }] : []),
|
|
392
|
+
...(config.alignment.right ? [{ value: 'right', label: 'Align right' }] : []),
|
|
576
393
|
];
|
|
577
394
|
const formattingValue = state.listType ?? state.blockType;
|
|
578
395
|
const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const fixedGroups = [
|
|
586
|
-
showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: strings.toolbar.save, dirty: Boolean(isDirty), onClick: onSave }) }, "save")),
|
|
587
|
-
config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: strings.toolbar.undo, disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: strings.toolbar.redo, disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] }, "history")),
|
|
588
|
-
showFormattingGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: strings.toolbar.formattingLabel, width: 148, value: formattingValue, data: formattingOptions, comingSoon: strings.toolbar.comingSoon, renderOption: iconOptionRenderer(FORMATTING_ICONS, IconPilcrow), onChange: withRefocus((v) => applyFormatting(v)) }) }, "formatting")),
|
|
589
|
-
showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: strings.toolbar.bold, active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: strings.toolbar.italic, active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: strings.toolbar.underline, active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] }, "inline")),
|
|
590
|
-
(config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: strings.toolbar.fontFamily, width: 150, value: fontOptions.some((f) => f.family === shownFontFamily) ? shownFontFamily : null, placeholder: strings.toolbar.fontFamilyPlaceholder, data: fontData, searchable: true, noMatchMessage: strings.toolbar.noMatch, renderOption: ({ option }) => _jsx("span", { style: { fontFamily: option.value }, children: option.label }), onChange: withRefocus((v) => applyFont('font-family', v)) })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: strings.toolbar.fontSize, width: 84, value: FONT_SIZES_PX.some((px) => `${px}px` === shownFontSize) ? shownFontSize : null, placeholder: strings.toolbar.fontSizePlaceholder, data: FONT_SIZES_PX.map((px) => ({ value: `${px}px`, label: String(px) })), onChange: withRefocus((v) => applyFont('font-size', v)) }))] }, "font")),
|
|
591
|
-
showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: strings.toolbar.alignment, width: 138, value: state.elementFormat || 'start', data: alignOptions, renderOption: iconOptionRenderer(ALIGN_ICONS, IconAlignLeft), onChange: withRefocus((v) => formatElement(v)) }) }, "align")),
|
|
592
|
-
config.links && (_jsx("div", { className: "likhari-toolbar-group", children: state.isLink ? (
|
|
593
|
-
// Caret is in a link: the button opens a menu to see, edit or remove it.
|
|
594
|
-
_jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.linkOptions, title: strings.toolbar.linkOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconLink, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: state.linkUrl, onEdit: () => {
|
|
595
|
-
restoreSelection();
|
|
596
|
-
openLinkDialog();
|
|
597
|
-
}, onRemove: removeLink, strings: strings }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.insertLink, onClick: openLinkDialog })) }, "link")),
|
|
598
|
-
showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: strings.toolbar.insertImage, onClick: openImageDialog })), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: strings.toolbar.poetryBlocks, comingSoon: strings.toolbar.comingSoon })] }, "stubInsert")),
|
|
599
|
-
showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: strings.toolbar.autocorrect, comingSoon: strings.toolbar.comingSoon }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: strings.toolbar.textCleanup, comingSoon: strings.toolbar.comingSoon }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: strings.toolbar.spellChecker, comingSoon: strings.toolbar.comingSoon })] }, "language")),
|
|
600
|
-
];
|
|
601
|
-
return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": strings.toolbar.ariaLabel, ref: toolbarContainerRef, children: [fixedGroups, movableGroups
|
|
602
|
-
.filter((group) => !overflowGroupKeys.has(group.key))
|
|
603
|
-
.map((group) => (_jsx(MovableGroupButtons, { group: group }, group.key))), overflowGroupKeys.size > 0 && (_jsxs(Menu, { position: "bottom-end", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "aria-label": strings.toolbar.moreFormatting, title: strings.toolbar.moreFormatting, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconDots, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: movableGroups
|
|
604
|
-
.filter((group) => overflowGroupKeys.has(group.key))
|
|
605
|
-
.flatMap((group) => group.items.map((item) => (_jsx(OverflowItem, { icon: item.icon, label: item.label, active: item.active, onClick: runOverflowAction(item.onClick) }, item.key)))) })] })), config.links && (_jsxs(Menu, { opened: linkContext !== null, onChange: (opened) => {
|
|
396
|
+
return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": "Formatting", children: [showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: "Save", dirty: Boolean(isDirty), onClick: onSave }) })), config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: "Undo", disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: "Redo", disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] })), showFormattingGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: "Formatting", width: 148, value: formattingValue, data: formattingOptions, onChange: withRefocus((v) => applyFormatting(v)) }) })), showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: "Bold", active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: "Italic", active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: "Underline", active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] })), (config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: "Font family", width: 150, value: fontOptions.some((f) => f.family === shownFontFamily) ? shownFontFamily : null, placeholder: "Font", data: fontData, searchable: true, renderOption: ({ option }) => _jsx("span", { style: { fontFamily: option.value }, children: option.label }), onChange: withRefocus((v) => applyFont('font-family', v)) })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: "Font size", width: 84, value: FONT_SIZES_PX.some((px) => `${px}px` === shownFontSize) ? shownFontSize : null, placeholder: "Size", data: FONT_SIZES_PX.map((px) => ({ value: `${px}px`, label: String(px) })), onChange: withRefocus((v) => applyFont('font-size', v)) }))] })), showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: "Alignment", width: 138, value: state.elementFormat || 'start', data: alignOptions, onChange: withRefocus((v) => formatElement(v)) }) })), config.links && (_jsx("div", { className: "likhari-toolbar-group", children: state.isLink ? (
|
|
397
|
+
// Caret is in a link: the button opens a menu to see, edit or remove it.
|
|
398
|
+
_jsxs(Menu, { position: "bottom-start", withinPortal: true, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": "Link options", title: "Link options", onMouseDown: (e) => e.preventDefault(), children: _jsx(IconLink, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: state.linkUrl, onEdit: () => {
|
|
399
|
+
restoreSelection();
|
|
400
|
+
openLinkDialog();
|
|
401
|
+
}, onRemove: removeLink }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: "Insert link (Ctrl+K)", onClick: openLinkDialog })) })), config.links && (_jsxs(Menu, { opened: linkContext !== null, onChange: (opened) => {
|
|
606
402
|
if (!opened)
|
|
607
403
|
setLinkContext(null);
|
|
608
|
-
}, position: "bottom-start", withinPortal: true,
|
|
404
|
+
}, position: "bottom-start", withinPortal: true, shadow: "sm", width: 240, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: linkContext?.x ?? -9999, top: linkContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: linkContext?.url ?? '', onEdit: () => {
|
|
609
405
|
restoreSelection();
|
|
610
406
|
openLinkDialog();
|
|
611
|
-
}, onRemove: removeLink,
|
|
612
|
-
position: 'absolute',
|
|
613
|
-
visibility: 'hidden',
|
|
614
|
-
pointerEvents: 'none',
|
|
615
|
-
top: 0,
|
|
616
|
-
left: 0,
|
|
617
|
-
display: 'flex',
|
|
618
|
-
flexWrap: 'nowrap',
|
|
619
|
-
gap: 8,
|
|
620
|
-
}, children: [fixedGroups, movableGroups.map((group) => (_jsx(MovableGroupButtons, { group: group }, group.key)))] }), (config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog }))] }));
|
|
407
|
+
}, onRemove: removeLink }) })] })), showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && _jsx(StubButton, { icon: IconPhoto, title: "Insert image" }), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: "Poetry blocks" })] })), showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: "Auto-correct" }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: "Text cleanup" }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: "Spell-checker" })] })), showOverflowMenu && (_jsxs(Menu, { position: "bottom-end", withinPortal: true, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "aria-label": "More formatting", title: "More formatting", onMouseDown: (e) => e.preventDefault(), children: _jsx(IconDots, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsxs(Menu.Dropdown, { children: [fmt.strikethrough && (_jsx(OverflowItem, { icon: IconStrikethrough, label: "Strikethrough", active: state.activeFormats.has('strikethrough'), onClick: runOverflowAction(() => formatText('strikethrough')) })), fmt.superscript && (_jsx(OverflowItem, { icon: IconSuperscript, label: "Superscript", active: state.activeFormats.has('superscript'), onClick: runOverflowAction(() => formatText('superscript')) })), fmt.subscript && (_jsx(OverflowItem, { icon: IconSubscript, label: "Subscript", active: state.activeFormats.has('subscript'), onClick: runOverflowAction(() => formatText('subscript')) })), fmt.caseTransforms && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconLetterCaseUpper, label: "UPPERCASE", onClick: runOverflowAction(() => applyCaseTransform('upper')) }), _jsx(OverflowItem, { icon: IconLetterCaseLower, label: "lowercase", onClick: runOverflowAction(() => applyCaseTransform('lower')) }), _jsx(OverflowItem, { icon: IconLetterCase, label: "Capitalize", onClick: runOverflowAction(() => applyCaseTransform('capitalize')) })] })), fmt.clearFormatting && _jsx(OverflowItem, { icon: IconClearFormatting, label: "Clear formatting", onClick: runOverflowAction(clearFormatting) }), config.indent && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconIndentDecrease, label: "Outdent", onClick: runOverflowAction(() => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)) }), _jsx(OverflowItem, { icon: IconIndentIncrease, label: "Indent", onClick: runOverflowAction(() => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)) })] }))] })] })), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog }))] }));
|
|
621
408
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
export { EditorRoot } from './EditorRoot.js';
|
|
2
2
|
export type { EditorRootProps, EditorRef, EditorInitialContent } from './EditorRoot.js';
|
|
3
3
|
export { defaultMantineTheme } from './theme/mantineTheme.js';
|
|
4
|
-
export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
|
|
5
|
-
export type { ImagePayload, ImageLinkType, SerializedImageNode } from './image/ImageNode.js';
|
|
6
4
|
export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
|
7
5
|
export type { FontOption } from './fonts/index.js';
|
|
8
|
-
export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
|
|
9
|
-
export type { Locale, Strings } from './i18n/index.js';
|
|
10
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
export { EditorRoot } from './EditorRoot.js';
|
|
2
2
|
export { defaultMantineTheme } from './theme/mantineTheme.js';
|
|
3
|
-
export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
|
|
4
3
|
export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
|
5
|
-
export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
|