@lofcz/platejs-core 52.3.4 → 53.0.0

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.
@@ -112,6 +112,7 @@ function createSlatePlugin(config = {}) {
112
112
  render: {},
113
113
  rules: {},
114
114
  shortcuts: {},
115
+ inputRules: [],
115
116
  transforms: {}
116
117
  }, config);
117
118
  if (plugin.node.isLeaf && !isDefined(plugin.node.isDecoration)) plugin.node.isDecoration = true;
@@ -283,6 +284,11 @@ function getEditorPlugin(editor, p) {
283
284
 
284
285
  //#endregion
285
286
  //#region src/internal/plugin/resolvePlugin.ts
287
+ const normalizeConfiguredInputRules = (config) => {
288
+ if (config === void 0) return [];
289
+ if (Array.isArray(config)) return [...config];
290
+ throw new Error("inputRules config must be an array of explicit rule instances.");
291
+ };
286
292
  /**
287
293
  * Resolves and finalizes a plugin configuration for use in a Plate editor.
288
294
  *
@@ -303,6 +309,11 @@ const resolvePlugin = (editor, _plugin) => {
303
309
  plugin.__resolved = true;
304
310
  if (plugin.__configuration) {
305
311
  const configResult = plugin.__configuration(getEditorPlugin(editor, plugin));
312
+ if (configResult.inputRules !== void 0) {
313
+ const normalizedInputRules = normalizeConfiguredInputRules(configResult.inputRules);
314
+ plugin.__configuredInputRules = [...normalizeConfiguredInputRules(plugin.__configuredInputRules), ...normalizedInputRules];
315
+ configResult.inputRules = void 0;
316
+ }
306
317
  plugin = mergePlugins(plugin, configResult);
307
318
  plugin.__configuration = void 0;
308
319
  }
@@ -361,11 +372,390 @@ const getPluginByType = (editor, type) => {
361
372
  };
362
373
  const getContainerTypes = (editor) => getPluginTypes(editor, editor.meta.pluginCache.node.isContainer);
363
374
 
375
+ //#endregion
376
+ //#region src/lib/plugins/input-rules/defineInputRule.ts
377
+ function defineInputRule(rule) {
378
+ return rule;
379
+ }
380
+
381
+ //#endregion
382
+ //#region src/lib/plugins/input-rules/createInputRules.ts
383
+ const noWhiteSpaceRegex = /\S+/;
384
+ const isPreviousCharacterEmpty = (editor, at) => {
385
+ const range = editor.api.range("before", at);
386
+ if (!range) return true;
387
+ const text = editor.api.string(range);
388
+ return text ? !noWhiteSpaceRegex.exec(text) : true;
389
+ };
390
+ const getMarkMatch = (editor, { end = "", start }) => {
391
+ const { selection } = editor;
392
+ if (!selection) return;
393
+ let beforeEndMatchPoint = selection.anchor;
394
+ if (end) {
395
+ beforeEndMatchPoint = editor.api.before(selection, { matchString: end });
396
+ if (!beforeEndMatchPoint) return;
397
+ }
398
+ const afterStartMatchPoint = editor.api.before(beforeEndMatchPoint, {
399
+ afterMatch: true,
400
+ matchString: start,
401
+ skipInvalid: true
402
+ });
403
+ if (!afterStartMatchPoint) return;
404
+ const beforeStartMatchPoint = editor.api.before(beforeEndMatchPoint, {
405
+ matchString: start,
406
+ skipInvalid: true
407
+ });
408
+ if (!beforeStartMatchPoint) return;
409
+ if (!isPreviousCharacterEmpty(editor, beforeStartMatchPoint)) return;
410
+ return {
411
+ afterStartMatchPoint,
412
+ beforeEndMatchPoint,
413
+ beforeStartMatchPoint
414
+ };
415
+ };
416
+ const createMarkInputRule = (config) => defineInputRule({
417
+ enabled: config.enabled,
418
+ priority: config.priority,
419
+ target: "insertText",
420
+ trigger: config.trigger,
421
+ resolve: ({ editor, text }) => {
422
+ if (text !== config.trigger || !editor.selection || !editor.api.isCollapsed()) return;
423
+ const match = getMarkMatch(editor, {
424
+ end: config.end,
425
+ start: config.start
426
+ });
427
+ if (!match) return;
428
+ const range = {
429
+ anchor: match.afterStartMatchPoint,
430
+ focus: match.beforeEndMatchPoint
431
+ };
432
+ const matchText = editor.api.string(range);
433
+ if (config.trim !== "allow" && matchText.trim() !== matchText) return;
434
+ return {
435
+ ...match,
436
+ end: config.end
437
+ };
438
+ },
439
+ apply: ({ editor, pluginKey }, match) => {
440
+ const marks = config.marks ? [...config.marks] : [config.mark ?? pluginKey];
441
+ if (match.beforeEndMatchPoint !== editor.selection?.anchor) editor.tf.delete({ at: {
442
+ anchor: match.beforeEndMatchPoint,
443
+ focus: editor.selection.anchor
444
+ } });
445
+ editor.tf.select({
446
+ anchor: match.afterStartMatchPoint,
447
+ focus: match.beforeEndMatchPoint
448
+ });
449
+ marks.forEach((mark) => {
450
+ editor.tf.addMark(editor.getType(mark), true);
451
+ });
452
+ editor.tf.collapse({ edge: "end" });
453
+ editor.tf.removeMarks(marks.map((mark) => editor.getType(mark)), { shouldChange: false });
454
+ editor.tf.delete({ at: {
455
+ anchor: match.beforeStartMatchPoint,
456
+ focus: match.afterStartMatchPoint
457
+ } });
458
+ return true;
459
+ }
460
+ });
461
+ const matchBlockStart = (context, config) => {
462
+ if (!context.isCollapsed) return;
463
+ const pattern = typeof config.match === "function" ? config.match(context) : config.match;
464
+ if (!pattern) return;
465
+ const range = context.getBlockStartRange();
466
+ const blockText = context.getBlockStartText();
467
+ if (!range || blockText === void 0) return;
468
+ const baseMatch = {
469
+ range,
470
+ text: blockText
471
+ };
472
+ if (typeof pattern === "string") {
473
+ if (blockText !== pattern) return;
474
+ if (config.resolveMatch) {
475
+ const resolved = config.resolveMatch({
476
+ match: pattern,
477
+ range,
478
+ text: blockText
479
+ });
480
+ if (resolved === void 0) return;
481
+ return {
482
+ ...baseMatch,
483
+ ...resolved
484
+ };
485
+ }
486
+ return baseMatch;
487
+ }
488
+ const regexMatch = blockText.match(pattern);
489
+ if (!regexMatch) return;
490
+ if (config.resolveMatch) {
491
+ const resolved = config.resolveMatch({
492
+ match: regexMatch,
493
+ range,
494
+ text: blockText
495
+ });
496
+ if (resolved === void 0) return;
497
+ return {
498
+ ...baseMatch,
499
+ ...resolved
500
+ };
501
+ }
502
+ return baseMatch;
503
+ };
504
+ const createBlockStartInputRule = (config) => defineInputRule({
505
+ enabled: config.enabled,
506
+ priority: config.priority,
507
+ target: "insertText",
508
+ trigger: config.trigger,
509
+ resolve: (context) => matchBlockStart(context, config),
510
+ apply: (context, match) => {
511
+ if (config.apply) return config.apply(context, match);
512
+ const { editor, pluginKey } = context;
513
+ const defaultMatch = match;
514
+ if (config.removeMatchedText !== false) editor.tf.delete({ at: defaultMatch.range });
515
+ const node = editor.getType(config.node ?? pluginKey);
516
+ if (config.mode === "wrap") {
517
+ editor.tf.toggleBlock(node, { wrap: true });
518
+ return true;
519
+ }
520
+ if (config.mode === "toggle") {
521
+ editor.tf.toggleBlock(node);
522
+ return true;
523
+ }
524
+ editor.tf.setNodes({ type: node }, { match: (entryNode) => editor.api.isBlock(entryNode) });
525
+ return true;
526
+ }
527
+ });
528
+ const matchBlockFence = (context, config) => {
529
+ const { editor } = context;
530
+ const { selection } = editor;
531
+ if (!context.isCollapsed || !selection) return;
532
+ const blockEntry = context.getBlockEntry();
533
+ if (!blockEntry) return;
534
+ const [blockNode, path] = blockEntry;
535
+ const endPoint = editor.api.end(path);
536
+ if (config.block && blockNode.type !== editor.getType(config.block)) return;
537
+ if (!endPoint || !editor.api.isEnd(selection.focus, path)) return;
538
+ const range = context.getBlockStartRange();
539
+ const blockText = context.getBlockStartText();
540
+ if (!range || blockText === void 0 || blockText !== config.fence) return;
541
+ return config.resolveMatch ? config.resolveMatch({
542
+ fence: config.fence,
543
+ path,
544
+ range,
545
+ text: blockText
546
+ }) : {
547
+ path,
548
+ range,
549
+ text: blockText
550
+ };
551
+ };
552
+ function createBlockFenceInputRule(config) {
553
+ if (config.on === "break") return defineInputRule({
554
+ priority: config.priority,
555
+ target: "insertBreak",
556
+ enabled: config.enabled,
557
+ resolve: (context) => matchBlockFence(context, {
558
+ block: config.block,
559
+ fence: config.fence,
560
+ resolveMatch: config.resolveMatch
561
+ }),
562
+ apply: config.apply
563
+ });
564
+ const trigger = config.fence.at(-1);
565
+ if (!trigger) throw new Error("createBlockFenceInputRule requires a non-empty fence.");
566
+ return defineInputRule({
567
+ priority: config.priority,
568
+ target: "insertText",
569
+ enabled: config.enabled,
570
+ trigger,
571
+ resolve: (context) => {
572
+ if (context.text !== trigger) return;
573
+ return matchBlockFence(context, {
574
+ block: config.block,
575
+ fence: config.fence.slice(0, -trigger.length),
576
+ resolveMatch: config.resolveMatch
577
+ });
578
+ },
579
+ apply: config.apply
580
+ });
581
+ }
582
+ const matchDelimitedInline = (context, { boundaryRe, close, followRe, open, requireClosingDelimiter = true, rejectRepeatedOpen = true, trim = "reject" }) => {
583
+ const { editor } = context;
584
+ const { selection } = editor;
585
+ if (!selection || !context.isCollapsed) return;
586
+ const blockRange = context.getBlockStartRange();
587
+ if (!blockRange) return;
588
+ const openingDelimiter = open;
589
+ const closingDelimiter = close ?? open;
590
+ const textBefore = editor.api.string(blockRange);
591
+ const beforeClose = requireClosingDelimiter ? (() => {
592
+ const closeLength = closingDelimiter.length;
593
+ if (textBefore.length < closeLength) return;
594
+ if (!textBefore.endsWith(closingDelimiter)) return;
595
+ return textBefore.slice(0, -closeLength);
596
+ })() : textBefore;
597
+ if (!beforeClose) return;
598
+ const openIndex = beforeClose.lastIndexOf(openingDelimiter);
599
+ if (openIndex < 0) return;
600
+ const prefix = beforeClose.slice(0, openIndex);
601
+ const content = beforeClose.slice(openIndex + openingDelimiter.length);
602
+ if (!content) return;
603
+ if (trim === "reject" && content.trim() !== content) return;
604
+ if (rejectRepeatedOpen && openingDelimiter === closingDelimiter && prefix.endsWith(openingDelimiter)) return;
605
+ const previousChar = prefix.at(-1);
606
+ if (previousChar && boundaryRe && !boundaryRe.test(previousChar)) return;
607
+ const nextPoint = editor.api.after(selection, {
608
+ distance: 1,
609
+ unit: "character"
610
+ });
611
+ if (nextPoint && followRe) {
612
+ const nextChar = editor.api.string({
613
+ anchor: selection.anchor,
614
+ focus: nextPoint
615
+ });
616
+ if (nextChar && !followRe.test(nextChar)) return;
617
+ }
618
+ const startPoint = editor.api.before(selection, {
619
+ distance: content.length + openingDelimiter.length,
620
+ unit: "character"
621
+ });
622
+ if (!startPoint) return;
623
+ return {
624
+ content,
625
+ deleteRange: {
626
+ anchor: startPoint,
627
+ focus: selection.anchor
628
+ }
629
+ };
630
+ };
631
+ const getTextSubstitutionMatchRange = ({ match, trigger }) => {
632
+ const start = match;
633
+ const reversed = start.split("").reverse().join("");
634
+ const triggers = trigger ? Array.isArray(trigger) ? [...trigger] : [trigger] : [reversed.slice(-1)];
635
+ return {
636
+ end: trigger ? reversed : reversed.slice(0, -1),
637
+ start,
638
+ triggers
639
+ };
640
+ };
641
+ const getTextSubstitutionMatchPoints = (editor, { end, start }) => {
642
+ const { selection } = editor;
643
+ if (!selection) return;
644
+ let beforeEndMatchPoint = selection.anchor;
645
+ if (end) {
646
+ beforeEndMatchPoint = editor.api.before(selection, { matchString: end });
647
+ if (!beforeEndMatchPoint) return;
648
+ }
649
+ let afterStartMatchPoint;
650
+ let beforeStartMatchPoint;
651
+ if (start) {
652
+ afterStartMatchPoint = editor.api.before(beforeEndMatchPoint, {
653
+ afterMatch: true,
654
+ matchString: start,
655
+ skipInvalid: true
656
+ });
657
+ if (!afterStartMatchPoint) return;
658
+ beforeStartMatchPoint = editor.api.before(beforeEndMatchPoint, {
659
+ matchString: start,
660
+ skipInvalid: true
661
+ });
662
+ if (!beforeStartMatchPoint) return;
663
+ if (!isPreviousCharacterEmpty(editor, beforeStartMatchPoint)) return;
664
+ }
665
+ return {
666
+ afterStartMatchPoint,
667
+ beforeEndMatchPoint,
668
+ beforeStartMatchPoint
669
+ };
670
+ };
671
+ const getTextSubstitutionTriggers = (patterns) => Array.from(new Set(patterns.flatMap((pattern) => {
672
+ return (Array.isArray(pattern.match) ? [...pattern.match] : [pattern.match]).flatMap((match) => getTextSubstitutionMatchRange({
673
+ match,
674
+ trigger: pattern.trigger
675
+ }).triggers);
676
+ })));
677
+ const resolveTextSubstitution = ({ editor, patterns, text }) => {
678
+ for (const pattern of patterns) {
679
+ const matches = Array.isArray(pattern.match) ? [...pattern.match] : [pattern.match];
680
+ for (const match of matches) {
681
+ const { end, start, triggers } = getTextSubstitutionMatchRange({
682
+ match,
683
+ trigger: pattern.trigger
684
+ });
685
+ if (!triggers.includes(text)) continue;
686
+ const points = getTextSubstitutionMatchPoints(editor, {
687
+ end: Array.isArray(pattern.format) ? "" : end,
688
+ start
689
+ });
690
+ if (!points) continue;
691
+ return {
692
+ end: Array.isArray(pattern.format) ? "" : end,
693
+ pattern,
694
+ points
695
+ };
696
+ }
697
+ }
698
+ };
699
+ const applyTextSubstitution = (editor, match) => {
700
+ const selection = editor.selection;
701
+ if (!selection || !match) return false;
702
+ if (match.end) editor.tf.delete({ at: {
703
+ anchor: match.points.beforeEndMatchPoint,
704
+ focus: selection.anchor
705
+ } });
706
+ const formatEnd = Array.isArray(match.pattern.format) ? match.pattern.format[1] : match.pattern.format;
707
+ editor.tf.insertText(formatEnd);
708
+ if (match.points.beforeStartMatchPoint && match.points.afterStartMatchPoint) {
709
+ const formatStart = Array.isArray(match.pattern.format) ? match.pattern.format[0] : match.pattern.format;
710
+ editor.tf.delete({ at: {
711
+ anchor: match.points.beforeStartMatchPoint,
712
+ focus: match.points.afterStartMatchPoint
713
+ } });
714
+ editor.tf.insertText(formatStart, { at: match.points.beforeStartMatchPoint });
715
+ }
716
+ return true;
717
+ };
718
+ const createTextSubstitutionInputRule = ({ enabled, patterns, priority }) => defineInputRule({
719
+ enabled,
720
+ priority,
721
+ target: "insertText",
722
+ trigger: getTextSubstitutionTriggers(patterns),
723
+ resolve: ({ editor, text }) => {
724
+ if (!editor.selection || !editor.api.isCollapsed()) return;
725
+ return resolveTextSubstitution({
726
+ editor,
727
+ patterns,
728
+ text
729
+ });
730
+ },
731
+ apply: ({ editor }, match) => applyTextSubstitution(editor, match)
732
+ });
733
+
734
+ //#endregion
735
+ //#region src/lib/plugins/input-rules/internal/createInputRuleBuilder.ts
736
+ const createInputRuleBuilder = () => ({
737
+ blockFence: (config) => createBlockFenceInputRule(config),
738
+ blockStart: (config) => createBlockStartInputRule(config),
739
+ insertBreak: (rule) => defineInputRule(rule),
740
+ insertData: (rule) => defineInputRule(rule),
741
+ insertText: (rule) => defineInputRule(rule),
742
+ mark: (config) => createMarkInputRule(config)
743
+ });
744
+
364
745
  //#endregion
365
746
  //#region src/internal/plugin/resolvePlugins.ts
366
747
  const resolvePlugins = (editor, plugins = [], createStore = createVanillaStore) => {
367
748
  editor.plugins = {};
368
749
  editor.meta.pluginList = [];
750
+ editor.meta.inputRules = {
751
+ insertBreak: [],
752
+ insertData: [],
753
+ insertText: {
754
+ all: [],
755
+ byTrigger: {}
756
+ },
757
+ plugins: {}
758
+ };
369
759
  editor.meta.shortcuts = {};
370
760
  editor.meta.components = {};
371
761
  editor.meta.pluginCache = {
@@ -435,6 +825,8 @@ const resolvePlugins = (editor, plugins = [], createStore = createVanillaStore)
435
825
  if (plugin.handlers?.onTextChange) editor.meta.pluginCache.handlers.onTextChange.push(plugin.key);
436
826
  });
437
827
  resolvePluginShortcuts(editor);
828
+ resolvePluginInputRules(editor);
829
+ validateRemovedRuntimePlugins(editor);
438
830
  return editor;
439
831
  };
440
832
  const resolvePluginStores = (editor, createStore) => {
@@ -512,6 +904,68 @@ const resolvePluginShortcuts = (editor) => {
512
904
  });
513
905
  });
514
906
  };
907
+ const resolvePluginInputRules = (editor) => {
908
+ const resolvedMeta = {
909
+ insertBreak: [],
910
+ insertData: [],
911
+ insertText: {
912
+ all: [],
913
+ byTrigger: {}
914
+ },
915
+ plugins: {}
916
+ };
917
+ editor.meta.pluginList.forEach((plugin, pluginIndex) => {
918
+ const pluginKey = plugin.key;
919
+ const inputRulesDefinition = plugin.inputRules;
920
+ const definitionRules = typeof inputRulesDefinition === "function" ? inputRulesDefinition({ rule: createInputRuleBuilder() }) : inputRulesDefinition ?? [];
921
+ const configuredRules = plugin.__configuredInputRules ?? [];
922
+ const ruleDefinitions = [...definitionRules, ...configuredRules];
923
+ resolvedMeta.plugins[pluginKey] = { rules: [] };
924
+ ruleDefinitions.forEach((definition, ruleIndex) => {
925
+ if (!definition) return;
926
+ const mergedRule = mergePlugins({}, definition);
927
+ const resolvedRule = {
928
+ ...mergedRule,
929
+ id: `${pluginKey}.${ruleIndex}`,
930
+ pluginIndex,
931
+ pluginKey,
932
+ priority: mergedRule.priority ?? plugin.priority,
933
+ ruleIndex
934
+ };
935
+ resolvedMeta.plugins[pluginKey].rules.push(resolvedRule);
936
+ if (resolvedRule.target === "insertText") {
937
+ const triggers = Array.isArray(resolvedRule.trigger) ? [...resolvedRule.trigger] : [resolvedRule.trigger];
938
+ resolvedMeta.insertText.all.push(resolvedRule);
939
+ triggers.forEach((trigger) => {
940
+ if (!resolvedMeta.insertText.byTrigger[trigger]) resolvedMeta.insertText.byTrigger[trigger] = [];
941
+ resolvedMeta.insertText.byTrigger[trigger].push(resolvedRule);
942
+ });
943
+ } else if (resolvedRule.target === "insertBreak") resolvedMeta.insertBreak.push(resolvedRule);
944
+ else if (resolvedRule.target === "insertData") resolvedMeta.insertData.push(resolvedRule);
945
+ });
946
+ });
947
+ const sortRules = (a, b) => {
948
+ if (b.priority !== a.priority) return b.priority - a.priority;
949
+ if (a.pluginIndex !== b.pluginIndex) return a.pluginIndex - b.pluginIndex;
950
+ return a.ruleIndex - b.ruleIndex;
951
+ };
952
+ resolvedMeta.insertBreak.sort(sortRules);
953
+ resolvedMeta.insertData.sort(sortRules);
954
+ resolvedMeta.insertText.all.sort(sortRules);
955
+ Object.values(resolvedMeta.insertText.byTrigger).forEach((rules) => {
956
+ rules.sort(sortRules);
957
+ });
958
+ editor.meta.inputRules = resolvedMeta;
959
+ };
960
+ const validateRemovedRuntimePlugins = (editor) => {
961
+ const hasAutoformatPlugin = !!editor.plugins.autoformat;
962
+ const hasResolvedInputRules = editor.meta.inputRules.insertBreak.length > 0 || editor.meta.inputRules.insertData.length > 0 || editor.meta.inputRules.insertText.all.length > 0;
963
+ if (hasAutoformatPlugin && hasResolvedInputRules) throw new Error([
964
+ "AutoformatPlugin cannot be used with plugin-owned input rules.",
965
+ "Remove AutoformatPlugin from your editor plugins.",
966
+ "Enable inputRules on the feature plugins you use instead."
967
+ ].join(" "));
968
+ };
515
969
  const flattenAndResolvePlugins = (editor, plugins) => {
516
970
  const pluginMap = /* @__PURE__ */ new Map();
517
971
  const processPlugin = (plugin) => {
@@ -643,11 +1097,11 @@ const withBreakRules = (ctx) => {
643
1097
  node: blockNode,
644
1098
  path: blockPath,
645
1099
  rule
646
- })) return overridePlugin.rules.break;
1100
+ })) return overridePlugin;
647
1101
  }
648
1102
  return null;
649
1103
  };
650
- const executeBreakAction = (action, blockPath) => {
1104
+ const executeBreakAction = (action, blockPath, type) => {
651
1105
  if (action === "reset") {
652
1106
  editor.tf.resetBlock({ at: blockPath });
653
1107
  return true;
@@ -665,30 +1119,40 @@ const withBreakRules = (ctx) => {
665
1119
  editor.tf.insertSoftBreak();
666
1120
  return true;
667
1121
  }
1122
+ if (action === "lift" && type) return !!editor.tf.liftBlock({
1123
+ at: blockPath,
1124
+ match: { type }
1125
+ });
668
1126
  return false;
669
1127
  };
670
1128
  return { transforms: { insertBreak() {
671
- if (editor.selection && editor.api.isCollapsed()) {
1129
+ if (editor.selection) {
672
1130
  const block = editor.api.block();
673
1131
  if (block) {
674
1132
  const [blockNode, blockPath] = block;
675
1133
  const breakRules = getPluginByType(editor, blockNode.type)?.rules.break;
676
- if (editor.api.isEmpty(editor.selection, { block: true })) {
677
- const emptyAction = (checkMatchRulesOverride("break.empty", blockNode, blockPath) || breakRules)?.empty;
678
- if (executeBreakAction(emptyAction, blockPath)) return;
1134
+ if (editor.api.isCollapsed() && editor.api.isEmpty(editor.selection, { block: true })) {
1135
+ const overridePlugin = checkMatchRulesOverride("break.empty", blockNode, blockPath);
1136
+ const emptyAction = (overridePlugin?.rules.break ?? breakRules)?.empty;
1137
+ const actionType = overridePlugin?.node.type;
1138
+ if (executeBreakAction(emptyAction, blockPath, actionType)) return;
679
1139
  }
680
- if (!editor.api.isEmpty(editor.selection, { block: true }) && editor.api.isAt({ end: true })) {
1140
+ if (editor.api.isCollapsed() && !editor.api.isEmpty(editor.selection, { block: true }) && editor.api.isAt({ end: true })) {
681
1141
  const range = editor.api.range("before", editor.selection);
682
1142
  if (range) {
683
1143
  if (editor.api.string(range) === "\n") {
684
- const emptyLineEndAction = (checkMatchRulesOverride("break.emptyLineEnd", blockNode, blockPath) || breakRules)?.emptyLineEnd;
685
- if (executeBreakAction(emptyLineEndAction, blockPath)) return;
1144
+ const overridePlugin = checkMatchRulesOverride("break.emptyLineEnd", blockNode, blockPath);
1145
+ const emptyLineEndAction = (overridePlugin?.rules.break ?? breakRules)?.emptyLineEnd;
1146
+ const actionType = overridePlugin?.node.type;
1147
+ if (executeBreakAction(emptyLineEndAction, blockPath, actionType)) return;
686
1148
  }
687
1149
  }
688
1150
  }
689
- const defaultAction = (checkMatchRulesOverride("break.default", blockNode, blockPath) || breakRules)?.default;
690
- if (executeBreakAction(defaultAction, blockPath)) return;
691
- if (checkMatchRulesOverride("break.splitReset", blockNode, blockPath)?.splitReset ?? breakRules?.splitReset) {
1151
+ const overrideDefaultPlugin = checkMatchRulesOverride("break.default", blockNode, blockPath);
1152
+ const defaultAction = (overrideDefaultPlugin?.rules.break ?? breakRules)?.default;
1153
+ const defaultActionType = overrideDefaultPlugin?.node.type;
1154
+ if (executeBreakAction(defaultAction, blockPath, defaultActionType)) return;
1155
+ if ((checkMatchRulesOverride("break.splitReset", blockNode, blockPath)?.rules.break?.splitReset ?? breakRules?.splitReset) && !editor.api.isAt({ blocks: true })) {
692
1156
  const isAtStart = editor.api.isAt({ start: true });
693
1157
  insertBreak();
694
1158
  editor.tf.resetBlock({ at: isAtStart ? blockPath : PathApi.next(blockPath) });
@@ -716,15 +1180,19 @@ const withDeleteRules = (ctx) => {
716
1180
  node: blockNode,
717
1181
  path: blockPath,
718
1182
  rule
719
- })) return overridePlugin.rules.delete;
1183
+ })) return overridePlugin;
720
1184
  }
721
1185
  return null;
722
1186
  };
723
- const executeDeleteAction = (action, blockPath) => {
1187
+ const executeDeleteAction = (action, blockPath, type) => {
724
1188
  if (action === "reset") {
725
1189
  editor.tf.resetBlock({ at: blockPath });
726
1190
  return true;
727
1191
  }
1192
+ if (action === "lift" && type) return !!editor.tf.liftBlock({
1193
+ at: blockPath,
1194
+ match: { type }
1195
+ });
728
1196
  return false;
729
1197
  };
730
1198
  return { transforms: {
@@ -735,12 +1203,16 @@ const withDeleteRules = (ctx) => {
735
1203
  const [blockNode, blockPath] = block;
736
1204
  const deleteRules = getPluginByType(editor, blockNode.type)?.rules.delete;
737
1205
  if (editor.api.isAt({ start: true })) {
738
- const startAction = (checkMatchRulesOverride("delete.start", blockNode, blockPath) || deleteRules)?.start;
739
- if (executeDeleteAction(startAction, blockPath)) return;
1206
+ const overridePlugin = checkMatchRulesOverride("delete.start", blockNode, blockPath);
1207
+ const startAction = (overridePlugin?.rules.delete ?? deleteRules)?.start;
1208
+ const actionType = overridePlugin?.node.type;
1209
+ if (executeDeleteAction(startAction, blockPath, actionType)) return;
740
1210
  }
741
1211
  if (editor.api.isEmpty(editor.selection, { block: true })) {
742
- const emptyAction = (checkMatchRulesOverride("delete.empty", blockNode, blockPath) || deleteRules)?.empty;
743
- if (executeDeleteAction(emptyAction, blockPath)) return;
1212
+ const overridePlugin = checkMatchRulesOverride("delete.empty", blockNode, blockPath);
1213
+ const emptyAction = (overridePlugin?.rules.delete ?? deleteRules)?.empty;
1214
+ const actionType = overridePlugin?.node.type;
1215
+ if (executeDeleteAction(emptyAction, blockPath, actionType)) return;
744
1216
  }
745
1217
  }
746
1218
  if (PointApi.equals(editor.selection.anchor, editor.api.start([]))) {
@@ -994,6 +1466,7 @@ const getInjectMatch = (editor, plugin) => {
994
1466
  if (targetPlugins && !targetPlugins.includes(getPluginKey(editor, element.type))) return false;
995
1467
  }
996
1468
  if (excludeBelowPlugins || maxLevel) {
1469
+ if (!path) return false;
997
1470
  if (maxLevel && path.length > maxLevel) return false;
998
1471
  if (excludeBelowPlugins) {
999
1472
  const excludeTypes = getPluginKeys(editor, excludeBelowPlugins);
@@ -1328,16 +1801,28 @@ const withScrolling = (editor, fn, options) => {
1328
1801
  const prevOptions = editor.getOptions(DOMPlugin);
1329
1802
  const prevAutoScroll = AUTO_SCROLL.get(editor) ?? false;
1330
1803
  if (options) {
1804
+ const scrollOptions = typeof options.scrollOptions === "object" && options.scrollOptions ? {
1805
+ ...typeof prevOptions.scrollOptions === "object" ? prevOptions.scrollOptions : {},
1806
+ ...omitBy(options.scrollOptions, isUndefined)
1807
+ } : options.scrollOptions ?? prevOptions.scrollOptions;
1331
1808
  const ops = {
1332
1809
  ...prevOptions,
1333
- ...omitBy(options, isUndefined)
1810
+ scrollOperations: {
1811
+ ...prevOptions.scrollOperations,
1812
+ ...omitBy(options.operations ?? {}, isUndefined)
1813
+ },
1814
+ scrollOptions,
1815
+ ...omitBy({ scrollMode: options.mode }, isUndefined)
1334
1816
  };
1335
1817
  editor.setOptions(DOMPlugin, ops);
1336
1818
  }
1337
1819
  AUTO_SCROLL.set(editor, true);
1338
- fn();
1339
- AUTO_SCROLL.set(editor, prevAutoScroll);
1340
- editor.setOptions(DOMPlugin, prevOptions);
1820
+ try {
1821
+ fn();
1822
+ } finally {
1823
+ AUTO_SCROLL.set(editor, prevAutoScroll);
1824
+ editor.setOptions(DOMPlugin, prevOptions);
1825
+ }
1341
1826
  };
1342
1827
 
1343
1828
  //#endregion
@@ -2055,6 +2540,182 @@ const LengthPlugin = createTSlatePlugin({ key: "length" }).overrideEditor(({ edi
2055
2540
  });
2056
2541
  } } }));
2057
2542
 
2543
+ //#endregion
2544
+ //#region src/lib/plugins/navigation-feedback/types.ts
2545
+ const NAVIGATION_FEEDBACK_KEY = "navigationFeedback";
2546
+ const NavigationFeedbackPluginKey = { key: NAVIGATION_FEEDBACK_KEY };
2547
+
2548
+ //#endregion
2549
+ //#region src/lib/plugins/navigation-feedback/transforms/flashTarget.ts
2550
+ const NAVIGATION_FEEDBACK_TIMEOUT = /* @__PURE__ */ new WeakMap();
2551
+ const NAVIGATION_FEEDBACK_PULSE = /* @__PURE__ */ new WeakMap();
2552
+ const NAVIGATION_FEEDBACK_ATTRIBUTES = [
2553
+ "data-nav-cycle",
2554
+ "data-nav-highlight",
2555
+ "data-nav-pulse",
2556
+ "data-nav-target"
2557
+ ];
2558
+ const clearNavigationPathRef = (target) => {
2559
+ target?.pathRef.unref();
2560
+ };
2561
+ const resolveNavigationFeedbackTarget = (target) => {
2562
+ const path = target?.pathRef.current;
2563
+ if (!target || !path) return null;
2564
+ const { pathRef: _pathRef, ...rest } = target;
2565
+ return {
2566
+ ...rest,
2567
+ path
2568
+ };
2569
+ };
2570
+ const getNavigationElement = (editor, target) => {
2571
+ const node = NodeApi.get(editor, target.path);
2572
+ if (!node) return;
2573
+ try {
2574
+ return editor.api.toDOMNode(node);
2575
+ } catch {
2576
+ return;
2577
+ }
2578
+ };
2579
+ const clearNavigationElement = (editor, target) => {
2580
+ if (!target) return;
2581
+ const element = getNavigationElement(editor, target);
2582
+ if (!element) return;
2583
+ for (const attribute of NAVIGATION_FEEDBACK_ATTRIBUTES) element.removeAttribute(attribute);
2584
+ element.style.removeProperty("--plate-nav-feedback-duration");
2585
+ };
2586
+ const setNavigationElement = (editor, target) => {
2587
+ const element = getNavigationElement(editor, target);
2588
+ if (!element) return;
2589
+ element.setAttribute("data-nav-cycle", String(target.cycle));
2590
+ element.setAttribute("data-nav-highlight", target.variant);
2591
+ element.setAttribute("data-nav-pulse", String(target.pulse));
2592
+ element.setAttribute("data-nav-target", "true");
2593
+ element.style.setProperty("--plate-nav-feedback-duration", `${target.duration}ms`);
2594
+ };
2595
+ const clearNavigationTimeout = (editor) => {
2596
+ const timeoutId = NAVIGATION_FEEDBACK_TIMEOUT.get(editor);
2597
+ if (timeoutId) {
2598
+ clearTimeout(timeoutId);
2599
+ NAVIGATION_FEEDBACK_TIMEOUT.delete(editor);
2600
+ }
2601
+ };
2602
+ const nextPulse = (editor) => {
2603
+ const pulse = (NAVIGATION_FEEDBACK_PULSE.get(editor) ?? 0) + 1;
2604
+ NAVIGATION_FEEDBACK_PULSE.set(editor, pulse);
2605
+ return pulse;
2606
+ };
2607
+ const clearNavigationFeedbackTarget = (editor, pulse) => {
2608
+ const storedTarget = editor.getOption(NavigationFeedbackPluginKey, "activeTarget");
2609
+ const activeTarget = resolveNavigationFeedbackTarget(storedTarget);
2610
+ if (!storedTarget) return false;
2611
+ if (pulse !== void 0 && storedTarget.pulse !== pulse) return false;
2612
+ clearNavigationTimeout(editor);
2613
+ clearNavigationElement(editor, activeTarget);
2614
+ clearNavigationPathRef(storedTarget);
2615
+ editor.setOption(NavigationFeedbackPluginKey, "activeTarget", null);
2616
+ return true;
2617
+ };
2618
+ const flashTarget = (editor, { duration, target, variant = "navigated" }) => {
2619
+ if (!editor.api.node(target.path)) return false;
2620
+ const pulse = nextPulse(editor);
2621
+ const timeoutMs = duration ?? editor.getOption(NavigationFeedbackPluginKey, "duration") ?? 800;
2622
+ const previousTarget = editor.getOption(NavigationFeedbackPluginKey, "activeTarget");
2623
+ clearNavigationTimeout(editor);
2624
+ clearNavigationElement(editor, resolveNavigationFeedbackTarget(previousTarget));
2625
+ clearNavigationPathRef(previousTarget);
2626
+ const pathRef = editor.api.pathRef(target.path);
2627
+ const activeTarget = {
2628
+ cycle: pulse % 2,
2629
+ duration: timeoutMs,
2630
+ pathRef,
2631
+ pulse,
2632
+ type: target.type,
2633
+ variant
2634
+ };
2635
+ editor.setOption(NavigationFeedbackPluginKey, "activeTarget", activeTarget);
2636
+ setNavigationElement(editor, resolveNavigationFeedbackTarget(activeTarget) ?? {
2637
+ cycle: activeTarget.cycle,
2638
+ duration: activeTarget.duration,
2639
+ path: target.path,
2640
+ pulse: activeTarget.pulse,
2641
+ type: activeTarget.type,
2642
+ variant: activeTarget.variant
2643
+ });
2644
+ const timeoutId = setTimeout(() => {
2645
+ clearNavigationFeedbackTarget(editor, pulse);
2646
+ }, timeoutMs);
2647
+ NAVIGATION_FEEDBACK_TIMEOUT.set(editor, timeoutId);
2648
+ return true;
2649
+ };
2650
+
2651
+ //#endregion
2652
+ //#region src/lib/plugins/navigation-feedback/transforms/navigate.ts
2653
+ const getScrollTarget = (editor, { scrollTarget, select, target }) => {
2654
+ if (scrollTarget) return scrollTarget;
2655
+ if (select && "focus" in select && select.focus) return select.focus;
2656
+ if (select && "anchor" in select && select.anchor) return select.anchor;
2657
+ if (select && "path" in select) return select;
2658
+ return editor.api.start(target.path);
2659
+ };
2660
+ const navigate = (editor, { flash, focus = true, scroll = true, scrollTarget, select, target }) => {
2661
+ if (!editor.api.node(target.path)) return false;
2662
+ if (select) if ("focus" in select) editor.tf.select(select);
2663
+ else editor.tf.select({
2664
+ anchor: select,
2665
+ focus: select
2666
+ });
2667
+ if (focus) editor.tf.focus();
2668
+ if (scroll) {
2669
+ const point = getScrollTarget(editor, {
2670
+ flash,
2671
+ focus,
2672
+ scroll,
2673
+ scrollTarget,
2674
+ select,
2675
+ target
2676
+ });
2677
+ if (point) editor.api.scrollIntoView(point);
2678
+ }
2679
+ if (flash !== false) flashTarget(editor, {
2680
+ duration: flash?.duration,
2681
+ target,
2682
+ variant: flash?.variant
2683
+ });
2684
+ return true;
2685
+ };
2686
+
2687
+ //#endregion
2688
+ //#region src/lib/plugins/navigation-feedback/NavigationFeedbackPlugin.ts
2689
+ const NavigationFeedbackPlugin = createTSlatePlugin({
2690
+ key: NAVIGATION_FEEDBACK_KEY,
2691
+ options: {
2692
+ activeTarget: null,
2693
+ duration: 1600
2694
+ }
2695
+ }).extendEditorApi(({ editor }) => {
2696
+ const getActiveTarget = () => {
2697
+ const storedTarget = editor.getOption(NavigationFeedbackPluginKey, "activeTarget");
2698
+ const activeTarget = resolveNavigationFeedbackTarget(storedTarget);
2699
+ if (!activeTarget && storedTarget) {
2700
+ clearNavigationFeedbackTarget(editor);
2701
+ return null;
2702
+ }
2703
+ return activeTarget;
2704
+ };
2705
+ return { navigation: {
2706
+ activeTarget: getActiveTarget,
2707
+ clear: () => clearNavigationFeedbackTarget(editor),
2708
+ isTarget: (path) => {
2709
+ const activeTarget = getActiveTarget();
2710
+ return !!activeTarget && PathApi.equals(activeTarget.path, path);
2711
+ }
2712
+ } };
2713
+ }).extendEditorTransforms(({ editor }) => ({ navigation: {
2714
+ clear: () => clearNavigationFeedbackTarget(editor),
2715
+ flashTarget: (options) => flashTarget(editor, options),
2716
+ navigate: (options) => navigate(editor, options)
2717
+ } }));
2718
+
2058
2719
  //#endregion
2059
2720
  //#region src/lib/plugins/node-id/withNodeId.ts
2060
2721
  /** Enables support for inserting nodes with an id key. */
@@ -2390,18 +3051,45 @@ const insertExitBreak = (editor, { match, reverse } = {}) => {
2390
3051
  return true;
2391
3052
  };
2392
3053
 
3054
+ //#endregion
3055
+ //#region src/lib/plugins/slate-extension/transforms/liftBlock.ts
3056
+ /**
3057
+ * Lift the current block out of the nearest matching ancestor container.
3058
+ *
3059
+ * This unwraps only the current block and splits the ancestor around it when
3060
+ * needed, so one keypress changes one structural level instead of exploding the
3061
+ * whole container.
3062
+ */
3063
+ const liftBlock = (editor, { at, match } = {}) => {
3064
+ const block = editor.api.block({ at });
3065
+ if (!block || !match) return;
3066
+ const [, blockPath] = block;
3067
+ if (!editor.api.above({
3068
+ at: blockPath,
3069
+ match: combineMatchOptions(editor, (_node, path) => path.length < blockPath.length, { match })
3070
+ })) return;
3071
+ editor.tf.unwrapNodes({
3072
+ at: blockPath,
3073
+ match,
3074
+ split: true
3075
+ });
3076
+ return true;
3077
+ };
3078
+
2393
3079
  //#endregion
2394
3080
  //#region src/lib/plugins/slate-extension/transforms/resetBlock.ts
2395
3081
  /**
2396
- * Reset the current block to a paragraph, removing all properties except id and
2397
- * type.
3082
+ * Reset the current block to a paragraph, removing all properties except the
3083
+ * configured node id key and type.
2398
3084
  */
2399
3085
  const resetBlock = (editor, { at } = {}) => {
2400
3086
  const entry = editor.api.block({ at });
2401
3087
  if (!entry?.[0]) return;
2402
3088
  const [block, path] = entry;
3089
+ const idKey = editor.getOptions(NodeIdPlugin).idKey ?? "id";
2403
3090
  editor.tf.withoutNormalizing(() => {
2404
- const { id, type, ...otherProps } = NodeApi.extractProps(block);
3091
+ const { type, ...otherProps } = NodeApi.extractProps(block);
3092
+ delete otherProps[idKey];
2405
3093
  Object.keys(otherProps).forEach((key) => {
2406
3094
  editor.tf.unsetNodes(key, { at: path });
2407
3095
  });
@@ -2425,93 +3113,99 @@ const setValue = (editor, value) => {
2425
3113
 
2426
3114
  //#endregion
2427
3115
  //#region src/lib/plugins/slate-extension/SlateExtensionPlugin.ts
3116
+ const NOOP_ON_NODE_CHANGE = () => {};
3117
+ const NOOP_ON_TEXT_CHANGE = () => {};
2428
3118
  /** Opinionated extension of slate default behavior. */
2429
3119
  const SlateExtensionPlugin = createTSlatePlugin({
3120
+ api: { redecorate: () => {} },
2430
3121
  key: "slateExtension",
2431
3122
  options: {
2432
- onNodeChange: () => {},
2433
- onTextChange: () => {}
3123
+ onNodeChange: NOOP_ON_NODE_CHANGE,
3124
+ onTextChange: NOOP_ON_TEXT_CHANGE
2434
3125
  }
2435
- }).extendEditorTransforms(({ editor, getOption, tf: { apply } }) => ({
2436
- init: bindFirst(init, editor),
2437
- insertExitBreak: bindFirst(insertExitBreak, editor),
2438
- resetBlock: bindFirst(resetBlock, editor),
2439
- setValue: bindFirst(setValue, editor),
2440
- apply(operation) {
2441
- const noop = () => {};
2442
- const hasNodeHandlers = editor.meta.pluginCache.handlers.onNodeChange.length > 0 || getOption("onNodeChange") !== noop;
2443
- const hasTextHandlers = editor.meta.pluginCache.handlers.onTextChange.length > 0 || getOption("onTextChange") !== noop;
2444
- if (!hasNodeHandlers && !hasTextHandlers) {
2445
- apply(operation);
2446
- return;
2447
- }
2448
- let prevNode;
2449
- let node;
2450
- let prevText;
2451
- let text;
2452
- let parentNode;
2453
- if (OperationApi.isNodeOperation(operation) && hasNodeHandlers) switch (operation.type) {
2454
- case "insert_node":
2455
- prevNode = operation.node;
2456
- node = operation.node;
2457
- break;
2458
- case "merge_node":
2459
- case "move_node":
2460
- case "set_node":
2461
- case "split_node":
2462
- prevNode = NodeApi.get(editor, operation.path);
2463
- break;
2464
- case "remove_node":
2465
- prevNode = operation.node;
2466
- node = operation.node;
2467
- break;
2468
- }
2469
- else if (OperationApi.isTextOperation(operation) && hasTextHandlers) {
2470
- const parentPath = PathApi.parent(operation.path);
2471
- parentNode = NodeApi.get(editor, parentPath);
2472
- prevText = NodeApi.get(editor, operation.path).text;
2473
- }
2474
- apply(operation);
2475
- if (OperationApi.isNodeOperation(operation) && hasNodeHandlers) {
2476
- switch (operation.type) {
3126
+ }).extendEditorTransforms(({ editor, getOption, tf }) => {
3127
+ const apply = tf?.apply ?? editor.tf.apply;
3128
+ return {
3129
+ init: bindFirst(init, editor),
3130
+ insertExitBreak: bindFirst(insertExitBreak, editor),
3131
+ liftBlock: bindFirst(liftBlock, editor),
3132
+ resetBlock: bindFirst(resetBlock, editor),
3133
+ setValue: bindFirst(setValue, editor),
3134
+ apply(operation) {
3135
+ const hasNodeHandlers = editor.meta.pluginCache.handlers.onNodeChange.length > 0 || getOption("onNodeChange") !== NOOP_ON_NODE_CHANGE;
3136
+ const hasTextHandlers = editor.meta.pluginCache.handlers.onTextChange.length > 0 || getOption("onTextChange") !== NOOP_ON_TEXT_CHANGE;
3137
+ if (!hasNodeHandlers && !hasTextHandlers) {
3138
+ apply(operation);
3139
+ return;
3140
+ }
3141
+ let prevNode;
3142
+ let node;
3143
+ let prevText;
3144
+ let text;
3145
+ let parentNode;
3146
+ if (OperationApi.isNodeOperation(operation) && hasNodeHandlers) switch (operation.type) {
2477
3147
  case "insert_node":
2478
- case "remove_node": break;
2479
- case "merge_node": {
2480
- const prevPath = PathApi.previous(operation.path);
2481
- if (prevPath) node = NodeApi.get(editor, prevPath);
3148
+ prevNode = operation.node;
3149
+ node = operation.node;
2482
3150
  break;
2483
- }
3151
+ case "merge_node":
2484
3152
  case "move_node":
2485
- node = NodeApi.get(editor, operation.newPath);
2486
- break;
2487
3153
  case "set_node":
2488
- node = NodeApi.get(editor, operation.path);
2489
- break;
2490
3154
  case "split_node":
2491
- node = NodeApi.get(editor, operation.path);
3155
+ prevNode = NodeApi.get(editor, operation.path);
3156
+ break;
3157
+ case "remove_node":
3158
+ prevNode = operation.node;
3159
+ node = operation.node;
2492
3160
  break;
2493
3161
  }
2494
- if (!node) node = prevNode;
2495
- if (!pipeOnNodeChange(editor, node, prevNode, operation)) getOption("onNodeChange")({
2496
- editor,
2497
- node,
2498
- operation,
2499
- prevNode
2500
- });
2501
- }
2502
- if (OperationApi.isTextOperation(operation) && hasTextHandlers) {
2503
- const textNodeAfter = NodeApi.get(editor, operation.path);
2504
- if (textNodeAfter) text = textNodeAfter.text;
2505
- if (!pipeOnTextChange(editor, parentNode, text, prevText, operation)) getOption("onTextChange")({
2506
- editor,
2507
- node: parentNode,
2508
- operation,
2509
- prevText,
2510
- text
2511
- });
3162
+ else if (OperationApi.isTextOperation(operation) && hasTextHandlers) {
3163
+ const parentPath = PathApi.parent(operation.path);
3164
+ parentNode = NodeApi.get(editor, parentPath);
3165
+ prevText = NodeApi.get(editor, operation.path).text;
3166
+ }
3167
+ apply(operation);
3168
+ if (OperationApi.isNodeOperation(operation) && hasNodeHandlers) {
3169
+ switch (operation.type) {
3170
+ case "insert_node":
3171
+ case "remove_node": break;
3172
+ case "merge_node": {
3173
+ const prevPath = PathApi.previous(operation.path);
3174
+ if (prevPath) node = NodeApi.get(editor, prevPath);
3175
+ break;
3176
+ }
3177
+ case "move_node":
3178
+ node = NodeApi.get(editor, operation.newPath);
3179
+ break;
3180
+ case "set_node":
3181
+ node = NodeApi.get(editor, operation.path);
3182
+ break;
3183
+ case "split_node":
3184
+ node = NodeApi.get(editor, operation.path);
3185
+ break;
3186
+ }
3187
+ if (!node) node = prevNode;
3188
+ if (!pipeOnNodeChange(editor, node, prevNode, operation)) getOption("onNodeChange")({
3189
+ editor,
3190
+ node,
3191
+ operation,
3192
+ prevNode
3193
+ });
3194
+ }
3195
+ if (OperationApi.isTextOperation(operation) && hasTextHandlers) {
3196
+ const textNodeAfter = NodeApi.get(editor, operation.path);
3197
+ if (textNodeAfter) text = textNodeAfter.text;
3198
+ if (!pipeOnTextChange(editor, parentNode, text, prevText, operation)) getOption("onTextChange")({
3199
+ editor,
3200
+ node: parentNode,
3201
+ operation,
3202
+ prevText,
3203
+ text
3204
+ });
3205
+ }
2512
3206
  }
2513
- }
2514
- }));
3207
+ };
3208
+ });
2515
3209
 
2516
3210
  //#endregion
2517
3211
  //#region src/lib/utils/normalizeDescendantsToDocumentFragment.ts
@@ -2637,16 +3331,159 @@ const ParserPlugin = createSlatePlugin({ key: "parser" }).overrideEditor(({ edit
2637
3331
  insertData(dataTransfer);
2638
3332
  } } }));
2639
3333
 
3334
+ //#endregion
3335
+ //#region src/lib/plugins/input-rules/internal/InputRulesPlugin.ts
3336
+ const createCachedGetter = (compute) => {
3337
+ let hasValue = false;
3338
+ let value;
3339
+ return () => {
3340
+ if (!hasValue) {
3341
+ value = compute();
3342
+ hasValue = true;
3343
+ }
3344
+ return value;
3345
+ };
3346
+ };
3347
+ const createSelectionContext = ({ editor }) => {
3348
+ const { selection } = editor;
3349
+ const isCollapsed = !!selection && editor.api.isCollapsed();
3350
+ const getBlockStartRange = createCachedGetter(() => {
3351
+ if (!selection) return;
3352
+ return editor.api.range("start", selection);
3353
+ });
3354
+ const getBlockStartText = createCachedGetter(() => {
3355
+ const range = getBlockStartRange();
3356
+ return range ? editor.api.string(range) : void 0;
3357
+ });
3358
+ return {
3359
+ editor,
3360
+ getBlockEntry: createCachedGetter(() => {
3361
+ if (!selection) return;
3362
+ return editor.api.block({ at: selection });
3363
+ }),
3364
+ getBlockStartRange,
3365
+ getBlockStartText,
3366
+ getBlockTextBeforeSelection: createCachedGetter(() => getBlockStartText() ?? ""),
3367
+ getCharAfter: createCachedGetter(() => {
3368
+ if (!selection || !isCollapsed) return;
3369
+ const afterPoint = editor.api.after(selection, {
3370
+ distance: 1,
3371
+ unit: "character"
3372
+ });
3373
+ if (!afterPoint) return;
3374
+ return editor.api.string({
3375
+ anchor: selection.anchor,
3376
+ focus: afterPoint
3377
+ }) || void 0;
3378
+ }),
3379
+ getCharBefore: createCachedGetter(() => {
3380
+ if (!selection || !isCollapsed) return;
3381
+ const beforePoint = editor.api.before(selection, {
3382
+ distance: 1,
3383
+ unit: "character"
3384
+ });
3385
+ if (!beforePoint) return;
3386
+ return editor.api.string({
3387
+ anchor: beforePoint,
3388
+ focus: selection.anchor
3389
+ }) || void 0;
3390
+ }),
3391
+ isCollapsed
3392
+ };
3393
+ };
3394
+ const isTriggerMatch = (trigger, text) => Array.isArray(trigger) ? trigger.includes(text) : trigger === text;
3395
+ const InputRulesPlugin = createTSlatePlugin({
3396
+ editOnly: true,
3397
+ key: "inputRules"
3398
+ }).overrideEditor(({ editor, tf: { insertBreak, insertData, insertText } }) => ({ transforms: {
3399
+ insertBreak() {
3400
+ const selectionContext = createSelectionContext({ editor });
3401
+ let handled = false;
3402
+ for (const rule of editor.meta.inputRules.insertBreak) {
3403
+ const context = {
3404
+ cause: "insertBreak",
3405
+ insertBreak,
3406
+ pluginKey: rule.pluginKey,
3407
+ ...selectionContext
3408
+ };
3409
+ if (rule.enabled?.(context) === false) continue;
3410
+ const match = rule.resolve ? rule.resolve(context) : true;
3411
+ if (match === void 0) continue;
3412
+ if (rule.apply(context, match) !== false) {
3413
+ handled = true;
3414
+ break;
3415
+ }
3416
+ }
3417
+ if (handled) return;
3418
+ insertBreak();
3419
+ },
3420
+ insertData(data) {
3421
+ const text = data.getData("text/plain") || null;
3422
+ const selectionContext = createSelectionContext({ editor });
3423
+ let handled = false;
3424
+ for (const rule of editor.meta.inputRules.insertData) {
3425
+ const context = {
3426
+ cause: "insertData",
3427
+ data,
3428
+ insertData,
3429
+ pluginKey: rule.pluginKey,
3430
+ text,
3431
+ ...selectionContext
3432
+ };
3433
+ if (rule.enabled?.(context) === false) continue;
3434
+ if (rule.mimeTypes && rule.mimeTypes.length > 0 && !rule.mimeTypes.some((type) => !!context.data.getData(type))) continue;
3435
+ const match = rule.resolve ? rule.resolve(context) : true;
3436
+ if (match === void 0) continue;
3437
+ if (rule.apply(context, match) !== false) {
3438
+ handled = true;
3439
+ break;
3440
+ }
3441
+ }
3442
+ if (handled) return;
3443
+ insertData(data);
3444
+ },
3445
+ insertText(text, options) {
3446
+ const rules = editor.meta.inputRules.insertText.byTrigger[text] ?? [];
3447
+ const selectionContext = createSelectionContext({ editor });
3448
+ let handled = false;
3449
+ for (const rule of rules) {
3450
+ const context = {
3451
+ cause: "insertText",
3452
+ insertText,
3453
+ options,
3454
+ pluginKey: rule.pluginKey,
3455
+ text,
3456
+ ...selectionContext
3457
+ };
3458
+ if (!isTriggerMatch(rule.trigger, context.text)) continue;
3459
+ if (rule.enabled?.(context) === false) continue;
3460
+ const match = rule.resolve ? rule.resolve(context) : true;
3461
+ if (match === void 0) continue;
3462
+ if (rule.apply(context, match) !== false) {
3463
+ handled = true;
3464
+ break;
3465
+ }
3466
+ }
3467
+ if (handled) return;
3468
+ insertText(text, options);
3469
+ }
3470
+ } }));
3471
+
2640
3472
  //#endregion
2641
3473
  //#region src/lib/plugins/getCorePlugins.ts
2642
- const getCorePlugins = ({ affinity, chunking, maxLength, nodeId, plugins = [] }) => {
3474
+ const getCorePlugins = ({ affinity, chunking, maxLength, navigationFeedback, nodeId, plugins = [] }) => {
2643
3475
  let resolvedNodeId = nodeId;
2644
3476
  if (process.env.NODE_ENV === "test" && nodeId === void 0) resolvedNodeId = false;
2645
3477
  let corePlugins = [
2646
3478
  DebugPlugin,
2647
3479
  SlateExtensionPlugin,
2648
3480
  DOMPlugin,
3481
+ NavigationFeedbackPlugin.configure({
3482
+ enabled: navigationFeedback !== false,
3483
+ options: typeof navigationFeedback === "boolean" ? void 0 : navigationFeedback
3484
+ }),
2649
3485
  HistoryPlugin,
3486
+ InputRulesPlugin,
2650
3487
  OverridePlugin,
2651
3488
  ParserPlugin,
2652
3489
  maxLength ? LengthPlugin.configure({ options: { maxLength } }) : LengthPlugin,
@@ -2689,7 +3526,7 @@ const getCorePlugins = ({ affinity, chunking, maxLength, nodeId, plugins = [] })
2689
3526
  * @see {@link usePlateEditor} for a memoized React version.
2690
3527
  * @see {@link withPlate} for the React-specific enhancement function.
2691
3528
  */
2692
- const withSlate = (e, { id, affinity = true, autoSelect, chunking = true, maxLength, nodeId, optionsStoreFactory, plugins = [], readOnly = false, rootPlugin, selection, shouldNormalizeEditor, skipInitialization, userId, value, onReady, ...pluginConfig } = {}) => {
3529
+ const withSlate = (e, { id, affinity = true, autoSelect, chunking = true, maxLength, navigationFeedback, nodeId, optionsStoreFactory, plugins = [], readOnly = false, rootPlugin, selection, shouldNormalizeEditor, skipInitialization, userId, value, onReady, ...pluginConfig } = {}) => {
2693
3530
  const editor = e;
2694
3531
  editor.id = id ?? editor.id ?? nanoid();
2695
3532
  editor.meta.key = editor.meta.key ?? nanoid();
@@ -2747,6 +3584,7 @@ const withSlate = (e, { id, affinity = true, autoSelect, chunking = true, maxLen
2747
3584
  affinity,
2748
3585
  chunking,
2749
3586
  maxLength,
3587
+ navigationFeedback,
2750
3588
  nodeId,
2751
3589
  plugins
2752
3590
  });
@@ -2819,5 +3657,4 @@ const withSlate = (e, { id, affinity = true, autoSelect, chunking = true, maxLen
2819
3657
  const createSlateEditor = ({ editor = createEditor(), ...options } = {}) => withSlate(editor, options);
2820
3658
 
2821
3659
  //#endregion
2822
- export { DebugPlugin as $, pluginDeserializeHtml as A, withNormalizeRules as At, collapseWhiteSpaceText as B, getPluginKey as Bt, deserializeHtmlElement as C, isSlatePluginNode as Ct, pipeDeserializeHtmlLeaf as D, applyDeepToNodes as Dt, htmlElementToLeaf as E, isSlateVoid as Et, collapseWhiteSpace as F, HistoryPlugin as Ft, isHtmlBlockElement as G, getEditorPlugin as Gt, upsertInlineFormattingContext as H, getPluginType as Ht, collapseWhiteSpaceElement as I, withPlateHistory as It, isHtmlText as J, isHtmlInlineElement as K, createSlatePlugin as Kt, inferWhiteSpaceRule as L, AstPlugin as Lt, htmlBrToNewLine as M, withDeleteRules as Mt, htmlBodyToFragment as N, withBreakRules as Nt, htmlElementToElement as O, OverridePlugin as Ot, deserializeHtmlNodeChildren as P, BaseParagraphPlugin as Pt, withScrolling as Q, collapseWhiteSpaceChildren as R, getContainerTypes as Rt, htmlStringToDOMNode as S, isSlatePluginElement as St, htmlTextNodeToString as T, isSlateText as Tt, isLastNonEmptyTextOfInlineFormattingContext as U, getPluginTypes as Ut, endInlineFormattingContext as V, getPluginKeys as Vt, collapseString as W, getSlatePlugin as Wt, AUTO_SCROLL as X, isHtmlElement as Y, DOMPlugin as Z, withNodeId as _, getSlateElements as _t, pipeInsertDataQuery as a, isNodeAffinity as at, parseHtmlDocument as b, isSlateLeaf as bt, setValue as c, getEdgeNodes as ct, init as d, getPluginNodeProps as dt, PlateError as et, isEditOnly as f, getNodeDataAttributeKeys as ft, normalizeNodeId as g, defaultsDeepToNodes as gt, NodeIdPlugin as h, getInjectMatch as ht, ParserPlugin as i, setAffinitySelection as it, getDataNodeProps as j, withMergeRules as jt, pipeDeserializeHtmlElement as k, withOverrides as kt, resetBlock as l, mergeDeepToNodes as lt, pipeOnNodeChange as m, getInjectedPlugins as mt, withSlate as n, withChunking as nt, normalizeDescendantsToDocumentFragment as o, isNodesAffinity as ot, pipeOnTextChange as p, keyToDataAttribute as pt, inlineTagNames as q, createTSlatePlugin as qt, getCorePlugins as r, AffinityPlugin as rt, SlateExtensionPlugin as s, getMarkBoundaryAffinity as st, createSlateEditor as t, ChunkingPlugin as tt, insertExitBreak as u, getSlateClass as ut, LengthPlugin as v, isSlateEditor as vt, deserializeHtmlNode as w, isSlateString as wt, deserializeHtml as x, isSlateNode as xt, HtmlPlugin as y, isSlateElement as yt, collapseWhiteSpaceNode as z, getPluginByType as zt };
2823
- //# sourceMappingURL=withSlate-1B0SfAWG.js.map
3660
+ export { isHtmlBlockElement as $, defineInputRule as $t, htmlStringToDOMNode as A, isSlatePluginElement as At, htmlBrToNewLine as B, withDeleteRules as Bt, resolveNavigationFeedbackTarget as C, getInjectMatch as Ct, HtmlPlugin as D, isSlateElement as Dt, LengthPlugin as E, isSlateEditor as Et, pipeDeserializeHtmlLeaf as F, applyDeepToNodes as Ft, inferWhiteSpaceRule as G, AstPlugin as Gt, deserializeHtmlNodeChildren as H, BaseParagraphPlugin as Ht, htmlElementToElement as I, OverridePlugin as It, collapseWhiteSpaceText as J, createMarkInputRule as Jt, collapseWhiteSpaceChildren as K, createBlockFenceInputRule as Kt, pipeDeserializeHtmlElement as L, withOverrides as Lt, deserializeHtmlNode as M, isSlateString as Mt, htmlTextNodeToString as N, isSlateText as Nt, parseHtmlDocument as O, isSlateLeaf as Ot, htmlElementToLeaf as P, isSlateVoid as Pt, collapseString as Q, matchDelimitedInline as Qt, pluginDeserializeHtml as R, withNormalizeRules as Rt, flashTarget as S, getInjectedPlugins as St, NavigationFeedbackPluginKey as T, getSlateElements as Tt, collapseWhiteSpace as U, HistoryPlugin as Ut, htmlBodyToFragment as V, withBreakRules as Vt, collapseWhiteSpaceElement as W, withPlateHistory as Wt, upsertInlineFormattingContext as X, matchBlockFence as Xt, endInlineFormattingContext as Y, createTextSubstitutionInputRule as Yt, isLastNonEmptyTextOfInlineFormattingContext as Z, matchBlockStart as Zt, normalizeNodeId as _, mergeDeepToNodes as _t, pipeInsertDataQuery as a, getPluginTypes as an, DOMPlugin as at, navigate as b, getNodeDataAttributeKeys as bt, setValue as c, createSlatePlugin as cn, PlateError as ct, insertExitBreak as d, AffinityPlugin as dt, getContainerTypes as en, isHtmlInlineElement as et, init as f, setAffinitySelection as ft, NodeIdPlugin as g, getEdgeNodes as gt, pipeOnNodeChange as h, getMarkBoundaryAffinity as ht, ParserPlugin as i, getPluginType as in, AUTO_SCROLL as it, deserializeHtmlElement as j, isSlatePluginNode as jt, deserializeHtml as k, isSlateNode as kt, resetBlock as l, createTSlatePlugin as ln, ChunkingPlugin as lt, pipeOnTextChange as m, isNodesAffinity as mt, withSlate as n, getPluginKey as nn, isHtmlText as nt, normalizeDescendantsToDocumentFragment as o, getSlatePlugin as on, withScrolling as ot, isEditOnly as p, isNodeAffinity as pt, collapseWhiteSpaceNode as q, createBlockStartInputRule as qt, getCorePlugins as r, getPluginKeys as rn, isHtmlElement as rt, SlateExtensionPlugin as s, getEditorPlugin as sn, DebugPlugin as st, createSlateEditor as t, getPluginByType as tn, inlineTagNames as tt, liftBlock as u, withChunking as ut, withNodeId as v, getSlateClass as vt, NAVIGATION_FEEDBACK_KEY as w, defaultsDeepToNodes as wt, clearNavigationFeedbackTarget as x, keyToDataAttribute as xt, NavigationFeedbackPlugin as y, getPluginNodeProps as yt, getDataNodeProps as z, withMergeRules as zt };