@inshapardaz/likhari-react 0.1.48 → 0.1.49-dev.391
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/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +178 -28
- package/dist/i18n/strings.js +3 -3
- package/dist/icons/poetryIcons.d.ts +22 -0
- package/dist/icons/poetryIcons.d.ts.map +1 -0
- package/dist/icons/poetryIcons.js +94 -0
- package/dist/join/joinLinesActions.d.ts +4 -2
- package/dist/join/joinLinesActions.d.ts.map +1 -1
- package/dist/join/joinLinesActions.js +66 -25
- package/dist/spellcheck/spellingMenu.d.ts +9 -0
- package/dist/spellcheck/spellingMenu.d.ts.map +1 -1
- package/dist/spellcheck/spellingMenu.js +34 -6
- package/dist/theme/editor.css +73 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA2CA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACjF,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAA2B,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAmDtF,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAAkF,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAI1I,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA2CA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACjF,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAA2B,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAmDtF,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAAkF,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAI1I,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AA0sBvD,0FAA0F;AAC1F,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,gCAAgC;IAChC,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,aAAa,EAAE,CAAC;IACnC,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;IACnE,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,iGAAiG;IACjG,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,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;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yBAAyB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gJAAgJ;IAChJ,kBAAkB,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;IAC5C,kGAAkG;IAClG,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC1B;AAED;iFACiF;AACjF,eAAO,MAAM,gBAAgB,8EAA8E,CAAC;AAE5G,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,gBAAqB,EAAE,eAAoB,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAgB,EAAE,YAAY,EAAE,SAAiB,EAAE,aAAa,EAAE,eAAuB,EAAE,mBAAmB,EAAE,WAAkC,EAAE,QAAsB,EAAE,iBAAiB,EAAE,IAAU,EAAE,yBAAyB,EAAE,YAAY,EAAE,SAAiB,EAAE,MAAa,EAAE,kBAA2B,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,YAAY,+BAksD3d"}
|
|
@@ -14,7 +14,7 @@ import { $replaceMatch } from '../find/findReplaceActions.js';
|
|
|
14
14
|
import { getSpeller } from '../spellcheck/spellDictionaries.js';
|
|
15
15
|
import { lookupSynonyms } from '../thesaurus/thesaurusStores.js';
|
|
16
16
|
import { addUserWord, ignoreWord } from '../spellcheck/userWords.js';
|
|
17
|
-
import { $wordAtDomPoint, textPointAt } from '../spellcheck/spellingMenu.js';
|
|
17
|
+
import { $selectionIsWithinWord, $wordAtDomPoint, $wordAtSelection, textPointAt } from '../spellcheck/spellingMenu.js';
|
|
18
18
|
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
19
19
|
import { ImageDialog } from '../image/ImageDialog.js';
|
|
20
20
|
import { $createImageNode } from '../image/ImageNode.js';
|
|
@@ -36,7 +36,8 @@ import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX, ZOOM_LEVELS_
|
|
|
36
36
|
import { setStyleProperty } from '../utils/style.js';
|
|
37
37
|
import { useStrings } from '../i18n/useStrings.js';
|
|
38
38
|
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
39
|
-
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconArrowsJoin, IconBold, IconClearFormatting, IconDeviceFloppy, IconChevronDown, IconChevronUp, IconDots, IconHelpCircle, IconHistory, IconExternalLink, IconFeather, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconList, IconListCheck, IconListNumbers, IconPageBreak, IconPencil, IconPhoto, IconPilcrow, IconQuote, IconSeparatorHorizontal, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconArrowMergeBoth, IconArrowsSplit2, IconColumnInsertLeft, IconColumnInsertRight, IconColumnRemove, IconColumns, IconClipboard, IconCopy, IconCut, IconSearch, IconSelectAll, IconColumns1, IconColumns2, IconNumber1Small, IconRowInsertBottom, IconRowInsertTop, IconRowRemove,
|
|
39
|
+
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconArrowsJoin, IconBold, IconClearFormatting, IconDeviceFloppy, IconBook, IconChevronDown, IconChevronUp, IconDots, IconHelpCircle, IconHistory, IconExternalLink, IconFeather, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconList, IconListCheck, IconListNumbers, IconPageBreak, IconPencil, IconPhoto, IconPilcrow, IconQuote, IconSeparatorHorizontal, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconArrowMergeBoth, IconArrowsSplit2, IconColumnInsertLeft, IconColumnInsertRight, IconColumnRemove, IconColumns, IconClipboard, IconCopy, IconCut, IconSearch, IconSelectAll, IconColumns1, IconColumns2, IconNumber1Small, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconArrowsShuffle, IconTable, IconTableMinus, IconTableOptions, IconMinus, IconPlus, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
|
|
40
|
+
import { IconCoupletCenterNarrow, IconCoupletCenterWide, IconCoupletDelete, IconCoupletGutterNarrow, IconCoupletGutterWide, IconCoupletSpacingLoose, IconCoupletSpacingTight, IconCoupletStaggerNarrow, IconCoupletStaggerWide, IconPoetryDelete, } from '../icons/poetryIcons.js';
|
|
40
41
|
/** Sentinel for "no inline style": Lexical returns the default it is given when
|
|
41
42
|
* the selection has none, so this tells "unstyled" (show the canvas default)
|
|
42
43
|
* apart from "styled with a value the dropdown doesn't list" (show nothing). */
|
|
@@ -65,6 +66,23 @@ const INITIAL_STATE = {
|
|
|
65
66
|
};
|
|
66
67
|
const ICON_SIZE = 17;
|
|
67
68
|
const ICON_STROKE = 1.75;
|
|
69
|
+
/** The right-click menu: compact items, and a gap kept between it and the edge of the window. */
|
|
70
|
+
const CONTEXT_MENU_WIDTH = 220;
|
|
71
|
+
const CONTEXT_MENU_VIEWPORT_PADDING = 8;
|
|
72
|
+
/** `likhari-context-menu` (editor.css) shrinks the icons to match the smaller text. */
|
|
73
|
+
const CONTEXT_MENU_CLASS_NAMES = { dropdown: 'likhari-context-menu' };
|
|
74
|
+
/** The submenus: opened to the side, flipped or shifted to stay in the window, and positioned against the window (`fixed`) so
|
|
75
|
+
* the menu's own scrolling does not clip them. */
|
|
76
|
+
const CONTEXT_SUBMENU_PROPS = {
|
|
77
|
+
floatingStrategy: 'fixed',
|
|
78
|
+
middlewares: { flip: true, shift: { crossAxis: true, padding: CONTEXT_MENU_VIEWPORT_PADDING }, size: { padding: CONTEXT_MENU_VIEWPORT_PADDING } },
|
|
79
|
+
};
|
|
80
|
+
const CONTEXT_MENU_STYLES = {
|
|
81
|
+
dropdown: { overflowY: 'auto', padding: 4 },
|
|
82
|
+
item: { fontSize: 'var(--mantine-font-size-xs)', padding: '4px 8px', minHeight: 0 },
|
|
83
|
+
label: { fontSize: 'var(--mantine-font-size-xs)', padding: '2px 8px' },
|
|
84
|
+
divider: { margin: '3px 0' },
|
|
85
|
+
};
|
|
68
86
|
/** The CSS Custom Highlight name; editor.css paints it like a native text selection. */
|
|
69
87
|
const SELECTION_HIGHLIGHT = 'likhari-toolbar-selection';
|
|
70
88
|
/** Paints the current native selection through the CSS Custom Highlight API, so it
|
|
@@ -172,16 +190,25 @@ function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeh
|
|
|
172
190
|
function MoreMenuItem({ icon: Icon, label, active, onClick, }) {
|
|
173
191
|
return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
|
|
174
192
|
}
|
|
175
|
-
/**
|
|
193
|
+
/** A menu item's keyboard shortcut, shown dimmed at its trailing edge. Hidden from
|
|
194
|
+
* assistive tech (the item's `aria-keyshortcuts` carries that instead), so it never
|
|
195
|
+
* changes the item's accessible name. */
|
|
196
|
+
function ShortcutHint({ keys }) {
|
|
197
|
+
return (_jsx("span", { "aria-hidden": "true", className: "likhari-context-shortcut", children: keys }));
|
|
198
|
+
}
|
|
199
|
+
/** One command, as a menu item, a ribbon button, or an icon-only menu item (`icons`) — in every case the label becomes its tooltip and accessible name except in a labelled menu. */
|
|
176
200
|
function CommandItem({ mode, icon: Icon, label, onClick, disabled, color, }) {
|
|
177
201
|
if (mode === 'ribbon') {
|
|
178
202
|
return _jsx(ToolbarButton, { icon: Icon, title: label, disabled: disabled, "data-danger": color === 'red' ? 'true' : undefined, onClick: onClick });
|
|
179
203
|
}
|
|
204
|
+
if (mode === 'icons') {
|
|
205
|
+
return (_jsx(Menu.Item, { "aria-label": label, title: label, color: color, disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }) }));
|
|
206
|
+
}
|
|
180
207
|
return (_jsx(Menu.Item, { color: color, disabled: disabled, leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
|
|
181
208
|
}
|
|
182
|
-
/** A group heading in a menu; the ribbon's tab already
|
|
183
|
-
const ItemLabel = ({ mode, children }) => (mode === '
|
|
184
|
-
const ItemDivider = ({ mode }) =>
|
|
209
|
+
/** A group heading in a menu; the ribbon's tab, a submenu's parent item, and an icon row's own aria-label already name the group, so they show nothing. */
|
|
210
|
+
const ItemLabel = ({ mode, children }) => (mode === 'menu' ? _jsx(Menu.Label, { children: children }) : null);
|
|
211
|
+
const ItemDivider = ({ mode }) => mode === 'ribbon' ? _jsx(MantineToolbar.Divider, {}) : mode === 'icons' ? _jsx("span", { className: "likhari-context-icons-divider" }) : _jsx(Menu.Divider, {});
|
|
185
212
|
function LinkMenuItems({ url, onEdit, onRemove, strings, mode = 'menu' }) {
|
|
186
213
|
// Only offer the link as clickable if it's a URL the editor would have accepted.
|
|
187
214
|
const safeUrl = normalizeLinkUrl(url);
|
|
@@ -199,7 +226,14 @@ function TableMenuItems({ strings, direction, rows, columns, canMergeCells, canU
|
|
|
199
226
|
const manyRows = rows > 1;
|
|
200
227
|
const manyColumns = columns > 1;
|
|
201
228
|
const item = (icon, label, action, color) => (_jsx(CommandItem, { mode: mode, icon: icon, label: label, onClick: onAction(action), color: color }));
|
|
202
|
-
|
|
229
|
+
// `icons` mode leaves "Delete table" out of the icon row — it comes back as a full
|
|
230
|
+
// labelled item below, since a single trash icon among several would be too easy to
|
|
231
|
+
// hit by mistake (same treatment as "Delete poetry" in PoetryMenuItems).
|
|
232
|
+
const rowItems = (_jsxs(_Fragment, { children: [_jsx(ItemLabel, { mode: mode, children: t.menuLabel }), item(IconRowInsertTop, manyRows ? t.insertRowsBefore : t.insertRowBefore, () => $insertTableRows(false)), item(IconRowInsertBottom, manyRows ? t.insertRowsAfter : t.insertRowAfter, () => $insertTableRows(true)), item(rtl ? IconColumnInsertRight : IconColumnInsertLeft, manyColumns ? t.insertColumnsBefore : t.insertColumnBefore, () => $insertTableColumns(false)), item(rtl ? IconColumnInsertLeft : IconColumnInsertRight, manyColumns ? t.insertColumnsAfter : t.insertColumnAfter, () => $insertTableColumns(true)), (canMergeCells || canUnmergeCell) && _jsx(ItemDivider, { mode: mode }), canMergeCells && item(IconArrowMergeBoth, t.mergeCells, $mergeTableCells), canUnmergeCell && item(IconArrowsSplit2, t.unmergeCell, $unmergeTableCell), _jsx(ItemDivider, { mode: mode }), item(IconRowRemove, manyRows ? t.deleteRows : t.deleteRow, $deleteTableRows), item(IconColumnRemove, manyColumns ? t.deleteColumns : t.deleteColumn, $deleteTableColumns), mode !== 'icons' && item(IconTableMinus, t.deleteTable, $deleteTable, 'red')] }));
|
|
233
|
+
if (mode === 'icons') {
|
|
234
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "likhari-context-icons", role: "group", "aria-label": t.menuLabel, children: rowItems }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconTableMinus, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onAction($deleteTable), children: t.deleteTable })] }));
|
|
235
|
+
}
|
|
236
|
+
return rowItems;
|
|
203
237
|
}
|
|
204
238
|
/** Whether a one-notch step from `value` stays on the scale — a step that
|
|
205
239
|
* would fall off either end is disabled rather than silently clamped. */
|
|
@@ -215,7 +249,20 @@ function PoetryMenuItems({ strings, layout, scale, centered, onAction, mode = 'm
|
|
|
215
249
|
if (node)
|
|
216
250
|
$setPoetryLayout(node, next);
|
|
217
251
|
};
|
|
218
|
-
|
|
252
|
+
// Which layout applies (single/two-column/staggered), kept apart from what that layout's
|
|
253
|
+
// selection then allows (insert, spacing, centering, delete) — two rows of icon buttons
|
|
254
|
+
// in `icons` mode, one heading-less list everywhere else.
|
|
255
|
+
const layoutItems = (_jsxs(_Fragment, { children: [item(IconColumns1, t.singleColumn, setLayout('single'), layout === 'single'), item(IconColumns2, t.twoColumn, setLayout('two-column'), layout === 'two-column'), item(IconArrowsShuffle, t.staggered, setLayout('staggered'), layout === 'staggered')] }));
|
|
256
|
+
// `icons` mode puts a divider between each group of similar actions (insert /
|
|
257
|
+
// spacing / this layout's own adjustments), and leaves "Delete poetry" out —
|
|
258
|
+
// it comes back as a full labelled item below, since a single trash icon in a
|
|
259
|
+
// row of several would be too easy to hit by mistake.
|
|
260
|
+
const groupDivider = mode === 'icons' ? _jsx(ItemDivider, { mode: mode }) : null;
|
|
261
|
+
const actionItems = (_jsxs(_Fragment, { children: [item(IconRowInsertTop, t.insertCoupletBefore, () => $insertCoupletRelativeToSelection('before')), item(IconRowInsertBottom, t.insertCoupletAfter, () => $insertCoupletRelativeToSelection('after')), groupDivider, item(IconCoupletSpacingTight, t.tighterSpacing, () => $adjustPoetrySpacing(-1), !canStep(POETRY_SPACINGS, scale.spacing, -1)), item(IconCoupletSpacingLoose, t.looserSpacing, () => $adjustPoetrySpacing(1), !canStep(POETRY_SPACINGS, scale.spacing, 1)), layout === 'staggered' && (_jsxs(_Fragment, { children: [groupDivider, item(IconCoupletStaggerNarrow, t.narrowerCouplets, () => $adjustPoetryStagger(-1), !canStep(POETRY_STAGGERS, scale.stagger, -1)), item(IconCoupletStaggerWide, t.widerCouplets, () => $adjustPoetryStagger(1), !canStep(POETRY_STAGGERS, scale.stagger, 1))] })), layout === 'two-column' && (_jsxs(_Fragment, { children: [groupDivider, item(IconAlignCenter, centered ? t.uncenterCouplet : t.centerCouplet, () => $setCoupletCentered(!centered)), centered && (_jsxs(_Fragment, { children: [item(IconCoupletCenterNarrow, t.narrowerCenter, () => $adjustPoetryCenterWidth(-1), !canStep(POETRY_CENTER_WIDTHS, scale.centerWidth, -1)), item(IconCoupletCenterWide, t.widerCenter, () => $adjustPoetryCenterWidth(1), !canStep(POETRY_CENTER_WIDTHS, scale.centerWidth, 1))] })), item(IconCoupletGutterNarrow, t.narrowerGutter, () => $adjustPoetryGutter(-1), !canStep(POETRY_GUTTERS, scale.gutter, -1)), item(IconCoupletGutterWide, t.widerGutter, () => $adjustPoetryGutter(1), !canStep(POETRY_GUTTERS, scale.gutter, 1))] })), groupDivider, item(IconCoupletDelete, t.deleteCouplet, $deletePoetryCouplet, false, 'red'), mode !== 'icons' && item(IconPoetryDelete, t.deletePoetry, $deletePoetryBlock, false, 'red')] }));
|
|
262
|
+
if (mode === 'icons') {
|
|
263
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "likhari-context-icon-rows", role: "group", "aria-label": t.menuLabel, children: [_jsx("div", { className: "likhari-context-icons", children: layoutItems }), _jsx("div", { className: "likhari-context-icon-rows-divider" }), _jsx("div", { className: "likhari-context-icons", children: actionItems })] }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconPoetryDelete, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onAction($deletePoetryBlock), children: t.deletePoetry })] }));
|
|
264
|
+
}
|
|
265
|
+
return (_jsxs(_Fragment, { children: [_jsx(ItemLabel, { mode: mode, children: t.menuLabel }), layoutItems, _jsx(ItemDivider, { mode: mode }), _jsx(ItemDivider, { mode: mode }), actionItems] }));
|
|
219
266
|
}
|
|
220
267
|
/** `docs/markdown-dialect.md` at the repository's default branch — the Markdown
|
|
221
268
|
* format this editor reads and writes, including the poetry container syntax. */
|
|
@@ -635,11 +682,16 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
|
|
|
635
682
|
block.selectStart();
|
|
636
683
|
}
|
|
637
684
|
}
|
|
638
|
-
if (point)
|
|
639
|
-
|
|
685
|
+
if (point) {
|
|
686
|
+
const atPoint = $wordAtDomPoint(point.node, point.offset);
|
|
687
|
+
// A selection that reaches beyond this one word (several words, a whole line, several
|
|
688
|
+
// lines) means the user is after something else entirely: no spelling fixes or
|
|
689
|
+
// synonyms for a word that's merely under the pointer, not what's actually selected.
|
|
690
|
+
if (atPoint && $selectionIsWithinWord(atPoint))
|
|
691
|
+
word = atPoint;
|
|
692
|
+
}
|
|
640
693
|
if (!cell && !couplet && config.joinLines) {
|
|
641
|
-
menu.text =
|
|
642
|
-
menu.canJoin = $canJoinLines();
|
|
694
|
+
menu.text = $canJoinLines();
|
|
643
695
|
}
|
|
644
696
|
}, { discrete: true });
|
|
645
697
|
const language = spellCheckLanguage !== 'auto' ? spellCheckLanguage : word?.direction === 'rtl' ? 'ur' : 'en';
|
|
@@ -655,6 +707,14 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
|
|
|
655
707
|
}
|
|
656
708
|
}
|
|
657
709
|
}
|
|
710
|
+
// A misspelled word's menu is its spelling fixes and the clipboard, nothing else: no synonyms to look up either.
|
|
711
|
+
if (menu.spelling) {
|
|
712
|
+
menu.thesaurus = undefined;
|
|
713
|
+
menu.link = undefined;
|
|
714
|
+
menu.table = undefined;
|
|
715
|
+
menu.poetry = undefined;
|
|
716
|
+
menu.text = undefined;
|
|
717
|
+
}
|
|
658
718
|
setContextMenu(menu);
|
|
659
719
|
if (word && menu.thesaurus) {
|
|
660
720
|
// Synonyms can take a while (the first WordNet lookup downloads the database), so the menu
|
|
@@ -708,12 +768,14 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
|
|
|
708
768
|
}, { discrete: true });
|
|
709
769
|
editor.focus();
|
|
710
770
|
};
|
|
711
|
-
// Ctrl/Cmd+K opens the link dialog.
|
|
771
|
+
// Ctrl/Cmd+K opens the link dialog. Matched on event.code (the physical key),
|
|
772
|
+
// not event.key, so it still fires while typing in Urdu or another non-Latin
|
|
773
|
+
// layout, where event.key would be whatever character that layout produces.
|
|
712
774
|
useEffect(() => {
|
|
713
775
|
if (!config.links)
|
|
714
776
|
return;
|
|
715
777
|
return editor.registerCommand(KEY_MODIFIER_COMMAND, (event) => {
|
|
716
|
-
if (event.
|
|
778
|
+
if (event.code !== 'KeyK' || !(event.ctrlKey || event.metaKey))
|
|
717
779
|
return false;
|
|
718
780
|
event.preventDefault();
|
|
719
781
|
openLinkDialog();
|
|
@@ -894,9 +956,82 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
|
|
|
894
956
|
}, { discrete: true });
|
|
895
957
|
editor.focus();
|
|
896
958
|
};
|
|
959
|
+
/** Runs `action` on the misspelled word at the caret — the same check the right-click
|
|
960
|
+
* menu makes before offering Ignore / Add to dictionary / Add to auto-correct, so the
|
|
961
|
+
* matching keyboard shortcuts only fire where those menu items would actually appear. */
|
|
962
|
+
const wordActionAtCaret = async (action) => {
|
|
963
|
+
if (!config.language.spellCheck)
|
|
964
|
+
return;
|
|
965
|
+
const word = editor.getEditorState().read(() => $wordAtSelection());
|
|
966
|
+
if (!word)
|
|
967
|
+
return;
|
|
968
|
+
const language = spellCheckLanguage !== 'auto' ? spellCheckLanguage : word.direction === 'rtl' ? 'ur' : 'en';
|
|
969
|
+
const speller = getSpeller(language);
|
|
970
|
+
if (!speller)
|
|
971
|
+
return;
|
|
972
|
+
const checked = await speller;
|
|
973
|
+
if (checked.correct(word.word))
|
|
974
|
+
return;
|
|
975
|
+
await action(language, word.word);
|
|
976
|
+
};
|
|
977
|
+
const ignoreWordAtCaret = () => {
|
|
978
|
+
void wordActionAtCaret((language, word) => ignoreWord(language, word));
|
|
979
|
+
};
|
|
980
|
+
const addWordAtCaretToDictionary = () => {
|
|
981
|
+
void wordActionAtCaret(async (language, word) => {
|
|
982
|
+
const added = await addUserWord(dictionaryStores, language, word);
|
|
983
|
+
if (!added)
|
|
984
|
+
ignoreWord(language, word);
|
|
985
|
+
});
|
|
986
|
+
};
|
|
987
|
+
const addWordAtCaretToAutoCorrect = () => {
|
|
988
|
+
if (!onAddAutoCorrect || !config.language.autocorrect)
|
|
989
|
+
return;
|
|
990
|
+
void wordActionAtCaret((language, word) => onAddAutoCorrect(word, language));
|
|
991
|
+
};
|
|
992
|
+
// Keyboard shortcuts mirroring the right-click menu: Ctrl/Cmd+Alt+J joins the
|
|
993
|
+
// selected lines, and Ctrl/Cmd+Alt+I/D/A act on the misspelled word at the caret
|
|
994
|
+
// the way the menu's Ignore / Add to dictionary / Add to auto-correct items do.
|
|
995
|
+
// Matched on event.code (the physical key), not event.key, so they still fire
|
|
996
|
+
// while typing in Urdu or another non-Latin layout, where event.key would be
|
|
997
|
+
// whatever character that layout produces rather than "j"/"i"/"d"/"a".
|
|
998
|
+
useEffect(() => {
|
|
999
|
+
return editor.registerCommand(KEY_MODIFIER_COMMAND, (event) => {
|
|
1000
|
+
if (!(event.ctrlKey || event.metaKey) || !event.altKey)
|
|
1001
|
+
return false;
|
|
1002
|
+
switch (event.code) {
|
|
1003
|
+
case 'KeyJ':
|
|
1004
|
+
if (!config.joinLines)
|
|
1005
|
+
return false;
|
|
1006
|
+
event.preventDefault();
|
|
1007
|
+
joinLines();
|
|
1008
|
+
return true;
|
|
1009
|
+
case 'KeyI':
|
|
1010
|
+
if (!config.language.spellCheck)
|
|
1011
|
+
return false;
|
|
1012
|
+
event.preventDefault();
|
|
1013
|
+
ignoreWordAtCaret();
|
|
1014
|
+
return true;
|
|
1015
|
+
case 'KeyD':
|
|
1016
|
+
if (!config.language.spellCheck)
|
|
1017
|
+
return false;
|
|
1018
|
+
event.preventDefault();
|
|
1019
|
+
addWordAtCaretToDictionary();
|
|
1020
|
+
return true;
|
|
1021
|
+
case 'KeyA':
|
|
1022
|
+
if (!config.language.spellCheck || !onAddAutoCorrect || !config.language.autocorrect)
|
|
1023
|
+
return false;
|
|
1024
|
+
event.preventDefault();
|
|
1025
|
+
addWordAtCaretToAutoCorrect();
|
|
1026
|
+
return true;
|
|
1027
|
+
default:
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
}, COMMAND_PRIORITY_NORMAL);
|
|
1031
|
+
});
|
|
897
1032
|
// The tools group is always built, even when every command in it is off, so the ribbon asks first.
|
|
898
1033
|
const hasToolsGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck || config.joinLines;
|
|
899
|
-
const toolsSection = (_jsxs(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: [config.language.autocorrect && (_jsx(ToolbarButton, { icon: IconWand, title: strings.toolbar.autocorrect, active: autoCorrectOpen, onClick: onToggleAutoCorrect })), config.language.textCleanup && (_jsx(ToolbarButton, { icon: IconSparkles, title: strings.autoCorrect.correctDocument, onClick: () => editor.dispatchCommand(CORRECT_DOCUMENT_COMMAND, undefined) })), config.language.spellCheck && (_jsx(ToolbarButton, { icon: IconAbc, title: strings.toolbar.spellChecker, active: spellOpen, onClick: onToggleSpell })), config.joinLines && (_jsx(ToolbarButton, { icon: IconArrowsJoin, title: strings.toolbar.joinLines,
|
|
1034
|
+
const toolsSection = (_jsxs(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: [config.language.autocorrect && (_jsx(ToolbarButton, { icon: IconWand, title: strings.toolbar.autocorrect, active: autoCorrectOpen, onClick: onToggleAutoCorrect })), config.language.textCleanup && (_jsx(ToolbarButton, { icon: IconSparkles, title: strings.autoCorrect.correctDocument, onClick: () => editor.dispatchCommand(CORRECT_DOCUMENT_COMMAND, undefined) })), config.language.spellCheck && (_jsx(ToolbarButton, { icon: IconAbc, title: strings.toolbar.spellChecker, active: spellOpen, onClick: onToggleSpell })), config.joinLines && state.canJoinLines && (_jsx(ToolbarButton, { icon: IconArrowsJoin, title: strings.toolbar.joinLines, onClick: joinLines }))] }, "tools"));
|
|
900
1035
|
// Uniform mode's zoom: [-] 100% [+], stepping through the preset levels; the
|
|
901
1036
|
// percentage resets to 100%. Always laid out left-to-right, like a number.
|
|
902
1037
|
const showZoom = uniform && config.font.size;
|
|
@@ -989,14 +1124,23 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
|
|
|
989
1124
|
const contextMenuEl = (_jsxs(Menu, { opened: contextMenu !== null, onChange: (opened) => {
|
|
990
1125
|
if (!opened)
|
|
991
1126
|
setContextMenu(null);
|
|
992
|
-
}, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width:
|
|
1127
|
+
}, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: CONTEXT_MENU_WIDTH,
|
|
1128
|
+
// Stays inside the visible area: flips above the click or shifts sideways when it fits that
|
|
1129
|
+
// way, and otherwise is cut to the room there is and scrolls (a long menu in a short window).
|
|
1130
|
+
middlewares: { flip: true, shift: true, size: { padding: CONTEXT_MENU_VIEWPORT_PADDING } },
|
|
1131
|
+
// Positioned against the window, not the editor: the menu then takes no room in the editor's layout (so it can
|
|
1132
|
+
// never grow a scroll bar there or make the text jump) and is not cut off at the editor's edge. No open
|
|
1133
|
+
// animation either, as its transform would make the submenus position against the menu instead.
|
|
1134
|
+
floatingStrategy: "fixed", transitionProps: { duration: 0 }, styles: CONTEXT_MENU_STYLES, classNames: CONTEXT_MENU_CLASS_NAMES, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: contextMenu?.x ?? -9999, top: contextMenu?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsxs(Menu.Dropdown, { children: [contextMenu?.spelling ? (
|
|
1135
|
+
// A misspelled word gets its fixes and the clipboard row, and nothing else.
|
|
1136
|
+
_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: strings.contextMenu.spelling }), contextMenu.spelling.suggestions.length === 0 && _jsx(Menu.Item, { disabled: true, children: strings.contextMenu.noSuggestions }), contextMenu.spelling.suggestions.map((suggestion) => (_jsx(Menu.Item, { onClick: () => {
|
|
993
1137
|
const match = contextMenu.spelling.match;
|
|
994
1138
|
editor.update(() => $replaceMatch(match, suggestion), { discrete: true });
|
|
995
1139
|
editor.focus();
|
|
996
|
-
}, children: suggestion }, suggestion))), _jsx(Menu.Item, { onClick: () => {
|
|
1140
|
+
}, children: suggestion }, suggestion))), _jsx(Menu.Item, { rightSection: _jsx(ShortcutHint, { keys: "Ctrl+Alt+I" }), "aria-keyshortcuts": "Control+Alt+I", onClick: () => {
|
|
997
1141
|
ignoreWord(contextMenu.spelling.language, contextMenu.spelling.word);
|
|
998
1142
|
setContextMenu(null);
|
|
999
|
-
}, children: strings.contextMenu.ignore }), _jsx(Menu.Item, { onClick: () => {
|
|
1143
|
+
}, children: strings.contextMenu.ignore }), _jsx(Menu.Item, { rightSection: _jsx(ShortcutHint, { keys: "Ctrl+Alt+D" }), "aria-keyshortcuts": "Control+Alt+D", onClick: () => {
|
|
1000
1144
|
const { language, word } = contextMenu.spelling;
|
|
1001
1145
|
// Without a writable store, the word is ignored for this session instead.
|
|
1002
1146
|
void addUserWord(dictionaryStores, language, word).then((added) => {
|
|
@@ -1004,19 +1148,25 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
|
|
|
1004
1148
|
ignoreWord(language, word);
|
|
1005
1149
|
});
|
|
1006
1150
|
setContextMenu(null);
|
|
1007
|
-
}, children: strings.contextMenu.addToDictionary }), onAddAutoCorrect && config.language.autocorrect && (_jsx(Menu.Item, { onClick: () => {
|
|
1151
|
+
}, children: strings.contextMenu.addToDictionary }), onAddAutoCorrect && config.language.autocorrect && (_jsx(Menu.Item, { rightSection: _jsx(ShortcutHint, { keys: "Ctrl+Alt+A" }), "aria-keyshortcuts": "Control+Alt+A", onClick: () => {
|
|
1008
1152
|
onAddAutoCorrect(contextMenu.spelling.word, contextMenu.spelling.language);
|
|
1009
1153
|
setContextMenu(null);
|
|
1010
|
-
}, children: strings.contextMenu.addToAutoCorrect })), _jsx(Menu.Divider, {})] }))
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1154
|
+
}, children: strings.contextMenu.addToAutoCorrect })), _jsx(Menu.Divider, {})] })) : (
|
|
1155
|
+
// Everything that applies to what is under the pointer, each kind of content behind its own submenu.
|
|
1156
|
+
_jsxs(_Fragment, { children: [contextMenu?.thesaurus && contextMenu.thesaurus.items?.length !== 0 && (_jsxs(Menu.Sub, { ...CONTEXT_SUBMENU_PROPS, children: [_jsx(Menu.Sub.Target, { children: _jsx(Menu.Sub.Item, { leftSection: _jsx(IconBook, { size: ICON_SIZE, stroke: ICON_STROKE }), children: strings.thesaurus.synonyms }) }), _jsxs(Menu.Sub.Dropdown, { children: [contextMenu.thesaurus.items === undefined && _jsx(Menu.Item, { disabled: true, children: strings.thesaurus.loading }), contextMenu.thesaurus.items?.map((synonym) => (_jsx(Menu.Item, { onClick: () => {
|
|
1157
|
+
const match = contextMenu.thesaurus.match;
|
|
1158
|
+
editor.update(() => $replaceMatch(match, synonym), { discrete: true });
|
|
1159
|
+
editor.focus();
|
|
1160
|
+
}, children: synonym }, synonym)))] })] })), contextMenu?.link !== undefined && (_jsxs(Menu.Sub, { ...CONTEXT_SUBMENU_PROPS, children: [_jsx(Menu.Sub.Target, { children: _jsx(Menu.Sub.Item, { leftSection: _jsx(IconLink, { size: ICON_SIZE, stroke: ICON_STROKE }), children: strings.link.menuLabel }) }), _jsx(Menu.Sub.Dropdown, { children: _jsx(LinkMenuItems, { mode: "submenu", url: contextMenu.link, onEdit: () => {
|
|
1161
|
+
restoreSelection();
|
|
1162
|
+
openLinkDialog();
|
|
1163
|
+
}, onRemove: removeLink, strings: strings }) })] })), contextMenu?.table && (_jsx(TableMenuItems, { mode: "icons", strings: strings, direction: direction, rows: contextMenu.table.rows, columns: contextMenu.table.columns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction })), contextMenu?.poetry && (_jsx(PoetryMenuItems, { mode: "icons", strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction })), contextMenu?.text && (_jsx(Menu.Item, { leftSection: _jsx(IconArrowsJoin, { size: ICON_SIZE, stroke: ICON_STROKE }), rightSection: _jsx(ShortcutHint, { keys: "Ctrl+Alt+J" }), "aria-keyshortcuts": "Control+Alt+J", onClick: runMenuAction(() => {
|
|
1018
1164
|
$joinSelectedLines();
|
|
1019
|
-
}), children: strings.contextMenu.joinLines })
|
|
1165
|
+
}), children: strings.contextMenu.joinLines })), ((contextMenu?.thesaurus && contextMenu.thesaurus.items?.length !== 0) ||
|
|
1166
|
+
contextMenu?.link !== undefined ||
|
|
1167
|
+
contextMenu?.table ||
|
|
1168
|
+
contextMenu?.poetry ||
|
|
1169
|
+
contextMenu?.text) && _jsx(Menu.Divider, {})] })), _jsxs("div", { className: "likhari-context-row", role: "group", children: [_jsx(Menu.Item, { "aria-label": strings.contextMenu.cut, title: strings.contextMenu.cut, onClick: () => void copyOrCut(true)(), children: _jsx(IconCut, { size: ICON_SIZE, stroke: ICON_STROKE }) }), _jsx(Menu.Item, { "aria-label": strings.contextMenu.copy, title: strings.contextMenu.copy, onClick: () => void copyOrCut(false)(), children: _jsx(IconCopy, { size: ICON_SIZE, stroke: ICON_STROKE }) }), _jsx(Menu.Item, { "aria-label": strings.contextMenu.paste, title: strings.contextMenu.paste, onClick: () => void paste(), children: _jsx(IconClipboard, { size: ICON_SIZE, stroke: ICON_STROKE }) })] }), !contextMenu?.spelling && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsx(Menu.Item, { leftSection: _jsx(IconSelectAll, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: runMenuAction(() => $selectAll()), children: strings.contextMenu.selectAll })] }))] })] }));
|
|
1020
1170
|
const dialogsEl = (_jsxs(_Fragment, { children: [(config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.tables && _jsx(TableDialog, { opened: tableDialogOpen, onSubmit: insertTable, onClose: closeTableDialog }), config.columns && _jsx(LayoutDialog, { opened: layoutDialogOpen, onSubmit: insertLayout, onClose: closeLayoutDialog }), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog })), drafts && _jsx(DraftsDialog, { opened: draftsOpen, locale: locale, onClose: () => setDraftsOpen(false), ...drafts })] }));
|
|
1021
1171
|
// The ribbon layout (UI spec §3.4): the same command groups as above, on tabs.
|
|
1022
1172
|
if (ribbon) {
|
package/dist/i18n/strings.js
CHANGED
|
@@ -51,7 +51,7 @@ const en = {
|
|
|
51
51
|
autocorrect: 'Auto-correct',
|
|
52
52
|
textCleanup: 'Text cleanup',
|
|
53
53
|
spellChecker: 'Spell-checker',
|
|
54
|
-
joinLines: 'Join lines',
|
|
54
|
+
joinLines: 'Join lines (Ctrl+Alt+J)',
|
|
55
55
|
moreFormatting: 'More formatting',
|
|
56
56
|
strikethrough: 'Strikethrough',
|
|
57
57
|
superscript: 'Superscript',
|
|
@@ -349,7 +349,7 @@ const ur = {
|
|
|
349
349
|
autocorrect: 'خودکار تصحیح',
|
|
350
350
|
textCleanup: 'متن کی صفائی',
|
|
351
351
|
spellChecker: 'ہجے چیک کرنے والا',
|
|
352
|
-
joinLines: 'سطریں ملائیں',
|
|
352
|
+
joinLines: 'سطریں ملائیں (Ctrl+Alt+J)',
|
|
353
353
|
moreFormatting: 'مزید فارمیٹنگ',
|
|
354
354
|
strikethrough: 'خط زدہ',
|
|
355
355
|
superscript: 'بالا نویس',
|
|
@@ -647,7 +647,7 @@ const paShahmukhi = {
|
|
|
647
647
|
autocorrect: 'خودکار درستی',
|
|
648
648
|
textCleanup: 'متن دی صفائی',
|
|
649
649
|
spellChecker: 'ہجے چیکر',
|
|
650
|
-
joinLines: 'سطراں ملاؤ',
|
|
650
|
+
joinLines: 'سطراں ملاؤ (Ctrl+Alt+J)',
|
|
651
651
|
moreFormatting: 'ہور فارمیٹنگ',
|
|
652
652
|
strikethrough: 'کٹی لکیر',
|
|
653
653
|
superscript: 'اُتلا لکھت',
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IconProps } from '@tabler/icons-react';
|
|
2
|
+
/** A trash can with a single line inside it: delete this one couplet. */
|
|
3
|
+
export declare const IconCoupletDelete: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
4
|
+
/** A trash can with three lines inside it: delete every couplet in the block. */
|
|
5
|
+
export declare const IconPoetryDelete: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
6
|
+
/** Two full-width rows pinched together: tighten the vertical gap between couplets. */
|
|
7
|
+
export declare const IconCoupletSpacingTight: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
8
|
+
/** Two full-width rows pulled apart: loosen the vertical gap between couplets. */
|
|
9
|
+
export declare const IconCoupletSpacingLoose: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
10
|
+
/** Two staggered (alternating-side) rows pinched together: narrow the stagger offset. */
|
|
11
|
+
export declare const IconCoupletStaggerNarrow: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
12
|
+
/** Two staggered (alternating-side) rows pulled apart: widen the stagger offset. */
|
|
13
|
+
export declare const IconCoupletStaggerWide: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
14
|
+
/** Four column rails, the inner pair pinched together: narrow the center column. */
|
|
15
|
+
export declare const IconCoupletCenterNarrow: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
16
|
+
/** Four column rails, the inner pair pulled apart: widen the center column. */
|
|
17
|
+
export declare const IconCoupletCenterWide: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
18
|
+
/** Two column rails pinched together: narrow the gutter between the columns. */
|
|
19
|
+
export declare const IconCoupletGutterNarrow: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
20
|
+
/** Two column rails pulled apart: widen the gutter between the columns. */
|
|
21
|
+
export declare const IconCoupletGutterWide: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
22
|
+
//# sourceMappingURL=poetryIcons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poetryIcons.d.ts","sourceRoot":"","sources":["../../src/icons/poetryIcons.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AA2CrD,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,qGAK5B,CAAC;AAEH,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,qGAO3B,CAAC;AAEH,uFAAuF;AACvF,eAAO,MAAM,uBAAuB,qGAKlC,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,qGAKlC,CAAC;AAEH,yFAAyF;AACzF,eAAO,MAAM,wBAAwB,qGAKnC,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,qGAKjC,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,uBAAuB,qGAOlC,CAAC;AAEH,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,qGAOhC,CAAC;AAEH,gFAAgF;AAChF,eAAO,MAAM,uBAAuB,qGAKlC,CAAC;AAEH,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,qGAKhC,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* A handful of icons for poetry-couplet actions that @tabler/icons-react has no
|
|
5
|
+
* distinct enough pair for: deleting one couplet vs. the whole block (both would
|
|
6
|
+
* otherwise be a plain `IconTrash`), the couplet's row spacing vs. its staggered
|
|
7
|
+
* horizontal offset, and the two-column layout's center width vs. its gutter
|
|
8
|
+
* (all four would otherwise reuse the same arrows-in/arrows-out icon). Built in
|
|
9
|
+
* the same shape as a Tabler icon — 24x24, outline, round caps and joins — so
|
|
10
|
+
* they sit in the toolbar and context menu without looking out of place.
|
|
11
|
+
*/
|
|
12
|
+
function createPoetryIcon(name, paths) {
|
|
13
|
+
const Component = forwardRef(function PoetryIcon({ color = 'currentColor', size = 24, stroke = 2, title, className, ...rest }, ref) {
|
|
14
|
+
return (_jsxs("svg", { ref: ref, xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round", className: ['likhari-icon', `likhari-icon-${name}`, className].filter(Boolean).join(' '), ...rest, children: [title && _jsx("title", { children: title }), _jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), paths.map((d, index) => (_jsx("path", { d: d }, index)))] }));
|
|
15
|
+
});
|
|
16
|
+
Component.displayName = name;
|
|
17
|
+
return Component;
|
|
18
|
+
}
|
|
19
|
+
/** A trash can with a single line inside it: delete this one couplet. */
|
|
20
|
+
export const IconCoupletDelete = createPoetryIcon('couplet-delete', [
|
|
21
|
+
'M4 7h16',
|
|
22
|
+
'M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3',
|
|
23
|
+
'M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12',
|
|
24
|
+
'M12 11v6',
|
|
25
|
+
]);
|
|
26
|
+
/** A trash can with three lines inside it: delete every couplet in the block. */
|
|
27
|
+
export const IconPoetryDelete = createPoetryIcon('poetry-delete', [
|
|
28
|
+
'M4 7h16',
|
|
29
|
+
'M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3',
|
|
30
|
+
'M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12',
|
|
31
|
+
'M9 11v6',
|
|
32
|
+
'M12 11v6',
|
|
33
|
+
'M15 11v6',
|
|
34
|
+
]);
|
|
35
|
+
/** Two full-width rows pinched together: tighten the vertical gap between couplets. */
|
|
36
|
+
export const IconCoupletSpacingTight = createPoetryIcon('couplet-spacing-tight', [
|
|
37
|
+
'M4 5h16',
|
|
38
|
+
'M4 19h16',
|
|
39
|
+
'M9 9l3 3l3 -3',
|
|
40
|
+
'M9 15l3 -3l3 3',
|
|
41
|
+
]);
|
|
42
|
+
/** Two full-width rows pulled apart: loosen the vertical gap between couplets. */
|
|
43
|
+
export const IconCoupletSpacingLoose = createPoetryIcon('couplet-spacing-loose', [
|
|
44
|
+
'M4 5h16',
|
|
45
|
+
'M4 19h16',
|
|
46
|
+
'M9 10l3 -3l3 3',
|
|
47
|
+
'M9 14l3 3l3 -3',
|
|
48
|
+
]);
|
|
49
|
+
/** Two staggered (alternating-side) rows pinched together: narrow the stagger offset. */
|
|
50
|
+
export const IconCoupletStaggerNarrow = createPoetryIcon('couplet-stagger-narrow', [
|
|
51
|
+
'M4 7h7',
|
|
52
|
+
'M13 17h7',
|
|
53
|
+
'M9 9l3 3l-3 3',
|
|
54
|
+
'M15 9l-3 3l3 3',
|
|
55
|
+
]);
|
|
56
|
+
/** Two staggered (alternating-side) rows pulled apart: widen the stagger offset. */
|
|
57
|
+
export const IconCoupletStaggerWide = createPoetryIcon('couplet-stagger-wide', [
|
|
58
|
+
'M4 7h7',
|
|
59
|
+
'M13 17h7',
|
|
60
|
+
'M10 9l-3 3l3 3',
|
|
61
|
+
'M14 9l3 3l-3 3',
|
|
62
|
+
]);
|
|
63
|
+
/** Four column rails, the inner pair pinched together: narrow the center column. */
|
|
64
|
+
export const IconCoupletCenterNarrow = createPoetryIcon('couplet-center-narrow', [
|
|
65
|
+
'M4 4v16',
|
|
66
|
+
'M10 4v16',
|
|
67
|
+
'M14 4v16',
|
|
68
|
+
'M20 4v16',
|
|
69
|
+
'M9 9l3 3l-3 3',
|
|
70
|
+
'M15 9l-3 3l3 3',
|
|
71
|
+
]);
|
|
72
|
+
/** Four column rails, the inner pair pulled apart: widen the center column. */
|
|
73
|
+
export const IconCoupletCenterWide = createPoetryIcon('couplet-center-wide', [
|
|
74
|
+
'M4 4v16',
|
|
75
|
+
'M10 4v16',
|
|
76
|
+
'M14 4v16',
|
|
77
|
+
'M20 4v16',
|
|
78
|
+
'M10 9l-3 3l3 3',
|
|
79
|
+
'M14 9l3 3l-3 3',
|
|
80
|
+
]);
|
|
81
|
+
/** Two column rails pinched together: narrow the gutter between the columns. */
|
|
82
|
+
export const IconCoupletGutterNarrow = createPoetryIcon('couplet-gutter-narrow', [
|
|
83
|
+
'M10 4v16',
|
|
84
|
+
'M14 4v16',
|
|
85
|
+
'M9 9l3 3l-3 3',
|
|
86
|
+
'M15 9l-3 3l3 3',
|
|
87
|
+
]);
|
|
88
|
+
/** Two column rails pulled apart: widen the gutter between the columns. */
|
|
89
|
+
export const IconCoupletGutterWide = createPoetryIcon('couplet-gutter-wide', [
|
|
90
|
+
'M10 4v16',
|
|
91
|
+
'M14 4v16',
|
|
92
|
+
'M10 9l-3 3l3 3',
|
|
93
|
+
'M14 9l3 3l-3 3',
|
|
94
|
+
]);
|
|
@@ -2,8 +2,10 @@ export declare function $canJoinLines(): boolean;
|
|
|
2
2
|
/**
|
|
3
3
|
* Joins the lines in the selection into one paragraph. Soft breaks and the
|
|
4
4
|
* paragraph breaks between selected paragraphs become a single space each,
|
|
5
|
-
* unless there is already a space at that point.
|
|
6
|
-
* selection
|
|
5
|
+
* unless there is already a space at that point. Anything after the
|
|
6
|
+
* selection's end point — the unselected rest of a partly-selected line, or
|
|
7
|
+
* further soft-broken lines — splits off into its own paragraph, so it stands
|
|
8
|
+
* apart from the text that was actually joined.
|
|
7
9
|
*/
|
|
8
10
|
export declare function $joinSelectedLines(): boolean;
|
|
9
11
|
//# sourceMappingURL=joinLinesActions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"joinLinesActions.d.ts","sourceRoot":"","sources":["../../src/join/joinLinesActions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"joinLinesActions.d.ts","sourceRoot":"","sources":["../../src/join/joinLinesActions.ts"],"names":[],"mappings":"AAqCA,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAgFD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAgC5C"}
|
|
@@ -28,11 +28,10 @@ export function $canJoinLines() {
|
|
|
28
28
|
return selection.getNodes().some($isLineBreakNode) || $textNodesWithNewlines(selection).length > 0;
|
|
29
29
|
}
|
|
30
30
|
/** Replaces the newlines in the selected part of a text node, each by one space (none if one is already there). */
|
|
31
|
-
function $replaceNewlinesInText(node,
|
|
32
|
-
const [start, end] = selection.getStartEndPoints() ?? [];
|
|
31
|
+
function $replaceNewlinesInText(node, start, end) {
|
|
33
32
|
const text = node.getTextContent();
|
|
34
|
-
const from = start
|
|
35
|
-
const to = end
|
|
33
|
+
const from = start.key === node.getKey() ? start.offset : 0;
|
|
34
|
+
const to = end.key === node.getKey() ? end.offset : text.length;
|
|
36
35
|
const joined = text.slice(from, to).replace(/[ \t]*\r?\n[ \t]*/g, ' ');
|
|
37
36
|
node.setTextContent(text.slice(0, from) + joined + text.slice(to));
|
|
38
37
|
}
|
|
@@ -59,23 +58,62 @@ function $appendParagraph(target, paragraph) {
|
|
|
59
58
|
}
|
|
60
59
|
paragraph.remove();
|
|
61
60
|
}
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
rest.append(sibling);
|
|
69
|
-
sibling = next;
|
|
61
|
+
/** Moves `tail` into a new paragraph right after `paragraph`. A leading soft break is the
|
|
62
|
+
* split point itself, so it is dropped rather than carried over as the new paragraph's first child. */
|
|
63
|
+
function $moveTailToNewParagraph(paragraph, tail) {
|
|
64
|
+
if ($isLineBreakNode(tail[0])) {
|
|
65
|
+
tail[0].remove();
|
|
66
|
+
tail.shift();
|
|
70
67
|
}
|
|
71
|
-
|
|
68
|
+
if (tail.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
const rest = $createParagraphNode();
|
|
71
|
+
for (const node of tail)
|
|
72
|
+
rest.append(node);
|
|
72
73
|
paragraph.insertAfter(rest);
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Splits off whatever in `paragraph` comes after the selection's end point into a
|
|
77
|
+
* paragraph of its own, so content the user didn't select isn't swept into the join
|
|
78
|
+
* along with it — whether that's the rest of a partly-selected line, or the
|
|
79
|
+
* paragraph's remaining soft-broken lines.
|
|
80
|
+
*/
|
|
81
|
+
function $splitUnselectedTail(paragraph, end) {
|
|
82
|
+
if (end.type === 'text') {
|
|
83
|
+
const endNode = end.getNode();
|
|
84
|
+
if (endNode.getTopLevelElement()?.getKey() !== paragraph.getKey())
|
|
85
|
+
return;
|
|
86
|
+
const text = endNode.getTextContent();
|
|
87
|
+
const tail = [];
|
|
88
|
+
if (end.offset === 0) {
|
|
89
|
+
tail.push(endNode);
|
|
90
|
+
}
|
|
91
|
+
else if (end.offset < text.length) {
|
|
92
|
+
endNode.setTextContent(text.slice(0, end.offset));
|
|
93
|
+
tail.push($createTextNode(text.slice(end.offset)));
|
|
94
|
+
}
|
|
95
|
+
let sibling = endNode.getNextSibling();
|
|
96
|
+
while (sibling) {
|
|
97
|
+
const next = sibling.getNextSibling();
|
|
98
|
+
tail.push(sibling);
|
|
99
|
+
sibling = next;
|
|
100
|
+
}
|
|
101
|
+
$moveTailToNewParagraph(paragraph, tail);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const endNode = end.getNode();
|
|
105
|
+
if (endNode.getKey() !== paragraph.getKey())
|
|
106
|
+
return;
|
|
107
|
+
$moveTailToNewParagraph(paragraph, endNode.getChildren().slice(end.offset));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
74
110
|
/**
|
|
75
111
|
* Joins the lines in the selection into one paragraph. Soft breaks and the
|
|
76
112
|
* paragraph breaks between selected paragraphs become a single space each,
|
|
77
|
-
* unless there is already a space at that point.
|
|
78
|
-
* selection
|
|
113
|
+
* unless there is already a space at that point. Anything after the
|
|
114
|
+
* selection's end point — the unselected rest of a partly-selected line, or
|
|
115
|
+
* further soft-broken lines — splits off into its own paragraph, so it stands
|
|
116
|
+
* apart from the text that was actually joined.
|
|
79
117
|
*/
|
|
80
118
|
export function $joinSelectedLines() {
|
|
81
119
|
const selection = $getSelection();
|
|
@@ -87,23 +125,26 @@ export function $joinSelectedLines() {
|
|
|
87
125
|
const textsWithNewlines = $textNodesWithNewlines(selection);
|
|
88
126
|
if (paragraphs.length < 2 && lineBreaks.length === 0 && textsWithNewlines.length === 0)
|
|
89
127
|
return false;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
// RangeSelection's anchor/focus track drag direction, not document order (and
|
|
129
|
+
// getStartEndPoints() returns them as-is, anchor first) — reorder them so "start"/"end"
|
|
130
|
+
// below always mean "earlier"/"later" in the document, regardless of which way the user
|
|
131
|
+
// dragged, or Shift+arrow'd, the selection. Getting this wrong silently skips the tail
|
|
132
|
+
// split below (sweeping unselected content into the join) for any backward selection.
|
|
133
|
+
const [start, end] = selection.isBackward() ? [selection.focus, selection.anchor] : [selection.anchor, selection.focus];
|
|
93
134
|
for (const lineBreak of lineBreaks)
|
|
94
135
|
$replaceBreakWithSpace(lineBreak);
|
|
95
136
|
for (const textNode of textsWithNewlines)
|
|
96
|
-
$replaceNewlinesInText(textNode,
|
|
137
|
+
$replaceNewlinesInText(textNode, start, end);
|
|
97
138
|
const [target, ...rest] = paragraphs;
|
|
98
139
|
if (!target)
|
|
99
140
|
return false;
|
|
141
|
+
const lastParagraph = paragraphs[paragraphs.length - 1];
|
|
142
|
+
$splitUnselectedTail(lastParagraph, end);
|
|
100
143
|
for (const paragraph of rest)
|
|
101
144
|
$appendParagraph(target, paragraph);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
target.selectEnd();
|
|
145
|
+
// Keep the caret where the selection began, rather than at the end of everything
|
|
146
|
+
// just joined — useful on its own, and essential when many lines were joined at
|
|
147
|
+
// once, where "the end" could otherwise land far past where the user was working.
|
|
148
|
+
start.getNode().select(start.offset, start.offset);
|
|
108
149
|
return true;
|
|
109
150
|
}
|
|
@@ -12,4 +12,13 @@ export declare function textPointAt(x: number, y: number): {
|
|
|
12
12
|
} | null;
|
|
13
13
|
/** The whole word around an offset in a Lexical text node. Must run inside an editor read or update. */
|
|
14
14
|
export declare function $wordAtDomPoint(domText: Node, offset: number): WordAtPoint | null;
|
|
15
|
+
/** The whole word at the caret, for keyboard shortcuts that act on "the word here"
|
|
16
|
+
* without a mouse position. Must run inside an editor read or update. */
|
|
17
|
+
export declare function $wordAtSelection(): WordAtPoint | null;
|
|
18
|
+
/** Whether the current selection is no wider than `word` — a caret, or a selection that
|
|
19
|
+
* sits entirely inside that one word. A selection that reaches beyond it (more of that
|
|
20
|
+
* word's line, another word, another line) means the user selected something other than
|
|
21
|
+
* just this word, even though it happens to be the word under the pointer. Must run
|
|
22
|
+
* inside an editor read or update. */
|
|
23
|
+
export declare function $selectionIsWithinWord(word: WordAtPoint): boolean;
|
|
15
24
|
//# sourceMappingURL=spellingMenu.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spellingMenu.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellingMenu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG5D,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUvF;
|
|
1
|
+
{"version":3,"file":"spellingMenu.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellingMenu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG5D,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUvF;AAcD,wGAAwG;AACxG,wBAAgB,eAAe,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAIjF;AAED;yEACyE;AACzE,wBAAgB,gBAAgB,IAAI,WAAW,GAAG,IAAI,CAMrD;AAED;;;;sCAIsC;AACtC,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAMjE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $getNearestNodeFromDOMNode, $isTextNode } from 'lexical';
|
|
1
|
+
import { $getNearestNodeFromDOMNode, $getSelection, $isRangeSelection, $isTextNode } from 'lexical';
|
|
2
2
|
import { wordsIn } from './spellDictionaries.js';
|
|
3
3
|
/** The DOM text node and offset under a viewport point, or null if the point is not on text. */
|
|
4
4
|
export function textPointAt(x, y) {
|
|
@@ -10,11 +10,8 @@ export function textPointAt(x, y) {
|
|
|
10
10
|
const range = doc.caretRangeFromPoint?.(x, y);
|
|
11
11
|
return range && range.startContainer.nodeType === Node.TEXT_NODE ? { node: range.startContainer, offset: range.startOffset } : null;
|
|
12
12
|
}
|
|
13
|
-
/** The whole word around an offset in a Lexical text node.
|
|
14
|
-
|
|
15
|
-
const node = $getNearestNodeFromDOMNode(domText);
|
|
16
|
-
if (!$isTextNode(node))
|
|
17
|
-
return null;
|
|
13
|
+
/** The whole word around an offset in a Lexical text node. */
|
|
14
|
+
function $wordAtTextOffset(node, offset) {
|
|
18
15
|
const text = node.getTextContent();
|
|
19
16
|
const span = wordsIn(text).find((s) => offset >= s.start && offset <= s.end);
|
|
20
17
|
if (!span)
|
|
@@ -25,3 +22,34 @@ export function $wordAtDomPoint(domText, offset) {
|
|
|
25
22
|
match: { anchorKey: node.getKey(), anchorOffset: span.start, focusKey: node.getKey(), focusOffset: span.end },
|
|
26
23
|
};
|
|
27
24
|
}
|
|
25
|
+
/** The whole word around an offset in a Lexical text node. Must run inside an editor read or update. */
|
|
26
|
+
export function $wordAtDomPoint(domText, offset) {
|
|
27
|
+
const node = $getNearestNodeFromDOMNode(domText);
|
|
28
|
+
if (!$isTextNode(node))
|
|
29
|
+
return null;
|
|
30
|
+
return $wordAtTextOffset(node, offset);
|
|
31
|
+
}
|
|
32
|
+
/** The whole word at the caret, for keyboard shortcuts that act on "the word here"
|
|
33
|
+
* without a mouse position. Must run inside an editor read or update. */
|
|
34
|
+
export function $wordAtSelection() {
|
|
35
|
+
const selection = $getSelection();
|
|
36
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
37
|
+
return null;
|
|
38
|
+
const node = selection.anchor.getNode();
|
|
39
|
+
if (!$isTextNode(node))
|
|
40
|
+
return null;
|
|
41
|
+
return $wordAtTextOffset(node, selection.anchor.offset);
|
|
42
|
+
}
|
|
43
|
+
/** Whether the current selection is no wider than `word` — a caret, or a selection that
|
|
44
|
+
* sits entirely inside that one word. A selection that reaches beyond it (more of that
|
|
45
|
+
* word's line, another word, another line) means the user selected something other than
|
|
46
|
+
* just this word, even though it happens to be the word under the pointer. Must run
|
|
47
|
+
* inside an editor read or update. */
|
|
48
|
+
export function $selectionIsWithinWord(word) {
|
|
49
|
+
const selection = $getSelection();
|
|
50
|
+
if (!$isRangeSelection(selection) || selection.isCollapsed())
|
|
51
|
+
return true;
|
|
52
|
+
const { anchorKey, anchorOffset, focusKey, focusOffset } = word.match;
|
|
53
|
+
const withinSpan = (key, offset) => key === anchorKey && offset >= anchorOffset && offset <= focusOffset;
|
|
54
|
+
return withinSpan(selection.anchor.key, selection.anchor.offset) && withinSpan(selection.focus.key, selection.focus.offset);
|
|
55
|
+
}
|
package/dist/theme/editor.css
CHANGED
|
@@ -1332,3 +1332,76 @@
|
|
|
1332
1332
|
.likhari-ribbon-quick [data-dirty='false'][data-disabled] {
|
|
1333
1333
|
color: var(--editor-ink-soft);
|
|
1334
1334
|
}
|
|
1335
|
+
|
|
1336
|
+
/* The right-click menu is compact: its icons follow the smaller item text (sizes set in Toolbar.tsx). */
|
|
1337
|
+
.likhari-context-menu svg {
|
|
1338
|
+
width: 14px;
|
|
1339
|
+
height: 14px;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/* A menu item's keyboard shortcut, dimmed at its trailing edge. */
|
|
1343
|
+
.likhari-context-shortcut {
|
|
1344
|
+
font-size: 11px;
|
|
1345
|
+
color: var(--editor-ink-soft);
|
|
1346
|
+
white-space: nowrap;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/* Cut, copy and paste as one row of icon buttons in the right-click menu. */
|
|
1350
|
+
.likhari-context-row {
|
|
1351
|
+
display: flex;
|
|
1352
|
+
gap: 2px;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
.likhari-context-row [data-menu-item] {
|
|
1356
|
+
flex: 1 1 0;
|
|
1357
|
+
justify-content: center;
|
|
1358
|
+
padding-block: 5px;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.likhari-context-row [data-menu-item] > * {
|
|
1362
|
+
display: flex;
|
|
1363
|
+
justify-content: center;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/* Table / poetry actions in the right-click menu: a wrapping grid of icon-only
|
|
1367
|
+
buttons instead of a submenu, so the common actions are one click away. */
|
|
1368
|
+
.likhari-context-icons {
|
|
1369
|
+
display: flex;
|
|
1370
|
+
flex-wrap: wrap;
|
|
1371
|
+
gap: 2px;
|
|
1372
|
+
padding: 2px 0;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.likhari-context-icons [data-menu-item] {
|
|
1376
|
+
flex: 0 0 auto;
|
|
1377
|
+
width: 30px;
|
|
1378
|
+
justify-content: center;
|
|
1379
|
+
padding-block: 5px;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
.likhari-context-icons [data-menu-item] > * {
|
|
1383
|
+
display: flex;
|
|
1384
|
+
justify-content: center;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/* A thin separator between groups of icon buttons (e.g. merge/unmerge from delete). */
|
|
1388
|
+
.likhari-context-icons-divider {
|
|
1389
|
+
align-self: stretch;
|
|
1390
|
+
width: 1px;
|
|
1391
|
+
margin-block: 4px;
|
|
1392
|
+
background: var(--mantine-color-default-border);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/* Poetry: layout (single/two-column/staggered) on its own row, apart from what that
|
|
1396
|
+
layout's selection then allows (insert couplet, spacing, centering, delete). */
|
|
1397
|
+
.likhari-context-icon-rows {
|
|
1398
|
+
display: flex;
|
|
1399
|
+
flex-direction: column;
|
|
1400
|
+
gap: 2px;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
.likhari-context-icon-rows-divider {
|
|
1404
|
+
height: 1px;
|
|
1405
|
+
margin-inline: 4px;
|
|
1406
|
+
background: var(--mantine-color-default-border);
|
|
1407
|
+
}
|