@inshapardaz/likhari-react 0.1.18-dev.158 → 0.1.18-dev.165
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/blocks/PoetryNode.d.ts +24 -13
- package/dist/blocks/PoetryNode.d.ts.map +1 -1
- package/dist/blocks/PoetryNode.js +64 -43
- package/dist/blocks/poetryActions.d.ts +35 -12
- package/dist/blocks/poetryActions.d.ts.map +1 -1
- package/dist/blocks/poetryActions.js +175 -43
- package/dist/components/FindReplaceBar.d.ts +8 -0
- package/dist/components/FindReplaceBar.d.ts.map +1 -0
- package/dist/components/FindReplaceBar.js +57 -0
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +25 -23
- package/dist/find/findReplaceActions.d.ts +23 -0
- package/dist/find/findReplaceActions.d.ts.map +1 -0
- package/dist/find/findReplaceActions.js +81 -0
- package/dist/i18n/strings.d.ts +21 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +63 -0
- package/dist/plugins/PoetryPlugin.d.ts +1 -2
- package/dist/plugins/PoetryPlugin.d.ts.map +1 -1
- package/dist/plugins/PoetryPlugin.js +9 -3
- package/dist/theme/editor.css +93 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { $findMatchingParent } from '@lexical/utils';
|
|
2
|
-
import { $createParagraphNode, $getSelection, $isElementNode, $isParagraphNode, $isRangeSelection } from 'lexical';
|
|
2
|
+
import { $createParagraphNode, $getSelection, $isElementNode, $isParagraphNode, $isRangeSelection, } from 'lexical';
|
|
3
3
|
import { $createLayoutContainerNode, $createLayoutItemNode, $isLayoutContainerNode, $isLayoutItemNode } from './LayoutNode.js';
|
|
4
|
-
import { $createPoetryBlockNode, $isPoetryBlockNode, POETRY_SPACINGS, } from './PoetryNode.js';
|
|
4
|
+
import { $createPoetryBlockNode, $isPoetryBlockNode, POETRY_GUTTERS, POETRY_SPACINGS, POETRY_STAGGERS, } from './PoetryNode.js';
|
|
5
5
|
/**
|
|
6
6
|
* A poetry block is a *section* of one or more couplets sharing one layout
|
|
7
7
|
* (single-column stacks each couplet's two misras directly as paragraph
|
|
@@ -11,7 +11,7 @@ import { $createPoetryBlockNode, $isPoetryBlockNode, POETRY_SPACINGS, } from './
|
|
|
11
11
|
* read either shape, so nothing else needs to.
|
|
12
12
|
*/
|
|
13
13
|
export function $getCouplets(node) {
|
|
14
|
-
if (node.getLayout()
|
|
14
|
+
if (node.getLayout() !== 'two-column') {
|
|
15
15
|
const paragraphs = node.getChildren().filter($isParagraphNode);
|
|
16
16
|
const couplets = [];
|
|
17
17
|
for (let i = 0; i + 1 < paragraphs.length; i += 2)
|
|
@@ -22,6 +22,12 @@ export function $getCouplets(node) {
|
|
|
22
22
|
const couplets = [];
|
|
23
23
|
for (const container of containers) {
|
|
24
24
|
const items = container.getChildren().filter($isLayoutItemNode);
|
|
25
|
+
if (items.length === 1) {
|
|
26
|
+
const [a, b] = items[0].getChildren();
|
|
27
|
+
if ($isParagraphNode(a) && $isParagraphNode(b))
|
|
28
|
+
couplets.push([a, b]);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
25
31
|
const a = items[0]?.getFirstChild();
|
|
26
32
|
const b = items[1]?.getFirstChild();
|
|
27
33
|
if ($isParagraphNode(a) && $isParagraphNode(b))
|
|
@@ -58,7 +64,7 @@ function $findCoupletPosition(couplets, target) {
|
|
|
58
64
|
function $appendCoupletTo(node) {
|
|
59
65
|
const misraA = $createParagraphNode();
|
|
60
66
|
const misraB = $createParagraphNode();
|
|
61
|
-
if (node.getLayout()
|
|
67
|
+
if (node.getLayout() !== 'two-column') {
|
|
62
68
|
node.append(misraA, misraB);
|
|
63
69
|
}
|
|
64
70
|
else {
|
|
@@ -80,7 +86,7 @@ function $appendCoupletTo(node) {
|
|
|
80
86
|
function $insertCoupletRelativeTo(existingFirstMisra, layout, position) {
|
|
81
87
|
const misraA = $createParagraphNode();
|
|
82
88
|
const misraB = $createParagraphNode();
|
|
83
|
-
if (layout
|
|
89
|
+
if (layout !== 'two-column') {
|
|
84
90
|
if (position === 'before') {
|
|
85
91
|
existingFirstMisra.insertBefore(misraA);
|
|
86
92
|
misraA.insertAfter(misraB);
|
|
@@ -143,7 +149,7 @@ export function $setPoetryLayout(node, layout) {
|
|
|
143
149
|
}
|
|
144
150
|
for (const leftover of node.getChildren())
|
|
145
151
|
leftover.remove();
|
|
146
|
-
if (layout
|
|
152
|
+
if (layout !== 'two-column') {
|
|
147
153
|
for (const [a, b] of couplets)
|
|
148
154
|
node.append(a, b);
|
|
149
155
|
}
|
|
@@ -174,10 +180,10 @@ export function $ensureTrailingParagraph(node) {
|
|
|
174
180
|
* semantics, in its existing layout: "one poetry block can contain one or
|
|
175
181
|
* more couplets, in one or two column layout", so a block's layout is
|
|
176
182
|
* fixed once it has couplets, not chosen per couplet) and focuses its first
|
|
177
|
-
* misra. Otherwise creates a new block (in `layout
|
|
183
|
+
* misra. Otherwise creates a new block (in `layout`) with one
|
|
178
184
|
* couplet, after the selection's top-level element.
|
|
179
185
|
*/
|
|
180
|
-
export function $insertPoetryCouplet(layout
|
|
186
|
+
export function $insertPoetryCouplet(layout) {
|
|
181
187
|
const selection = $getSelection();
|
|
182
188
|
if (!$isRangeSelection(selection))
|
|
183
189
|
return false;
|
|
@@ -186,7 +192,7 @@ export function $insertPoetryCouplet(layout, align) {
|
|
|
186
192
|
return $insertCoupletRelativeToSelection('after');
|
|
187
193
|
}
|
|
188
194
|
const anchorTopLevel = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
189
|
-
const node = $createPoetryBlockNode(layout
|
|
195
|
+
const node = $createPoetryBlockNode(layout);
|
|
190
196
|
const [misraA] = $appendCoupletTo(node);
|
|
191
197
|
anchorTopLevel.insertAfter(node);
|
|
192
198
|
$ensureTrailingParagraph(node);
|
|
@@ -223,16 +229,23 @@ export function $deletePoetryCouplet() {
|
|
|
223
229
|
}
|
|
224
230
|
return true;
|
|
225
231
|
}
|
|
232
|
+
/** Whether a point sits at the very start (or, with `atEnd`, very end) of a paragraph. */
|
|
233
|
+
function $isPointAt(paragraph, point, atEnd) {
|
|
234
|
+
if (point.key === paragraph.getKey())
|
|
235
|
+
return point.offset === (atEnd ? paragraph.getChildrenSize() : 0);
|
|
236
|
+
const edge = atEnd ? paragraph.getLastDescendant() : paragraph.getFirstDescendant();
|
|
237
|
+
if (point.getNode() !== edge)
|
|
238
|
+
return false;
|
|
239
|
+
return point.offset === (atEnd ? point.getNode().getTextContent().length : 0);
|
|
240
|
+
}
|
|
226
241
|
/**
|
|
227
|
-
* Backspace at the
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* other caret position, including a non-empty couplet, so normal Backspace
|
|
235
|
-
* handling (e.g. deleting within text) proceeds.
|
|
242
|
+
* Backspace at the start of a misra never merges it into its neighbour: the
|
|
243
|
+
* block's lines are paired into couplets, so merging one line away would
|
|
244
|
+
* shift every couplet after it (and take the previous couplet's last line
|
|
245
|
+
* with it). Instead, Backspace at the start of an empty couplet removes that
|
|
246
|
+
* couplet, and at the start of any other line it moves the caret to the end
|
|
247
|
+
* of the previous line. Other caret positions (deleting within text) fall
|
|
248
|
+
* through to normal handling.
|
|
236
249
|
*/
|
|
237
250
|
export function $deletePoetryOnBackspace() {
|
|
238
251
|
const selection = $getSelection();
|
|
@@ -242,35 +255,60 @@ export function $deletePoetryOnBackspace() {
|
|
|
242
255
|
if (!block)
|
|
243
256
|
return false;
|
|
244
257
|
const couplets = $getCouplets(block);
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
if (!position || position.misraIndex !== 0)
|
|
248
|
-
return false;
|
|
249
|
-
const [firstMisra] = couplets[position.coupletIndex];
|
|
250
|
-
const anchor = selection.anchor;
|
|
251
|
-
const isAtStart = anchor.key === firstMisra.getKey() && anchor.offset === 0;
|
|
252
|
-
if (!isAtStart)
|
|
258
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
259
|
+
if (!position)
|
|
253
260
|
return false;
|
|
254
|
-
const
|
|
255
|
-
|
|
261
|
+
const { coupletIndex, misraIndex } = position;
|
|
262
|
+
const [a, b] = couplets[coupletIndex];
|
|
263
|
+
const misra = couplets[coupletIndex][misraIndex];
|
|
264
|
+
if (!$isPointAt(misra, selection.anchor, false))
|
|
256
265
|
return false;
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
previous
|
|
266
|
+
if (a.isEmpty() && b.isEmpty()) {
|
|
267
|
+
if (couplets.length <= 1) {
|
|
268
|
+
const previous = block.getPreviousSibling();
|
|
269
|
+
block.remove();
|
|
270
|
+
if ($isElementNode(previous))
|
|
271
|
+
previous.selectEnd();
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
const container = $findMatchingParent(a, $isLayoutContainerNode);
|
|
275
|
+
if (container)
|
|
276
|
+
container.remove();
|
|
277
|
+
else {
|
|
278
|
+
a.remove();
|
|
279
|
+
b.remove();
|
|
280
|
+
}
|
|
281
|
+
const remaining = $getCouplets(block);
|
|
282
|
+
const previousCouplet = remaining[coupletIndex - 1] ?? remaining[0];
|
|
283
|
+
previousCouplet?.[1].selectEnd();
|
|
262
284
|
return true;
|
|
263
285
|
}
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
286
|
+
const previousLine = misraIndex === 1 ? a : couplets[coupletIndex - 1]?.[1];
|
|
287
|
+
previousLine?.selectEnd();
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Forward Delete at the end of a misra never merges the next line into it, for
|
|
292
|
+
* the same reason as $deletePoetryOnBackspace: the caret moves to the start of
|
|
293
|
+
* the next line instead. Other caret positions fall through to normal handling.
|
|
294
|
+
*/
|
|
295
|
+
export function $deletePoetryForward() {
|
|
296
|
+
const selection = $getSelection();
|
|
297
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
298
|
+
return false;
|
|
299
|
+
const block = $getPoetryBlockFromSelection();
|
|
300
|
+
if (!block)
|
|
301
|
+
return false;
|
|
302
|
+
const couplets = $getCouplets(block);
|
|
303
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
304
|
+
if (!position)
|
|
305
|
+
return false;
|
|
306
|
+
const { coupletIndex, misraIndex } = position;
|
|
307
|
+
const misra = couplets[coupletIndex][misraIndex];
|
|
308
|
+
if (!$isPointAt(misra, selection.anchor, true))
|
|
309
|
+
return false;
|
|
310
|
+
const nextLine = misraIndex === 0 ? couplets[coupletIndex][1] : couplets[coupletIndex + 1]?.[0];
|
|
311
|
+
nextLine?.selectStart();
|
|
274
312
|
return true;
|
|
275
313
|
}
|
|
276
314
|
/**
|
|
@@ -347,3 +385,97 @@ export function $adjustPoetrySpacing(step) {
|
|
|
347
385
|
block.setSpacing(next);
|
|
348
386
|
return true;
|
|
349
387
|
}
|
|
388
|
+
/** Steps the caret's two-column gutter one notch wider (+1) or narrower (-1);
|
|
389
|
+
* clamps at either end. */
|
|
390
|
+
export function $adjustPoetryGutter(step) {
|
|
391
|
+
const block = $getPoetryBlockFromSelection();
|
|
392
|
+
if (!block)
|
|
393
|
+
return false;
|
|
394
|
+
const index = POETRY_GUTTERS.indexOf(block.getGutter());
|
|
395
|
+
const next = POETRY_GUTTERS[Math.min(POETRY_GUTTERS.length - 1, Math.max(0, index + step))];
|
|
396
|
+
block.setGutter(next);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
/** Steps the caret's staggered couplet width one notch narrower (-1) or wider
|
|
400
|
+
* (+1); clamps at either end. */
|
|
401
|
+
export function $adjustPoetryStagger(step) {
|
|
402
|
+
const block = $getPoetryBlockFromSelection();
|
|
403
|
+
if (!block)
|
|
404
|
+
return false;
|
|
405
|
+
const index = POETRY_STAGGERS.indexOf(block.getStagger());
|
|
406
|
+
const next = POETRY_STAGGERS[Math.min(POETRY_STAGGERS.length - 1, Math.max(0, index + step))];
|
|
407
|
+
block.setStagger(next);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
/** A centered couplet is a two-column row holding both misras in one item
|
|
411
|
+
* (a single-item row), rather than one item per misra. */
|
|
412
|
+
function $isCoupletRowCentered(row) {
|
|
413
|
+
return row.getChildren().filter($isLayoutItemNode).length === 1;
|
|
414
|
+
}
|
|
415
|
+
function $findCoupletRow(couplet) {
|
|
416
|
+
return $findMatchingParent(couplet[0], $isLayoutContainerNode);
|
|
417
|
+
}
|
|
418
|
+
/** Whether the couplet the caret is in is centered, or null outside a
|
|
419
|
+
* two-column poetry block (centering only applies there). */
|
|
420
|
+
export function $getCoupletCenteredFromSelection() {
|
|
421
|
+
const block = $getPoetryBlockFromSelection();
|
|
422
|
+
if (!block || block.getLayout() !== 'two-column')
|
|
423
|
+
return null;
|
|
424
|
+
const selection = $getSelection();
|
|
425
|
+
if (!$isRangeSelection(selection))
|
|
426
|
+
return null;
|
|
427
|
+
const couplets = $getCouplets(block);
|
|
428
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
429
|
+
if (!position)
|
|
430
|
+
return null;
|
|
431
|
+
const row = $findCoupletRow(couplets[position.coupletIndex]);
|
|
432
|
+
return row ? $isCoupletRowCentered(row) : false;
|
|
433
|
+
}
|
|
434
|
+
/** Centers (or un-centers) the caret's couplet within a two-column block by
|
|
435
|
+
* rebuilding its row around the same two misra paragraphs. */
|
|
436
|
+
export function $setCoupletCentered(centered) {
|
|
437
|
+
const block = $getPoetryBlockFromSelection();
|
|
438
|
+
if (!block || block.getLayout() !== 'two-column')
|
|
439
|
+
return false;
|
|
440
|
+
const selection = $getSelection();
|
|
441
|
+
if (!$isRangeSelection(selection))
|
|
442
|
+
return false;
|
|
443
|
+
const couplets = $getCouplets(block);
|
|
444
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
445
|
+
if (!position)
|
|
446
|
+
return false;
|
|
447
|
+
const couplet = couplets[position.coupletIndex];
|
|
448
|
+
const row = $findCoupletRow(couplet);
|
|
449
|
+
if (!row || $isCoupletRowCentered(row) === centered)
|
|
450
|
+
return true;
|
|
451
|
+
const [a, b] = couplet;
|
|
452
|
+
const newRow = $createLayoutContainerNode(centered ? '1fr' : 'repeat(2, 1fr)');
|
|
453
|
+
if (centered) {
|
|
454
|
+
newRow.append($createLayoutItemNode().append(a, b));
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
newRow.append($createLayoutItemNode().append(a), $createLayoutItemNode().append(b));
|
|
458
|
+
}
|
|
459
|
+
row.insertAfter(newRow);
|
|
460
|
+
row.remove();
|
|
461
|
+
a.selectStart();
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
/** Removes the whole poetry block the caret is in, including every couplet. */
|
|
465
|
+
export function $deletePoetryBlock() {
|
|
466
|
+
const block = $getPoetryBlockFromSelection();
|
|
467
|
+
if (!block)
|
|
468
|
+
return false;
|
|
469
|
+
block.remove();
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
/** Single-column blocks pair their paragraphs into couplets, so a line left
|
|
473
|
+
* without a partner (from pasted or imported content) would be invisible to
|
|
474
|
+
* the couplet logic. Give it an empty partner instead, so every line belongs
|
|
475
|
+
* to a couplet and can be deleted like any other. */
|
|
476
|
+
export function $completeSingleCoupletBlock(node) {
|
|
477
|
+
if (node.getLayout() === 'two-column')
|
|
478
|
+
return;
|
|
479
|
+
if (node.getChildrenSize() % 2 === 1)
|
|
480
|
+
node.append($createParagraphNode());
|
|
481
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Strings } from '../i18n/strings.js';
|
|
2
|
+
/** Find-and-replace controls shown above the canvas. Matches are recomputed
|
|
3
|
+
* from the document on every action, so edits made in between are respected. */
|
|
4
|
+
export declare function FindReplaceBar({ strings, onClose }: {
|
|
5
|
+
strings: Strings;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=FindReplaceBar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FindReplaceBar.d.ts","sourceRoot":"","sources":["../../src/components/FindReplaceBar.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG/C;gFACgF;AAChF,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,+BA2F7F"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
|
+
import { IconX } from '@tabler/icons-react';
|
|
5
|
+
import { $findMatches, $replaceAll, $replaceMatch, $selectMatch } from '../find/findReplaceActions.js';
|
|
6
|
+
/** Find-and-replace controls shown above the canvas. Matches are recomputed
|
|
7
|
+
* from the document on every action, so edits made in between are respected. */
|
|
8
|
+
export function FindReplaceBar({ strings, onClose }) {
|
|
9
|
+
const [editor] = useLexicalComposerContext();
|
|
10
|
+
const t = strings.findReplace;
|
|
11
|
+
const [query, setQuery] = useState('');
|
|
12
|
+
const [replacement, setReplacement] = useState('');
|
|
13
|
+
const [index, setIndex] = useState(0);
|
|
14
|
+
const [count, setCount] = useState(0);
|
|
15
|
+
// Runs `action` against the current matches and returns the new count, then
|
|
16
|
+
// syncs the index and selection. `discrete` makes the update commit before
|
|
17
|
+
// the next read, so the count reflects this action's changes.
|
|
18
|
+
const run = (action) => {
|
|
19
|
+
let result = { nextIndex: 0, matches: [] };
|
|
20
|
+
editor.update(() => {
|
|
21
|
+
result = action($findMatches(query));
|
|
22
|
+
if (result.matches[result.nextIndex])
|
|
23
|
+
$selectMatch(result.matches[result.nextIndex]);
|
|
24
|
+
}, { discrete: true });
|
|
25
|
+
setCount(result.matches.length);
|
|
26
|
+
setIndex(result.nextIndex);
|
|
27
|
+
const anchor = result.matches[result.nextIndex];
|
|
28
|
+
if (anchor)
|
|
29
|
+
editor.getElementByKey(anchor.anchorKey)?.scrollIntoView({ block: 'center' });
|
|
30
|
+
};
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
run((matches) => ({ nextIndex: 0, matches }));
|
|
33
|
+
// Re-run when the query changes; `run` closes over it.
|
|
34
|
+
}, [query]);
|
|
35
|
+
const step = (delta) => run((matches) => ({
|
|
36
|
+
matches,
|
|
37
|
+
nextIndex: matches.length === 0 ? 0 : (index + delta + matches.length) % matches.length,
|
|
38
|
+
}));
|
|
39
|
+
const replaceCurrent = () => run((matches) => {
|
|
40
|
+
const match = matches[index];
|
|
41
|
+
if (match)
|
|
42
|
+
$replaceMatch(match, replacement);
|
|
43
|
+
const after = $findMatches(query);
|
|
44
|
+
return { matches: after, nextIndex: Math.min(index, Math.max(0, after.length - 1)) };
|
|
45
|
+
});
|
|
46
|
+
const replaceEverything = () => run(() => {
|
|
47
|
+
$replaceAll(query, replacement);
|
|
48
|
+
return { matches: $findMatches(query), nextIndex: 0 };
|
|
49
|
+
});
|
|
50
|
+
const status = query === '' ? '' : count === 0 ? t.noMatches : `${index + 1} / ${count}`;
|
|
51
|
+
return (_jsxs("div", { className: "likhari-find-bar", role: "search", children: [_jsx("input", { "aria-label": t.find, placeholder: t.findPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => {
|
|
52
|
+
if (e.key === 'Enter')
|
|
53
|
+
step(e.shiftKey ? -1 : 1);
|
|
54
|
+
if (e.key === 'Escape')
|
|
55
|
+
onClose();
|
|
56
|
+
} }), _jsx("span", { className: "likhari-find-status", "aria-live": "polite", children: status }), _jsx("button", { type: "button", onClick: () => step(-1), disabled: count === 0, children: t.previous }), _jsx("button", { type: "button", onClick: () => step(1), disabled: count === 0, children: t.next }), _jsx("input", { "aria-label": t.replace, placeholder: t.replacePlaceholder, value: replacement, onChange: (e) => setReplacement(e.target.value) }), _jsx("button", { type: "button", onClick: replaceCurrent, disabled: count === 0, children: t.replaceOne }), _jsx("button", { type: "button", onClick: replaceEverything, disabled: count === 0, children: t.replaceAll }), _jsx("button", { type: "button", className: "likhari-find-close", onClick: onClose, "aria-label": t.close, title: t.close, children: _jsx(IconX, { size: 16, stroke: 1.75 }) })] }));
|
|
57
|
+
}
|
|
@@ -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":"AAsFA,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAItG,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AA+gBvD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,MAAa,EAAE,MAAM,EAAE,EAAE,YAAY,+BAwkCxJ"}
|
|
@@ -8,14 +8,15 @@ 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, $insertNodeToNearestRoot } from '@lexical/utils';
|
|
11
|
+
import { FindReplaceBar } from './FindReplaceBar.js';
|
|
11
12
|
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
12
13
|
import { ImageDialog } from '../image/ImageDialog.js';
|
|
13
14
|
import { $createImageNode } from '../image/ImageNode.js';
|
|
14
15
|
import { INSERT_PAGE_BREAK_COMMAND } from '../blocks/PageBreakNode.js';
|
|
15
16
|
import { INSERT_LAYOUT_COMMAND } from '../blocks/LayoutNode.js';
|
|
16
17
|
import { INSERT_FOOTNOTE_COMMAND } from '../blocks/FootnoteNode.js';
|
|
17
|
-
import { $isPoetryBlockNode } from '../blocks/PoetryNode.js';
|
|
18
|
-
import { $adjustPoetrySpacing, $deletePoetryCouplet, $getPoetryBlockFromSelection, $insertCoupletRelativeToSelection, $setPoetryLayout, } from '../blocks/poetryActions.js';
|
|
18
|
+
import { $isPoetryBlockNode, POETRY_GUTTERS, POETRY_SPACINGS, POETRY_STAGGERS, } from '../blocks/PoetryNode.js';
|
|
19
|
+
import { $adjustPoetryGutter, $getCoupletCenteredFromSelection, $setCoupletCentered, $adjustPoetryStagger, $adjustPoetrySpacing, $deletePoetryBlock, $deletePoetryCouplet, $getPoetryBlockFromSelection, $insertCoupletRelativeToSelection, $setPoetryLayout, } from '../blocks/poetryActions.js';
|
|
19
20
|
import { INSERT_POETRY_COUPLET_COMMAND } from '../plugins/PoetryPlugin.js';
|
|
20
21
|
import { TableDialog } from './TableDialog.js';
|
|
21
22
|
import { LayoutDialog } from './LayoutDialog.js';
|
|
@@ -29,7 +30,7 @@ import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fo
|
|
|
29
30
|
import { setStyleProperty } from '../utils/style.js';
|
|
30
31
|
import { useStrings } from '../i18n/useStrings.js';
|
|
31
32
|
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
32
|
-
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, 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, IconColumns1, IconColumns2, IconNumber1Small, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconArrowsMaximize, IconArrowsMinimize, IconTable, IconTableMinus, IconTrash, IconTableOptions, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
|
|
33
|
+
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, 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, IconSearch, IconColumns1, IconColumns2, IconNumber1Small, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconArrowsMaximize, IconArrowsMinimize, IconArrowsShuffle, IconTable, IconTableMinus, IconTrash, IconTableOptions, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
|
|
33
34
|
/** Sentinel for "no inline style": Lexical returns the default it is given when
|
|
34
35
|
* the selection has none, so this tells "unstyled" (show the canvas default)
|
|
35
36
|
* apart from "styled with a value the dropdown doesn't list" (show nothing). */
|
|
@@ -50,7 +51,8 @@ const INITIAL_STATE = {
|
|
|
50
51
|
canUnmergeCell: false,
|
|
51
52
|
inPoetry: false,
|
|
52
53
|
poetryLayout: 'single',
|
|
53
|
-
|
|
54
|
+
poetryScale: { spacing: 'normal', gutter: 'normal', stagger: 'normal' },
|
|
55
|
+
poetryCentered: false,
|
|
54
56
|
canUndo: false,
|
|
55
57
|
canRedo: false,
|
|
56
58
|
};
|
|
@@ -162,16 +164,14 @@ function TableMenuItems({ strings, direction, rows, columns, canMergeCells, canU
|
|
|
162
164
|
};
|
|
163
165
|
return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { 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(Menu.Divider, {}), canMergeCells && item(IconArrowMergeBoth, t.mergeCells, $mergeTableCells), canUnmergeCell && item(IconArrowsSplit2, t.unmergeCell, $unmergeTableCell), _jsx(Menu.Divider, {}), item(IconRowRemove, manyRows ? t.deleteRows : t.deleteRow, $deleteTableRows), item(IconColumnRemove, manyColumns ? t.deleteColumns : t.deleteColumn, $deleteTableColumns), item(IconTableMinus, t.deleteTable, $deleteTable, 'red')] }));
|
|
164
166
|
}
|
|
165
|
-
/**
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
function PoetryMenuItems({ strings, alignIcons, layout, align, onAction, }) {
|
|
167
|
+
/** Whether a one-notch step from `value` stays on the scale — a step that
|
|
168
|
+
* would fall off either end is disabled rather than silently clamped. */
|
|
169
|
+
function canStep(scale, value, step) {
|
|
170
|
+
const next = scale.indexOf(value) + step;
|
|
171
|
+
return next >= 0 && next < scale.length;
|
|
172
|
+
}
|
|
173
|
+
function PoetryMenuItems({ strings, layout, scale, centered, onAction, }) {
|
|
173
174
|
const t = strings.poetryMenu;
|
|
174
|
-
const a = strings.toolbar.alignOptions;
|
|
175
175
|
const item = (icon, label, action, active = false, color) => {
|
|
176
176
|
const Icon = icon;
|
|
177
177
|
return (_jsx(Menu.Item, { color: color, disabled: active, leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onAction(action), children: label }));
|
|
@@ -181,10 +181,7 @@ function PoetryMenuItems({ strings, alignIcons, layout, align, onAction, }) {
|
|
|
181
181
|
if (node)
|
|
182
182
|
$setPoetryLayout(node, next);
|
|
183
183
|
};
|
|
184
|
-
|
|
185
|
-
$getPoetryBlockFromSelection()?.setAlign(next);
|
|
186
|
-
};
|
|
187
|
-
return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: t.menuLabel }), item(IconColumns1, t.singleColumn, setLayout('single'), layout === 'single'), item(IconColumns2, t.twoColumn, setLayout('two-column'), layout === 'two-column'), _jsx(Menu.Divider, {}), item(alignIcons.justify ?? IconAlignJustified, a.justify, setAlign('justify'), align === 'justify'), item(alignIcons.start ?? IconAlignLeft, a.start, setAlign('start'), align === 'start'), item(IconAlignLeft, a.left, setAlign('left'), align === 'left'), item(IconAlignRight, a.right, setAlign('right'), align === 'right'), _jsx(Menu.Divider, {}), item(IconRowInsertTop, t.insertCoupletBefore, () => $insertCoupletRelativeToSelection('before')), item(IconRowInsertBottom, t.insertCoupletAfter, () => $insertCoupletRelativeToSelection('after')), item(IconArrowsMinimize, t.tighterSpacing, () => $adjustPoetrySpacing(-1)), item(IconArrowsMaximize, t.looserSpacing, () => $adjustPoetrySpacing(1)), item(IconTrash, t.deleteCouplet, $deletePoetryCouplet, false, 'red')] }));
|
|
184
|
+
return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: t.menuLabel }), 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'), _jsx(Menu.Divider, {}), _jsx(Menu.Divider, {}), item(IconRowInsertTop, t.insertCoupletBefore, () => $insertCoupletRelativeToSelection('before')), item(IconRowInsertBottom, t.insertCoupletAfter, () => $insertCoupletRelativeToSelection('after')), item(IconArrowsMinimize, t.tighterSpacing, () => $adjustPoetrySpacing(-1), !canStep(POETRY_SPACINGS, scale.spacing, -1)), item(IconArrowsMaximize, t.looserSpacing, () => $adjustPoetrySpacing(1), !canStep(POETRY_SPACINGS, scale.spacing, 1)), layout === 'staggered' && (_jsxs(_Fragment, { children: [item(IconArrowsMinimize, t.narrowerCouplets, () => $adjustPoetryStagger(-1), !canStep(POETRY_STAGGERS, scale.stagger, -1)), item(IconArrowsMaximize, t.widerCouplets, () => $adjustPoetryStagger(1), !canStep(POETRY_STAGGERS, scale.stagger, 1))] })), layout === 'two-column' && (_jsxs(_Fragment, { children: [item(IconAlignCenter, centered ? t.uncenterCouplet : t.centerCouplet, () => $setCoupletCentered(!centered)), item(IconColumns2, t.narrowerGutter, () => $adjustPoetryGutter(-1), !canStep(POETRY_GUTTERS, scale.gutter, -1)), item(IconColumns2, t.widerGutter, () => $adjustPoetryGutter(1), !canStep(POETRY_GUTTERS, scale.gutter, 1))] })), item(IconTrash, t.deleteCouplet, $deletePoetryCouplet, false, 'red'), item(IconTrash, t.deletePoetry, $deletePoetryBlock, false, 'red')] }));
|
|
188
185
|
}
|
|
189
186
|
export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr', locale = 'en', drafts }) {
|
|
190
187
|
const [editor] = useLexicalComposerContext();
|
|
@@ -246,7 +243,10 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
246
243
|
// node method (getFormatType() included) must not be deferred into it.
|
|
247
244
|
const elementFormat = ($isElementNode(element) ? element.getFormatType() : 'start') || 'start';
|
|
248
245
|
const poetryLayout = poetryBlock?.getLayout();
|
|
249
|
-
const
|
|
246
|
+
const poetryCentered = $getCoupletCenteredFromSelection();
|
|
247
|
+
const poetryScale = poetryBlock
|
|
248
|
+
? { spacing: poetryBlock.getSpacing(), gutter: poetryBlock.getGutter(), stagger: poetryBlock.getStagger() }
|
|
249
|
+
: null;
|
|
250
250
|
setState((s) => ({
|
|
251
251
|
...s,
|
|
252
252
|
blockType,
|
|
@@ -264,7 +264,8 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
264
264
|
canUnmergeCell,
|
|
265
265
|
inPoetry: poetryBlock !== null,
|
|
266
266
|
poetryLayout: poetryLayout ?? s.poetryLayout,
|
|
267
|
-
|
|
267
|
+
poetryScale: poetryScale ?? s.poetryScale,
|
|
268
|
+
poetryCentered: poetryCentered ?? false,
|
|
268
269
|
}));
|
|
269
270
|
});
|
|
270
271
|
}, [editor]);
|
|
@@ -385,6 +386,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
385
386
|
closeTableDialog();
|
|
386
387
|
};
|
|
387
388
|
const [layoutDialogOpen, setLayoutDialogOpen] = useState(false);
|
|
389
|
+
const [findOpen, setFindOpen] = useState(false);
|
|
388
390
|
const openLayoutDialog = () => {
|
|
389
391
|
snapshotSelection();
|
|
390
392
|
setLayoutDialogOpen(true);
|
|
@@ -808,9 +810,9 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
808
810
|
restoreSelection();
|
|
809
811
|
openLinkDialog();
|
|
810
812
|
}, onRemove: removeLink, strings: strings }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.insertLink, onClick: openLinkDialog })) }, "link")),
|
|
811
|
-
showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: strings.toolbar.insertImage, onClick: openImageDialog })), config.blocks.pageBreak && (_jsx(ToolbarButton, { icon: IconPageBreak, title: strings.toolbar.insertPageBreak, onClick: () => editor.dispatchCommand(INSERT_PAGE_BREAK_COMMAND, undefined) })), config.tables && _jsx(ToolbarButton, { icon: IconTable, title: strings.toolbar.insertTable, onClick: openTableDialog }), config.columns && _jsx(ToolbarButton, { icon: IconColumns, title: strings.toolbar.insertColumns, onClick: openLayoutDialog }), config.footnotes && (_jsx(ToolbarButton, { icon: IconNumber1Small, title: strings.toolbar.insertFootnote, onClick: () => editor.dispatchCommand(INSERT_FOOTNOTE_COMMAND, undefined) })), config.blocks.horizontalRule && (_jsx(ToolbarButton, { icon: IconSeparatorHorizontal, title: strings.toolbar.insertHorizontalRule, onClick: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) })), config.poetry.enabled && locale !== 'en' && (_jsx(ToolbarButton, { icon: IconFeather, title: strings.toolbar.insertPoetryCouplet, onClick: () => editor.dispatchCommand(INSERT_POETRY_COUPLET_COMMAND, { layout: config.poetry.defaultLayout
|
|
813
|
+
showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: strings.toolbar.insertImage, onClick: openImageDialog })), config.blocks.pageBreak && (_jsx(ToolbarButton, { icon: IconPageBreak, title: strings.toolbar.insertPageBreak, onClick: () => editor.dispatchCommand(INSERT_PAGE_BREAK_COMMAND, undefined) })), config.tables && _jsx(ToolbarButton, { icon: IconTable, title: strings.toolbar.insertTable, onClick: openTableDialog }), config.columns && _jsx(ToolbarButton, { icon: IconColumns, title: strings.toolbar.insertColumns, onClick: openLayoutDialog }), config.footnotes && (_jsx(ToolbarButton, { icon: IconNumber1Small, title: strings.toolbar.insertFootnote, onClick: () => editor.dispatchCommand(INSERT_FOOTNOTE_COMMAND, undefined) })), config.blocks.horizontalRule && (_jsx(ToolbarButton, { icon: IconSeparatorHorizontal, title: strings.toolbar.insertHorizontalRule, onClick: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) })), config.findReplace && (_jsx(ToolbarButton, { icon: IconSearch, title: strings.findReplace.toggle, active: findOpen, onClick: () => setFindOpen((v) => !v) })), config.poetry.enabled && locale !== 'en' && (_jsx(ToolbarButton, { icon: IconFeather, title: strings.toolbar.insertPoetryCouplet, onClick: () => editor.dispatchCommand(INSERT_POETRY_COUPLET_COMMAND, { layout: config.poetry.defaultLayout }) }))] }, "stubInsert")),
|
|
812
814
|
config.tables && state.inTable && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.tableOptions, title: strings.toolbar.tableOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconTableOptions, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(TableMenuItems, { strings: strings, direction: direction, rows: state.tableRows, columns: state.tableColumns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction }) })] }) }, "tableActions")),
|
|
813
|
-
config.poetry.enabled && state.inPoetry && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.poetryOptions, title: strings.toolbar.poetryOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconFeather, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings,
|
|
815
|
+
config.poetry.enabled && state.inPoetry && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.poetryOptions, title: strings.toolbar.poetryOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconFeather, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction }) })] }) }, "poetryActions")),
|
|
814
816
|
showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: strings.toolbar.autocorrect, comingSoon: strings.toolbar.comingSoon }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: strings.toolbar.textCleanup, comingSoon: strings.toolbar.comingSoon }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: strings.toolbar.spellChecker, comingSoon: strings.toolbar.comingSoon })] }, "language")),
|
|
815
817
|
];
|
|
816
818
|
return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": strings.toolbar.ariaLabel, ref: toolbarContainerRef, children: [fixedGroups, movableGroups
|
|
@@ -829,7 +831,7 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
829
831
|
}, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: tableContext?.x ?? -9999, top: tableContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(TableMenuItems, { strings: strings, direction: direction, rows: tableContext?.rows ?? 1, columns: tableContext?.columns ?? 1, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction }) })] })), config.poetry.enabled && (_jsxs(Menu, { opened: poetryContext !== null, onChange: (opened) => {
|
|
830
832
|
if (!opened)
|
|
831
833
|
setPoetryContext(null);
|
|
832
|
-
}, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: poetryContext?.x ?? -9999, top: poetryContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings,
|
|
834
|
+
}, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: poetryContext?.x ?? -9999, top: poetryContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction }) })] })), _jsxs("div", { "aria-hidden": "true", ref: toolbarMeasureRef, style: {
|
|
833
835
|
position: 'absolute',
|
|
834
836
|
visibility: 'hidden',
|
|
835
837
|
pointerEvents: 'none',
|
|
@@ -838,5 +840,5 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
838
840
|
display: 'flex',
|
|
839
841
|
flexWrap: 'nowrap',
|
|
840
842
|
gap: 8,
|
|
841
|
-
}, children: [fixedGroups, movableGroups.map((group) => (_jsx(MovableGroupButtons, { group: group }, group.key)))] }), (config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.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 })] }));
|
|
843
|
+
}, children: [fixedGroups, movableGroups.map((group) => (_jsx(MovableGroupButtons, { group: group }, group.key)))] }), (config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.tables && _jsx(TableDialog, { opened: tableDialogOpen, onSubmit: insertTable, onClose: closeTableDialog }), config.findReplace && findOpen && _jsx(FindReplaceBar, { strings: strings, onClose: () => setFindOpen(false) }), 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 })] }));
|
|
842
844
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A match as a Lexical selection range: start and end points, each in a text node. */
|
|
2
|
+
export interface FindMatch {
|
|
3
|
+
anchorKey: string;
|
|
4
|
+
anchorOffset: number;
|
|
5
|
+
focusKey: string;
|
|
6
|
+
focusOffset: number;
|
|
7
|
+
}
|
|
8
|
+
/** Every non-overlapping occurrence of `query` in the document, case-sensitive. */
|
|
9
|
+
export declare function $findMatches(query: string): FindMatch[];
|
|
10
|
+
/** Makes the match the editor's selection, so it's highlighted and scrolled to. */
|
|
11
|
+
export declare function $selectMatch(match: FindMatch): void;
|
|
12
|
+
/**
|
|
13
|
+
* Replaces one match, keeping the formatting of the text around it. The
|
|
14
|
+
* replacement takes the formatting of the match's first character.
|
|
15
|
+
*/
|
|
16
|
+
export declare function $replaceMatch(match: FindMatch, replacement: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Replaces every match. Walks from the last match to the first, so each
|
|
19
|
+
* replacement leaves the positions of the matches before it unchanged.
|
|
20
|
+
* Returns how many were replaced.
|
|
21
|
+
*/
|
|
22
|
+
export declare function $replaceAll(query: string, replacement: string): number;
|
|
23
|
+
//# sourceMappingURL=findReplaceActions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findReplaceActions.d.ts","sourceRoot":"","sources":["../../src/find/findReplaceActions.ts"],"names":[],"mappings":"AAEA,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAgCD,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,CAcvD;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAKnD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAMzE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAItE"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { $createRangeSelection, $getRoot, $getSelection, $isRangeSelection, $setSelection } from 'lexical';
|
|
2
|
+
/**
|
|
3
|
+
* Text nodes grouped by their parent, in document order. A match is searched
|
|
4
|
+
* for within one parent only, so it never spans two paragraphs or table
|
|
5
|
+
* cells, but it can span the several text nodes that formatting splits a
|
|
6
|
+
* paragraph into.
|
|
7
|
+
*/
|
|
8
|
+
function $textGroups() {
|
|
9
|
+
const groups = new Map();
|
|
10
|
+
for (const node of $getRoot().getAllTextNodes()) {
|
|
11
|
+
const parentKey = node.getParentOrThrow().getKey();
|
|
12
|
+
const group = groups.get(parentKey);
|
|
13
|
+
if (group)
|
|
14
|
+
group.push(node);
|
|
15
|
+
else
|
|
16
|
+
groups.set(parentKey, [node]);
|
|
17
|
+
}
|
|
18
|
+
return [...groups.values()];
|
|
19
|
+
}
|
|
20
|
+
/** The text node and offset that a character index in a group falls in. */
|
|
21
|
+
function $locate(group, index, end) {
|
|
22
|
+
let start = 0;
|
|
23
|
+
for (const node of group) {
|
|
24
|
+
const length = node.getTextContent().length;
|
|
25
|
+
const inside = end ? index > start && index <= start + length : index >= start && index < start + length;
|
|
26
|
+
if (inside)
|
|
27
|
+
return { key: node.getKey(), offset: index - start };
|
|
28
|
+
start += length;
|
|
29
|
+
}
|
|
30
|
+
const last = group[group.length - 1];
|
|
31
|
+
return { key: last.getKey(), offset: last.getTextContent().length };
|
|
32
|
+
}
|
|
33
|
+
/** Every non-overlapping occurrence of `query` in the document, case-sensitive. */
|
|
34
|
+
export function $findMatches(query) {
|
|
35
|
+
if (query === '')
|
|
36
|
+
return [];
|
|
37
|
+
const matches = [];
|
|
38
|
+
for (const group of $textGroups()) {
|
|
39
|
+
const text = group.map((node) => node.getTextContent()).join('');
|
|
40
|
+
let index = text.indexOf(query);
|
|
41
|
+
while (index !== -1) {
|
|
42
|
+
const start = $locate(group, index, false);
|
|
43
|
+
const end = $locate(group, index + query.length, true);
|
|
44
|
+
matches.push({ anchorKey: start.key, anchorOffset: start.offset, focusKey: end.key, focusOffset: end.offset });
|
|
45
|
+
index = text.indexOf(query, index + query.length);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return matches;
|
|
49
|
+
}
|
|
50
|
+
/** Makes the match the editor's selection, so it's highlighted and scrolled to. */
|
|
51
|
+
export function $selectMatch(match) {
|
|
52
|
+
const selection = $createRangeSelection();
|
|
53
|
+
selection.anchor.set(match.anchorKey, match.anchorOffset, 'text');
|
|
54
|
+
selection.focus.set(match.focusKey, match.focusOffset, 'text');
|
|
55
|
+
$setSelection(selection);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Replaces one match, keeping the formatting of the text around it. The
|
|
59
|
+
* replacement takes the formatting of the match's first character.
|
|
60
|
+
*/
|
|
61
|
+
export function $replaceMatch(match, replacement) {
|
|
62
|
+
$selectMatch(match);
|
|
63
|
+
const selection = $getSelection();
|
|
64
|
+
if (!$isRangeSelection(selection))
|
|
65
|
+
return;
|
|
66
|
+
if (replacement === '')
|
|
67
|
+
selection.removeText();
|
|
68
|
+
else
|
|
69
|
+
selection.insertText(replacement);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Replaces every match. Walks from the last match to the first, so each
|
|
73
|
+
* replacement leaves the positions of the matches before it unchanged.
|
|
74
|
+
* Returns how many were replaced.
|
|
75
|
+
*/
|
|
76
|
+
export function $replaceAll(query, replacement) {
|
|
77
|
+
const matches = $findMatches(query);
|
|
78
|
+
for (let i = matches.length - 1; i >= 0; i--)
|
|
79
|
+
$replaceMatch(matches[i], replacement);
|
|
80
|
+
return matches.length;
|
|
81
|
+
}
|