lexical 0.25.1-nightly.20250227.0 → 0.26.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.
- package/Lexical.dev.js +668 -23
- package/Lexical.dev.mjs +663 -24
- package/Lexical.js.flow +2 -0
- package/Lexical.mjs +6 -0
- package/Lexical.node.mjs +6 -0
- package/Lexical.prod.js +1 -1
- package/Lexical.prod.mjs +1 -1
- package/LexicalConstants.d.ts +1 -0
- package/LexicalNode.d.ts +5 -1
- package/LexicalNodeState.d.ts +380 -0
- package/caret/LexicalCaretUtils.d.ts +1 -1
- package/index.d.ts +2 -1
- package/nodes/LexicalLineBreakNode.d.ts +1 -0
- package/nodes/LexicalTextNode.d.ts +4 -0
- package/package.json +1 -1
package/Lexical.dev.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
+
var lexical = require('lexical');
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
13
15
|
*
|
|
@@ -176,6 +178,7 @@ const TEXT_TYPE_TO_MODE = {
|
|
|
176
178
|
[IS_SEGMENTED]: 'segmented',
|
|
177
179
|
[IS_TOKEN]: 'token'
|
|
178
180
|
};
|
|
181
|
+
const NODE_STATE_KEY = '$';
|
|
179
182
|
|
|
180
183
|
/**
|
|
181
184
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
@@ -456,6 +459,601 @@ function initMutationObserver(editor) {
|
|
|
456
459
|
});
|
|
457
460
|
}
|
|
458
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
464
|
+
*
|
|
465
|
+
* This source code is licensed under the MIT license found in the
|
|
466
|
+
* LICENSE file in the root directory of this source tree.
|
|
467
|
+
*
|
|
468
|
+
*/
|
|
469
|
+
|
|
470
|
+
function coerceToJSON(v) {
|
|
471
|
+
return v;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The return value of {@link createState}, for use with
|
|
476
|
+
* {@link $getState} and {@link $setState}.
|
|
477
|
+
*/
|
|
478
|
+
class StateConfig {
|
|
479
|
+
/** The string key used when serializing this state to JSON */
|
|
480
|
+
|
|
481
|
+
/** The parse function from the StateValueConfig passed to createState */
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The unparse function from the StateValueConfig passed to createState,
|
|
485
|
+
* with a default that is simply a pass-through that assumes the value is
|
|
486
|
+
* JSON serializable.
|
|
487
|
+
*/
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* An equality function from the StateValueConfig, with a default of
|
|
491
|
+
* Object.is.
|
|
492
|
+
*/
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The result of `stateValueConfig.parse(undefined)`, which is computed only
|
|
496
|
+
* once and used as the default value. When the current value `isEqual` to
|
|
497
|
+
* the `defaultValue`, it will not be serialized to JSON.
|
|
498
|
+
*/
|
|
499
|
+
|
|
500
|
+
constructor(key, stateValueConfig) {
|
|
501
|
+
this.key = key;
|
|
502
|
+
this.parse = stateValueConfig.parse.bind(stateValueConfig);
|
|
503
|
+
this.unparse = (stateValueConfig.unparse || coerceToJSON).bind(stateValueConfig);
|
|
504
|
+
this.isEqual = (stateValueConfig.isEqual || Object.is).bind(stateValueConfig);
|
|
505
|
+
this.defaultValue = this.parse(undefined);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* For advanced use cases, using this type is not recommended unless
|
|
511
|
+
* it is required (due to TypeScript's lack of features like
|
|
512
|
+
* higher-kinded types).
|
|
513
|
+
*
|
|
514
|
+
* A {@link StateConfig} type with any key and any value that can be
|
|
515
|
+
* used in situations where the key and value type can not be known,
|
|
516
|
+
* such as in a generic constraint when working with a collection of
|
|
517
|
+
* StateConfig.
|
|
518
|
+
*
|
|
519
|
+
* {@link StateConfigKey} and {@link StateConfigValue} will be
|
|
520
|
+
* useful when this is used as a generic constraint.
|
|
521
|
+
*/
|
|
522
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get the value type (V) from a StateConfig
|
|
526
|
+
*/
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get the key type (K) from a StateConfig
|
|
530
|
+
*/
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* A value type, or an updater for that value type. For use with
|
|
534
|
+
* {@link $setState} or any user-defined wrappers around it.
|
|
535
|
+
*/
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Configure a value to be used with StateConfig.
|
|
539
|
+
*
|
|
540
|
+
* The value type should be inferred from the definition of parse.
|
|
541
|
+
*
|
|
542
|
+
* If the value type is not JSON serializable, then unparse must also be provided.
|
|
543
|
+
*
|
|
544
|
+
* Values should be treated as immutable, much like React.useState. Mutating
|
|
545
|
+
* stored values directly will cause unpredictable behavior, is not supported,
|
|
546
|
+
* and may trigger errors in the future.
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* ```ts
|
|
550
|
+
* const numberOrNullState = createState('numberOrNull', {parse: (v) => typeof v === 'number' ? v : null});
|
|
551
|
+
* // ^? State<'numberOrNull', StateValueConfig<number | null>>
|
|
552
|
+
* const numberState = createState('number', {parse: (v) => typeof v === 'number' ? v : 0});
|
|
553
|
+
* // ^? State<'number', StateValueConfig<number>>
|
|
554
|
+
* ```
|
|
555
|
+
*
|
|
556
|
+
* Only the parse option is required, it is generally not useful to
|
|
557
|
+
* override `unparse` or `isEqual`. However, if you are using
|
|
558
|
+
* non-primitive types such as Array, Object, Date, or something
|
|
559
|
+
* more exotic then you would want to override this. In these
|
|
560
|
+
* cases you might want to reach for third party libraries.
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* ```ts
|
|
564
|
+
* const isoDateState = createState('isoDate', {
|
|
565
|
+
* parse: (v): null | Date => {
|
|
566
|
+
* const date = typeof v === 'string' ? new Date(v) : null;
|
|
567
|
+
* return date && !isNaN(date.valueOf()) ? date : null;
|
|
568
|
+
* }
|
|
569
|
+
* isEqual: (a, b) => a === b || (a && b && a.valueOf() === b.valueOf()),
|
|
570
|
+
* unparse: (v) => v && v.toString()
|
|
571
|
+
* });
|
|
572
|
+
* ```
|
|
573
|
+
*
|
|
574
|
+
* You may find it easier to write a parse function using libraries like
|
|
575
|
+
* zod, valibot, ajv, Effect, TypeBox, etc. perhaps with a wrapper function.
|
|
576
|
+
*/
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Create a StateConfig for the given string key and StateValueConfig.
|
|
580
|
+
*
|
|
581
|
+
* The key must be locally unique. In dev you wil get a key collision error
|
|
582
|
+
* when you use two separate StateConfig on the same node with the same key.
|
|
583
|
+
*
|
|
584
|
+
* The returned StateConfig value should be used with {@link $getState} and
|
|
585
|
+
* {@link $setState}.
|
|
586
|
+
*
|
|
587
|
+
* @param key The key to use
|
|
588
|
+
* @param valueConfig Configuration for the value type
|
|
589
|
+
* @returns a StateConfig
|
|
590
|
+
*/
|
|
591
|
+
function createState(key, valueConfig) {
|
|
592
|
+
return new StateConfig(key, valueConfig);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Given two versions of a node and a stateConfig, compare their state values
|
|
597
|
+
* using `$getState(nodeVersion, stateConfig, 'direct')`.
|
|
598
|
+
* If the values are equal according to `stateConfig.isEqual`, return `null`,
|
|
599
|
+
* otherwise return `[value, prevValue]`.
|
|
600
|
+
*
|
|
601
|
+
* This is useful for implementing updateDOM. Note that the `'direct'`
|
|
602
|
+
* version argument is used for both nodes.
|
|
603
|
+
*
|
|
604
|
+
* @param node Any LexicalNode
|
|
605
|
+
* @param prevNode A previous version of node
|
|
606
|
+
* @param stateConfig The configuration of the state to read
|
|
607
|
+
* @returns `[value, prevValue]` if changed, otherwise `null`
|
|
608
|
+
*/
|
|
609
|
+
function $getStateChange(node, prevNode, stateConfig) {
|
|
610
|
+
const value = $getState(node, stateConfig, 'direct');
|
|
611
|
+
const prevValue = $getState(prevNode, stateConfig, 'direct');
|
|
612
|
+
return stateConfig.isEqual(value, prevValue) ? null : [value, prevValue];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* The accessor for working with node state. This will read the value for the
|
|
617
|
+
* state on the given node, and will return `stateConfig.defaultValue` if the
|
|
618
|
+
* state has never been set on this node.
|
|
619
|
+
*
|
|
620
|
+
* The `version` parameter is optional and should generally be `'latest'`,
|
|
621
|
+
* consistent with the behavior of other node methods and functions,
|
|
622
|
+
* but for certain use cases such as `updateDOM` you may have a need to
|
|
623
|
+
* use `'direct'` to read the state from a previous version of the node.
|
|
624
|
+
*
|
|
625
|
+
* For very advanced use cases, you can expect that 'direct' does not
|
|
626
|
+
* require an editor state, just like directly accessing other properties
|
|
627
|
+
* of a node without an accessor (e.g. `textNode.__text`).
|
|
628
|
+
*
|
|
629
|
+
* @param node Any LexicalNode
|
|
630
|
+
* @param stateConfig The configuration of the state to read
|
|
631
|
+
* @param version The default value 'latest' will read the latest version of the node state, 'direct' will read the version that is stored on this LexicalNode which not reflect the version used in the current editor state
|
|
632
|
+
* @returns The current value from the state, or the default value provided by the configuration.
|
|
633
|
+
*/
|
|
634
|
+
function $getState(node, stateConfig, version = 'latest') {
|
|
635
|
+
const latestOrDirectNode = version === 'latest' ? node.getLatest() : node;
|
|
636
|
+
const state = latestOrDirectNode.__state;
|
|
637
|
+
if (state) {
|
|
638
|
+
$checkCollision(node, stateConfig, state);
|
|
639
|
+
return state.getValue(stateConfig);
|
|
640
|
+
}
|
|
641
|
+
return stateConfig.defaultValue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* @internal
|
|
646
|
+
*
|
|
647
|
+
* Register the config to this node's sharedConfigMap and throw an exception in
|
|
648
|
+
* `true` when a collision is detected.
|
|
649
|
+
*/
|
|
650
|
+
function $checkCollision(node, stateConfig, state) {
|
|
651
|
+
{
|
|
652
|
+
const collision = state.sharedConfigMap.get(stateConfig.key);
|
|
653
|
+
if (collision !== undefined && collision !== stateConfig) {
|
|
654
|
+
{
|
|
655
|
+
throw Error(`$setState: State key collision ${JSON.stringify(stateConfig.key)} detected in ${node.constructor.name} node with type ${node.getType()} and key ${node.getKey()}. Only one StateConfig with a given key should be used on a node.`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Set the state defined by stateConfig on node. Like with `React.useState`
|
|
663
|
+
* you may directly specify the value or use an updater function that will
|
|
664
|
+
* be called with the previous value of the state on that node (which will
|
|
665
|
+
* be the `stateConfig.defaultValue` if not set).
|
|
666
|
+
*
|
|
667
|
+
* When an updater function is used, the node will only be marked dirty if
|
|
668
|
+
* `stateConfig.isEqual(prevValue, value)` is false.
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```ts
|
|
672
|
+
* const toggle = createState('toggle', {parse: Boolean});
|
|
673
|
+
* // set it direction
|
|
674
|
+
* $setState(node, counterState, true);
|
|
675
|
+
* // use an updater
|
|
676
|
+
* $setState(node, counterState, (prev) => !prev);
|
|
677
|
+
* ```
|
|
678
|
+
*
|
|
679
|
+
* @param node The LexicalNode to set the state on
|
|
680
|
+
* @param stateConfig The configuration for this state
|
|
681
|
+
* @param valueOrUpdater The value or updater function
|
|
682
|
+
* @returns node
|
|
683
|
+
*/
|
|
684
|
+
function $setState(node, stateConfig, valueOrUpdater) {
|
|
685
|
+
errorOnReadOnly();
|
|
686
|
+
let value;
|
|
687
|
+
if (typeof valueOrUpdater === 'function') {
|
|
688
|
+
const latest = node.getLatest();
|
|
689
|
+
const prevValue = $getState(latest, stateConfig);
|
|
690
|
+
value = valueOrUpdater(prevValue);
|
|
691
|
+
if (stateConfig.isEqual(prevValue, value)) {
|
|
692
|
+
return latest;
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
value = valueOrUpdater;
|
|
696
|
+
}
|
|
697
|
+
const writable = node.getWritable();
|
|
698
|
+
const state = $getWritableNodeState(writable);
|
|
699
|
+
$checkCollision(node, stateConfig, state);
|
|
700
|
+
state.updateFromKnown(stateConfig, value);
|
|
701
|
+
return writable;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* @internal
|
|
705
|
+
*/
|
|
706
|
+
class NodeState {
|
|
707
|
+
/**
|
|
708
|
+
* @internal
|
|
709
|
+
*
|
|
710
|
+
* Track the (versioned) node that this NodeState was created for, to
|
|
711
|
+
* facilitate copy-on-write for NodeState. When a LexicalNode is cloned,
|
|
712
|
+
* it will *reference* the NodeState from its prevNode. From the nextNode
|
|
713
|
+
* you can continue to read state without copying, but the first $setState
|
|
714
|
+
* will trigger a copy of the prevNode's NodeState with the node property
|
|
715
|
+
* updated.
|
|
716
|
+
*/
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* @internal
|
|
720
|
+
*
|
|
721
|
+
* State that has already been parsed in a get state, so it is safe. (can be returned with
|
|
722
|
+
* just a cast since the proof was given before).
|
|
723
|
+
*
|
|
724
|
+
* Note that it uses StateConfig, so in addition to (1) the CURRENT VALUE, it has access to
|
|
725
|
+
* (2) the State key (3) the DEFAULT VALUE and (4) the PARSE FUNCTION
|
|
726
|
+
*/
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* @internal
|
|
730
|
+
*
|
|
731
|
+
* A copy of serializedNode[NODE_STATE_KEY] that is made when JSON is
|
|
732
|
+
* imported but has not been parsed yet.
|
|
733
|
+
*
|
|
734
|
+
* It stays here until a get state requires us to parse it, and since we
|
|
735
|
+
* then know the value is safe we move it to knownState and garbage collect
|
|
736
|
+
* it at the next version.
|
|
737
|
+
*
|
|
738
|
+
* Note that since only string keys are used here, we can only allow this
|
|
739
|
+
* state to pass-through on export or on the next version since there is
|
|
740
|
+
* no known value configuration. This pass-through is to support scenarios
|
|
741
|
+
* where multiple versions of the editor code are working in parallel so
|
|
742
|
+
* an old version of your code doesnt erase metadata that was
|
|
743
|
+
* set by a newer version of your code.
|
|
744
|
+
*/
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* @internal
|
|
748
|
+
*
|
|
749
|
+
* This sharedConfigMap is preserved across all versions of a given node and
|
|
750
|
+
* remains writable. It is how keys are resolved to configuration.
|
|
751
|
+
*/
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* @internal
|
|
755
|
+
*
|
|
756
|
+
* The count of known or unknown keys in this state, ignoring the
|
|
757
|
+
* intersection between the two sets.
|
|
758
|
+
*/
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* @internal
|
|
762
|
+
*/
|
|
763
|
+
constructor(node, sharedConfigMap = new Map(), unknownState = undefined, knownState = new Map(), size = undefined) {
|
|
764
|
+
this.node = node;
|
|
765
|
+
this.sharedConfigMap = sharedConfigMap;
|
|
766
|
+
this.unknownState = unknownState;
|
|
767
|
+
this.knownState = knownState;
|
|
768
|
+
const computedSize = size !== undefined ? size : computeSize(sharedConfigMap, unknownState, knownState);
|
|
769
|
+
{
|
|
770
|
+
if (!(size === undefined || computedSize === size)) {
|
|
771
|
+
throw Error(`NodeState: size != computedSize (${String(size)} != ${String(computedSize)})`);
|
|
772
|
+
}
|
|
773
|
+
for (const stateConfig of knownState.keys()) {
|
|
774
|
+
if (!sharedConfigMap.has(stateConfig.key)) {
|
|
775
|
+
throw Error(`NodeState: sharedConfigMap missing knownState key ${stateConfig.key}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
this.size = computedSize;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** @internal */
|
|
783
|
+
getValue(stateConfig) {
|
|
784
|
+
const known = this.knownState.get(stateConfig);
|
|
785
|
+
if (known !== undefined) {
|
|
786
|
+
return known;
|
|
787
|
+
}
|
|
788
|
+
this.sharedConfigMap.set(stateConfig.key, stateConfig);
|
|
789
|
+
let parsed = stateConfig.defaultValue;
|
|
790
|
+
if (this.unknownState && stateConfig.key in this.unknownState) {
|
|
791
|
+
const jsonValue = this.unknownState[stateConfig.key];
|
|
792
|
+
if (jsonValue !== undefined) {
|
|
793
|
+
parsed = stateConfig.parse(jsonValue);
|
|
794
|
+
}
|
|
795
|
+
// Only update if the key was unknown
|
|
796
|
+
this.updateFromKnown(stateConfig, parsed);
|
|
797
|
+
}
|
|
798
|
+
return parsed;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* @internal
|
|
803
|
+
*
|
|
804
|
+
* Used only for advanced use cases, such as collab. The intent here is to
|
|
805
|
+
* allow you to diff states with a more stable interface than the properties
|
|
806
|
+
* of this class.
|
|
807
|
+
*/
|
|
808
|
+
getInternalState() {
|
|
809
|
+
return [this.unknownState, this.knownState];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Encode this NodeState to JSON in the format that its node expects.
|
|
814
|
+
* This returns `{[NODE_STATE_KEY]?: UnknownStateRecord}` rather than
|
|
815
|
+
* `UnknownStateRecord | undefined` so that we can support flattening
|
|
816
|
+
* specific entries in the future when nodes can declare what
|
|
817
|
+
* their required StateConfigs are.
|
|
818
|
+
*/
|
|
819
|
+
toJSON() {
|
|
820
|
+
const state = {
|
|
821
|
+
...this.unknownState
|
|
822
|
+
};
|
|
823
|
+
for (const [stateConfig, v] of this.knownState) {
|
|
824
|
+
if (stateConfig.isEqual(v, stateConfig.defaultValue)) {
|
|
825
|
+
delete state[stateConfig.key];
|
|
826
|
+
} else {
|
|
827
|
+
state[stateConfig.key] = stateConfig.unparse(v);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return undefinedIfEmpty(state) ? {
|
|
831
|
+
[NODE_STATE_KEY]: state
|
|
832
|
+
} : {};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* @internal
|
|
837
|
+
*
|
|
838
|
+
* A NodeState is writable when the node to update matches
|
|
839
|
+
* the node associated with the NodeState. This basically
|
|
840
|
+
* mirrors how the EditorState NodeMap works, but in a
|
|
841
|
+
* bottom-up organization rather than a top-down organization.
|
|
842
|
+
*
|
|
843
|
+
* This allows us to implement the same "copy on write"
|
|
844
|
+
* pattern for state, without having the state version
|
|
845
|
+
* update every time the node version changes (e.g. when
|
|
846
|
+
* its parent or siblings change).
|
|
847
|
+
*
|
|
848
|
+
* @param node The node to associate with the state
|
|
849
|
+
* @returns The next writaable state
|
|
850
|
+
*/
|
|
851
|
+
getWritable(node) {
|
|
852
|
+
if (this.node === node) {
|
|
853
|
+
return this;
|
|
854
|
+
}
|
|
855
|
+
const nextKnownState = new Map(this.knownState);
|
|
856
|
+
const nextUnknownState = cloneUnknownState(this.unknownState);
|
|
857
|
+
if (nextUnknownState) {
|
|
858
|
+
// Garbage collection
|
|
859
|
+
for (const stateConfig of nextKnownState.keys()) {
|
|
860
|
+
delete nextUnknownState[stateConfig.key];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return new NodeState(node, this.sharedConfigMap, undefinedIfEmpty(nextUnknownState), nextKnownState, this.size);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** @internal */
|
|
867
|
+
updateFromKnown(stateConfig, value) {
|
|
868
|
+
const key = stateConfig.key;
|
|
869
|
+
this.sharedConfigMap.set(key, stateConfig);
|
|
870
|
+
const {
|
|
871
|
+
knownState,
|
|
872
|
+
unknownState
|
|
873
|
+
} = this;
|
|
874
|
+
if (!(knownState.has(stateConfig) || unknownState && key in unknownState)) {
|
|
875
|
+
this.size++;
|
|
876
|
+
}
|
|
877
|
+
knownState.set(stateConfig, value);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* @internal
|
|
882
|
+
*
|
|
883
|
+
* This is intended for advanced use cases only, such
|
|
884
|
+
* as collab or dev tools.
|
|
885
|
+
*
|
|
886
|
+
* Update a single key value pair from unknown state,
|
|
887
|
+
* parsing it if the key is known to this node. This is
|
|
888
|
+
* basically like updateFromJSON, but the effect is
|
|
889
|
+
* isolated to a single entry.
|
|
890
|
+
*
|
|
891
|
+
* @param k The string key from an UnknownStateRecord
|
|
892
|
+
* @param v The unknown value from an UnknownStateRecord
|
|
893
|
+
*/
|
|
894
|
+
updateFromUnknown(k, v) {
|
|
895
|
+
const stateConfig = this.sharedConfigMap.get(k);
|
|
896
|
+
if (stateConfig) {
|
|
897
|
+
this.updateFromKnown(stateConfig, stateConfig.parse(v));
|
|
898
|
+
} else {
|
|
899
|
+
this.unknownState = this.unknownState || {};
|
|
900
|
+
if (!(k in this.unknownState)) {
|
|
901
|
+
this.size++;
|
|
902
|
+
}
|
|
903
|
+
this.unknownState[k] = v;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* @internal
|
|
909
|
+
*
|
|
910
|
+
* Reset all existing state to default or empty values,
|
|
911
|
+
* and perform any updates from the given unknownState.
|
|
912
|
+
*
|
|
913
|
+
* This is used when initializing a node's state from JSON,
|
|
914
|
+
* or when resetting a node's state from JSON.
|
|
915
|
+
*
|
|
916
|
+
* @param unknownState The new state in serialized form
|
|
917
|
+
*/
|
|
918
|
+
updateFromJSON(unknownState) {
|
|
919
|
+
const {
|
|
920
|
+
knownState
|
|
921
|
+
} = this;
|
|
922
|
+
// Reset all known state to defaults
|
|
923
|
+
for (const stateConfig of knownState.keys()) {
|
|
924
|
+
knownState.set(stateConfig, stateConfig.defaultValue);
|
|
925
|
+
}
|
|
926
|
+
// Since we are resetting all state to this new record,
|
|
927
|
+
// the size starts at the number of known keys
|
|
928
|
+
// and will be updated as we traverse the new state
|
|
929
|
+
this.size = knownState.size;
|
|
930
|
+
this.unknownState = {};
|
|
931
|
+
if (unknownState) {
|
|
932
|
+
for (const [k, v] of Object.entries(unknownState)) {
|
|
933
|
+
this.updateFromUnknown(k, v);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
this.unknownState = undefinedIfEmpty(this.unknownState);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function computeSize(sharedConfigMap, unknownState, knownState) {
|
|
940
|
+
let size = knownState.size;
|
|
941
|
+
if (unknownState) {
|
|
942
|
+
for (const k in unknownState) {
|
|
943
|
+
const sharedConfig = sharedConfigMap.get(k);
|
|
944
|
+
if (!sharedConfig || !knownState.has(sharedConfig)) {
|
|
945
|
+
size++;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return size;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Return obj if it is an object with at least one property, otherwise
|
|
954
|
+
* return undefined.
|
|
955
|
+
*/
|
|
956
|
+
function undefinedIfEmpty(obj) {
|
|
957
|
+
if (obj) {
|
|
958
|
+
for (const key in obj) {
|
|
959
|
+
return obj;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Return undefined if unknownState is undefined or an empty object,
|
|
967
|
+
* otherwise return a shallow clone of it.
|
|
968
|
+
*/
|
|
969
|
+
function cloneUnknownState(unknownState) {
|
|
970
|
+
return undefinedIfEmpty(unknownState) && {
|
|
971
|
+
...unknownState
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* @internal
|
|
977
|
+
*
|
|
978
|
+
* Only for direct use in very advanced integrations, such as lexical-yjs.
|
|
979
|
+
* Typically you would only use {@link createState}, {@link $getState}, and
|
|
980
|
+
* {@link $setState}. This is effectively the preamble for {@link $setState}.
|
|
981
|
+
*/
|
|
982
|
+
function $getWritableNodeState(node) {
|
|
983
|
+
const writable = node.getWritable();
|
|
984
|
+
const state = writable.__state ? writable.__state.getWritable(writable) : new NodeState(writable);
|
|
985
|
+
writable.__state = state;
|
|
986
|
+
return state;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* @internal
|
|
991
|
+
*
|
|
992
|
+
* This is used to implement LexicalNode.updateFromJSON and is
|
|
993
|
+
* not intended to be exported from the package.
|
|
994
|
+
*
|
|
995
|
+
* @param node any LexicalNode
|
|
996
|
+
* @param unknownState undefined or a serialized State
|
|
997
|
+
* @returns A writable version of node, with the state set.
|
|
998
|
+
*/
|
|
999
|
+
function $updateStateFromJSON(node, unknownState) {
|
|
1000
|
+
const writable = node.getWritable();
|
|
1001
|
+
if (unknownState || writable.__state) {
|
|
1002
|
+
$getWritableNodeState(node).updateFromJSON(unknownState);
|
|
1003
|
+
}
|
|
1004
|
+
return writable;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* @internal
|
|
1009
|
+
*
|
|
1010
|
+
* Return true if the two nodes have equivalent NodeState, to be used
|
|
1011
|
+
* to determine when TextNode are being merged, not a lot of use cases
|
|
1012
|
+
* otherwise.
|
|
1013
|
+
*/
|
|
1014
|
+
function $nodeStatesAreEquivalent(a, b) {
|
|
1015
|
+
if (a === b) {
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
if (a && b && a.size !== b.size) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
const keys = new Set();
|
|
1022
|
+
const hasUnequalMapEntry = (sourceState, otherState) => {
|
|
1023
|
+
for (const [stateConfig, value] of sourceState.knownState) {
|
|
1024
|
+
if (keys.has(stateConfig.key)) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
keys.add(stateConfig.key);
|
|
1028
|
+
const otherValue = otherState ? otherState.getValue(stateConfig) : stateConfig.defaultValue;
|
|
1029
|
+
if (otherValue !== value && !stateConfig.isEqual(otherValue, value)) {
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return false;
|
|
1034
|
+
};
|
|
1035
|
+
const hasUnequalRecordEntry = (sourceState, otherState) => {
|
|
1036
|
+
const {
|
|
1037
|
+
unknownState
|
|
1038
|
+
} = sourceState;
|
|
1039
|
+
const otherUnknownState = otherState ? otherState.unknownState : undefined;
|
|
1040
|
+
if (unknownState) {
|
|
1041
|
+
for (const [key, value] of Object.entries(unknownState)) {
|
|
1042
|
+
if (keys.has(key)) {
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
keys.add(key);
|
|
1046
|
+
const otherValue = otherUnknownState ? otherUnknownState[key] : undefined;
|
|
1047
|
+
if (value !== otherValue) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return false;
|
|
1053
|
+
};
|
|
1054
|
+
return !(a && hasUnequalMapEntry(a, b) || b && hasUnequalMapEntry(b, a) || a && hasUnequalRecordEntry(a, b) || b && hasUnequalRecordEntry(b, a));
|
|
1055
|
+
}
|
|
1056
|
+
|
|
459
1057
|
/**
|
|
460
1058
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
461
1059
|
*
|
|
@@ -471,7 +1069,9 @@ function $canSimpleTextNodesBeMerged(node1, node2) {
|
|
|
471
1069
|
const node2Mode = node2.__mode;
|
|
472
1070
|
const node2Format = node2.__format;
|
|
473
1071
|
const node2Style = node2.__style;
|
|
474
|
-
|
|
1072
|
+
const node1State = node1.__state;
|
|
1073
|
+
const node2State = node2.__state;
|
|
1074
|
+
return (node1Mode === null || node1Mode === node2Mode) && (node1Format === null || node1Format === node2Format) && (node1Style === null || node1Style === node2Style) && (node1.__state === null || node1State === node2State || $nodeStatesAreEquivalent(node1State, node2State));
|
|
475
1075
|
}
|
|
476
1076
|
function $mergeTextNodes(node1, node2) {
|
|
477
1077
|
const writableNode1 = node1.mergeWithSibling(node2);
|
|
@@ -1237,6 +1837,7 @@ let isInsertLineBreak = false;
|
|
|
1237
1837
|
let isFirefoxEndingComposition = false;
|
|
1238
1838
|
let isSafariEndingComposition = false;
|
|
1239
1839
|
let safariEndCompositionEventData = '';
|
|
1840
|
+
let postDeleteSelectionToRestore = null;
|
|
1240
1841
|
let collapsedSelectionFormat = [0, '', 0, 'root', 0];
|
|
1241
1842
|
|
|
1242
1843
|
// This function is used to determine if Lexical should attempt to override
|
|
@@ -1305,7 +1906,7 @@ function onSelectionChange(domSelection, editor, isActive) {
|
|
|
1305
1906
|
// We also need to check if the offset is at the boundary,
|
|
1306
1907
|
// because in this case, we might need to normalize to a
|
|
1307
1908
|
// sibling instead.
|
|
1308
|
-
if (shouldSkipSelectionChange(anchorDOM, anchorOffset) && shouldSkipSelectionChange(focusDOM, focusOffset)) {
|
|
1909
|
+
if (shouldSkipSelectionChange(anchorDOM, anchorOffset) && shouldSkipSelectionChange(focusDOM, focusOffset) && !postDeleteSelectionToRestore) {
|
|
1309
1910
|
return;
|
|
1310
1911
|
}
|
|
1311
1912
|
}
|
|
@@ -1319,7 +1920,23 @@ function onSelectionChange(domSelection, editor, isActive) {
|
|
|
1319
1920
|
if (!isSelectionWithinEditor(editor, anchorDOM, focusDOM)) {
|
|
1320
1921
|
return;
|
|
1321
1922
|
}
|
|
1322
|
-
|
|
1923
|
+
let selection = $getSelection();
|
|
1924
|
+
|
|
1925
|
+
// Restore selection in the event of incorrect rightward shift after deletion
|
|
1926
|
+
if (postDeleteSelectionToRestore && $isRangeSelection(selection) && selection.isCollapsed()) {
|
|
1927
|
+
const curAnchor = selection.anchor;
|
|
1928
|
+
const prevAnchor = postDeleteSelectionToRestore.anchor;
|
|
1929
|
+
if (
|
|
1930
|
+
// Rightward shift in same node
|
|
1931
|
+
curAnchor.key === prevAnchor.key && curAnchor.offset === prevAnchor.offset + 1 ||
|
|
1932
|
+
// Or rightward shift into sibling node
|
|
1933
|
+
curAnchor.offset === 1 && prevAnchor.getNode().is(curAnchor.getNode().getPreviousSibling())) {
|
|
1934
|
+
// Restore selection
|
|
1935
|
+
selection = postDeleteSelectionToRestore.clone();
|
|
1936
|
+
$setSelection(selection);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
postDeleteSelectionToRestore = null;
|
|
1323
1940
|
|
|
1324
1941
|
// Update the selection format
|
|
1325
1942
|
if ($isRangeSelection(selection)) {
|
|
@@ -1556,6 +2173,14 @@ function onBeforeInput(event, editor) {
|
|
|
1556
2173
|
}
|
|
1557
2174
|
if (!shouldLetBrowserHandleDelete) {
|
|
1558
2175
|
dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);
|
|
2176
|
+
// When deleting across paragraphs, Chrome on Android incorrectly shifts the selection rightwards
|
|
2177
|
+
// We save the correct selection to restore later during handling of selectionchange event
|
|
2178
|
+
const selectionAfterDelete = $getSelection();
|
|
2179
|
+
if (IS_ANDROID_CHROME && $isRangeSelection(selectionAfterDelete) && selectionAfterDelete.isCollapsed()) {
|
|
2180
|
+
postDeleteSelectionToRestore = selectionAfterDelete;
|
|
2181
|
+
// Cleanup in case selectionchange does not fire
|
|
2182
|
+
setTimeout(() => postDeleteSelectionToRestore = null);
|
|
2183
|
+
}
|
|
1559
2184
|
}
|
|
1560
2185
|
}
|
|
1561
2186
|
return;
|
|
@@ -2250,6 +2875,8 @@ class LexicalNode {
|
|
|
2250
2875
|
|
|
2251
2876
|
/** @internal */
|
|
2252
2877
|
|
|
2878
|
+
/** @internal */
|
|
2879
|
+
|
|
2253
2880
|
// Flow doesn't support abstract classes unfortunately, so we can't _force_
|
|
2254
2881
|
// subclasses of Node to implement statics. All subclasses of Node should have
|
|
2255
2882
|
// a static getType and clone method though. We define getType and clone here so we can call it
|
|
@@ -2333,6 +2960,7 @@ class LexicalNode {
|
|
|
2333
2960
|
this.__parent = prevNode.__parent;
|
|
2334
2961
|
this.__next = prevNode.__next;
|
|
2335
2962
|
this.__prev = prevNode.__prev;
|
|
2963
|
+
this.__state = prevNode.__state;
|
|
2336
2964
|
}
|
|
2337
2965
|
|
|
2338
2966
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -2342,6 +2970,12 @@ class LexicalNode {
|
|
|
2342
2970
|
this.__parent = null;
|
|
2343
2971
|
this.__prev = null;
|
|
2344
2972
|
this.__next = null;
|
|
2973
|
+
Object.defineProperty(this, '__state', {
|
|
2974
|
+
configurable: true,
|
|
2975
|
+
enumerable: false,
|
|
2976
|
+
value: undefined,
|
|
2977
|
+
writable: true
|
|
2978
|
+
});
|
|
2345
2979
|
$setNodeKey(this, key);
|
|
2346
2980
|
{
|
|
2347
2981
|
if (this.__type !== 'root') {
|
|
@@ -2859,9 +3493,12 @@ class LexicalNode {
|
|
|
2859
3493
|
*
|
|
2860
3494
|
* */
|
|
2861
3495
|
exportJSON() {
|
|
3496
|
+
// eslint-disable-next-line dot-notation
|
|
3497
|
+
const state = this.__state ? this.__state.toJSON() : undefined;
|
|
2862
3498
|
return {
|
|
2863
3499
|
type: this.__type,
|
|
2864
|
-
version: 1
|
|
3500
|
+
version: 1,
|
|
3501
|
+
...state
|
|
2865
3502
|
};
|
|
2866
3503
|
}
|
|
2867
3504
|
|
|
@@ -2907,7 +3544,7 @@ class LexicalNode {
|
|
|
2907
3544
|
* ```
|
|
2908
3545
|
**/
|
|
2909
3546
|
updateFromJSON(serializedNode) {
|
|
2910
|
-
return this;
|
|
3547
|
+
return $updateStateFromJSON(this, serializedNode[lexical.NODE_STATE_KEY]);
|
|
2911
3548
|
}
|
|
2912
3549
|
|
|
2913
3550
|
/**
|
|
@@ -3257,6 +3894,9 @@ class LineBreakNode extends LexicalNode {
|
|
|
3257
3894
|
updateDOM() {
|
|
3258
3895
|
return false;
|
|
3259
3896
|
}
|
|
3897
|
+
isInline() {
|
|
3898
|
+
return true;
|
|
3899
|
+
}
|
|
3260
3900
|
static importDOM() {
|
|
3261
3901
|
return {
|
|
3262
3902
|
br: node => {
|
|
@@ -3634,6 +4274,13 @@ class TextNode extends LexicalNode {
|
|
|
3634
4274
|
return true;
|
|
3635
4275
|
}
|
|
3636
4276
|
|
|
4277
|
+
/**
|
|
4278
|
+
* @returns true if the text node is inline, false otherwise.
|
|
4279
|
+
*/
|
|
4280
|
+
isInline() {
|
|
4281
|
+
return true;
|
|
4282
|
+
}
|
|
4283
|
+
|
|
3637
4284
|
// View
|
|
3638
4285
|
|
|
3639
4286
|
createDOM(config, editor) {
|
|
@@ -4526,22 +5173,12 @@ class Point {
|
|
|
4526
5173
|
return this.key === point.key && this.offset === point.offset && this.type === point.type;
|
|
4527
5174
|
}
|
|
4528
5175
|
isBefore(b) {
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
const aOffset = this.offset;
|
|
4532
|
-
const bOffset = b.offset;
|
|
4533
|
-
if ($isElementNode(aNode)) {
|
|
4534
|
-
const aNodeDescendant = aNode.getDescendantByIndex(aOffset);
|
|
4535
|
-
aNode = aNodeDescendant != null ? aNodeDescendant : aNode;
|
|
4536
|
-
}
|
|
4537
|
-
if ($isElementNode(bNode)) {
|
|
4538
|
-
const bNodeDescendant = bNode.getDescendantByIndex(bOffset);
|
|
4539
|
-
bNode = bNodeDescendant != null ? bNodeDescendant : bNode;
|
|
4540
|
-
}
|
|
4541
|
-
if (aNode === bNode) {
|
|
4542
|
-
return aOffset < bOffset;
|
|
5176
|
+
if (this.key === b.key) {
|
|
5177
|
+
return this.offset < b.offset;
|
|
4543
5178
|
}
|
|
4544
|
-
|
|
5179
|
+
const aCaret = $normalizeCaret($caretFromPoint(this, 'next'));
|
|
5180
|
+
const bCaret = $normalizeCaret($caretFromPoint(b, 'next'));
|
|
5181
|
+
return $comparePointCaretNext(aCaret, bCaret) < 0;
|
|
4545
5182
|
}
|
|
4546
5183
|
getNode() {
|
|
4547
5184
|
const key = this.key;
|
|
@@ -9644,7 +10281,7 @@ class LexicalEditor {
|
|
|
9644
10281
|
};
|
|
9645
10282
|
}
|
|
9646
10283
|
}
|
|
9647
|
-
LexicalEditor.version = "0.
|
|
10284
|
+
LexicalEditor.version = "0.26.0+dev.cjs";
|
|
9648
10285
|
|
|
9649
10286
|
/**
|
|
9650
10287
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
@@ -11956,8 +12593,10 @@ function $caretRangeFromSelection(selection) {
|
|
|
11956
12593
|
anchor,
|
|
11957
12594
|
focus
|
|
11958
12595
|
} = selection;
|
|
11959
|
-
const
|
|
11960
|
-
|
|
12596
|
+
const anchorCaret = $caretFromPoint(anchor, 'next');
|
|
12597
|
+
const focusCaret = $caretFromPoint(focus, 'next');
|
|
12598
|
+
const direction = $comparePointCaretNext(anchorCaret, focusCaret) <= 0 ? 'next' : 'previous';
|
|
12599
|
+
return $getCaretRange($getCaretInDirection(anchorCaret, direction), $getCaretInDirection(focusCaret, direction));
|
|
11961
12600
|
}
|
|
11962
12601
|
|
|
11963
12602
|
/**
|
|
@@ -12318,10 +12957,13 @@ exports.$getPreviousSelection = $getPreviousSelection;
|
|
|
12318
12957
|
exports.$getRoot = $getRoot;
|
|
12319
12958
|
exports.$getSelection = $getSelection;
|
|
12320
12959
|
exports.$getSiblingCaret = $getSiblingCaret;
|
|
12960
|
+
exports.$getState = $getState;
|
|
12961
|
+
exports.$getStateChange = $getStateChange;
|
|
12321
12962
|
exports.$getTextContent = $getTextContent;
|
|
12322
12963
|
exports.$getTextNodeOffset = $getTextNodeOffset;
|
|
12323
12964
|
exports.$getTextPointCaret = $getTextPointCaret;
|
|
12324
12965
|
exports.$getTextPointCaretSlice = $getTextPointCaretSlice;
|
|
12966
|
+
exports.$getWritableNodeState = $getWritableNodeState;
|
|
12325
12967
|
exports.$hasAncestor = $hasAncestor;
|
|
12326
12968
|
exports.$hasUpdateTag = $hasUpdateTag;
|
|
12327
12969
|
exports.$insertNodes = $insertNodes;
|
|
@@ -12356,6 +12998,7 @@ exports.$setCompositionKey = $setCompositionKey;
|
|
|
12356
12998
|
exports.$setPointFromCaret = $setPointFromCaret;
|
|
12357
12999
|
exports.$setSelection = $setSelection;
|
|
12358
13000
|
exports.$setSelectionFromCaretRange = $setSelectionFromCaretRange;
|
|
13001
|
+
exports.$setState = $setState;
|
|
12359
13002
|
exports.$splitNode = $splitNode;
|
|
12360
13003
|
exports.$updateRangeSelectionFromCaretRange = $updateRangeSelectionFromCaretRange;
|
|
12361
13004
|
exports.ArtificialNode__DO_NOT_USE = ArtificialNode__DO_NOT_USE;
|
|
@@ -12414,6 +13057,7 @@ exports.KEY_TAB_COMMAND = KEY_TAB_COMMAND;
|
|
|
12414
13057
|
exports.LineBreakNode = LineBreakNode;
|
|
12415
13058
|
exports.MOVE_TO_END = MOVE_TO_END;
|
|
12416
13059
|
exports.MOVE_TO_START = MOVE_TO_START;
|
|
13060
|
+
exports.NODE_STATE_KEY = NODE_STATE_KEY;
|
|
12417
13061
|
exports.OUTDENT_CONTENT_COMMAND = OUTDENT_CONTENT_COMMAND;
|
|
12418
13062
|
exports.PASTE_COMMAND = PASTE_COMMAND;
|
|
12419
13063
|
exports.ParagraphNode = ParagraphNode;
|
|
@@ -12429,6 +13073,7 @@ exports.TextNode = TextNode;
|
|
|
12429
13073
|
exports.UNDO_COMMAND = UNDO_COMMAND;
|
|
12430
13074
|
exports.createCommand = createCommand;
|
|
12431
13075
|
exports.createEditor = createEditor;
|
|
13076
|
+
exports.createState = createState;
|
|
12432
13077
|
exports.flipDirection = flipDirection;
|
|
12433
13078
|
exports.getDOMOwnerDocument = getDOMOwnerDocument;
|
|
12434
13079
|
exports.getDOMSelection = getDOMSelection;
|