1+ /* eslint-disable multiline-ternary */
2+
13import { toggleMark , setBlockType , wrapIn , lift } from 'prosemirror-commands' ;
24import { Schema , MarkType , NodeType , Attrs } from 'prosemirror-model' ;
3- import { findWrapping , liftTarget } from 'prosemirror-transform' ;
4- import {
5- Command ,
6- EditorState ,
7- Transaction ,
8- TextSelection ,
9- } from 'prosemirror-state' ;
5+ import { wrapInList , sinkListItem } from 'prosemirror-schema-list' ;
6+ import { Command , EditorState , TextSelection } from 'prosemirror-state' ;
107import { EditorMenuTypes , EditorTextLink , LevelMapping } from './types' ;
8+ import {
9+ setActiveMethodForMark ,
10+ setActiveMethodForNode ,
11+ setActiveMethodForWrap ,
12+ } from './utils/active-state-utils' ;
13+ import {
14+ isInListOfType ,
15+ getOtherListType ,
16+ removeListNodes ,
17+ convertAllListNodes ,
18+ toggleList ,
19+ } from './utils/list-utils' ;
20+ import { adjustSelectionToFullBlocks } from './utils/selection-utils' ;
21+ import { copyPasteLinkCommand } from './utils/link-utils' ;
22+ import { findAncestorDepthOfType } from './utils/node-utils' ;
1123
1224type CommandFunction = (
1325 schema : Schema ,
@@ -23,62 +35,6 @@ export interface CommandWithActive extends Command {
2335 active ?: ( state : EditorState ) => boolean ;
2436}
2537
26- const setActiveMethodForMark = (
27- command : CommandWithActive ,
28- markType : MarkType ,
29- ) => {
30- command . active = ( state ) => {
31- const { from, $from, to, empty } = state . selection ;
32- if ( empty ) {
33- return ! ! markType . isInSet ( state . storedMarks || $from . marks ( ) ) ;
34- } else {
35- return state . doc . rangeHasMark ( from , to , markType ) ;
36- }
37- } ;
38- } ;
39-
40- const setActiveMethodForNode = (
41- command : CommandWithActive ,
42- nodeType : NodeType ,
43- level ?: number ,
44- ) => {
45- command . active = ( state ) => {
46- const { $from } = state . selection ;
47- const node = $from . node ( $from . depth ) ;
48-
49- if ( node && node . type . name === nodeType . name ) {
50- if ( nodeType . name === LevelMapping . Heading && level ) {
51- return node . attrs . level === level ;
52- }
53-
54- return true ;
55- }
56-
57- return false ;
58- } ;
59- } ;
60-
61- const setActiveMethodForWrap = (
62- command : CommandWithActive ,
63- nodeType : NodeType ,
64- ) => {
65- command . active = ( state ) => {
66- const { from, to } = state . selection ;
67-
68- for ( let pos = from ; pos <= to ; pos ++ ) {
69- const resolvedPos = state . doc . resolve ( pos ) ;
70- for ( let i = resolvedPos . depth ; i > 0 ; i -- ) {
71- const node = resolvedPos . node ( i ) ;
72- if ( node && node . type . name === nodeType . name ) {
73- return true ;
74- }
75- }
76- }
77-
78- return false ;
79- } ;
80- } ;
81-
8238const createInsertLinkCommand : CommandFunction = (
8339 schema : Schema ,
8440 _ : EditorMenuTypes ,
@@ -212,16 +168,6 @@ const toggleNodeType = (
212168 } ;
213169} ;
214170
215- export const isValidUrl = ( text : string ) : boolean => {
216- try {
217- new URL ( text ) ;
218- } catch {
219- return false ;
220- }
221-
222- return true ;
223- } ;
224-
225171const createSetNodeTypeCommand = (
226172 schema : Schema ,
227173 nodeType : string ,
@@ -269,82 +215,128 @@ const createWrapInCommand = (
269215 return command ;
270216} ;
271217
272- const toggleList = ( listType ) => {
273- return ( state , dispatch ) => {
274- const { $from, $to } = state . selection ;
275- const range = $from . blockRange ( $to ) ;
218+ /**
219+ * Handles list operations when there is no selection.
220+ *
221+ * @param state - The current editor state.
222+ * @param type - The type of list to toggle.
223+ * @param schema - The ProseMirror schema.
224+ * @param otherType - The other type of list to convert to.
225+ * @param dispatch - The dispatch function.
226+ * @returns A command for handling list operations when there is no selection.
227+ */
228+ const handleListNoSelection = ( state , type , schema , otherType , dispatch ) => {
229+ const { $from } = state . selection ;
230+ const blockFrom = $from . start ( ) ;
231+ const blockTo = $from . end ( ) ;
232+ const adjustedTr = state . tr . setSelection (
233+ new TextSelection (
234+ state . doc . resolve ( blockFrom ) ,
235+ state . doc . resolve ( blockTo ) ,
236+ ) ,
237+ ) ;
238+ const newState = state . apply ( adjustedTr ) ;
276239
277- if ( ! range ) {
278- return false ;
279- }
240+ if ( isInListOfType ( newState , type ) ) {
241+ return removeListNodes ( newState , type , schema , dispatch ) ;
242+ }
280243
281- const wrapping = range && findWrapping ( range , listType ) ;
244+ if ( isInListOfType ( newState , otherType ) ) {
245+ return convertAllListNodes ( newState , otherType , type , dispatch ) ;
246+ }
282247
283- if ( wrapping ) {
284- // Wrap the selection in a list
285- if ( dispatch ) {
286- dispatch ( state . tr . wrap ( range , wrapping ) . scrollIntoView ( ) ) ;
287- }
248+ return toggleList ( type ) ( newState , dispatch ) ;
249+ } ;
288250
251+ /**
252+ * Handles list operations when there is a selection.
253+ *
254+ * @param state - The current editor state.
255+ * @param type - The type of list to toggle.
256+ * @param schema - The ProseMirror schema.
257+ * @param otherType - The other type of list to convert to.
258+ * @param dispatch - The dispatch function.
259+ * @returns A command for handling list operations when there is a selection.
260+ */
261+ const handleListWithSelection = ( state , type , schema , otherType , dispatch ) => {
262+ const { $from } = state . selection ;
263+ const listItemType = schema . nodes . list_item ;
264+ const ancestorDepth = findAncestorDepthOfType ( $from , listItemType ) ;
265+
266+ // If an ancestor of type list_item is found, attempt to sink that list_item.
267+ if ( ancestorDepth !== null ) {
268+ if ( sinkListItem ( listItemType ) ( state , dispatch ) ) {
289269 return true ;
290- } else {
291- // Check if we are in a list item and lift out of the list
292- const liftRange = range && liftTarget ( range ) ;
293- if ( liftRange !== null ) {
294- if ( dispatch ) {
295- dispatch ( state . tr . lift ( range , liftRange ) . scrollIntoView ( ) ) ;
296- }
270+ }
271+ }
297272
298- return true ;
299- }
273+ if ( isInListOfType ( state , type ) ) {
274+ return removeListNodes ( state , type , schema , dispatch ) ;
275+ }
300276
301- return false ;
302- }
303- } ;
304- } ;
277+ if ( otherType && isInListOfType ( state , otherType ) ) {
278+ return convertAllListNodes ( state , otherType , type , dispatch ) ;
279+ }
305280
306- const createListCommand = (
307- schema : Schema ,
308- listType : string ,
309- ) : CommandWithActive => {
310- const type : NodeType | undefined = schema . nodes [ listType ] ;
311- if ( ! type ) {
312- throw new Error ( `List type "${ listType } " not found in schema` ) ;
281+ const { from, to } = adjustSelectionToFullBlocks ( state ) ;
282+ if ( from >= to ) {
283+ return false ;
313284 }
314285
315- const command : CommandWithActive = toggleList ( type ) ;
316- setActiveMethodForWrap ( command , type ) ;
286+ const modifiedTr = state . tr . setSelection (
287+ new TextSelection ( state . doc . resolve ( from ) , state . doc . resolve ( to ) ) ,
288+ ) ;
289+ const updatedState = state . apply ( modifiedTr ) ;
317290
318- return command ;
291+ return wrapInList ( type ) ( updatedState , dispatch ) ;
319292} ;
320293
321- const copyPasteLinkCommand : Command = (
322- state : EditorState ,
323- dispatch : ( tr : Transaction ) => void ,
324- ) => {
325- const { from, to } = state . selection ;
326- if ( from === to ) {
327- return false ;
294+ /**
295+ * Creates a command for toggling list types.
296+ *
297+ * @param schema - The ProseMirror schema.
298+ * @param listTypeName - The name of the list type to toggle.
299+ * @returns A command for toggling list types.
300+ */
301+ export const createListCommand = ( schema , listTypeName ) => {
302+ const type = schema . nodes [ listTypeName ] ;
303+ if ( ! type ) {
304+ throw new Error ( `List type "${ listTypeName } " not found in schema` ) ;
328305 }
329306
330- const clipboardData = ( window as any ) . clipboardData ;
331- if ( ! clipboardData ) {
332- return false ;
333- }
307+ const command = ( state , dispatch ) => {
308+ const { $from, $to } = state . selection ;
309+ const noSelection = $from === $to ;
310+ // Get the other list type for the current list type
311+ // This is used to convert all list items to the other list type
312+ // when toggling list types
313+ const otherType = getOtherListType ( schema , listTypeName ) ;
314+
315+ return noSelection
316+ ? handleListNoSelection ( state , type , schema , otherType , dispatch )
317+ : handleListWithSelection ( state , type , schema , otherType , dispatch ) ;
318+ } ;
334319
335- const copyPastedText = clipboardData . getData ( 'text' ) ;
336- if ( ! isValidUrl ( copyPastedText ) ) {
337- return false ;
338- }
320+ command . active = ( state ) => {
321+ let isActive = false ;
322+ state . doc . nodesBetween (
323+ state . selection . from ,
324+ state . selection . to ,
325+ ( node ) => {
326+ if ( node . type === type ) {
327+ isActive = true ;
328+
329+ return false ;
330+ }
339331
340- const linkMark = state . schema . marks . link . create ( {
341- href : copyPastedText ,
342- target : isExternalLink ( copyPastedText ) ? '_blank' : null ,
343- } ) ;
332+ return true ;
333+ } ,
334+ ) ;
344335
345- const selectedText = state . doc . textBetween ( from , to , ' ' ) ;
346- const newLink = state . schema . text ( selectedText , [ linkMark ] ) ;
347- dispatch ( state . tr . replaceWith ( from , to , newLink ) ) ;
336+ return isActive ;
337+ } ;
338+
339+ return command ;
348340} ;
349341
350342const commandMapping : CommandMapping = {
0 commit comments