@portabletext/toolbar 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ import {useEditor, type Editor, type ListSchemaType} from '@portabletext/editor'
2
+ import * as selectors from '@portabletext/editor/selectors'
3
+ import {useActor} from '@xstate/react'
4
+ import {fromCallback, setup, type AnyEventObject} from 'xstate'
5
+ import {disableListener, type DisableListenerEvent} from './disable-listener'
6
+ import type {ToolbarListSchemaType} from './use-toolbar-schema'
7
+
8
+ const activeListener = fromCallback<
9
+ AnyEventObject,
10
+ {editor: Editor; schemaType: ListSchemaType},
11
+ {type: 'set active'} | {type: 'set inactive'}
12
+ >(({input, sendBack}) => {
13
+ return input.editor.on('*', () => {
14
+ const snapshot = input.editor.getSnapshot()
15
+
16
+ if (selectors.isActiveListItem(input.schemaType.name)(snapshot)) {
17
+ sendBack({type: 'set active'})
18
+ } else {
19
+ sendBack({type: 'set inactive'})
20
+ }
21
+ }).unsubscribe
22
+ })
23
+
24
+ const listButtonMachine = setup({
25
+ types: {
26
+ context: {} as {
27
+ editor: Editor
28
+ schemaType: ListSchemaType
29
+ },
30
+ input: {} as {
31
+ editor: Editor
32
+ schemaType: ListSchemaType
33
+ },
34
+ events: {} as DisableListenerEvent | ListButtonEvent,
35
+ },
36
+ actions: {
37
+ toggle: ({context, event}) => {
38
+ if (event.type !== 'toggle') {
39
+ return
40
+ }
41
+
42
+ context.editor.send({
43
+ type: 'list item.toggle',
44
+ listItem: context.schemaType.name,
45
+ })
46
+ context.editor.send({type: 'focus'})
47
+ },
48
+ },
49
+ actors: {
50
+ 'disable listener': disableListener,
51
+ 'active listener': activeListener,
52
+ },
53
+ }).createMachine({
54
+ id: 'list button',
55
+ context: ({input}) => ({
56
+ editor: input.editor,
57
+ schemaType: input.schemaType,
58
+ }),
59
+ invoke: [
60
+ {src: 'disable listener', input: ({context}) => ({editor: context.editor})},
61
+ {
62
+ src: 'active listener',
63
+ input: ({context}) => ({
64
+ editor: context.editor,
65
+ schemaType: context.schemaType,
66
+ }),
67
+ },
68
+ ],
69
+ initial: 'disabled',
70
+ states: {
71
+ disabled: {
72
+ initial: 'inactive',
73
+ states: {
74
+ inactive: {
75
+ on: {
76
+ 'enable': {
77
+ target: '#list button.enabled.inactive',
78
+ },
79
+ 'set active': {
80
+ target: 'active',
81
+ },
82
+ },
83
+ },
84
+ active: {
85
+ on: {
86
+ 'enable': {
87
+ target: '#list button.enabled.active',
88
+ },
89
+ 'set inactive': {
90
+ target: 'inactive',
91
+ },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ enabled: {
97
+ on: {
98
+ toggle: {
99
+ actions: ['toggle'],
100
+ },
101
+ },
102
+ initial: 'inactive',
103
+ states: {
104
+ inactive: {
105
+ on: {
106
+ 'disable': {
107
+ target: '#list button.disabled.inactive',
108
+ },
109
+ 'set active': {
110
+ target: 'active',
111
+ },
112
+ },
113
+ },
114
+ active: {
115
+ on: {
116
+ 'disable': {
117
+ target: '#list button.disabled.active',
118
+ },
119
+ 'set inactive': {
120
+ target: 'inactive',
121
+ },
122
+ },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ })
128
+
129
+ /**
130
+ * @beta
131
+ */
132
+ export type ListButtonEvent = {
133
+ type: 'toggle'
134
+ }
135
+
136
+ /**
137
+ * @beta
138
+ */
139
+ export type ListButton = {
140
+ snapshot: {
141
+ matches: (
142
+ state:
143
+ | 'disabled'
144
+ | 'enabled'
145
+ | {disabled: 'inactive'}
146
+ | {disabled: 'active'}
147
+ | {enabled: 'inactive'}
148
+ | {enabled: 'active'},
149
+ ) => boolean
150
+ }
151
+ send: (event: ListButtonEvent) => void
152
+ }
153
+
154
+ /**
155
+ * @beta
156
+ * Manages the state, keyboard shortcuts and available events for a list button.
157
+ */
158
+ export function useListButton(props: {
159
+ schemaType: ToolbarListSchemaType
160
+ }): ListButton {
161
+ const editor = useEditor()
162
+ const [actorSnapshot, send] = useActor(listButtonMachine, {
163
+ input: {
164
+ editor,
165
+ schemaType: props.schemaType,
166
+ },
167
+ })
168
+
169
+ return {
170
+ snapshot: {
171
+ matches: (state) => actorSnapshot.matches(state),
172
+ },
173
+ send,
174
+ }
175
+ }
@@ -0,0 +1,36 @@
1
+ import {useEditor} from '@portabletext/editor'
2
+ import {defineBehavior, forward, raise} from '@portabletext/editor/behaviors'
3
+ import {useEffect} from 'react'
4
+ import type {ToolbarDecoratorSchemaType} from './use-toolbar-schema'
5
+
6
+ export function useMutuallyExclusiveDecorator(props: {
7
+ schemaType: ToolbarDecoratorSchemaType
8
+ }) {
9
+ const editor = useEditor()
10
+
11
+ useEffect(() => {
12
+ const mutuallyExclusive = props.schemaType.mutuallyExclusive
13
+
14
+ if (!mutuallyExclusive) {
15
+ return
16
+ }
17
+
18
+ return editor.registerBehavior({
19
+ behavior: defineBehavior({
20
+ on: 'decorator.add',
21
+ guard: ({event}) => event.decorator === props.schemaType.name,
22
+ actions: [
23
+ ({event}) => [
24
+ forward(event),
25
+ ...mutuallyExclusive.map((decorator) =>
26
+ raise({
27
+ type: 'decorator.remove',
28
+ decorator,
29
+ }),
30
+ ),
31
+ ],
32
+ ],
33
+ }),
34
+ })
35
+ }, [editor, props.schemaType.name, props.schemaType.mutuallyExclusive])
36
+ }
@@ -0,0 +1,105 @@
1
+ import {useEditor, type Editor} from '@portabletext/editor'
2
+ import {defineBehavior, raise} from '@portabletext/editor/behaviors'
3
+ import {useActorRef} from '@xstate/react'
4
+ import {fromCallback, setup, type AnyEventObject} from 'xstate'
5
+ import {disableListener, type DisableListenerEvent} from './disable-listener'
6
+ import type {ToolbarStyleSchemaType} from './use-toolbar-schema'
7
+
8
+ const styleKeyboardShortcutsListener = fromCallback<
9
+ AnyEventObject,
10
+ {editor: Editor; schemaTypes: ReadonlyArray<ToolbarStyleSchemaType>}
11
+ >(({input}) => {
12
+ const unregisterBehaviors = input.schemaTypes.flatMap((schemaType) => {
13
+ const shortcut = schemaType.shortcut
14
+
15
+ if (!shortcut) {
16
+ return []
17
+ }
18
+
19
+ return [
20
+ input.editor.registerBehavior({
21
+ behavior: defineBehavior({
22
+ on: 'keyboard.keydown',
23
+ guard: ({event}) => shortcut.guard(event.originEvent),
24
+ actions: [
25
+ () => [raise({type: 'style.toggle', style: schemaType.name})],
26
+ ],
27
+ }),
28
+ }),
29
+ ]
30
+ })
31
+
32
+ return () => {
33
+ for (const unregisterBehavior of unregisterBehaviors) {
34
+ unregisterBehavior()
35
+ }
36
+ }
37
+ })
38
+
39
+ const styleKeyboardShortcutsMachine = setup({
40
+ types: {
41
+ context: {} as {
42
+ editor: Editor
43
+ schemaTypes: ReadonlyArray<ToolbarStyleSchemaType>
44
+ },
45
+ input: {} as {
46
+ editor: Editor
47
+ schemaTypes: ReadonlyArray<ToolbarStyleSchemaType>
48
+ },
49
+ events: {} as DisableListenerEvent | {type: 'style.toggle'},
50
+ },
51
+ actors: {
52
+ 'disable listener': disableListener,
53
+ 'style keyboard shortcuts listener': styleKeyboardShortcutsListener,
54
+ },
55
+ }).createMachine({
56
+ id: 'style keyboard shortcuts',
57
+ context: ({input}) => ({
58
+ editor: input.editor,
59
+ schemaTypes: input.schemaTypes,
60
+ }),
61
+ invoke: {
62
+ src: 'disable listener',
63
+ input: ({context}) => ({editor: context.editor}),
64
+ },
65
+ initial: 'disabled',
66
+ states: {
67
+ disabled: {
68
+ on: {
69
+ enable: {
70
+ target: 'enabled',
71
+ },
72
+ },
73
+ },
74
+ enabled: {
75
+ invoke: {
76
+ src: 'style keyboard shortcuts listener',
77
+ input: ({context}) => ({
78
+ editor: context.editor,
79
+ schemaTypes: context.schemaTypes,
80
+ }),
81
+ },
82
+ on: {
83
+ disable: {
84
+ target: 'disabled',
85
+ },
86
+ },
87
+ },
88
+ },
89
+ })
90
+
91
+ /**
92
+ * @beta
93
+ * Registers keyboard shortcuts for a set of style schema types.
94
+ */
95
+ export function useStyleKeyboardShortcuts(props: {
96
+ schemaTypes: ReadonlyArray<ToolbarStyleSchemaType>
97
+ }) {
98
+ const editor = useEditor()
99
+ useActorRef(styleKeyboardShortcutsMachine, {
100
+ input: {
101
+ editor,
102
+ schemaTypes: props.schemaTypes,
103
+ },
104
+ })
105
+ }
@@ -0,0 +1,149 @@
1
+ import {
2
+ useEditor,
3
+ type Editor,
4
+ type StyleSchemaType,
5
+ } from '@portabletext/editor'
6
+ import * as selectors from '@portabletext/editor/selectors'
7
+ import {useActor} from '@xstate/react'
8
+ import {assign, fromCallback, setup, type AnyEventObject} from 'xstate'
9
+ import {disableListener, type DisableListenerEvent} from './disable-listener'
10
+ import {useStyleKeyboardShortcuts} from './use-style-keyboard-shortcuts'
11
+ import type {ToolbarStyleSchemaType} from './use-toolbar-schema'
12
+
13
+ type ActiveStyleListenerEvent = {
14
+ type: 'set active style'
15
+ style: StyleSchemaType['name'] | undefined
16
+ }
17
+
18
+ const activeListener = fromCallback<
19
+ AnyEventObject,
20
+ {editor: Editor},
21
+ ActiveStyleListenerEvent
22
+ >(({input, sendBack}) => {
23
+ return input.editor.on('*', () => {
24
+ const snapshot = input.editor.getSnapshot()
25
+ const activeStyle = selectors.getActiveStyle(snapshot)
26
+
27
+ sendBack({type: 'set active style', style: activeStyle})
28
+ }).unsubscribe
29
+ })
30
+
31
+ const styleSelectorMachine = setup({
32
+ types: {
33
+ context: {} as {
34
+ editor: Editor
35
+ activeStyle: StyleSchemaType['name'] | undefined
36
+ },
37
+ input: {} as {
38
+ editor: Editor
39
+ },
40
+ events: {} as
41
+ | StyleSelectorEvent
42
+ | DisableListenerEvent
43
+ | ActiveStyleListenerEvent,
44
+ },
45
+ actors: {
46
+ 'disable listener': disableListener,
47
+ 'active listener': activeListener,
48
+ },
49
+ }).createMachine({
50
+ id: 'style selector',
51
+ context: ({input}) => ({
52
+ editor: input.editor,
53
+ activeStyle: undefined,
54
+ }),
55
+ invoke: [
56
+ {
57
+ src: 'disable listener',
58
+ input: ({context}) => ({
59
+ editor: context.editor,
60
+ }),
61
+ },
62
+ {
63
+ src: 'active listener',
64
+ input: ({context}) => ({
65
+ editor: context.editor,
66
+ }),
67
+ },
68
+ ],
69
+ on: {
70
+ 'set active style': {
71
+ actions: assign({
72
+ activeStyle: ({event}) => event.style,
73
+ }),
74
+ },
75
+ },
76
+ initial: 'disabled',
77
+ states: {
78
+ disabled: {
79
+ on: {
80
+ enable: {
81
+ target: 'enabled',
82
+ },
83
+ },
84
+ },
85
+ enabled: {
86
+ on: {
87
+ disable: {
88
+ target: 'disabled',
89
+ },
90
+ toggle: {
91
+ actions: [
92
+ ({context, event}) => {
93
+ context.editor.send({type: 'style.toggle', style: event.style})
94
+ context.editor.send({type: 'focus'})
95
+ },
96
+ ],
97
+ },
98
+ },
99
+ },
100
+ },
101
+ })
102
+
103
+ /**
104
+ * @beta
105
+ */
106
+ export type StyleSelectorEvent = {
107
+ type: 'toggle'
108
+ style: StyleSchemaType['name']
109
+ }
110
+
111
+ /**
112
+ * @beta
113
+ */
114
+ export type StyleSelector = {
115
+ snapshot: {
116
+ matches: (state: 'disabled' | 'enabled') => boolean
117
+ context: {
118
+ activeStyle: StyleSchemaType['name'] | undefined
119
+ }
120
+ }
121
+ send: (event: StyleSelectorEvent) => void
122
+ }
123
+
124
+ /**
125
+ * @beta
126
+ * Manages the state, keyboard shortcuts and available events for a style
127
+ * selector.
128
+ */
129
+ export function useStyleSelector(props: {
130
+ schemaTypes: ReadonlyArray<ToolbarStyleSchemaType>
131
+ }): StyleSelector {
132
+ const editor = useEditor()
133
+ const [actorSnapshot, send] = useActor(styleSelectorMachine, {
134
+ input: {
135
+ editor,
136
+ },
137
+ })
138
+ useStyleKeyboardShortcuts(props)
139
+
140
+ return {
141
+ snapshot: {
142
+ matches: (state) => actorSnapshot.matches(state),
143
+ context: {
144
+ activeStyle: actorSnapshot.context.activeStyle,
145
+ },
146
+ },
147
+ send,
148
+ }
149
+ }
@@ -0,0 +1,164 @@
1
+ import {
2
+ useEditor,
3
+ useEditorSelector,
4
+ type AnnotationSchemaType,
5
+ type BlockObjectSchemaType,
6
+ type DecoratorDefinition,
7
+ type DecoratorSchemaType,
8
+ type InlineObjectSchemaType,
9
+ type ListSchemaType,
10
+ type StyleSchemaType,
11
+ } from '@portabletext/editor'
12
+ import type {KeyboardShortcut} from '@portabletext/keyboard-shortcuts'
13
+
14
+ /**
15
+ * @beta
16
+ */
17
+ export type ExtendDecoratorSchemaType = (
18
+ decorator: DecoratorSchemaType,
19
+ ) => ToolbarDecoratorSchemaType
20
+
21
+ /**
22
+ * @beta
23
+ */
24
+ export type ExtendAnnotationSchemaType = (
25
+ annotation: AnnotationSchemaType,
26
+ ) => ToolbarAnnotationSchemaType
27
+
28
+ /**
29
+ * @beta
30
+ */
31
+ export type ExtendListSchemaType = (
32
+ list: ListSchemaType,
33
+ ) => ToolbarListSchemaType
34
+
35
+ /**
36
+ * @beta
37
+ */
38
+ export type ExtendBlockObjectSchemaType = (
39
+ blockObject: BlockObjectSchemaType,
40
+ ) => ToolbarBlockObjectSchemaType
41
+
42
+ /**
43
+ * @beta
44
+ */
45
+ export type ExtendInlineObjectSchemaType = (
46
+ inlineObject: InlineObjectSchemaType,
47
+ ) => ToolbarInlineObjectSchemaType
48
+
49
+ /**
50
+ * @beta
51
+ */
52
+ export type ExtendStyleSchemaType = (
53
+ style: StyleSchemaType,
54
+ ) => ToolbarStyleSchemaType
55
+
56
+ /**
57
+ * @beta
58
+ * Extend the editor's schema with default values, icons, shortcuts and more.
59
+ * This makes it easier to use the schema to render toolbars, forms and other
60
+ * UI components.
61
+ */
62
+ export function useToolbarSchema(props: {
63
+ extendDecorator?: (
64
+ decorator: DecoratorSchemaType,
65
+ ) => ToolbarDecoratorSchemaType
66
+ extendAnnotation?: (
67
+ annotation: AnnotationSchemaType,
68
+ ) => ToolbarAnnotationSchemaType
69
+ extendList?: (list: ListSchemaType) => ToolbarListSchemaType
70
+ extendBlockObject?: (
71
+ blockObject: BlockObjectSchemaType,
72
+ ) => ToolbarBlockObjectSchemaType
73
+ extendInlineObject?: (
74
+ inlineObject: InlineObjectSchemaType,
75
+ ) => ToolbarInlineObjectSchemaType
76
+ extendStyle?: (style: StyleSchemaType) => ToolbarStyleSchemaType
77
+ }): ToolbarSchema {
78
+ const editor = useEditor()
79
+ const schema = useEditorSelector(
80
+ editor,
81
+ (snapshot) => snapshot.context.schema,
82
+ )
83
+
84
+ return {
85
+ decorators: schema.decorators.map(
86
+ (decorator) => props.extendDecorator?.(decorator) ?? decorator,
87
+ ),
88
+ annotations: schema.annotations.map(
89
+ (annotation) => props.extendAnnotation?.(annotation) ?? annotation,
90
+ ),
91
+ lists: schema.lists.map((list) => props.extendList?.(list) ?? list),
92
+ blockObjects: schema.blockObjects.map(
93
+ (blockObject) => props.extendBlockObject?.(blockObject) ?? blockObject,
94
+ ),
95
+ inlineObjects: schema.inlineObjects.map(
96
+ (inlineObject) =>
97
+ props.extendInlineObject?.(inlineObject) ?? inlineObject,
98
+ ),
99
+ styles: schema.styles.map((style) => props.extendStyle?.(style) ?? style),
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @beta
105
+ */
106
+ export type ToolbarSchema = {
107
+ decorators?: ReadonlyArray<ToolbarDecoratorSchemaType>
108
+ annotations?: ReadonlyArray<ToolbarAnnotationSchemaType>
109
+ lists?: ReadonlyArray<ToolbarListSchemaType>
110
+ blockObjects?: ReadonlyArray<ToolbarBlockObjectSchemaType>
111
+ inlineObjects?: ReadonlyArray<ToolbarInlineObjectSchemaType>
112
+ styles?: ReadonlyArray<ToolbarStyleSchemaType>
113
+ }
114
+
115
+ /**
116
+ * @beta
117
+ */
118
+ export type ToolbarDecoratorSchemaType = DecoratorSchemaType & {
119
+ icon?: React.ComponentType
120
+ shortcut?: KeyboardShortcut
121
+ mutuallyExclusive?: ReadonlyArray<DecoratorDefinition['name']>
122
+ }
123
+
124
+ /**
125
+ * @beta
126
+ */
127
+ export type ToolbarAnnotationSchemaType = AnnotationSchemaType & {
128
+ icon?: React.ComponentType
129
+ defaultValues?: Record<string, unknown>
130
+ shortcut?: KeyboardShortcut
131
+ }
132
+
133
+ /**
134
+ * @beta
135
+ */
136
+ export type ToolbarListSchemaType = ListSchemaType & {
137
+ icon?: React.ComponentType
138
+ }
139
+
140
+ /**
141
+ * @beta
142
+ */
143
+ export type ToolbarBlockObjectSchemaType = BlockObjectSchemaType & {
144
+ icon?: React.ComponentType
145
+ defaultValues?: Record<string, unknown>
146
+ shortcut?: KeyboardShortcut
147
+ }
148
+
149
+ /**
150
+ * @beta
151
+ */
152
+ export type ToolbarInlineObjectSchemaType = InlineObjectSchemaType & {
153
+ icon?: React.ComponentType
154
+ defaultValues?: Record<string, unknown>
155
+ shortcut?: KeyboardShortcut
156
+ }
157
+
158
+ /**
159
+ * @beta
160
+ */
161
+ export type ToolbarStyleSchemaType = StyleSchemaType & {
162
+ icon?: React.ComponentType
163
+ shortcut?: KeyboardShortcut
164
+ }