@portabletext/editor 1.5.6 → 1.6.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/lib/index.d.mts +452 -28
- package/lib/index.d.ts +452 -28
- package/lib/index.esm.js +2533 -2398
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +2533 -2398
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +2533 -2398
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/editor/Editable.tsx +2 -2
- package/src/editor/__tests__/self-solving.test.tsx +9 -9
- package/src/editor/behavior/behavior.actions.ts +51 -4
- package/src/editor/behavior/behavior.core.decorators.ts +52 -0
- package/src/editor/behavior/behavior.core.ts +5 -0
- package/src/editor/behavior/behavior.markdown.ts +7 -6
- package/src/editor/behavior/behavior.types.ts +15 -0
- package/src/editor/editor-machine.ts +32 -4
- package/src/editor/plugins/create-with-event-listeners.ts +24 -0
- package/src/editor/plugins/createWithEditableAPI.ts +10 -2
- package/src/editor/plugins/createWithHotKeys.ts +10 -1
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +215 -220
- package/src/types/editor.ts +0 -14
|
@@ -13,10 +13,11 @@ import type {
|
|
|
13
13
|
PortableTextSlateEditor,
|
|
14
14
|
} from '../../types/editor'
|
|
15
15
|
import {debugWithName} from '../../utils/debug'
|
|
16
|
-
import {toPortableTextRange} from '../../utils/ranges'
|
|
17
16
|
import {getNextSpan, getPreviousSpan} from '../../utils/sibling-utils'
|
|
18
17
|
import {isChangingRemotely} from '../../utils/withChanges'
|
|
19
18
|
import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
|
|
19
|
+
import type {BehaviourActionImplementation} from '../behavior/behavior.actions'
|
|
20
|
+
import type {BehaviorAction, PickFromUnion} from '../behavior/behavior.types'
|
|
20
21
|
import type {EditorActor} from '../editor-machine'
|
|
21
22
|
|
|
22
23
|
const debug = debugWithName('plugin:withPortableTextMarkModel')
|
|
@@ -29,29 +30,6 @@ export function createWithPortableTextMarkModel(
|
|
|
29
30
|
const {apply, normalizeNode} = editor
|
|
30
31
|
const decorators = types.decorators.map((t) => t.value)
|
|
31
32
|
|
|
32
|
-
// Selections are normally emitted automatically via
|
|
33
|
-
// onChange, but they will keep the object reference if
|
|
34
|
-
// the selection is the same as the previous.
|
|
35
|
-
// When toggling marks however, it might not even
|
|
36
|
-
// result in a onChange event (for instance when nothing is selected),
|
|
37
|
-
// and if you toggle marks on a block with one single span,
|
|
38
|
-
// the selection would also stay the same.
|
|
39
|
-
// We should force a new selection object here when toggling marks,
|
|
40
|
-
// because toolbars and other things can very conveniently
|
|
41
|
-
// be memo'ed on the editor selection to update itself.
|
|
42
|
-
const forceNewSelection = () => {
|
|
43
|
-
if (editor.selection) {
|
|
44
|
-
Transforms.select(editor, {...editor.selection})
|
|
45
|
-
editor.selection = {...editor.selection} // Ensure new object
|
|
46
|
-
}
|
|
47
|
-
const ptRange = toPortableTextRange(
|
|
48
|
-
editor.children,
|
|
49
|
-
editor.selection,
|
|
50
|
-
types,
|
|
51
|
-
)
|
|
52
|
-
editorActor.send({type: 'selection', selection: ptRange})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
33
|
// Extend Slate's default normalization. Merge spans with same set of .marks when doing merge_node operations, and clean up markDefs / marks
|
|
56
34
|
editor.normalizeNode = (nodeEntry) => {
|
|
57
35
|
const [node, path] = nodeEntry
|
|
@@ -673,220 +651,237 @@ export function createWithPortableTextMarkModel(
|
|
|
673
651
|
apply(op)
|
|
674
652
|
}
|
|
675
653
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (Range.isExpanded(editor.selection)) {
|
|
680
|
-
Editor.withoutNormalizing(editor, () => {
|
|
681
|
-
// Split if needed
|
|
682
|
-
Transforms.setNodes(
|
|
683
|
-
editor,
|
|
684
|
-
{},
|
|
685
|
-
{match: Text.isText, split: true, hanging: true},
|
|
686
|
-
)
|
|
687
|
-
// Use new selection
|
|
688
|
-
const splitTextNodes = Range.isRange(editor.selection)
|
|
689
|
-
? [
|
|
690
|
-
...Editor.nodes(editor, {
|
|
691
|
-
at: editor.selection,
|
|
692
|
-
match: Text.isText,
|
|
693
|
-
}),
|
|
694
|
-
]
|
|
695
|
-
: []
|
|
696
|
-
const shouldRemoveMark =
|
|
697
|
-
splitTextNodes.length > 1 &&
|
|
698
|
-
splitTextNodes.every((node) => node[0].marks?.includes(mark))
|
|
699
|
-
|
|
700
|
-
if (shouldRemoveMark) {
|
|
701
|
-
editor.removeMark(mark)
|
|
702
|
-
} else {
|
|
703
|
-
splitTextNodes.forEach(([node, path]) => {
|
|
704
|
-
const marks = [
|
|
705
|
-
...(Array.isArray(node.marks) ? node.marks : []).filter(
|
|
706
|
-
(eMark: string) => eMark !== mark,
|
|
707
|
-
),
|
|
708
|
-
mark,
|
|
709
|
-
]
|
|
710
|
-
Transforms.setNodes(
|
|
711
|
-
editor,
|
|
712
|
-
{marks},
|
|
713
|
-
{at: path, match: Text.isText, split: true, hanging: true},
|
|
714
|
-
)
|
|
715
|
-
})
|
|
716
|
-
}
|
|
717
|
-
})
|
|
718
|
-
} else {
|
|
719
|
-
const [block, blockPath] = Editor.node(editor, editor.selection, {
|
|
720
|
-
depth: 1,
|
|
721
|
-
})
|
|
722
|
-
const lonelyEmptySpan =
|
|
723
|
-
editor.isTextBlock(block) &&
|
|
724
|
-
block.children.length === 1 &&
|
|
725
|
-
editor.isTextSpan(block.children[0]) &&
|
|
726
|
-
block.children[0].text === ''
|
|
727
|
-
? block.children[0]
|
|
728
|
-
: undefined
|
|
729
|
-
|
|
730
|
-
if (lonelyEmptySpan) {
|
|
731
|
-
const existingMarks = lonelyEmptySpan.marks ?? []
|
|
732
|
-
const existingMarksWithoutDecorator = existingMarks.filter(
|
|
733
|
-
(existingMark) => existingMark !== mark,
|
|
734
|
-
)
|
|
654
|
+
return editor
|
|
655
|
+
}
|
|
656
|
+
}
|
|
735
657
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
658
|
+
export const addDecoratorActionImplementation: BehaviourActionImplementation<
|
|
659
|
+
PickFromUnion<BehaviorAction, 'type', 'decorator.add'>
|
|
660
|
+
> = ({action}) => {
|
|
661
|
+
const editor = action.editor
|
|
662
|
+
const mark = action.decorator
|
|
663
|
+
|
|
664
|
+
if (editor.selection) {
|
|
665
|
+
if (Range.isExpanded(editor.selection)) {
|
|
666
|
+
// Split if needed
|
|
667
|
+
Transforms.setNodes(
|
|
668
|
+
editor,
|
|
669
|
+
{},
|
|
670
|
+
{match: Text.isText, split: true, hanging: true},
|
|
671
|
+
)
|
|
672
|
+
// Use new selection
|
|
673
|
+
const splitTextNodes = Range.isRange(editor.selection)
|
|
674
|
+
? [
|
|
675
|
+
...Editor.nodes(editor, {
|
|
676
|
+
at: editor.selection,
|
|
677
|
+
match: Text.isText,
|
|
678
|
+
}),
|
|
679
|
+
]
|
|
680
|
+
: []
|
|
681
|
+
const shouldRemoveMark =
|
|
682
|
+
splitTextNodes.length > 1 &&
|
|
683
|
+
splitTextNodes.every((node) => node[0].marks?.includes(mark))
|
|
684
|
+
|
|
685
|
+
if (shouldRemoveMark) {
|
|
686
|
+
editor.removeMark(mark)
|
|
687
|
+
} else {
|
|
688
|
+
splitTextNodes.forEach(([node, path]) => {
|
|
689
|
+
const marks = [
|
|
690
|
+
...(Array.isArray(node.marks) ? node.marks : []).filter(
|
|
691
|
+
(eMark: string) => eMark !== mark,
|
|
692
|
+
),
|
|
693
|
+
mark,
|
|
694
|
+
]
|
|
695
|
+
Transforms.setNodes(
|
|
696
|
+
editor,
|
|
697
|
+
{marks},
|
|
698
|
+
{at: path, match: Text.isText, split: true, hanging: true},
|
|
699
|
+
)
|
|
700
|
+
})
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
const [block, blockPath] = Editor.node(editor, editor.selection, {
|
|
704
|
+
depth: 1,
|
|
705
|
+
})
|
|
706
|
+
const lonelyEmptySpan =
|
|
707
|
+
editor.isTextBlock(block) &&
|
|
708
|
+
block.children.length === 1 &&
|
|
709
|
+
editor.isTextSpan(block.children[0]) &&
|
|
710
|
+
block.children[0].text === ''
|
|
711
|
+
? block.children[0]
|
|
712
|
+
: undefined
|
|
713
|
+
|
|
714
|
+
if (lonelyEmptySpan) {
|
|
715
|
+
const existingMarks = lonelyEmptySpan.marks ?? []
|
|
716
|
+
const existingMarksWithoutDecorator = existingMarks.filter(
|
|
717
|
+
(existingMark) => existingMark !== mark,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
Transforms.setNodes(
|
|
721
|
+
editor,
|
|
722
|
+
{
|
|
723
|
+
marks:
|
|
724
|
+
existingMarks.length === existingMarksWithoutDecorator.length
|
|
725
|
+
? [...existingMarks, mark]
|
|
726
|
+
: existingMarksWithoutDecorator,
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
at: blockPath,
|
|
730
|
+
match: (node) => editor.isTextSpan(node),
|
|
731
|
+
},
|
|
732
|
+
)
|
|
733
|
+
} else {
|
|
734
|
+
const existingMarks: string[] =
|
|
735
|
+
{
|
|
736
|
+
...(Editor.marks(editor) || {}),
|
|
737
|
+
}.marks || []
|
|
738
|
+
const marks = {
|
|
739
|
+
...(Editor.marks(editor) || {}),
|
|
740
|
+
marks: [...existingMarks, mark],
|
|
762
741
|
}
|
|
763
|
-
editor.
|
|
764
|
-
forceNewSelection()
|
|
742
|
+
editor.marks = marks as Text
|
|
765
743
|
}
|
|
766
|
-
return editor
|
|
767
744
|
}
|
|
745
|
+
editor.onChange()
|
|
746
|
+
}
|
|
747
|
+
}
|
|
768
748
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
marks: (Array.isArray(node.marks)
|
|
795
|
-
? node.marks
|
|
796
|
-
: []
|
|
797
|
-
).filter((eMark: string) => eMark !== mark),
|
|
798
|
-
_type: 'span',
|
|
799
|
-
},
|
|
800
|
-
{at: path},
|
|
801
|
-
)
|
|
802
|
-
}
|
|
803
|
-
})
|
|
804
|
-
}
|
|
805
|
-
})
|
|
806
|
-
Editor.normalize(editor)
|
|
807
|
-
} else {
|
|
808
|
-
const [block, blockPath] = Editor.node(editor, selection, {
|
|
809
|
-
depth: 1,
|
|
810
|
-
})
|
|
811
|
-
const lonelyEmptySpan =
|
|
812
|
-
editor.isTextBlock(block) &&
|
|
813
|
-
block.children.length === 1 &&
|
|
814
|
-
editor.isTextSpan(block.children[0]) &&
|
|
815
|
-
block.children[0].text === ''
|
|
816
|
-
? block.children[0]
|
|
817
|
-
: undefined
|
|
818
|
-
|
|
819
|
-
if (lonelyEmptySpan) {
|
|
820
|
-
const existingMarks = lonelyEmptySpan.marks ?? []
|
|
821
|
-
const existingMarksWithoutDecorator = existingMarks.filter(
|
|
822
|
-
(existingMark) => existingMark !== mark,
|
|
823
|
-
)
|
|
824
|
-
|
|
749
|
+
export const removeDecoratorActionImplementation: BehaviourActionImplementation<
|
|
750
|
+
PickFromUnion<BehaviorAction, 'type', 'decorator.remove'>
|
|
751
|
+
> = ({action}) => {
|
|
752
|
+
const editor = action.editor
|
|
753
|
+
const mark = action.decorator
|
|
754
|
+
const {selection} = editor
|
|
755
|
+
|
|
756
|
+
if (selection) {
|
|
757
|
+
if (Range.isExpanded(selection)) {
|
|
758
|
+
// Split if needed
|
|
759
|
+
Transforms.setNodes(
|
|
760
|
+
editor,
|
|
761
|
+
{},
|
|
762
|
+
{match: Text.isText, split: true, hanging: true},
|
|
763
|
+
)
|
|
764
|
+
if (editor.selection) {
|
|
765
|
+
const splitTextNodes = [
|
|
766
|
+
...Editor.nodes(editor, {
|
|
767
|
+
at: editor.selection,
|
|
768
|
+
match: Text.isText,
|
|
769
|
+
}),
|
|
770
|
+
]
|
|
771
|
+
splitTextNodes.forEach(([node, path]) => {
|
|
772
|
+
const block = editor.children[path[0]]
|
|
773
|
+
if (Element.isElement(block) && block.children.includes(node)) {
|
|
825
774
|
Transforms.setNodes(
|
|
826
775
|
editor,
|
|
827
776
|
{
|
|
828
|
-
marks:
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
match: (node) => editor.isTextSpan(node),
|
|
777
|
+
marks: (Array.isArray(node.marks) ? node.marks : []).filter(
|
|
778
|
+
(eMark: string) => eMark !== mark,
|
|
779
|
+
),
|
|
780
|
+
_type: 'span',
|
|
833
781
|
},
|
|
782
|
+
{at: path},
|
|
834
783
|
)
|
|
835
|
-
} else {
|
|
836
|
-
const existingMarks: string[] =
|
|
837
|
-
{
|
|
838
|
-
...(Editor.marks(editor) || {}),
|
|
839
|
-
}.marks || []
|
|
840
|
-
const marks = {
|
|
841
|
-
...(Editor.marks(editor) || {}),
|
|
842
|
-
marks: existingMarks.filter((eMark) => eMark !== mark),
|
|
843
|
-
} as Text
|
|
844
|
-
editor.marks = {marks: marks.marks, _type: 'span'} as Text
|
|
845
|
-
forceNewSelection()
|
|
846
|
-
return editor
|
|
847
784
|
}
|
|
848
|
-
}
|
|
849
|
-
editor.onChange()
|
|
850
|
-
forceNewSelection()
|
|
785
|
+
})
|
|
851
786
|
}
|
|
852
|
-
|
|
853
|
-
|
|
787
|
+
} else {
|
|
788
|
+
const [block, blockPath] = Editor.node(editor, selection, {
|
|
789
|
+
depth: 1,
|
|
790
|
+
})
|
|
791
|
+
const lonelyEmptySpan =
|
|
792
|
+
editor.isTextBlock(block) &&
|
|
793
|
+
block.children.length === 1 &&
|
|
794
|
+
editor.isTextSpan(block.children[0]) &&
|
|
795
|
+
block.children[0].text === ''
|
|
796
|
+
? block.children[0]
|
|
797
|
+
: undefined
|
|
798
|
+
|
|
799
|
+
if (lonelyEmptySpan) {
|
|
800
|
+
const existingMarks = lonelyEmptySpan.marks ?? []
|
|
801
|
+
const existingMarksWithoutDecorator = existingMarks.filter(
|
|
802
|
+
(existingMark) => existingMark !== mark,
|
|
803
|
+
)
|
|
854
804
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
805
|
+
Transforms.setNodes(
|
|
806
|
+
editor,
|
|
807
|
+
{
|
|
808
|
+
marks: existingMarksWithoutDecorator,
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
at: blockPath,
|
|
812
|
+
match: (node) => editor.isTextSpan(node),
|
|
813
|
+
},
|
|
814
|
+
)
|
|
815
|
+
} else {
|
|
816
|
+
const existingMarks: string[] =
|
|
817
|
+
{
|
|
818
|
+
...(Editor.marks(editor) || {}),
|
|
819
|
+
}.marks || []
|
|
820
|
+
const marks = {
|
|
821
|
+
...(Editor.marks(editor) || {}),
|
|
822
|
+
marks: existingMarks.filter((eMark) => eMark !== mark),
|
|
823
|
+
} as Text
|
|
824
|
+
editor.marks = {marks: marks.marks, _type: 'span'} as Text
|
|
858
825
|
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
859
829
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
830
|
+
export function isDecoratorActive({
|
|
831
|
+
editor,
|
|
832
|
+
decorator,
|
|
833
|
+
}: {
|
|
834
|
+
editor: PortableTextSlateEditor
|
|
835
|
+
decorator: string
|
|
836
|
+
}) {
|
|
837
|
+
if (!editor.selection) {
|
|
838
|
+
return false
|
|
839
|
+
}
|
|
863
840
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
841
|
+
const selectedNodes = Array.from(
|
|
842
|
+
Editor.nodes(editor, {match: Text.isText, at: editor.selection}),
|
|
843
|
+
)
|
|
867
844
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
845
|
+
if (Range.isExpanded(editor.selection)) {
|
|
846
|
+
return selectedNodes.every((n) => {
|
|
847
|
+
const [node] = n
|
|
871
848
|
|
|
872
|
-
return (
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}.marks || []
|
|
876
|
-
).includes(mark)
|
|
877
|
-
}
|
|
849
|
+
return node.marks?.includes(decorator)
|
|
850
|
+
})
|
|
851
|
+
}
|
|
878
852
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
853
|
+
return (
|
|
854
|
+
{
|
|
855
|
+
...(Editor.marks(editor) || {}),
|
|
856
|
+
}.marks || []
|
|
857
|
+
).includes(decorator)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export const toggleDecoratorActionImplementation: BehaviourActionImplementation<
|
|
861
|
+
PickFromUnion<BehaviorAction, 'type', 'decorator.toggle'>
|
|
862
|
+
> = ({context, action}) => {
|
|
863
|
+
const isActive = isDecoratorActive({
|
|
864
|
+
editor: action.editor,
|
|
865
|
+
decorator: action.decorator,
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
if (isActive) {
|
|
869
|
+
removeDecoratorActionImplementation({
|
|
870
|
+
context,
|
|
871
|
+
action: {
|
|
872
|
+
type: 'decorator.remove',
|
|
873
|
+
editor: action.editor,
|
|
874
|
+
decorator: action.decorator,
|
|
875
|
+
},
|
|
876
|
+
})
|
|
877
|
+
} else {
|
|
878
|
+
addDecoratorActionImplementation({
|
|
879
|
+
context,
|
|
880
|
+
action: {
|
|
881
|
+
type: 'decorator.add',
|
|
882
|
+
editor: action.editor,
|
|
883
|
+
decorator: action.decorator,
|
|
884
|
+
},
|
|
885
|
+
})
|
|
891
886
|
}
|
|
892
887
|
}
|
package/src/types/editor.ts
CHANGED
|
@@ -171,20 +171,6 @@ export interface PortableTextSlateEditor extends ReactEditor {
|
|
|
171
171
|
*/
|
|
172
172
|
pteEndList: () => boolean
|
|
173
173
|
|
|
174
|
-
/**
|
|
175
|
-
* Toggle marks in the selection
|
|
176
|
-
*
|
|
177
|
-
* @param mark - Mark to toggle on/off
|
|
178
|
-
*/
|
|
179
|
-
pteToggleMark: (mark: string) => void
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Test if a mark is active in the current selection
|
|
183
|
-
*
|
|
184
|
-
* @param mark - Mark to check whether or not is active
|
|
185
|
-
*/
|
|
186
|
-
pteIsMarkActive: (mark: string) => boolean
|
|
187
|
-
|
|
188
174
|
/**
|
|
189
175
|
* Toggle the selected block style
|
|
190
176
|
*
|