@inshapardaz/likhari-react 0.1.4-dev.27 → 0.1.4-dev.31
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 +7 -4
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +10 -3
- package/dist/components/Toolbar.d.ts +6 -2
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +58 -25
- package/dist/fonts/index.d.ts +22 -0
- package/dist/fonts/index.d.ts.map +1 -0
- package/dist/fonts/index.js +20 -0
- package/dist/fonts/urduWebFonts.d.ts +14 -0
- package/dist/fonts/urduWebFonts.d.ts.map +1 -0
- package/dist/fonts/urduWebFonts.js +66 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +0 -2
- package/dist/theme/editor.css +0 -44
- package/dist/theme/editorTheme.d.ts.map +1 -1
- package/dist/theme/editorTheme.js +0 -1
- package/dist/utils/style.d.ts +4 -0
- package/dist/utils/style.d.ts.map +1 -0
- package/dist/utils/style.js +15 -0
- package/package.json +1 -1
- package/dist/image/ImageDialog.d.ts +0 -25
- package/dist/image/ImageDialog.d.ts.map +0 -1
- package/dist/image/ImageDialog.js +0 -76
- package/dist/image/ImageNode.d.ts +0 -56
- package/dist/image/ImageNode.d.ts.map +0 -1
- package/dist/image/ImageNode.js +0 -151
- 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/EditorRoot.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { MantineThemeOverride } from '@mantine/core';
|
|
|
2
2
|
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
|
+
import { type FontOption } from './fonts/index.js';
|
|
5
6
|
export interface EditorInitialContent {
|
|
6
7
|
format: Extract<FormatId, 'lexical-json' | 'plain-text'>;
|
|
7
8
|
value: string;
|
|
@@ -38,11 +39,13 @@ export interface EditorRootProps {
|
|
|
38
39
|
*/
|
|
39
40
|
showSave?: boolean;
|
|
40
41
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* Entries for the toolbar's font-family dropdown. Defaults to
|
|
43
|
+
* `DEFAULT_FONT_OPTIONS` (generic Latin faces plus the Urdu/Arabic-script
|
|
44
|
+
* collection from inshapardaz/urdu-web-fonts, the same source qari uses).
|
|
45
|
+
* Spread the defaults to add your own, e.g.
|
|
46
|
+
* `[...DEFAULT_FONT_OPTIONS, { name: 'Mine', family: '"Mine", serif' }]`.
|
|
44
47
|
*/
|
|
45
|
-
|
|
48
|
+
fontOptions?: FontOption[];
|
|
46
49
|
}
|
|
47
50
|
export interface EditorRef {
|
|
48
51
|
getContent(format: FormatId): string;
|
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;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;
|
|
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;AAEjE,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,uGA4HrB,CAAC"}
|
package/dist/EditorRoot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
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';
|
|
@@ -14,14 +14,21 @@ import { EDITOR_NODES } from './nodes.js';
|
|
|
14
14
|
import { editorTheme } from './theme/editorTheme.js';
|
|
15
15
|
import { EditorThemeProvider } from './theme/EditorThemeProvider.js';
|
|
16
16
|
import { Toolbar } from './components/Toolbar.js';
|
|
17
|
+
import { injectUrduWebFontsCss } from './fonts/index.js';
|
|
17
18
|
function initialEditorStateJson(initialContent) {
|
|
18
19
|
if (!initialContent)
|
|
19
20
|
return undefined;
|
|
20
21
|
const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
|
|
21
22
|
return JSON.stringify(state);
|
|
22
23
|
}
|
|
23
|
-
export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder = 'Start writing…', height = '480px', onChange, onSave, showSave = Boolean(onSave),
|
|
24
|
+
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) {
|
|
24
25
|
const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
|
|
26
|
+
// The urdu-web-fonts stylesheets are needed for the font dropdown and for
|
|
27
|
+
// RTL content, whose canvas font (editor.css) is one of those families.
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (config.font.family || locale !== 'en')
|
|
30
|
+
injectUrduWebFontsCss();
|
|
31
|
+
}, [config.font.family, locale]);
|
|
25
32
|
const editorStateRef = useRef(null);
|
|
26
33
|
const lastSavedJsonRef = useRef(null);
|
|
27
34
|
const [isDirty, setIsDirty] = useState(false);
|
|
@@ -84,5 +91,5 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
84
91
|
onSave?.(content, format);
|
|
85
92
|
};
|
|
86
93
|
const dir = locale === 'en' ? 'ltr' : 'rtl';
|
|
87
|
-
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,
|
|
94
|
+
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, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }));
|
|
88
95
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { ResolvedEditorFeatureConfig } from '@inshapardaz/likhari-core';
|
|
2
|
+
import { type FontOption } from '../fonts/index.js';
|
|
2
3
|
export interface ToolbarProps {
|
|
3
4
|
config: ResolvedEditorFeatureConfig;
|
|
4
5
|
onSave?: () => void;
|
|
5
6
|
isDirty?: boolean;
|
|
6
7
|
showSave?: boolean;
|
|
7
|
-
|
|
8
|
+
/** Font-family dropdown entries; defaults to DEFAULT_FONT_OPTIONS. */
|
|
9
|
+
fontOptions?: FontOption[];
|
|
10
|
+
/** Text direction of the canvas; picks which default font and size are pre-selected. */
|
|
11
|
+
direction?: 'ltr' | 'rtl';
|
|
8
12
|
}
|
|
9
|
-
export declare function Toolbar({ config, onSave, isDirty, showSave,
|
|
13
|
+
export declare function Toolbar({ config, onSave, isDirty, showSave, fontOptions, direction }: ToolbarProps): import("react").JSX.Element;
|
|
10
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":"AAmCA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AA8LtG,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,+BA6ajI"}
|
|
@@ -2,20 +2,26 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
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
|
-
import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, 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';
|
|
6
|
-
import { $setBlocksType } from '@lexical/selection';
|
|
5
|
+
import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, $getRoot, 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';
|
|
6
|
+
import { $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType } from '@lexical/selection';
|
|
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 {
|
|
12
|
-
import {
|
|
10
|
+
import { $findMatchingParent } from '@lexical/utils';
|
|
11
|
+
import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fonts/index.js';
|
|
12
|
+
import { setStyleProperty } from '../utils/style.js';
|
|
13
13
|
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconFeather, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconPhoto, IconPilcrow, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
|
|
14
|
+
/** Sentinel for "no inline style": Lexical returns the default it is given when
|
|
15
|
+
* the selection has none, so this tells "unstyled" (show the canvas default)
|
|
16
|
+
* apart from "styled with a value the dropdown doesn't list" (show nothing). */
|
|
17
|
+
const UNSET_STYLE = '__unset__';
|
|
14
18
|
const INITIAL_STATE = {
|
|
15
19
|
blockType: 'paragraph',
|
|
16
20
|
activeFormats: new Set(),
|
|
17
21
|
elementFormat: 'start',
|
|
18
22
|
listType: null,
|
|
23
|
+
fontFamily: UNSET_STYLE,
|
|
24
|
+
fontSize: UNSET_STYLE,
|
|
19
25
|
canUndo: false,
|
|
20
26
|
canRedo: false,
|
|
21
27
|
};
|
|
@@ -43,15 +49,15 @@ function StubButton({ icon, title }) {
|
|
|
43
49
|
* labels the control itself, since a dropdown can't show one per option in
|
|
44
50
|
* its closed state. The dropdown portals to <body>, so it is not clipped by
|
|
45
51
|
* the editor's `overflow: hidden` frame. */
|
|
46
|
-
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, onChange }) {
|
|
47
|
-
return (_jsx(Select, { size: "xs", w: width, "aria-label": label, title: disabled ? `${label} (coming soon)` : label, data: data, value: value, placeholder: placeholder, disabled: disabled, 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) }));
|
|
52
|
+
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, searchable, renderOption, onChange }) {
|
|
53
|
+
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) }));
|
|
48
54
|
}
|
|
49
55
|
function OverflowItem({ icon: Icon, label, active, onClick, }) {
|
|
50
56
|
return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', className: "likhari-overflow-item",
|
|
51
57
|
// Keep the editor's selection: a mousedown here would blur the canvas.
|
|
52
58
|
onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
|
|
53
59
|
}
|
|
54
|
-
export function Toolbar({ config, onSave, isDirty, showSave,
|
|
60
|
+
export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr' }) {
|
|
55
61
|
const [editor] = useLexicalComposerContext();
|
|
56
62
|
const [state, setState] = useState(INITIAL_STATE);
|
|
57
63
|
const updateToolbar = useCallback(() => {
|
|
@@ -70,6 +76,8 @@ export function Toolbar({ config, onSave, isDirty, showSave, onImageUpload }) {
|
|
|
70
76
|
else if ($isQuoteNode(element)) {
|
|
71
77
|
blockType = 'quote';
|
|
72
78
|
}
|
|
79
|
+
const fontFamily = $getSelectionStyleValueForProperty(selection, 'font-family', UNSET_STYLE);
|
|
80
|
+
const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', UNSET_STYLE);
|
|
73
81
|
const activeFormats = new Set();
|
|
74
82
|
['bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript'].forEach((format) => {
|
|
75
83
|
if (selection.hasFormat(format))
|
|
@@ -86,6 +94,8 @@ export function Toolbar({ config, onSave, isDirty, showSave, onImageUpload }) {
|
|
|
86
94
|
activeFormats,
|
|
87
95
|
elementFormat,
|
|
88
96
|
listType,
|
|
97
|
+
fontFamily,
|
|
98
|
+
fontSize,
|
|
89
99
|
}));
|
|
90
100
|
});
|
|
91
101
|
}, [editor]);
|
|
@@ -159,24 +169,23 @@ export function Toolbar({ config, onSave, isDirty, showSave, onImageUpload }) {
|
|
|
159
169
|
action();
|
|
160
170
|
editor.focus();
|
|
161
171
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
setImageDialogOpen(true);
|
|
167
|
-
};
|
|
168
|
-
const closeImageDialog = () => {
|
|
169
|
-
setImageDialogOpen(false);
|
|
170
|
-
editor.focus();
|
|
171
|
-
};
|
|
172
|
-
const insertImage = (image) => {
|
|
173
|
-
const saved = menuSelectionRef.current;
|
|
174
|
-
if (saved)
|
|
175
|
-
editor.update(() => $setSelection(saved.clone()), { discrete: true });
|
|
172
|
+
/** Applies a font property per `config.font.scope`: to the selection, to
|
|
173
|
+
* every text node in the document, or (for 'both') to the selection when
|
|
174
|
+
* there is a range and to the whole document when there isn't. */
|
|
175
|
+
const applyFont = (property, value) => {
|
|
176
176
|
editor.update(() => {
|
|
177
|
-
$
|
|
177
|
+
const selection = $getSelection();
|
|
178
|
+
const scope = config.font.scope;
|
|
179
|
+
const wholeDocument = scope === 'document' || (scope === 'both' && (!$isRangeSelection(selection) || selection.isCollapsed()));
|
|
180
|
+
if (wholeDocument) {
|
|
181
|
+
for (const node of $getRoot().getAllTextNodes()) {
|
|
182
|
+
node.setStyle(setStyleProperty(node.getStyle(), property, value));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if ($isRangeSelection(selection)) {
|
|
186
|
+
$patchStyleText(selection, { [property]: value });
|
|
187
|
+
}
|
|
178
188
|
});
|
|
179
|
-
closeImageDialog();
|
|
180
189
|
};
|
|
181
190
|
const withRefocus = (fn) => (value) => {
|
|
182
191
|
fn(value);
|
|
@@ -229,6 +238,30 @@ export function Toolbar({ config, onSave, isDirty, showSave, onImageUpload }) {
|
|
|
229
238
|
const showInsertPoetryGroup = config.links || config.images.linked || config.images.embedded || config.poetry.enabled;
|
|
230
239
|
const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
|
|
231
240
|
const showOverflowMenu = fmt.strikethrough || fmt.superscript || fmt.subscript || fmt.caseTransforms || fmt.clearFormatting || config.indent;
|
|
241
|
+
// Grouped for the dropdown when options declare groups (Latin / Urdu).
|
|
242
|
+
const fontData = (() => {
|
|
243
|
+
const data = [];
|
|
244
|
+
const groups = new Map();
|
|
245
|
+
for (const f of fontOptions) {
|
|
246
|
+
const item = { value: f.family, label: f.name };
|
|
247
|
+
if (!f.group) {
|
|
248
|
+
data.push(item);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
let group = groups.get(f.group);
|
|
252
|
+
if (!group) {
|
|
253
|
+
group = { group: f.group, items: [] };
|
|
254
|
+
groups.set(f.group, group);
|
|
255
|
+
data.push(group);
|
|
256
|
+
}
|
|
257
|
+
group.items.push(item);
|
|
258
|
+
}
|
|
259
|
+
return data;
|
|
260
|
+
})();
|
|
261
|
+
// Unstyled text shows the canvas default (pre-selected).
|
|
262
|
+
const canvasDefaults = CANVAS_FONT_DEFAULTS[direction];
|
|
263
|
+
const shownFontFamily = state.fontFamily === UNSET_STYLE ? canvasDefaults.family : state.fontFamily;
|
|
264
|
+
const shownFontSize = state.fontSize === UNSET_STYLE ? canvasDefaults.size : state.fontSize;
|
|
232
265
|
const formattingOptions = [
|
|
233
266
|
{ value: 'paragraph', label: 'Paragraph' },
|
|
234
267
|
...headingLevels.map((level) => ({ value: `h${level}`, label: `Heading ${level}` })),
|
|
@@ -247,5 +280,5 @@ export function Toolbar({ config, onSave, isDirty, showSave, onImageUpload }) {
|
|
|
247
280
|
];
|
|
248
281
|
const formattingValue = state.listType ?? state.blockType;
|
|
249
282
|
const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
|
|
250
|
-
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
|
|
283
|
+
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)) }) })), showInsertPoetryGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.links && _jsx(StubButton, { icon: IconLink, title: "Insert link" }), (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)) })] }))] })] }))] }));
|
|
251
284
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FontOption } from './urduWebFonts.js';
|
|
2
|
+
export { URDU_WEB_FONT_OPTIONS, injectUrduWebFontsCss, type FontOption } from './urduWebFonts.js';
|
|
3
|
+
/** Three generic Latin faces plus the full urdu-web-fonts collection. */
|
|
4
|
+
export declare const DEFAULT_FONT_OPTIONS: FontOption[];
|
|
5
|
+
/** Sizes offered by the toolbar's font-size dropdown, in px. */
|
|
6
|
+
export declare const FONT_SIZES_PX: number[];
|
|
7
|
+
/**
|
|
8
|
+
* What the canvas renders when text has no explicit font style — kept in sync
|
|
9
|
+
* with `.likhari-content-editable` in theme/editor.css. The toolbar's font
|
|
10
|
+
* dropdowns show these as the pre-selected values.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CANVAS_FONT_DEFAULTS: {
|
|
13
|
+
readonly ltr: {
|
|
14
|
+
readonly family: string;
|
|
15
|
+
readonly size: "15px";
|
|
16
|
+
};
|
|
17
|
+
readonly rtl: {
|
|
18
|
+
readonly family: "\"Noto Nastaliq Urdu\", serif";
|
|
19
|
+
readonly size: "20px";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fonts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE/F,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,EAAE,UAAU,EAK5C,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,aAAa,UAAmD,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;CAGvB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { URDU_WEB_FONT_OPTIONS } from './urduWebFonts.js';
|
|
2
|
+
export { URDU_WEB_FONT_OPTIONS, injectUrduWebFontsCss } from './urduWebFonts.js';
|
|
3
|
+
/** Three generic Latin faces plus the full urdu-web-fonts collection. */
|
|
4
|
+
export const DEFAULT_FONT_OPTIONS = [
|
|
5
|
+
{ name: 'Serif', family: `'IBM Plex Serif', Georgia, serif`, group: 'Latin' },
|
|
6
|
+
{ name: 'Sans', family: `'IBM Plex Sans', system-ui, sans-serif`, group: 'Latin' },
|
|
7
|
+
{ name: 'Mono', family: `ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace`, group: 'Latin' },
|
|
8
|
+
...URDU_WEB_FONT_OPTIONS,
|
|
9
|
+
];
|
|
10
|
+
/** Sizes offered by the toolbar's font-size dropdown, in px. */
|
|
11
|
+
export const FONT_SIZES_PX = [12, 13, 14, 15, 16, 18, 20, 24, 28, 32, 36, 48];
|
|
12
|
+
/**
|
|
13
|
+
* What the canvas renders when text has no explicit font style — kept in sync
|
|
14
|
+
* with `.likhari-content-editable` in theme/editor.css. The toolbar's font
|
|
15
|
+
* dropdowns show these as the pre-selected values.
|
|
16
|
+
*/
|
|
17
|
+
export const CANVAS_FONT_DEFAULTS = {
|
|
18
|
+
ltr: { family: DEFAULT_FONT_OPTIONS[0].family, size: '15px' },
|
|
19
|
+
rtl: { family: `"Noto Nastaliq Urdu", serif`, size: '20px' },
|
|
20
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** One entry in the toolbar's font-family dropdown. */
|
|
2
|
+
export interface FontOption {
|
|
3
|
+
/** Label shown in the dropdown */
|
|
4
|
+
name: string;
|
|
5
|
+
/** CSS font-family value stored on the selected text, e.g. `"Amiri", serif` */
|
|
6
|
+
family: string;
|
|
7
|
+
/** Optional dropdown group heading */
|
|
8
|
+
group?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const URDU_WEB_FONT_OPTIONS: FontOption[];
|
|
11
|
+
/** Registers every urdu-web-fonts @font-face via a <link> per family. Safe to
|
|
12
|
+
* call repeatedly and from several editors: it injects once per page. */
|
|
13
|
+
export declare function injectUrduWebFontsCss(): void;
|
|
14
|
+
//# sourceMappingURL=urduWebFonts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"urduWebFonts.d.ts","sourceRoot":"","sources":["../../src/fonts/urduWebFonts.ts"],"names":[],"mappings":"AAqDA,uDAAuD;AACvD,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,qBAAqB,EAAE,UAAU,EAI3C,CAAC;AAIJ;yEACyE;AACzE,wBAAgB,qBAAqB,IAAI,IAAI,CAc5C"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the Urdu/Arabic-script font collection from the
|
|
3
|
+
* inshapardaz/urdu-web-fonts GitHub repository through jsDelivr's GitHub
|
|
4
|
+
* CDN — the same source, and the same pinned commit, that the qari EPUB
|
|
5
|
+
* reader uses, so both apps offer an identical font set. Nothing is bundled
|
|
6
|
+
* or added as an npm dependency.
|
|
7
|
+
*
|
|
8
|
+
* Pinned to a commit (not a branch) so the fonts served don't change under
|
|
9
|
+
* consumers unexpectedly; bump URDU_WEB_FONTS_COMMIT to pick up upstream
|
|
10
|
+
* additions. Each family's stylesheet only registers its @font-face rules —
|
|
11
|
+
* the browser downloads a font file when that font is actually used.
|
|
12
|
+
*/
|
|
13
|
+
const URDU_WEB_FONTS_COMMIT = '571e360d5c272252efb0b0b21af54d1f63457521';
|
|
14
|
+
const URDU_WEB_FONTS_BASE_URL = `https://cdn.jsdelivr.net/gh/inshapardaz/urdu-web-fonts@${URDU_WEB_FONTS_COMMIT}/src/fonts`;
|
|
15
|
+
const URDU_WEB_FONTS = [
|
|
16
|
+
{ dir: 'adobe-arabic', name: 'Adobe Arabic', family: 'AdobeArabic' },
|
|
17
|
+
{ dir: 'alvi-lahori-nastalique', name: 'Alvi Lahori Nastaleeq', family: 'AlviLahoriNastaleeq' },
|
|
18
|
+
{ dir: 'amiri', name: 'Amiri', family: 'Amiri' },
|
|
19
|
+
{ dir: 'aref-ruqqa', name: 'Aref Ruqaa', family: 'Aref Ruqaa' },
|
|
20
|
+
{ dir: 'dehalvi-khushkhat', name: 'Dehalvi Khush Khat', family: 'DehalviKhushKhat' },
|
|
21
|
+
{ dir: 'dubai', name: 'Dubai', family: 'Dubai' },
|
|
22
|
+
{ dir: 'emad-nastaleeq', name: 'Emad Nastaleeq', family: 'EmadNastaleeq' },
|
|
23
|
+
{ dir: 'fajer-noori-nastalique', name: 'Fajer Noori Nastalique', family: 'FajerNooriNastalique' },
|
|
24
|
+
{ dir: 'gulzar-nastalique', name: 'Gulzar Nastalique', family: 'gulzar-nastalique' },
|
|
25
|
+
{ dir: 'jameel-khushkhati', name: 'Jameel Khushkhati', family: 'jameel-khushkhati' },
|
|
26
|
+
{ dir: 'jameel-noori-kasheeda', name: 'Jameel Noori Nastaleeq Kasheeda', family: 'JameelNooriNastaleeqKasheeda' },
|
|
27
|
+
{ dir: 'jameel-noori-nastalique', name: 'Jameel Noori Nastaleeq', family: 'JameelNooriNastaleeq' },
|
|
28
|
+
{ dir: 'lalezar', name: 'Lalezar', family: 'Lalezar' },
|
|
29
|
+
{ dir: 'lateef', name: 'Lateef', family: 'Lateef' },
|
|
30
|
+
{ dir: 'mada', name: 'Mada', family: 'Mada' },
|
|
31
|
+
{ dir: 'mehr-nastalique', name: 'Mehr Nastaleeq', family: 'MehrNastaleeq' },
|
|
32
|
+
{ dir: 'mehr-nastalique-2', name: 'Mehr Nastaliq Web', family: 'Mehr Nastaliq Web' },
|
|
33
|
+
{ dir: 'nafees-nastaleeq', name: 'Nafees Nastaleeq', family: 'NafeesNastaleeq' },
|
|
34
|
+
{ dir: 'Nafees-web-naskh', name: 'Nafees Web Naskh', family: 'NafeesWebNaskh' },
|
|
35
|
+
{ dir: 'noto-naskh', name: 'Noto Naskh Arabic', family: 'Noto Naskh Arabic' },
|
|
36
|
+
{ dir: 'noto-nastalique', name: 'Noto Nastaliq Urdu', family: 'Noto Nastaliq Urdu' },
|
|
37
|
+
{ dir: 'pak-nastaleeq', name: 'Pak Nastaleeq', family: 'PakNastaleeq' },
|
|
38
|
+
{ dir: 'qahiri', name: 'Qahiri', family: 'Qahiri' },
|
|
39
|
+
{ dir: 'reem-kufi', name: 'Reem Kufi', family: 'Reem Kufi' },
|
|
40
|
+
{ dir: 'sameer-khashab-bold', name: 'Sameer Khashab Bold', family: 'sameer-khashab' },
|
|
41
|
+
{ dir: 'scheherazade', name: 'Scheherazade New', family: 'Scheherazade New' },
|
|
42
|
+
];
|
|
43
|
+
export const URDU_WEB_FONT_OPTIONS = URDU_WEB_FONTS.map((font) => ({
|
|
44
|
+
name: font.name,
|
|
45
|
+
family: `"${font.family}", serif`,
|
|
46
|
+
group: 'Urdu / Arabic script',
|
|
47
|
+
}));
|
|
48
|
+
let injected = false;
|
|
49
|
+
/** Registers every urdu-web-fonts @font-face via a <link> per family. Safe to
|
|
50
|
+
* call repeatedly and from several editors: it injects once per page. */
|
|
51
|
+
export function injectUrduWebFontsCss() {
|
|
52
|
+
if (injected || typeof document === 'undefined')
|
|
53
|
+
return;
|
|
54
|
+
if (document.querySelector('link[data-likhari-urdu-fonts]')) {
|
|
55
|
+
injected = true;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const font of URDU_WEB_FONTS) {
|
|
59
|
+
const link = document.createElement('link');
|
|
60
|
+
link.rel = 'stylesheet';
|
|
61
|
+
link.href = `${URDU_WEB_FONTS_BASE_URL}/${font.dir}/stylesheet.css`;
|
|
62
|
+
link.setAttribute('data-likhari-urdu-fonts', font.dir);
|
|
63
|
+
document.head.appendChild(link);
|
|
64
|
+
}
|
|
65
|
+
injected = true;
|
|
66
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +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 {
|
|
5
|
-
export type {
|
|
4
|
+
export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
|
5
|
+
export type { FontOption } from './fonts/index.js';
|
|
6
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,3 +1,3 @@
|
|
|
1
1
|
export { EditorRoot } from './EditorRoot.js';
|
|
2
2
|
export { defaultMantineTheme } from './theme/mantineTheme.js';
|
|
3
|
-
export {
|
|
3
|
+
export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
package/dist/nodes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAMlD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAO5C,CAAC"}
|
package/dist/nodes.js
CHANGED
|
@@ -2,7 +2,6 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
|
2
2
|
import { ListItemNode, ListNode } from '@lexical/list';
|
|
3
3
|
import { LinkNode } from '@lexical/link';
|
|
4
4
|
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
5
|
-
import { ImageNode } from './image/ImageNode.js';
|
|
6
5
|
/**
|
|
7
6
|
* Node types registered on the editor at construction time. Per
|
|
8
7
|
* editor-architecture-design.md §2, node registration cannot be conditional
|
|
@@ -19,5 +18,4 @@ export const EDITOR_NODES = [
|
|
|
19
18
|
ListItemNode,
|
|
20
19
|
LinkNode,
|
|
21
20
|
HorizontalRuleNode,
|
|
22
|
-
ImageNode,
|
|
23
21
|
];
|
package/dist/theme/editor.css
CHANGED
|
@@ -234,47 +234,3 @@
|
|
|
234
234
|
border-top: 1px solid var(--editor-line);
|
|
235
235
|
margin: 1.5em 0;
|
|
236
236
|
}
|
|
237
|
-
|
|
238
|
-
/* Images — block-level figure with an optional caption input. */
|
|
239
|
-
.likhari-image-block {
|
|
240
|
-
margin: 0 0 1em 0;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
.likhari-image-figure {
|
|
244
|
-
display: block;
|
|
245
|
-
margin: 0;
|
|
246
|
-
text-align: center;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.likhari-image {
|
|
250
|
-
display: inline-block;
|
|
251
|
-
max-width: 100%;
|
|
252
|
-
height: auto;
|
|
253
|
-
border-radius: 4px;
|
|
254
|
-
cursor: default;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
.likhari-image-figure[data-selected='true'] .likhari-image {
|
|
258
|
-
outline: 2px solid var(--editor-accent);
|
|
259
|
-
outline-offset: 2px;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
.likhari-image-caption {
|
|
263
|
-
display: block;
|
|
264
|
-
width: 100%;
|
|
265
|
-
margin-top: 6px;
|
|
266
|
-
padding: 2px 4px;
|
|
267
|
-
border: none;
|
|
268
|
-
border-bottom: 1px dashed transparent;
|
|
269
|
-
background: transparent;
|
|
270
|
-
color: var(--editor-ink-soft);
|
|
271
|
-
font: inherit;
|
|
272
|
-
font-size: 0.85em;
|
|
273
|
-
text-align: center;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.likhari-image-caption:hover,
|
|
277
|
-
.likhari-image-caption:focus {
|
|
278
|
-
border-bottom-color: var(--editor-line-strong);
|
|
279
|
-
outline: none;
|
|
280
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,kBA6BzB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/utils/style.ts"],"names":[],"mappings":"AAAA;yDACyD;AACzD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYrF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Sets (or replaces) one property in an inline CSS string like Lexical's
|
|
2
|
+
* TextNode style, keeping every other property as-is. */
|
|
3
|
+
export function setStyleProperty(css, property, value) {
|
|
4
|
+
const entries = css
|
|
5
|
+
.split(';')
|
|
6
|
+
.map((part) => part.trim())
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((part) => {
|
|
9
|
+
const i = part.indexOf(':');
|
|
10
|
+
return [part.slice(0, i).trim(), part.slice(i + 1).trim()];
|
|
11
|
+
})
|
|
12
|
+
.filter(([name]) => name !== property);
|
|
13
|
+
entries.push([property, value]);
|
|
14
|
+
return entries.map(([name, val]) => `${name}: ${val};`).join(' ');
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { ImageLinkType } from './ImageNode.js';
|
|
2
|
-
export interface ImageDialogResult {
|
|
3
|
-
src: string;
|
|
4
|
-
altText: string;
|
|
5
|
-
caption: string | null;
|
|
6
|
-
linkType: ImageLinkType;
|
|
7
|
-
}
|
|
8
|
-
export interface ImageDialogProps {
|
|
9
|
-
opened: boolean;
|
|
10
|
-
/** `images.linked` — allow an external URL. */
|
|
11
|
-
allowLinked: boolean;
|
|
12
|
-
/** `images.embedded` — allow uploading a file. */
|
|
13
|
-
allowEmbedded: boolean;
|
|
14
|
-
/** `images.caption` — image gets an (initially empty) caption. */
|
|
15
|
-
allowCaption: boolean;
|
|
16
|
-
/** `images.maxSizeMB` */
|
|
17
|
-
maxSizeMB: number;
|
|
18
|
-
/** Host upload handler; when absent, uploads are embedded as base64 data URIs. */
|
|
19
|
-
onImageUpload?: (file: File) => Promise<string>;
|
|
20
|
-
onSubmit: (image: ImageDialogResult) => void;
|
|
21
|
-
onClose: () => void;
|
|
22
|
-
}
|
|
23
|
-
/** Insert-image dialog (UI spec §6): an external URL and/or an uploaded file. */
|
|
24
|
-
export declare function ImageDialog({ opened, allowLinked, allowEmbedded, allowCaption, maxSizeMB, onImageUpload, onSubmit, onClose, }: ImageDialogProps): import("react").JSX.Element;
|
|
25
|
-
//# sourceMappingURL=ImageDialog.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ImageDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageDialog.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,WAAW,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,aAAa,EAAE,OAAO,CAAC;IACvB,kEAAkE;IAClE,YAAY,EAAE,OAAO,CAAC;IACtB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAWD,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,WAAW,EACX,aAAa,EACb,YAAY,EACZ,SAAS,EACT,aAAa,EACb,QAAQ,EACR,OAAO,GACR,EAAE,gBAAgB,+BAmHlB"}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Alert, Button, FileInput, Group, Modal, SegmentedControl, Stack, TextInput } from '@mantine/core';
|
|
4
|
-
import { ACCEPTED_IMAGE_TYPES, normalizeImageUrl, validateImageFile } from './imageUrl.js';
|
|
5
|
-
function readAsDataUrl(file) {
|
|
6
|
-
return new Promise((resolve, reject) => {
|
|
7
|
-
const reader = new FileReader();
|
|
8
|
-
reader.onload = () => resolve(String(reader.result));
|
|
9
|
-
reader.onerror = () => reject(reader.error ?? new Error('Could not read the file'));
|
|
10
|
-
reader.readAsDataURL(file);
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
/** Insert-image dialog (UI spec §6): an external URL and/or an uploaded file. */
|
|
14
|
-
export function ImageDialog({ opened, allowLinked, allowEmbedded, allowCaption, maxSizeMB, onImageUpload, onSubmit, onClose, }) {
|
|
15
|
-
const defaultSource = allowLinked ? 'linked' : 'embedded';
|
|
16
|
-
const [source, setSource] = useState(defaultSource);
|
|
17
|
-
const [url, setUrl] = useState('');
|
|
18
|
-
const [file, setFile] = useState(null);
|
|
19
|
-
const [altText, setAltText] = useState('');
|
|
20
|
-
const [error, setError] = useState(null);
|
|
21
|
-
const [busy, setBusy] = useState(false);
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
if (opened) {
|
|
24
|
-
setSource(defaultSource);
|
|
25
|
-
setUrl('');
|
|
26
|
-
setFile(null);
|
|
27
|
-
setAltText('');
|
|
28
|
-
setError(null);
|
|
29
|
-
setBusy(false);
|
|
30
|
-
}
|
|
31
|
-
}, [opened, defaultSource]);
|
|
32
|
-
const handleSubmit = async (e) => {
|
|
33
|
-
e.preventDefault();
|
|
34
|
-
setError(null);
|
|
35
|
-
try {
|
|
36
|
-
let src;
|
|
37
|
-
if (source === 'linked') {
|
|
38
|
-
src = normalizeImageUrl(url);
|
|
39
|
-
if (!src) {
|
|
40
|
-
setError('Enter an http(s) or relative image URL');
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
if (!file) {
|
|
46
|
-
setError('Choose an image file');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const problem = validateImageFile(file, maxSizeMB);
|
|
50
|
-
if (problem) {
|
|
51
|
-
setError(problem);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
setBusy(true);
|
|
55
|
-
src = onImageUpload ? await onImageUpload(file) : await readAsDataUrl(file);
|
|
56
|
-
if (!normalizeImageUrl(src)) {
|
|
57
|
-
setError('The upload handler returned an unusable image URL');
|
|
58
|
-
setBusy(false);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
onSubmit({ src, altText: altText.trim(), caption: allowCaption ? '' : null, linkType: source });
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
setError(err instanceof Error ? err.message : 'Could not add the image');
|
|
66
|
-
setBusy(false);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
return (_jsx(Modal, { opened: opened, onClose: onClose, title: "Insert image", centered: true, size: "sm", children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [allowLinked && allowEmbedded && (_jsx(SegmentedControl, { fullWidth: true, value: source, onChange: (v) => {
|
|
70
|
-
setSource(v);
|
|
71
|
-
setError(null);
|
|
72
|
-
}, data: [
|
|
73
|
-
{ value: 'linked', label: 'From URL' },
|
|
74
|
-
{ value: 'embedded', label: 'Upload' },
|
|
75
|
-
] })), source === 'linked' ? (_jsx(TextInput, { label: "Image URL", placeholder: "https://example.com/photo.jpg", value: url, onChange: (e) => setUrl(e.currentTarget.value), "data-autofocus": true })) : (_jsx(FileInput, { label: "Image file", placeholder: "Choose an image", accept: ACCEPTED_IMAGE_TYPES.join(','), value: file, onChange: setFile, description: `Up to ${maxSizeMB} MB${onImageUpload ? '' : '; stored inside the document'}` })), _jsx(TextInput, { label: "Alt text", description: "Describes the image for screen readers", value: altText, onChange: (e) => setAltText(e.currentTarget.value) }), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", loading: busy, children: "Insert" })] })] }) }) }));
|
|
76
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { type ReactElement } from 'react';
|
|
2
|
-
import { DecoratorNode, type DOMExportOutput, type EditorConfig, type LexicalNode, type NodeKey, type SerializedLexicalNode, type Spread } from 'lexical';
|
|
3
|
-
/** How the image's bytes are held: an external URL, or embedded in the
|
|
4
|
-
* document (a base64 data URI, or a URL returned by the host's upload handler). */
|
|
5
|
-
export type ImageLinkType = 'linked' | 'embedded';
|
|
6
|
-
export interface ImagePayload {
|
|
7
|
-
src: string;
|
|
8
|
-
altText?: string;
|
|
9
|
-
/** null = no caption; '' = caption enabled but empty */
|
|
10
|
-
caption?: string | null;
|
|
11
|
-
linkType: ImageLinkType;
|
|
12
|
-
width?: number | null;
|
|
13
|
-
height?: number | null;
|
|
14
|
-
key?: NodeKey;
|
|
15
|
-
}
|
|
16
|
-
export type SerializedImageNode = Spread<{
|
|
17
|
-
src: string;
|
|
18
|
-
altText: string;
|
|
19
|
-
caption: string | null;
|
|
20
|
-
linkType: ImageLinkType;
|
|
21
|
-
width: number | null;
|
|
22
|
-
height: number | null;
|
|
23
|
-
}, SerializedLexicalNode>;
|
|
24
|
-
/**
|
|
25
|
-
* Block-level image (editor-architecture-design.md §3.5). The caption is a
|
|
26
|
-
* plain string edited through an input under the image — not the nested
|
|
27
|
-
* sub-editor the architecture doc leaves open — which keeps caption edits in
|
|
28
|
-
* the parent editor's undo history.
|
|
29
|
-
*/
|
|
30
|
-
export declare class ImageNode extends DecoratorNode<ReactElement> {
|
|
31
|
-
__src: string;
|
|
32
|
-
__altText: string;
|
|
33
|
-
__caption: string | null;
|
|
34
|
-
__linkType: ImageLinkType;
|
|
35
|
-
__width: number | null;
|
|
36
|
-
__height: number | null;
|
|
37
|
-
static getType(): string;
|
|
38
|
-
static clone(node: ImageNode): ImageNode;
|
|
39
|
-
static importJSON(json: SerializedImageNode): ImageNode;
|
|
40
|
-
constructor(src: string, altText?: string, caption?: string | null, linkType?: ImageLinkType, width?: number | null, height?: number | null, key?: NodeKey);
|
|
41
|
-
exportJSON(): SerializedImageNode;
|
|
42
|
-
/** `<figure><img><figcaption>` — the figure/figcaption semantics from spec §4.7. */
|
|
43
|
-
exportDOM(): DOMExportOutput;
|
|
44
|
-
createDOM(config: EditorConfig): HTMLElement;
|
|
45
|
-
updateDOM(): false;
|
|
46
|
-
isInline(): false;
|
|
47
|
-
getSrc(): string;
|
|
48
|
-
getAltText(): string;
|
|
49
|
-
getCaption(): string | null;
|
|
50
|
-
setCaption(caption: string | null): this;
|
|
51
|
-
getTextContent(): string;
|
|
52
|
-
decorate(): ReactElement;
|
|
53
|
-
}
|
|
54
|
-
export declare function $createImageNode({ src, altText, caption, linkType, width, height, key }: ImagePayload): ImageNode;
|
|
55
|
-
export declare function $isImageNode(node: LexicalNode | null | undefined): node is ImageNode;
|
|
56
|
-
//# sourceMappingURL=ImageNode.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ImageNode.d.ts","sourceRoot":"","sources":["../../src/image/ImageNode.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAG1E,OAAO,EAML,aAAa,EAGb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EACZ,MAAM,SAAS,CAAC;AAEjB;mFACmF;AACnF,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,CAAC;AAElD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,mBAAmB,GAAG,MAAM,CACtC;IACE,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,EACD,qBAAqB,CACtB,CAAC;AAEF;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,aAAa,CAAC,YAAY,CAAC;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,aAAa,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS;IAIxC,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,mBAAmB,GAAG,SAAS;gBAYrD,GAAG,EAAE,MAAM,EACX,OAAO,SAAK,EACZ,OAAO,GAAE,MAAM,GAAG,IAAW,EAC7B,QAAQ,GAAE,aAAwB,EAClC,KAAK,GAAE,MAAM,GAAG,IAAW,EAC3B,MAAM,GAAE,MAAM,GAAG,IAAW,EAC5B,GAAG,CAAC,EAAE,OAAO;IAWf,UAAU,IAAI,mBAAmB;IAajC,oFAAoF;IACpF,SAAS,IAAI,eAAe;IAgB5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAO5C,SAAS,IAAI,KAAK;IAIlB,QAAQ,IAAI,KAAK;IAIjB,MAAM,IAAI,MAAM;IAIhB,UAAU,IAAI,MAAM;IAIpB,UAAU,IAAI,MAAM,GAAG,IAAI;IAI3B,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAMxC,cAAc,IAAI,MAAM;IAIxB,QAAQ,IAAI,YAAY;CAYzB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,YAAY,GAAG,SAAS,CAEjH;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,SAAS,CAEpF"}
|
package/dist/image/ImageNode.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
-
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
|
-
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
|
|
5
|
-
import { $getNodeByKey, $getSelection, $isNodeSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, DecoratorNode, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, } from 'lexical';
|
|
6
|
-
/**
|
|
7
|
-
* Block-level image (editor-architecture-design.md §3.5). The caption is a
|
|
8
|
-
* plain string edited through an input under the image — not the nested
|
|
9
|
-
* sub-editor the architecture doc leaves open — which keeps caption edits in
|
|
10
|
-
* the parent editor's undo history.
|
|
11
|
-
*/
|
|
12
|
-
export class ImageNode extends DecoratorNode {
|
|
13
|
-
static getType() {
|
|
14
|
-
return 'image';
|
|
15
|
-
}
|
|
16
|
-
static clone(node) {
|
|
17
|
-
return new ImageNode(node.__src, node.__altText, node.__caption, node.__linkType, node.__width, node.__height, node.__key);
|
|
18
|
-
}
|
|
19
|
-
static importJSON(json) {
|
|
20
|
-
return $createImageNode({
|
|
21
|
-
src: json.src,
|
|
22
|
-
altText: json.altText,
|
|
23
|
-
caption: json.caption,
|
|
24
|
-
linkType: json.linkType,
|
|
25
|
-
width: json.width,
|
|
26
|
-
height: json.height,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
constructor(src, altText = '', caption = null, linkType = 'linked', width = null, height = null, key) {
|
|
30
|
-
super(key);
|
|
31
|
-
this.__src = src;
|
|
32
|
-
this.__altText = altText;
|
|
33
|
-
this.__caption = caption;
|
|
34
|
-
this.__linkType = linkType;
|
|
35
|
-
this.__width = width;
|
|
36
|
-
this.__height = height;
|
|
37
|
-
}
|
|
38
|
-
exportJSON() {
|
|
39
|
-
return {
|
|
40
|
-
type: 'image',
|
|
41
|
-
version: 1,
|
|
42
|
-
src: this.__src,
|
|
43
|
-
altText: this.__altText,
|
|
44
|
-
caption: this.__caption,
|
|
45
|
-
linkType: this.__linkType,
|
|
46
|
-
width: this.__width,
|
|
47
|
-
height: this.__height,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
/** `<figure><img><figcaption>` — the figure/figcaption semantics from spec §4.7. */
|
|
51
|
-
exportDOM() {
|
|
52
|
-
const figure = document.createElement('figure');
|
|
53
|
-
const img = document.createElement('img');
|
|
54
|
-
img.setAttribute('src', this.__src);
|
|
55
|
-
img.setAttribute('alt', this.__altText);
|
|
56
|
-
if (this.__width)
|
|
57
|
-
img.setAttribute('width', String(this.__width));
|
|
58
|
-
if (this.__height)
|
|
59
|
-
img.setAttribute('height', String(this.__height));
|
|
60
|
-
figure.appendChild(img);
|
|
61
|
-
if (this.__caption) {
|
|
62
|
-
const caption = document.createElement('figcaption');
|
|
63
|
-
caption.textContent = this.__caption;
|
|
64
|
-
figure.appendChild(caption);
|
|
65
|
-
}
|
|
66
|
-
return { element: figure };
|
|
67
|
-
}
|
|
68
|
-
createDOM(config) {
|
|
69
|
-
const div = document.createElement('div');
|
|
70
|
-
const className = config.theme.image;
|
|
71
|
-
if (typeof className === 'string')
|
|
72
|
-
div.className = className;
|
|
73
|
-
return div;
|
|
74
|
-
}
|
|
75
|
-
updateDOM() {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
isInline() {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
getSrc() {
|
|
82
|
-
return this.__src;
|
|
83
|
-
}
|
|
84
|
-
getAltText() {
|
|
85
|
-
return this.__altText;
|
|
86
|
-
}
|
|
87
|
-
getCaption() {
|
|
88
|
-
return this.__caption;
|
|
89
|
-
}
|
|
90
|
-
setCaption(caption) {
|
|
91
|
-
const writable = this.getWritable();
|
|
92
|
-
writable.__caption = caption;
|
|
93
|
-
return writable;
|
|
94
|
-
}
|
|
95
|
-
getTextContent() {
|
|
96
|
-
return this.__altText || this.__caption || '';
|
|
97
|
-
}
|
|
98
|
-
decorate() {
|
|
99
|
-
return (_jsx(ImageComponent, { nodeKey: this.getKey(), src: this.__src, altText: this.__altText, caption: this.__caption, width: this.__width, height: this.__height }));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
export function $createImageNode({ src, altText, caption, linkType, width, height, key }) {
|
|
103
|
-
return new ImageNode(src, altText, caption ?? null, linkType, width ?? null, height ?? null, key);
|
|
104
|
-
}
|
|
105
|
-
export function $isImageNode(node) {
|
|
106
|
-
return node instanceof ImageNode;
|
|
107
|
-
}
|
|
108
|
-
function ImageComponent({ nodeKey, src, altText, caption, width, height }) {
|
|
109
|
-
const [editor] = useLexicalComposerContext();
|
|
110
|
-
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
|
|
111
|
-
const imageRef = useRef(null);
|
|
112
|
-
// Selecting must go through CLICK_COMMAND (returning true) rather than a
|
|
113
|
-
// React onClick: otherwise Lexical's own click handling turns the DOM
|
|
114
|
-
// selection into a range selection right after, undoing the node selection.
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
return editor.registerCommand(CLICK_COMMAND, (event) => {
|
|
117
|
-
if (event.target !== imageRef.current)
|
|
118
|
-
return false;
|
|
119
|
-
clearSelection();
|
|
120
|
-
setSelected(true);
|
|
121
|
-
return true;
|
|
122
|
-
}, COMMAND_PRIORITY_LOW);
|
|
123
|
-
}, [editor, clearSelection, setSelected]);
|
|
124
|
-
// Delete/Backspace removes the selected image — but not while typing in the
|
|
125
|
-
// caption input, whose keystrokes also reach the editor's root listener.
|
|
126
|
-
const onDelete = useCallback((event) => {
|
|
127
|
-
if (!isSelected || event.target instanceof HTMLInputElement)
|
|
128
|
-
return false;
|
|
129
|
-
if (!$isNodeSelection($getSelection()))
|
|
130
|
-
return false;
|
|
131
|
-
event.preventDefault();
|
|
132
|
-
$getNodeByKey(nodeKey)?.remove();
|
|
133
|
-
return true;
|
|
134
|
-
}, [isSelected, nodeKey]);
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
const unregisterDelete = editor.registerCommand(KEY_DELETE_COMMAND, onDelete, COMMAND_PRIORITY_LOW);
|
|
137
|
-
const unregisterBackspace = editor.registerCommand(KEY_BACKSPACE_COMMAND, onDelete, COMMAND_PRIORITY_LOW);
|
|
138
|
-
return () => {
|
|
139
|
-
unregisterDelete();
|
|
140
|
-
unregisterBackspace();
|
|
141
|
-
};
|
|
142
|
-
}, [editor, onDelete]);
|
|
143
|
-
const setCaption = (value) => {
|
|
144
|
-
editor.update(() => {
|
|
145
|
-
const node = $getNodeByKey(nodeKey);
|
|
146
|
-
if ($isImageNode(node))
|
|
147
|
-
node.setCaption(value);
|
|
148
|
-
});
|
|
149
|
-
};
|
|
150
|
-
return (_jsxs("figure", { className: "likhari-image-figure", "data-selected": isSelected ? 'true' : 'false', contentEditable: false, children: [_jsx("img", { ref: imageRef, className: "likhari-image", src: src, alt: altText, width: width ?? undefined, height: height ?? undefined, draggable: false }), caption !== null && (_jsx("input", { className: "likhari-image-caption", type: "text", "aria-label": "Image caption", placeholder: "Add a caption\u2026", value: caption, onChange: (e) => setCaption(e.currentTarget.value) }))] }));
|
|
151
|
-
}
|
package/dist/image/imageUrl.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates the source of a linked image. Accepts http(s) URLs, relative
|
|
3
|
-
* paths, and base64 raster/SVG data URIs (what an embedded image stores);
|
|
4
|
-
* anything else (javascript:, other data: types, file:, ...) is rejected.
|
|
5
|
-
* Returns the trimmed URL, or null when it should be refused.
|
|
6
|
-
*/
|
|
7
|
-
export declare function normalizeImageUrl(input: string): string | null;
|
|
8
|
-
export declare const ACCEPTED_IMAGE_TYPES: string[];
|
|
9
|
-
/** Returns an error message if the file can't be inserted, otherwise null. */
|
|
10
|
-
export declare function validateImageFile(file: {
|
|
11
|
-
type: string;
|
|
12
|
-
size: number;
|
|
13
|
-
}, maxSizeMB: number): string | null;
|
|
14
|
-
//# sourceMappingURL=imageUrl.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"imageUrl.d.ts","sourceRoot":"","sources":["../../src/image/imageUrl.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO9D;AAED,eAAO,MAAM,oBAAoB,UAA0E,CAAC;AAE5G,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIxG"}
|
package/dist/image/imageUrl.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates the source of a linked image. Accepts http(s) URLs, relative
|
|
3
|
-
* paths, and base64 raster/SVG data URIs (what an embedded image stores);
|
|
4
|
-
* anything else (javascript:, other data: types, file:, ...) is rejected.
|
|
5
|
-
* Returns the trimmed URL, or null when it should be refused.
|
|
6
|
-
*/
|
|
7
|
-
export function normalizeImageUrl(input) {
|
|
8
|
-
const url = input.trim();
|
|
9
|
-
if (!url)
|
|
10
|
-
return null;
|
|
11
|
-
if (/^https?:\/\/\S+$/i.test(url))
|
|
12
|
-
return url;
|
|
13
|
-
if (/^(\/|\.\.?\/)\S*$/.test(url))
|
|
14
|
-
return url;
|
|
15
|
-
if (/^data:image\/(png|jpe?g|gif|webp|svg\+xml);base64,[a-z0-9+/=]+$/i.test(url))
|
|
16
|
-
return url;
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
export const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
|
|
20
|
-
/** Returns an error message if the file can't be inserted, otherwise null. */
|
|
21
|
-
export function validateImageFile(file, maxSizeMB) {
|
|
22
|
-
if (!ACCEPTED_IMAGE_TYPES.includes(file.type))
|
|
23
|
-
return 'Choose a PNG, JPEG, GIF, WebP or SVG image';
|
|
24
|
-
if (file.size > maxSizeMB * 1024 * 1024)
|
|
25
|
-
return `Image is larger than ${maxSizeMB} MB`;
|
|
26
|
-
return null;
|
|
27
|
-
}
|