@inshapardaz/likhari-react 0.1.4-dev.38 → 0.1.4
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 +0 -9
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +3 -11
- package/dist/components/Toolbar.d.ts +1 -6
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +9 -187
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/theme/editor.css +0 -11
- package/package.json +1 -1
- package/dist/components/LinkDialog.d.ts +0 -18
- package/dist/components/LinkDialog.d.ts.map +0 -1
- package/dist/components/LinkDialog.js +0 -31
- package/dist/fonts/index.d.ts +0 -22
- package/dist/fonts/index.d.ts.map +0 -1
- package/dist/fonts/index.js +0 -20
- package/dist/fonts/urduWebFonts.d.ts +0 -14
- package/dist/fonts/urduWebFonts.d.ts.map +0 -1
- package/dist/fonts/urduWebFonts.js +0 -66
- package/dist/plugins/LinkPastePlugin.d.ts +0 -12
- package/dist/plugins/LinkPastePlugin.d.ts.map +0 -1
- package/dist/plugins/LinkPastePlugin.js +0 -50
- package/dist/utils/linkUrl.d.ts +0 -25
- package/dist/utils/linkUrl.d.ts.map +0 -1
- package/dist/utils/linkUrl.js +0 -62
- package/dist/utils/style.d.ts +0 -4
- package/dist/utils/style.d.ts.map +0 -1
- package/dist/utils/style.js +0 -15
package/dist/EditorRoot.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ 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';
|
|
6
5
|
export interface EditorInitialContent {
|
|
7
6
|
format: Extract<FormatId, 'lexical-json' | 'plain-text'>;
|
|
8
7
|
value: string;
|
|
@@ -38,14 +37,6 @@ export interface EditorRootProps {
|
|
|
38
37
|
* the unsaved-changes state) or to hide it while keeping `onSave`.
|
|
39
38
|
*/
|
|
40
39
|
showSave?: boolean;
|
|
41
|
-
/**
|
|
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' }]`.
|
|
47
|
-
*/
|
|
48
|
-
fontOptions?: FontOption[];
|
|
49
40
|
}
|
|
50
41
|
export interface EditorRef {
|
|
51
42
|
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;AAMzH,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;CACpB;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,uGAqHrB,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,
|
|
2
|
+
import { forwardRef, 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,22 +14,14 @@ 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';
|
|
18
|
-
import { LinkPastePlugin } from './plugins/LinkPastePlugin.js';
|
|
19
17
|
function initialEditorStateJson(initialContent) {
|
|
20
18
|
if (!initialContent)
|
|
21
19
|
return undefined;
|
|
22
20
|
const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
|
|
23
21
|
return JSON.stringify(state);
|
|
24
22
|
}
|
|
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),
|
|
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), }, ref) {
|
|
26
24
|
const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
|
|
27
|
-
// The urdu-web-fonts stylesheets are needed for the font dropdown and for
|
|
28
|
-
// RTL content, whose canvas font (editor.css) is one of those families.
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (config.font.family || locale !== 'en')
|
|
31
|
-
injectUrduWebFontsCss();
|
|
32
|
-
}, [config.font.family, locale]);
|
|
33
25
|
const editorStateRef = useRef(null);
|
|
34
26
|
const lastSavedJsonRef = useRef(null);
|
|
35
27
|
const [isDirty, setIsDirty] = useState(false);
|
|
@@ -92,5 +84,5 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
92
84
|
onSave?.(content, format);
|
|
93
85
|
};
|
|
94
86
|
const dir = locale === 'en' ? 'ltr' : 'rtl';
|
|
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
|
|
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 }), _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 })] }) }) }));
|
|
96
88
|
});
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import type { ResolvedEditorFeatureConfig } from '@inshapardaz/likhari-core';
|
|
2
|
-
import { type FontOption } from '../fonts/index.js';
|
|
3
2
|
export interface ToolbarProps {
|
|
4
3
|
config: ResolvedEditorFeatureConfig;
|
|
5
4
|
onSave?: () => void;
|
|
6
5
|
isDirty?: boolean;
|
|
7
6
|
showSave?: boolean;
|
|
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';
|
|
12
7
|
}
|
|
13
|
-
export declare function Toolbar({ config, onSave, isDirty, showSave
|
|
8
|
+
export declare function Toolbar({ config, onSave, isDirty, showSave }: ToolbarProps): import("react").JSX.Element;
|
|
14
9
|
//# 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":"AAkCA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AA4K7E,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;CACpB;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAY,+BA4W1E"}
|
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
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,
|
|
6
|
-
import { $
|
|
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';
|
|
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
10
|
import { $findMatchingParent } from '@lexical/utils';
|
|
11
|
-
import {
|
|
12
|
-
import { LinkDialog } from './LinkDialog.js';
|
|
13
|
-
import { normalizeLinkUrl } from '../utils/linkUrl.js';
|
|
14
|
-
import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fonts/index.js';
|
|
15
|
-
import { setStyleProperty } from '../utils/style.js';
|
|
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';
|
|
17
|
-
/** Sentinel for "no inline style": Lexical returns the default it is given when
|
|
18
|
-
* the selection has none, so this tells "unstyled" (show the canvas default)
|
|
19
|
-
* apart from "styled with a value the dropdown doesn't list" (show nothing). */
|
|
20
|
-
const UNSET_STYLE = '__unset__';
|
|
11
|
+
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';
|
|
21
12
|
const INITIAL_STATE = {
|
|
22
13
|
blockType: 'paragraph',
|
|
23
14
|
activeFormats: new Set(),
|
|
24
15
|
elementFormat: 'start',
|
|
25
16
|
listType: null,
|
|
26
|
-
fontFamily: UNSET_STYLE,
|
|
27
|
-
fontSize: UNSET_STYLE,
|
|
28
|
-
isLink: false,
|
|
29
|
-
linkUrl: '',
|
|
30
17
|
canUndo: false,
|
|
31
18
|
canRedo: false,
|
|
32
19
|
};
|
|
@@ -54,24 +41,15 @@ function StubButton({ icon, title }) {
|
|
|
54
41
|
* labels the control itself, since a dropdown can't show one per option in
|
|
55
42
|
* its closed state. The dropdown portals to <body>, so it is not clipped by
|
|
56
43
|
* the editor's `overflow: hidden` frame. */
|
|
57
|
-
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder,
|
|
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,
|
|
44
|
+
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, onChange }) {
|
|
45
|
+
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) }));
|
|
59
46
|
}
|
|
60
47
|
function OverflowItem({ icon: Icon, label, active, onClick, }) {
|
|
61
48
|
return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', className: "likhari-overflow-item",
|
|
62
49
|
// Keep the editor's selection: a mousedown here would blur the canvas.
|
|
63
50
|
onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
|
|
64
51
|
}
|
|
65
|
-
|
|
66
|
-
* menu: the URL (opens in a new tab), Edit link, Remove link. */
|
|
67
|
-
function LinkMenuItems({ url, onEdit, onRemove }) {
|
|
68
|
-
// Only offer the link as clickable if it's a URL the editor would have accepted.
|
|
69
|
-
const safeUrl = normalizeLinkUrl(url);
|
|
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 })) : (
|
|
71
|
-
// Not a URL the editor would accept (e.g. loaded from a document): show it, don't make it clickable.
|
|
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" })] }));
|
|
73
|
-
}
|
|
74
|
-
export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr' }) {
|
|
52
|
+
export function Toolbar({ config, onSave, isDirty, showSave }) {
|
|
75
53
|
const [editor] = useLexicalComposerContext();
|
|
76
54
|
const [state, setState] = useState(INITIAL_STATE);
|
|
77
55
|
const updateToolbar = useCallback(() => {
|
|
@@ -90,11 +68,6 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
90
68
|
else if ($isQuoteNode(element)) {
|
|
91
69
|
blockType = 'quote';
|
|
92
70
|
}
|
|
93
|
-
const fontFamily = $getSelectionStyleValueForProperty(selection, 'font-family', UNSET_STYLE);
|
|
94
|
-
const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', UNSET_STYLE);
|
|
95
|
-
const linkNode = $findMatchingParent(anchorNode, $isLinkNode);
|
|
96
|
-
const isLink = Boolean(linkNode);
|
|
97
|
-
const linkUrl = linkNode && $isLinkNode(linkNode) ? linkNode.getURL() : '';
|
|
98
71
|
const activeFormats = new Set();
|
|
99
72
|
['bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript'].forEach((format) => {
|
|
100
73
|
if (selection.hasFormat(format))
|
|
@@ -111,18 +84,9 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
111
84
|
activeFormats,
|
|
112
85
|
elementFormat,
|
|
113
86
|
listType,
|
|
114
|
-
fontFamily,
|
|
115
|
-
fontSize,
|
|
116
|
-
isLink,
|
|
117
|
-
linkUrl,
|
|
118
87
|
}));
|
|
119
88
|
});
|
|
120
89
|
}, [editor]);
|
|
121
|
-
// Selection changes alone miss edits that change the toolbar's state without
|
|
122
|
-
// moving the caret (e.g. removing the link the caret is in).
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
return editor.registerUpdateListener(() => updateToolbar());
|
|
125
|
-
}, [editor, updateToolbar]);
|
|
126
90
|
useEffect(() => {
|
|
127
91
|
return editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
128
92
|
updateToolbar();
|
|
@@ -186,8 +150,6 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
186
150
|
menuSelectionRef.current = selection ? selection.clone() : null;
|
|
187
151
|
});
|
|
188
152
|
};
|
|
189
|
-
const snapshotSelectionRef = useRef(snapshotSelection);
|
|
190
|
-
snapshotSelectionRef.current = snapshotSelection;
|
|
191
153
|
const runOverflowAction = (action) => () => {
|
|
192
154
|
const saved = menuSelectionRef.current;
|
|
193
155
|
if (saved)
|
|
@@ -195,111 +157,6 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
195
157
|
action();
|
|
196
158
|
editor.focus();
|
|
197
159
|
};
|
|
198
|
-
/** Applies a font property per `config.font.scope`: to the selection, to
|
|
199
|
-
* every text node in the document, or (for 'both') to the selection when
|
|
200
|
-
* there is a range and to the whole document when there isn't. */
|
|
201
|
-
const applyFont = (property, value) => {
|
|
202
|
-
editor.update(() => {
|
|
203
|
-
const selection = $getSelection();
|
|
204
|
-
const scope = config.font.scope;
|
|
205
|
-
const wholeDocument = scope === 'document' || (scope === 'both' && (!$isRangeSelection(selection) || selection.isCollapsed()));
|
|
206
|
-
if (wholeDocument) {
|
|
207
|
-
for (const node of $getRoot().getAllTextNodes()) {
|
|
208
|
-
node.setStyle(setStyleProperty(node.getStyle(), property, value));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else if ($isRangeSelection(selection)) {
|
|
212
|
-
$patchStyleText(selection, { [property]: value });
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
};
|
|
216
|
-
const [linkDialogOpen, setLinkDialogOpen] = useState(false);
|
|
217
|
-
const [linkNeedsText, setLinkNeedsText] = useState(false);
|
|
218
|
-
const openLinkDialog = useCallback(() => {
|
|
219
|
-
// Snapshot first: the dialog's focus trap makes Lexical drop the selection.
|
|
220
|
-
let collapsed = false;
|
|
221
|
-
editor.getEditorState().read(() => {
|
|
222
|
-
const selection = $getSelection();
|
|
223
|
-
menuSelectionRef.current = selection ? selection.clone() : null;
|
|
224
|
-
collapsed = $isRangeSelection(selection) && selection.isCollapsed();
|
|
225
|
-
});
|
|
226
|
-
setLinkNeedsText(collapsed && !state.isLink);
|
|
227
|
-
setLinkDialogOpen(true);
|
|
228
|
-
}, [editor, state.isLink]);
|
|
229
|
-
const closeLinkDialog = () => {
|
|
230
|
-
setLinkDialogOpen(false);
|
|
231
|
-
editor.focus();
|
|
232
|
-
};
|
|
233
|
-
const restoreSelection = () => {
|
|
234
|
-
const saved = menuSelectionRef.current;
|
|
235
|
-
if (saved)
|
|
236
|
-
editor.update(() => $setSelection(saved.clone()), { discrete: true });
|
|
237
|
-
};
|
|
238
|
-
const applyLink = ({ url, text }) => {
|
|
239
|
-
restoreSelection();
|
|
240
|
-
if (linkNeedsText) {
|
|
241
|
-
// Nothing selected: insert a new link with its own text.
|
|
242
|
-
editor.update(() => {
|
|
243
|
-
const selection = $getSelection();
|
|
244
|
-
if (!$isRangeSelection(selection))
|
|
245
|
-
return;
|
|
246
|
-
const link = $createLinkNode(url);
|
|
247
|
-
link.append($createTextNode(text));
|
|
248
|
-
selection.insertNodes([link]);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
|
|
253
|
-
}
|
|
254
|
-
closeLinkDialog();
|
|
255
|
-
};
|
|
256
|
-
const removeLink = () => {
|
|
257
|
-
restoreSelection();
|
|
258
|
-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
259
|
-
closeLinkDialog();
|
|
260
|
-
};
|
|
261
|
-
// Right-clicking a link in the text opens the same menu, at the pointer.
|
|
262
|
-
const [linkContext, setLinkContext] = useState(null);
|
|
263
|
-
useEffect(() => {
|
|
264
|
-
if (!config.links)
|
|
265
|
-
return;
|
|
266
|
-
const onContextMenu = (event) => {
|
|
267
|
-
const anchor = event.target?.closest?.('a');
|
|
268
|
-
if (!anchor)
|
|
269
|
-
return;
|
|
270
|
-
let url = null;
|
|
271
|
-
// Select the whole link first, so Edit / Remove act on it wherever the caret was.
|
|
272
|
-
editor.update(() => {
|
|
273
|
-
const node = $getNearestNodeFromDOMNode(anchor);
|
|
274
|
-
const link = node ? $findMatchingParent(node, $isLinkNode) : null;
|
|
275
|
-
if (!link || !$isLinkNode(link))
|
|
276
|
-
return;
|
|
277
|
-
url = link.getURL();
|
|
278
|
-
link.select(0, link.getChildrenSize());
|
|
279
|
-
}, { discrete: true });
|
|
280
|
-
if (url === null)
|
|
281
|
-
return;
|
|
282
|
-
event.preventDefault();
|
|
283
|
-
snapshotSelectionRef.current();
|
|
284
|
-
setLinkContext({ x: event.clientX, y: event.clientY, url });
|
|
285
|
-
};
|
|
286
|
-
return editor.registerRootListener((root, previous) => {
|
|
287
|
-
previous?.removeEventListener('contextmenu', onContextMenu);
|
|
288
|
-
root?.addEventListener('contextmenu', onContextMenu);
|
|
289
|
-
});
|
|
290
|
-
}, [editor, config.links]);
|
|
291
|
-
// Ctrl/Cmd+K opens the link dialog.
|
|
292
|
-
useEffect(() => {
|
|
293
|
-
if (!config.links)
|
|
294
|
-
return;
|
|
295
|
-
return editor.registerCommand(KEY_MODIFIER_COMMAND, (event) => {
|
|
296
|
-
if (event.key.toLowerCase() !== 'k' || !(event.ctrlKey || event.metaKey))
|
|
297
|
-
return false;
|
|
298
|
-
event.preventDefault();
|
|
299
|
-
openLinkDialog();
|
|
300
|
-
return true;
|
|
301
|
-
}, COMMAND_PRIORITY_NORMAL);
|
|
302
|
-
}, [editor, config.links, openLinkDialog]);
|
|
303
160
|
const withRefocus = (fn) => (value) => {
|
|
304
161
|
fn(value);
|
|
305
162
|
editor.focus();
|
|
@@ -348,33 +205,9 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
348
205
|
const showFormattingGroup = headingLevels.length > 0 || config.lists.bullet || config.lists.numbered || config.lists.check || config.blocks.quote;
|
|
349
206
|
const showInlineGroup = fmt.bold || fmt.italic || fmt.underline;
|
|
350
207
|
const showAlignGroup = config.alignment.start || config.alignment.center || config.alignment.justify || config.alignment.left || config.alignment.right;
|
|
351
|
-
const
|
|
208
|
+
const showInsertPoetryGroup = config.links || config.images.linked || config.images.embedded || config.poetry.enabled;
|
|
352
209
|
const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
|
|
353
210
|
const showOverflowMenu = fmt.strikethrough || fmt.superscript || fmt.subscript || fmt.caseTransforms || fmt.clearFormatting || config.indent;
|
|
354
|
-
// Grouped for the dropdown when options declare groups (Latin / Urdu).
|
|
355
|
-
const fontData = (() => {
|
|
356
|
-
const data = [];
|
|
357
|
-
const groups = new Map();
|
|
358
|
-
for (const f of fontOptions) {
|
|
359
|
-
const item = { value: f.family, label: f.name };
|
|
360
|
-
if (!f.group) {
|
|
361
|
-
data.push(item);
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
let group = groups.get(f.group);
|
|
365
|
-
if (!group) {
|
|
366
|
-
group = { group: f.group, items: [] };
|
|
367
|
-
groups.set(f.group, group);
|
|
368
|
-
data.push(group);
|
|
369
|
-
}
|
|
370
|
-
group.items.push(item);
|
|
371
|
-
}
|
|
372
|
-
return data;
|
|
373
|
-
})();
|
|
374
|
-
// Unstyled text shows the canvas default (pre-selected).
|
|
375
|
-
const canvasDefaults = CANVAS_FONT_DEFAULTS[direction];
|
|
376
|
-
const shownFontFamily = state.fontFamily === UNSET_STYLE ? canvasDefaults.family : state.fontFamily;
|
|
377
|
-
const shownFontSize = state.fontSize === UNSET_STYLE ? canvasDefaults.size : state.fontSize;
|
|
378
211
|
const formattingOptions = [
|
|
379
212
|
{ value: 'paragraph', label: 'Paragraph' },
|
|
380
213
|
...headingLevels.map((level) => ({ value: `h${level}`, label: `Heading ${level}` })),
|
|
@@ -393,16 +226,5 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
393
226
|
];
|
|
394
227
|
const formattingValue = state.listType ?? state.blockType;
|
|
395
228
|
const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
|
|
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:
|
|
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) => {
|
|
402
|
-
if (!opened)
|
|
403
|
-
setLinkContext(null);
|
|
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: () => {
|
|
405
|
-
restoreSelection();
|
|
406
|
-
openLinkDialog();
|
|
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 }))] }));
|
|
229
|
+
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 likhari-toolbar-group--collapse-tablet", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: "Font family", width: 110, value: null, placeholder: "Font", data: [], disabled: true })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: "Font size", width: 90, value: null, placeholder: "Size", data: [], disabled: true }))] })), 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)) })] }))] })] }))] }));
|
|
408
230
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
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 { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
|
5
|
-
export type { FontOption } from './fonts/index.js';
|
|
6
4
|
//# 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
|
|
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"}
|
package/dist/index.js
CHANGED
package/dist/theme/editor.css
CHANGED
|
@@ -234,14 +234,3 @@
|
|
|
234
234
|
border-top: 1px solid var(--editor-line);
|
|
235
235
|
margin: 1.5em 0;
|
|
236
236
|
}
|
|
237
|
-
|
|
238
|
-
/* Link menu: the URL item shows one truncated line. */
|
|
239
|
-
.likhari-link-menu-url {
|
|
240
|
-
max-width: 100%;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
.likhari-link-menu-url .mantine-Menu-itemLabel {
|
|
244
|
-
overflow: hidden;
|
|
245
|
-
text-overflow: ellipsis;
|
|
246
|
-
white-space: nowrap;
|
|
247
|
-
}
|
package/package.json
CHANGED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface LinkDialogProps {
|
|
2
|
-
opened: boolean;
|
|
3
|
-
/** Existing link's URL when editing; empty when inserting. */
|
|
4
|
-
initialUrl: string;
|
|
5
|
-
/** Show a "Text" field — used when nothing is selected, so there is no text to link. */
|
|
6
|
-
showTextField: boolean;
|
|
7
|
-
onSubmit: (link: {
|
|
8
|
-
url: string;
|
|
9
|
-
text: string;
|
|
10
|
-
}) => void;
|
|
11
|
-
/** Present only when editing an existing link. */
|
|
12
|
-
onRemove?: () => void;
|
|
13
|
-
onClose: () => void;
|
|
14
|
-
}
|
|
15
|
-
/** Insert/edit-link dialog (UI spec §6). Mantine Modal supplies focus
|
|
16
|
-
* trapping, Escape-to-close and the overlay. */
|
|
17
|
-
export declare function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemove, onClose }: LinkDialogProps): import("react").JSX.Element;
|
|
18
|
-
//# sourceMappingURL=LinkDialog.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Button, Group, Modal, TextInput } from '@mantine/core';
|
|
4
|
-
import { normalizeLinkUrl } from '../utils/linkUrl.js';
|
|
5
|
-
/** Insert/edit-link dialog (UI spec §6). Mantine Modal supplies focus
|
|
6
|
-
* trapping, Escape-to-close and the overlay. */
|
|
7
|
-
export function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemove, onClose }) {
|
|
8
|
-
const [url, setUrl] = useState(initialUrl);
|
|
9
|
-
const [text, setText] = useState('');
|
|
10
|
-
const [error, setError] = useState(null);
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (opened) {
|
|
13
|
-
setUrl(initialUrl);
|
|
14
|
-
setText('');
|
|
15
|
-
setError(null);
|
|
16
|
-
}
|
|
17
|
-
}, [opened, initialUrl]);
|
|
18
|
-
const handleSubmit = (e) => {
|
|
19
|
-
e.preventDefault();
|
|
20
|
-
const normalized = normalizeLinkUrl(url);
|
|
21
|
-
if (!normalized) {
|
|
22
|
-
setError('Enter a valid http(s), mailto, tel or relative URL');
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
onSubmit({ url: normalized, text: text.trim() || normalized });
|
|
26
|
-
};
|
|
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) => {
|
|
28
|
-
setUrl(e.currentTarget.value);
|
|
29
|
-
setError(null);
|
|
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" })] })] })] }) }));
|
|
31
|
-
}
|
package/dist/fonts/index.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/fonts/index.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Turns pasted URLs into links instead of inserting them as bare text:
|
|
3
|
-
*
|
|
4
|
-
* - a lone URL pasted over selected text links that text (the URL isn't inserted);
|
|
5
|
-
* - a lone URL pasted at a caret becomes a link whose text is the URL;
|
|
6
|
-
* - a single line of plain text containing URLs gets a link node per URL.
|
|
7
|
-
*
|
|
8
|
-
* Everything else — multi-line text, or anything copied with HTML (Lexical's
|
|
9
|
-
* own paste already turns `<a>` into links) — falls through to the default paste.
|
|
10
|
-
*/
|
|
11
|
-
export declare function LinkPastePlugin(): null;
|
|
12
|
-
//# sourceMappingURL=LinkPastePlugin.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"LinkPastePlugin.d.ts","sourceRoot":"","sources":["../../src/plugins/LinkPastePlugin.tsx"],"names":[],"mappings":"AAaA;;;;;;;;;GASG;AACH,wBAAgB,eAAe,SAyC9B"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
-
import { $createLinkNode, $toggleLink } from '@lexical/link';
|
|
4
|
-
import { $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND, } from 'lexical';
|
|
5
|
-
import { splitPastedText } from '../utils/linkUrl.js';
|
|
6
|
-
/**
|
|
7
|
-
* Turns pasted URLs into links instead of inserting them as bare text:
|
|
8
|
-
*
|
|
9
|
-
* - a lone URL pasted over selected text links that text (the URL isn't inserted);
|
|
10
|
-
* - a lone URL pasted at a caret becomes a link whose text is the URL;
|
|
11
|
-
* - a single line of plain text containing URLs gets a link node per URL.
|
|
12
|
-
*
|
|
13
|
-
* Everything else — multi-line text, or anything copied with HTML (Lexical's
|
|
14
|
-
* own paste already turns `<a>` into links) — falls through to the default paste.
|
|
15
|
-
*/
|
|
16
|
-
export function LinkPastePlugin() {
|
|
17
|
-
const [editor] = useLexicalComposerContext();
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
return editor.registerCommand(PASTE_COMMAND, (event) => {
|
|
20
|
-
const clipboard = 'clipboardData' in event ? event.clipboardData : null;
|
|
21
|
-
const text = clipboard?.getData('text/plain');
|
|
22
|
-
if (!clipboard || !text)
|
|
23
|
-
return false;
|
|
24
|
-
const segments = splitPastedText(text.trim());
|
|
25
|
-
const hasLink = segments.some((s) => s.type === 'link');
|
|
26
|
-
const isLoneUrl = segments.length === 1 && segments[0].type === 'link';
|
|
27
|
-
const isPlainSingleLine = !clipboard.getData('text/html') && !/[\r\n]/.test(text.trim());
|
|
28
|
-
if (!hasLink || !(isLoneUrl || isPlainSingleLine))
|
|
29
|
-
return false;
|
|
30
|
-
const selection = $getSelection();
|
|
31
|
-
if (!$isRangeSelection(selection))
|
|
32
|
-
return false;
|
|
33
|
-
event.preventDefault();
|
|
34
|
-
if (isLoneUrl && !selection.isCollapsed()) {
|
|
35
|
-
$toggleLink(segments[0].url);
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
const nodes = segments.map((segment) => {
|
|
39
|
-
if (segment.type === 'text')
|
|
40
|
-
return $createTextNode(segment.text);
|
|
41
|
-
const link = $createLinkNode(segment.url);
|
|
42
|
-
link.append($createTextNode(segment.text));
|
|
43
|
-
return link;
|
|
44
|
-
});
|
|
45
|
-
selection.insertNodes(nodes);
|
|
46
|
-
return true;
|
|
47
|
-
}, COMMAND_PRIORITY_LOW);
|
|
48
|
-
}, [editor]);
|
|
49
|
-
return null;
|
|
50
|
-
}
|
package/dist/utils/linkUrl.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalizes a URL typed into the link dialog, or returns null if it should
|
|
3
|
-
* be rejected. Only http(s), mailto, tel, in-page (#), and relative URLs are
|
|
4
|
-
* accepted — any other scheme (javascript:, data:, vbscript:, ...) is
|
|
5
|
-
* refused so a pasted link can't carry script into exported HTML. A bare
|
|
6
|
-
* domain like "example.com/page" gets https:// prefixed.
|
|
7
|
-
*/
|
|
8
|
-
export declare function normalizeLinkUrl(input: string): string | null;
|
|
9
|
-
export type PastedSegment = {
|
|
10
|
-
type: 'text';
|
|
11
|
-
text: string;
|
|
12
|
-
} | {
|
|
13
|
-
type: 'link';
|
|
14
|
-
text: string;
|
|
15
|
-
url: string;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Splits pasted plain text into text and link segments. Only explicit URLs
|
|
19
|
-
* count (http(s)://, mailto:, or a www. prefix) — a bare "file.txt" or
|
|
20
|
-
* "example.com" is left as text, since pasted prose is full of those. Trailing
|
|
21
|
-
* sentence punctuation stays outside the link, and anything normalizeLinkUrl
|
|
22
|
-
* rejects stays text.
|
|
23
|
-
*/
|
|
24
|
-
export declare function splitPastedText(text: string): PastedSegment[];
|
|
25
|
-
//# sourceMappingURL=linkUrl.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"linkUrl.d.ts","sourceRoot":"","sources":["../../src/utils/linkUrl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc7D;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAKzG;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CA0B7D"}
|
package/dist/utils/linkUrl.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalizes a URL typed into the link dialog, or returns null if it should
|
|
3
|
-
* be rejected. Only http(s), mailto, tel, in-page (#), and relative URLs are
|
|
4
|
-
* accepted — any other scheme (javascript:, data:, vbscript:, ...) is
|
|
5
|
-
* refused so a pasted link can't carry script into exported HTML. A bare
|
|
6
|
-
* domain like "example.com/page" gets https:// prefixed.
|
|
7
|
-
*/
|
|
8
|
-
export function normalizeLinkUrl(input) {
|
|
9
|
-
const url = input.trim();
|
|
10
|
-
if (!url)
|
|
11
|
-
return null;
|
|
12
|
-
if (/^(https?:\/\/|mailto:|tel:)\S+$/i.test(url))
|
|
13
|
-
return url;
|
|
14
|
-
if (/^(#|\/|\.\.?\/)\S*$/.test(url))
|
|
15
|
-
return url;
|
|
16
|
-
// Any other explicit scheme is rejected.
|
|
17
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(url))
|
|
18
|
-
return null;
|
|
19
|
-
// Looks like a bare domain (no spaces, has a dot in its host part).
|
|
20
|
-
if (/^[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$/.test(url))
|
|
21
|
-
return `https://${url}`;
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
const PASTED_URL_PATTERN = /(?:https?:\/\/|mailto:|www\.)[^\s<>"']+/gi;
|
|
25
|
-
const TRAILING_PUNCTUATION = /[.,;:!?)\]}]+$/;
|
|
26
|
-
/**
|
|
27
|
-
* Splits pasted plain text into text and link segments. Only explicit URLs
|
|
28
|
-
* count (http(s)://, mailto:, or a www. prefix) — a bare "file.txt" or
|
|
29
|
-
* "example.com" is left as text, since pasted prose is full of those. Trailing
|
|
30
|
-
* sentence punctuation stays outside the link, and anything normalizeLinkUrl
|
|
31
|
-
* rejects stays text.
|
|
32
|
-
*/
|
|
33
|
-
export function splitPastedText(text) {
|
|
34
|
-
const segments = [];
|
|
35
|
-
let last = 0;
|
|
36
|
-
const pushText = (value) => {
|
|
37
|
-
if (!value)
|
|
38
|
-
return;
|
|
39
|
-
const previous = segments[segments.length - 1];
|
|
40
|
-
if (previous?.type === 'text')
|
|
41
|
-
previous.text += value;
|
|
42
|
-
else
|
|
43
|
-
segments.push({ type: 'text', text: value });
|
|
44
|
-
};
|
|
45
|
-
for (const match of text.matchAll(PASTED_URL_PATTERN)) {
|
|
46
|
-
const start = match.index ?? 0;
|
|
47
|
-
const raw = match[0];
|
|
48
|
-
const trimmed = raw.replace(TRAILING_PUNCTUATION, '');
|
|
49
|
-
const url = normalizeLinkUrl(/^www\./i.test(trimmed) ? `https://${trimmed}` : trimmed);
|
|
50
|
-
pushText(text.slice(last, start));
|
|
51
|
-
if (url && trimmed.length > 'www.'.length) {
|
|
52
|
-
segments.push({ type: 'link', text: trimmed, url });
|
|
53
|
-
pushText(raw.slice(trimmed.length));
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
pushText(raw);
|
|
57
|
-
}
|
|
58
|
-
last = start + raw.length;
|
|
59
|
-
}
|
|
60
|
-
pushText(text.slice(last));
|
|
61
|
-
return segments;
|
|
62
|
-
}
|
package/dist/utils/style.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/utils/style.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
}
|