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