@lexical/link 0.35.1-nightly.20250924.0 → 0.36.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.
@@ -6,8 +6,9 @@
6
6
  *
7
7
  */
8
8
 
9
- import { addClassNamesToElement, isHTMLAnchorElement, $findMatchingParent } from '@lexical/utils';
10
- import { createCommand, ElementNode, $isRangeSelection, $applyNodeReplacement, $isElementNode, $getSelection, $isNodeSelection, $normalizeSelection__EXPERIMENTAL, $setSelection } from 'lexical';
9
+ import { addClassNamesToElement, isHTMLAnchorElement, $findMatchingParent, mergeRegister, objectKlassEquals } from '@lexical/utils';
10
+ import { createCommand, ElementNode, $isRangeSelection, $applyNodeReplacement, $isElementNode, $getSelection, $isNodeSelection, $normalizeSelection__EXPERIMENTAL, $setSelection, defineExtension, COMMAND_PRIORITY_LOW, PASTE_COMMAND, safeCast, isDOMNode, getNearestEditorFromDOMNode, $getNearestNodeFromDOMNode, shallowMergeConfig, TextNode, $isTextNode, $isLineBreakNode, $createTextNode } from 'lexical';
11
+ import { namedSignals, effect } from '@lexical/extension';
11
12
 
12
13
  /**
13
14
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -28,13 +29,13 @@ const SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'sms:', '
28
29
  /** @noInheritDoc */
29
30
  class LinkNode extends ElementNode {
30
31
  /** @internal */
31
-
32
+ __url;
32
33
  /** @internal */
33
-
34
+ __target;
34
35
  /** @internal */
35
-
36
+ __rel;
36
37
  /** @internal */
37
-
38
+ __title;
38
39
  static getType() {
39
40
  return 'link';
40
41
  }
@@ -229,7 +230,7 @@ function $isLinkNode(node) {
229
230
  class AutoLinkNode extends LinkNode {
230
231
  /** @internal */
231
232
  /** Indicates whether the autolink was ever unlinked. **/
232
-
233
+ __isUnlinked;
233
234
  constructor(url = '', attributes = {}, key) {
234
235
  super(url, attributes, key);
235
236
  this.__isUnlinked = attributes.isUnlinked !== undefined && attributes.isUnlinked !== null ? attributes.isUnlinked : false;
@@ -369,10 +370,24 @@ function $withSelectedNodes($fn) {
369
370
  /**
370
371
  * Generates or updates a LinkNode. It can also delete a LinkNode if the URL is null,
371
372
  * but saves any children and brings them up to the parent node.
372
- * @param url - The URL the link directs to.
373
+ * @param urlOrAttributes - The URL the link directs to, or an attributes object with an url property
373
374
  * @param attributes - Optional HTML a tag attributes. \\{ target, rel, title \\}
374
375
  */
375
- function $toggleLink(url, attributes = {}) {
376
+ function $toggleLink(urlOrAttributes, attributes = {}) {
377
+ let url;
378
+ if (urlOrAttributes && typeof urlOrAttributes === 'object') {
379
+ const {
380
+ url: urlProp,
381
+ ...rest
382
+ } = urlOrAttributes;
383
+ url = urlProp;
384
+ attributes = {
385
+ ...rest,
386
+ ...attributes
387
+ };
388
+ } else {
389
+ url = urlOrAttributes;
390
+ }
376
391
  const {
377
392
  target,
378
393
  title
@@ -514,8 +529,6 @@ function $toggleLink(url, attributes = {}) {
514
529
  }
515
530
  });
516
531
  }
517
- /** @deprecated renamed to {@link $toggleLink} by @lexical/eslint-plugin rules-of-lexical */
518
- const toggleLink = $toggleLink;
519
532
  const PHONE_NUMBER_REGEX = /^\+?[0-9\s()-]{5,}$/;
520
533
 
521
534
  /**
@@ -550,4 +563,560 @@ function formatUrl(url) {
550
563
  return `https://${url}`;
551
564
  }
552
565
 
553
- export { $createAutoLinkNode, $createLinkNode, $isAutoLinkNode, $isLinkNode, $toggleLink, AutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND, formatUrl, toggleLink };
566
+ /**
567
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
568
+ *
569
+ * This source code is licensed under the MIT license found in the
570
+ * LICENSE file in the root directory of this source tree.
571
+ *
572
+ */
573
+
574
+ const defaultProps = {
575
+ attributes: undefined,
576
+ validateUrl: undefined
577
+ };
578
+
579
+ /** @internal */
580
+ function registerLink(editor, stores) {
581
+ return mergeRegister(effect(() => editor.registerCommand(TOGGLE_LINK_COMMAND, payload => {
582
+ const validateUrl = stores.validateUrl.peek();
583
+ const attributes = stores.attributes.peek();
584
+ if (payload === null) {
585
+ $toggleLink(null);
586
+ return true;
587
+ } else if (typeof payload === 'string') {
588
+ if (validateUrl === undefined || validateUrl(payload)) {
589
+ $toggleLink(payload, attributes);
590
+ return true;
591
+ }
592
+ return false;
593
+ } else {
594
+ const {
595
+ url,
596
+ target,
597
+ rel,
598
+ title
599
+ } = payload;
600
+ $toggleLink(url, {
601
+ ...attributes,
602
+ rel,
603
+ target,
604
+ title
605
+ });
606
+ return true;
607
+ }
608
+ }, COMMAND_PRIORITY_LOW)), effect(() => {
609
+ const validateUrl = stores.validateUrl.value;
610
+ if (!validateUrl) {
611
+ return;
612
+ }
613
+ const attributes = stores.attributes.value;
614
+ return editor.registerCommand(PASTE_COMMAND, event => {
615
+ const selection = $getSelection();
616
+ if (!$isRangeSelection(selection) || selection.isCollapsed() || !objectKlassEquals(event, ClipboardEvent)) {
617
+ return false;
618
+ }
619
+ if (event.clipboardData === null) {
620
+ return false;
621
+ }
622
+ const clipboardText = event.clipboardData.getData('text');
623
+ if (!validateUrl(clipboardText)) {
624
+ return false;
625
+ }
626
+ // If we select nodes that are elements then avoid applying the link.
627
+ if (!selection.getNodes().some(node => $isElementNode(node))) {
628
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
629
+ ...attributes,
630
+ url: clipboardText
631
+ });
632
+ event.preventDefault();
633
+ return true;
634
+ }
635
+ return false;
636
+ }, COMMAND_PRIORITY_LOW);
637
+ }));
638
+ }
639
+
640
+ /**
641
+ * Provides {@link LinkNode}, an implementation of
642
+ * {@link TOGGLE_LINK_COMMAND}, and a {@link PASTE_COMMAND}
643
+ * listener to wrap selected nodes in a link when a
644
+ * URL is pasted and `validateUrl` is defined.
645
+ */
646
+ const LinkExtension = defineExtension({
647
+ build(editor, config, state) {
648
+ return namedSignals(config);
649
+ },
650
+ config: defaultProps,
651
+ name: '@lexical/link/Link',
652
+ nodes: [LinkNode],
653
+ register(editor, config, state) {
654
+ return registerLink(editor, state.getOutput());
655
+ }
656
+ });
657
+
658
+ /**
659
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
660
+ *
661
+ * This source code is licensed under the MIT license found in the
662
+ * LICENSE file in the root directory of this source tree.
663
+ *
664
+ */
665
+
666
+ function findMatchingDOM(startNode, predicate) {
667
+ let node = startNode;
668
+ while (node != null) {
669
+ if (predicate(node)) {
670
+ return node;
671
+ }
672
+ node = node.parentNode;
673
+ }
674
+ return null;
675
+ }
676
+ function registerClickableLink(editor, stores, eventOptions = {}) {
677
+ const onClick = event => {
678
+ const target = event.target;
679
+ if (!isDOMNode(target)) {
680
+ return;
681
+ }
682
+ const nearestEditor = getNearestEditorFromDOMNode(target);
683
+ if (nearestEditor === null) {
684
+ return;
685
+ }
686
+ let url = null;
687
+ let urlTarget = null;
688
+ nearestEditor.update(() => {
689
+ const clickedNode = $getNearestNodeFromDOMNode(target);
690
+ if (clickedNode !== null) {
691
+ const maybeLinkNode = $findMatchingParent(clickedNode, $isElementNode);
692
+ if (!stores.disabled.peek()) {
693
+ if ($isLinkNode(maybeLinkNode)) {
694
+ url = maybeLinkNode.sanitizeUrl(maybeLinkNode.getURL());
695
+ urlTarget = maybeLinkNode.getTarget();
696
+ } else {
697
+ const a = findMatchingDOM(target, isHTMLAnchorElement);
698
+ if (a !== null) {
699
+ url = a.href;
700
+ urlTarget = a.target;
701
+ }
702
+ }
703
+ }
704
+ }
705
+ });
706
+ if (url === null || url === '') {
707
+ return;
708
+ }
709
+
710
+ // Allow user to select link text without following url
711
+ const selection = editor.getEditorState().read($getSelection);
712
+ if ($isRangeSelection(selection) && !selection.isCollapsed()) {
713
+ event.preventDefault();
714
+ return;
715
+ }
716
+ const isMiddle = event.type === 'auxclick' && event.button === 1;
717
+ window.open(url, stores.newTab.peek() || isMiddle || event.metaKey || event.ctrlKey || urlTarget === '_blank' ? '_blank' : '_self');
718
+ event.preventDefault();
719
+ };
720
+ const onMouseUp = event => {
721
+ if (event.button === 1) {
722
+ onClick(event);
723
+ }
724
+ };
725
+ return editor.registerRootListener((rootElement, prevRootElement) => {
726
+ if (prevRootElement !== null) {
727
+ prevRootElement.removeEventListener('click', onClick);
728
+ prevRootElement.removeEventListener('mouseup', onMouseUp);
729
+ }
730
+ if (rootElement !== null) {
731
+ rootElement.addEventListener('click', onClick, eventOptions);
732
+ rootElement.addEventListener('mouseup', onMouseUp, eventOptions);
733
+ }
734
+ });
735
+ }
736
+
737
+ /**
738
+ * Normally in a Lexical editor the `CLICK_COMMAND` on a LinkNode will cause the
739
+ * selection to change instead of opening a link. This extension can be used to
740
+ * restore the default behavior, e.g. when the editor is not editable.
741
+ */
742
+ const ClickableLinkExtension = defineExtension({
743
+ build(editor, config, state) {
744
+ return namedSignals(config);
745
+ },
746
+ config: safeCast({
747
+ disabled: false,
748
+ newTab: false
749
+ }),
750
+ dependencies: [LinkExtension],
751
+ name: '@lexical/link/ClickableLink',
752
+ register(editor, config, state) {
753
+ return registerClickableLink(editor, state.getOutput());
754
+ }
755
+ });
756
+
757
+ /**
758
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
759
+ *
760
+ * This source code is licensed under the MIT license found in the
761
+ * LICENSE file in the root directory of this source tree.
762
+ *
763
+ */
764
+
765
+ function createLinkMatcherWithRegExp(regExp, urlTransformer = text => text) {
766
+ return text => {
767
+ const match = regExp.exec(text);
768
+ if (match === null) {
769
+ return null;
770
+ }
771
+ return {
772
+ index: match.index,
773
+ length: match[0].length,
774
+ text: match[0],
775
+ url: urlTransformer(match[0])
776
+ };
777
+ };
778
+ }
779
+ function findFirstMatch(text, matchers) {
780
+ for (let i = 0; i < matchers.length; i++) {
781
+ const match = matchers[i](text);
782
+ if (match) {
783
+ return match;
784
+ }
785
+ }
786
+ return null;
787
+ }
788
+ const PUNCTUATION_OR_SPACE = /[.,;\s]/;
789
+ function isSeparator(char) {
790
+ return PUNCTUATION_OR_SPACE.test(char);
791
+ }
792
+ function endsWithSeparator(textContent) {
793
+ return isSeparator(textContent[textContent.length - 1]);
794
+ }
795
+ function startsWithSeparator(textContent) {
796
+ return isSeparator(textContent[0]);
797
+ }
798
+
799
+ /**
800
+ * Check if the text content starts with a fullstop followed by a top-level domain.
801
+ * Meaning if the text content can be a beginning of a top level domain.
802
+ * @param textContent
803
+ * @param isEmail
804
+ * @returns boolean
805
+ */
806
+ function startsWithTLD(textContent, isEmail) {
807
+ if (isEmail) {
808
+ return /^\.[a-zA-Z]{2,}/.test(textContent);
809
+ } else {
810
+ return /^\.[a-zA-Z0-9]{1,}/.test(textContent);
811
+ }
812
+ }
813
+ function isPreviousNodeValid(node) {
814
+ let previousNode = node.getPreviousSibling();
815
+ if ($isElementNode(previousNode)) {
816
+ previousNode = previousNode.getLastDescendant();
817
+ }
818
+ return previousNode === null || $isLineBreakNode(previousNode) || $isTextNode(previousNode) && endsWithSeparator(previousNode.getTextContent());
819
+ }
820
+ function isNextNodeValid(node) {
821
+ let nextNode = node.getNextSibling();
822
+ if ($isElementNode(nextNode)) {
823
+ nextNode = nextNode.getFirstDescendant();
824
+ }
825
+ return nextNode === null || $isLineBreakNode(nextNode) || $isTextNode(nextNode) && startsWithSeparator(nextNode.getTextContent());
826
+ }
827
+ function isContentAroundIsValid(matchStart, matchEnd, text, nodes) {
828
+ const contentBeforeIsValid = matchStart > 0 ? isSeparator(text[matchStart - 1]) : isPreviousNodeValid(nodes[0]);
829
+ if (!contentBeforeIsValid) {
830
+ return false;
831
+ }
832
+ const contentAfterIsValid = matchEnd < text.length ? isSeparator(text[matchEnd]) : isNextNodeValid(nodes[nodes.length - 1]);
833
+ return contentAfterIsValid;
834
+ }
835
+ function extractMatchingNodes(nodes, startIndex, endIndex) {
836
+ const unmodifiedBeforeNodes = [];
837
+ const matchingNodes = [];
838
+ const unmodifiedAfterNodes = [];
839
+ let matchingOffset = 0;
840
+ let currentOffset = 0;
841
+ const currentNodes = [...nodes];
842
+ while (currentNodes.length > 0) {
843
+ const currentNode = currentNodes[0];
844
+ const currentNodeText = currentNode.getTextContent();
845
+ const currentNodeLength = currentNodeText.length;
846
+ const currentNodeStart = currentOffset;
847
+ const currentNodeEnd = currentOffset + currentNodeLength;
848
+ if (currentNodeEnd <= startIndex) {
849
+ unmodifiedBeforeNodes.push(currentNode);
850
+ matchingOffset += currentNodeLength;
851
+ } else if (currentNodeStart >= endIndex) {
852
+ unmodifiedAfterNodes.push(currentNode);
853
+ } else {
854
+ matchingNodes.push(currentNode);
855
+ }
856
+ currentOffset += currentNodeLength;
857
+ currentNodes.shift();
858
+ }
859
+ return [matchingOffset, unmodifiedBeforeNodes, matchingNodes, unmodifiedAfterNodes];
860
+ }
861
+ function $createAutoLinkNode_(nodes, startIndex, endIndex, match) {
862
+ const linkNode = $createAutoLinkNode(match.url, match.attributes);
863
+ if (nodes.length === 1) {
864
+ let remainingTextNode = nodes[0];
865
+ let linkTextNode;
866
+ if (startIndex === 0) {
867
+ [linkTextNode, remainingTextNode] = remainingTextNode.splitText(endIndex);
868
+ } else {
869
+ [, linkTextNode, remainingTextNode] = remainingTextNode.splitText(startIndex, endIndex);
870
+ }
871
+ const textNode = $createTextNode(match.text);
872
+ textNode.setFormat(linkTextNode.getFormat());
873
+ textNode.setDetail(linkTextNode.getDetail());
874
+ textNode.setStyle(linkTextNode.getStyle());
875
+ linkNode.append(textNode);
876
+ linkTextNode.replace(linkNode);
877
+ return remainingTextNode;
878
+ } else if (nodes.length > 1) {
879
+ const firstTextNode = nodes[0];
880
+ let offset = firstTextNode.getTextContent().length;
881
+ let firstLinkTextNode;
882
+ if (startIndex === 0) {
883
+ firstLinkTextNode = firstTextNode;
884
+ } else {
885
+ [, firstLinkTextNode] = firstTextNode.splitText(startIndex);
886
+ }
887
+ const linkNodes = [];
888
+ let remainingTextNode;
889
+ for (let i = 1; i < nodes.length; i++) {
890
+ const currentNode = nodes[i];
891
+ const currentNodeText = currentNode.getTextContent();
892
+ const currentNodeLength = currentNodeText.length;
893
+ const currentNodeStart = offset;
894
+ const currentNodeEnd = offset + currentNodeLength;
895
+ if (currentNodeStart < endIndex) {
896
+ if (currentNodeEnd <= endIndex) {
897
+ linkNodes.push(currentNode);
898
+ } else {
899
+ const [linkTextNode, endNode] = currentNode.splitText(endIndex - currentNodeStart);
900
+ linkNodes.push(linkTextNode);
901
+ remainingTextNode = endNode;
902
+ }
903
+ }
904
+ offset += currentNodeLength;
905
+ }
906
+ const selection = $getSelection();
907
+ const selectedTextNode = selection ? selection.getNodes().find($isTextNode) : undefined;
908
+ const textNode = $createTextNode(firstLinkTextNode.getTextContent());
909
+ textNode.setFormat(firstLinkTextNode.getFormat());
910
+ textNode.setDetail(firstLinkTextNode.getDetail());
911
+ textNode.setStyle(firstLinkTextNode.getStyle());
912
+ linkNode.append(textNode, ...linkNodes);
913
+ // it does not preserve caret position if caret was at the first text node
914
+ // so we need to restore caret position
915
+ if (selectedTextNode && selectedTextNode === firstLinkTextNode) {
916
+ if ($isRangeSelection(selection)) {
917
+ textNode.select(selection.anchor.offset, selection.focus.offset);
918
+ } else if ($isNodeSelection(selection)) {
919
+ textNode.select(0, textNode.getTextContent().length);
920
+ }
921
+ }
922
+ firstLinkTextNode.replace(linkNode);
923
+ return remainingTextNode;
924
+ }
925
+ return undefined;
926
+ }
927
+ function $handleLinkCreation(nodes, matchers, onChange) {
928
+ let currentNodes = [...nodes];
929
+ const initialText = currentNodes.map(node => node.getTextContent()).join('');
930
+ let text = initialText;
931
+ let match;
932
+ let invalidMatchEnd = 0;
933
+ while ((match = findFirstMatch(text, matchers)) && match !== null) {
934
+ const matchStart = match.index;
935
+ const matchLength = match.length;
936
+ const matchEnd = matchStart + matchLength;
937
+ const isValid = isContentAroundIsValid(invalidMatchEnd + matchStart, invalidMatchEnd + matchEnd, initialText, currentNodes);
938
+ if (isValid) {
939
+ const [matchingOffset,, matchingNodes, unmodifiedAfterNodes] = extractMatchingNodes(currentNodes, invalidMatchEnd + matchStart, invalidMatchEnd + matchEnd);
940
+ const actualMatchStart = invalidMatchEnd + matchStart - matchingOffset;
941
+ const actualMatchEnd = invalidMatchEnd + matchEnd - matchingOffset;
942
+ const remainingTextNode = $createAutoLinkNode_(matchingNodes, actualMatchStart, actualMatchEnd, match);
943
+ currentNodes = remainingTextNode ? [remainingTextNode, ...unmodifiedAfterNodes] : unmodifiedAfterNodes;
944
+ onChange(match.url, null);
945
+ invalidMatchEnd = 0;
946
+ } else {
947
+ invalidMatchEnd += matchEnd;
948
+ }
949
+ text = text.substring(matchEnd);
950
+ }
951
+ }
952
+ function handleLinkEdit(linkNode, matchers, onChange) {
953
+ // Check children are simple text
954
+ const children = linkNode.getChildren();
955
+ const childrenLength = children.length;
956
+ for (let i = 0; i < childrenLength; i++) {
957
+ const child = children[i];
958
+ if (!$isTextNode(child) || !child.isSimpleText()) {
959
+ replaceWithChildren(linkNode);
960
+ onChange(null, linkNode.getURL());
961
+ return;
962
+ }
963
+ }
964
+
965
+ // Check text content fully matches
966
+ const text = linkNode.getTextContent();
967
+ const match = findFirstMatch(text, matchers);
968
+ if (match === null || match.text !== text) {
969
+ replaceWithChildren(linkNode);
970
+ onChange(null, linkNode.getURL());
971
+ return;
972
+ }
973
+
974
+ // Check neighbors
975
+ if (!isPreviousNodeValid(linkNode) || !isNextNodeValid(linkNode)) {
976
+ replaceWithChildren(linkNode);
977
+ onChange(null, linkNode.getURL());
978
+ return;
979
+ }
980
+ const url = linkNode.getURL();
981
+ if (url !== match.url) {
982
+ linkNode.setURL(match.url);
983
+ onChange(match.url, url);
984
+ }
985
+ if (match.attributes) {
986
+ const rel = linkNode.getRel();
987
+ if (rel !== match.attributes.rel) {
988
+ linkNode.setRel(match.attributes.rel || null);
989
+ onChange(match.attributes.rel || null, rel);
990
+ }
991
+ const target = linkNode.getTarget();
992
+ if (target !== match.attributes.target) {
993
+ linkNode.setTarget(match.attributes.target || null);
994
+ onChange(match.attributes.target || null, target);
995
+ }
996
+ }
997
+ }
998
+
999
+ // Bad neighbors are edits in neighbor nodes that make AutoLinks incompatible.
1000
+ // Given the creation preconditions, these can only be simple text nodes.
1001
+ function handleBadNeighbors(textNode, matchers, onChange) {
1002
+ const previousSibling = textNode.getPreviousSibling();
1003
+ const nextSibling = textNode.getNextSibling();
1004
+ const text = textNode.getTextContent();
1005
+ if ($isAutoLinkNode(previousSibling) && !previousSibling.getIsUnlinked() && (!startsWithSeparator(text) || startsWithTLD(text, previousSibling.isEmailURI()))) {
1006
+ previousSibling.append(textNode);
1007
+ handleLinkEdit(previousSibling, matchers, onChange);
1008
+ onChange(null, previousSibling.getURL());
1009
+ }
1010
+ if ($isAutoLinkNode(nextSibling) && !nextSibling.getIsUnlinked() && !endsWithSeparator(text)) {
1011
+ replaceWithChildren(nextSibling);
1012
+ handleLinkEdit(nextSibling, matchers, onChange);
1013
+ onChange(null, nextSibling.getURL());
1014
+ }
1015
+ }
1016
+ function replaceWithChildren(node) {
1017
+ const children = node.getChildren();
1018
+ const childrenLength = children.length;
1019
+ for (let j = childrenLength - 1; j >= 0; j--) {
1020
+ node.insertAfter(children[j]);
1021
+ }
1022
+ node.remove();
1023
+ return children.map(child => child.getLatest());
1024
+ }
1025
+ function getTextNodesToMatch(textNode) {
1026
+ // check if next siblings are simple text nodes till a node contains a space separator
1027
+ const textNodesToMatch = [textNode];
1028
+ let nextSibling = textNode.getNextSibling();
1029
+ while (nextSibling !== null && $isTextNode(nextSibling) && nextSibling.isSimpleText()) {
1030
+ textNodesToMatch.push(nextSibling);
1031
+ if (/[\s]/.test(nextSibling.getTextContent())) {
1032
+ break;
1033
+ }
1034
+ nextSibling = nextSibling.getNextSibling();
1035
+ }
1036
+ return textNodesToMatch;
1037
+ }
1038
+ const defaultConfig = {
1039
+ changeHandlers: [],
1040
+ matchers: []
1041
+ };
1042
+ function registerAutoLink(editor, config = defaultConfig) {
1043
+ const {
1044
+ matchers,
1045
+ changeHandlers
1046
+ } = config;
1047
+ const onChange = (url, prevUrl) => {
1048
+ for (const handler of changeHandlers) {
1049
+ handler(url, prevUrl);
1050
+ }
1051
+ };
1052
+ return mergeRegister(editor.registerNodeTransform(TextNode, textNode => {
1053
+ const parent = textNode.getParentOrThrow();
1054
+ const previous = textNode.getPreviousSibling();
1055
+ if ($isAutoLinkNode(parent) && !parent.getIsUnlinked()) {
1056
+ handleLinkEdit(parent, matchers, onChange);
1057
+ } else if (!$isLinkNode(parent)) {
1058
+ if (textNode.isSimpleText() && (startsWithSeparator(textNode.getTextContent()) || !$isAutoLinkNode(previous))) {
1059
+ const textNodesToMatch = getTextNodesToMatch(textNode);
1060
+ $handleLinkCreation(textNodesToMatch, matchers, onChange);
1061
+ }
1062
+ handleBadNeighbors(textNode, matchers, onChange);
1063
+ }
1064
+ }), editor.registerCommand(TOGGLE_LINK_COMMAND, payload => {
1065
+ const selection = $getSelection();
1066
+ if (payload !== null || !$isRangeSelection(selection)) {
1067
+ return false;
1068
+ }
1069
+ const nodes = selection.extract();
1070
+ nodes.forEach(node => {
1071
+ const parent = node.getParent();
1072
+ if ($isAutoLinkNode(parent)) {
1073
+ // invert the value
1074
+ parent.setIsUnlinked(!parent.getIsUnlinked());
1075
+ parent.markDirty();
1076
+ }
1077
+ });
1078
+ return false;
1079
+ }, COMMAND_PRIORITY_LOW));
1080
+ }
1081
+
1082
+ /**
1083
+ * An extension to automatically create AutoLinkNode from text
1084
+ * that matches the configured matchers. No default implementation
1085
+ * is provided for any matcher, see {@link createLinkMatcherWithRegExp}
1086
+ * for a helper function to create a matcher from a RegExp, and the
1087
+ * Playground's [AutoLinkPlugin](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/AutoLinkPlugin/index.tsx)
1088
+ * for some example RegExps that could be used.
1089
+ *
1090
+ * The given `matchers` and `changeHandlers` will be merged by
1091
+ * concatenating the configured arrays.
1092
+ */
1093
+ const AutoLinkExtension = defineExtension({
1094
+ config: defaultConfig,
1095
+ dependencies: [LinkExtension],
1096
+ mergeConfig(config, overrides) {
1097
+ const merged = shallowMergeConfig(config, overrides);
1098
+ for (const k of ['matchers', 'changeHandlers']) {
1099
+ const v = overrides[k];
1100
+ if (Array.isArray(v)) {
1101
+ merged[k] = [...config[k], ...v];
1102
+ }
1103
+ }
1104
+ return merged;
1105
+ },
1106
+ name: '@lexical/link/AutoLink',
1107
+ register: registerAutoLink
1108
+ });
1109
+
1110
+ /**
1111
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1112
+ *
1113
+ * This source code is licensed under the MIT license found in the
1114
+ * LICENSE file in the root directory of this source tree.
1115
+ *
1116
+ */
1117
+
1118
+
1119
+ /** @deprecated renamed to {@link $toggleLink} by @lexical/eslint-plugin rules-of-lexical */
1120
+ const toggleLink = $toggleLink;
1121
+
1122
+ export { $createAutoLinkNode, $createLinkNode, $isAutoLinkNode, $isLinkNode, $toggleLink, AutoLinkExtension, AutoLinkNode, ClickableLinkExtension, LinkExtension, LinkNode, TOGGLE_LINK_COMMAND, createLinkMatcherWithRegExp, formatUrl, registerAutoLink, registerClickableLink, registerLink, toggleLink };