11/* eslint-disable no-console */
22import { toggleMark , setBlockType , wrapIn , lift } from 'prosemirror-commands' ;
3- import {
4- Schema ,
5- MarkType ,
6- NodeType ,
7- Attrs ,
8- Node ,
9- Fragment ,
10- } from 'prosemirror-model' ;
11-
3+ import { Schema , MarkType , NodeType , Attrs , Fragment } from 'prosemirror-model' ;
4+ import { wrapInList } from 'prosemirror-schema-list' ;
125import { findWrapping , liftTarget } from 'prosemirror-transform' ;
136import {
147 Command ,
@@ -340,40 +333,6 @@ const removeListNodes = (state, targetType, schema, dispatch) => {
340333 return changed ;
341334} ;
342335
343- const toggleList = ( listType ) => {
344- return ( state , dispatch ) => {
345- const { $from, $to } = state . selection ;
346- const range = $from . blockRange ( $to ) ;
347-
348- if ( ! range ) {
349- return false ;
350- }
351-
352- const wrapping = range && findWrapping ( range , listType ) ;
353-
354- if ( wrapping ) {
355- // Wrap the selection in a list
356- if ( dispatch ) {
357- dispatch ( state . tr . wrap ( range , wrapping ) . scrollIntoView ( ) ) ;
358- }
359-
360- return true ;
361- } else {
362- // Check if we are in a list item and lift out of the list
363- const liftRange = range && liftTarget ( range ) ;
364- if ( liftRange !== null ) {
365- if ( dispatch ) {
366- dispatch ( state . tr . lift ( range , liftRange ) . scrollIntoView ( ) ) ;
367- }
368-
369- return true ;
370- }
371-
372- return false ;
373- }
374- } ;
375- } ;
376-
377336const isInListOfType = ( state : EditorState , listType : NodeType ) : boolean => {
378337 const { $from } = state . selection ;
379338 for ( let depth = $from . depth ; depth > 0 ; depth -- ) {
@@ -395,12 +354,10 @@ const LIST_TYPES = [
395354type ListType = ( typeof LIST_TYPES ) [ number ] ;
396355
397356const getOtherListType = ( schema : Schema , currentType : string ) : NodeType => {
398- // Validate current type is a valid list type
399357 if ( ! LIST_TYPES . includes ( currentType as ListType ) ) {
400358 console . error ( `Invalid list type: ${ currentType } ` ) ;
401359 }
402360
403- // Find the other list type
404361 const otherType = LIST_TYPES . find ( ( type ) => type !== currentType ) ;
405362
406363 if ( ! otherType || ! schema . nodes [ otherType ] ) {
@@ -410,13 +367,48 @@ const getOtherListType = (schema: Schema, currentType: string): NodeType => {
410367 return schema . nodes [ otherType ] ;
411368} ;
412369
370+ const fromOrderedToBulletList = ( fromType : NodeType , toType : NodeType ) => {
371+ return (
372+ fromType . name === EditorMenuTypes . OrderedList &&
373+ toType . name === EditorMenuTypes . BulletList
374+ ) ;
375+ } ;
376+
377+ const fromBulletToOrderedList = ( fromType : NodeType , toType : NodeType ) => {
378+ return (
379+ fromType . name === EditorMenuTypes . BulletList &&
380+ toType . name === EditorMenuTypes . OrderedList
381+ ) ;
382+ } ;
383+
384+ /**
385+ * Returns the converted attributes for a list node when converting from one type to another.
386+ *
387+ * @param NodeType - fromType - The current list type.
388+ * @param NodeType - toType - The target list type.
389+ * @param Object - attrs - The current attributes.
390+ * @returns Object - The updated attributes.
391+ */
392+ const convertAttributes = ( fromType , toType , attrs ) => {
393+ const newAttrs = { ...attrs } ;
394+ if ( fromOrderedToBulletList ( fromType , toType ) ) {
395+ // Bullet lists generally do not need an "order" attribute.
396+ delete newAttrs . order ;
397+ } else if ( fromBulletToOrderedList ( fromType , toType ) ) {
398+ // For ordered lists, set a default start if not present.
399+ newAttrs . order = newAttrs . order || 1 ;
400+ }
401+
402+ return newAttrs ;
403+ } ;
404+
413405/**
414406 * Iterates through all list nodes (including nested ones) in the selection
415407 * and converts each node from one type to another.
416408 *
417409 * This helper also handles attribute conversion:
418- * - When converting from an ordered list to a bullet list, attributes like ` order` are removed.
419- * - When converting from a bullet list to an ordered list, you can set a default start (e.g. 1) .
410+ * - When converting from an ordered list to a bullet list, attributes like " order" are removed.
411+ * - When converting from a bullet list to an ordered list, a default start (1) is set if not present .
420412 *
421413 * @param EditorState - state - The current editor state.
422414 * @param NodeType - fromType - The list node type to convert from.
@@ -433,26 +425,11 @@ const convertAllListNodes = (state, fromType, toType, dispatch) => {
433425 state . selection . to ,
434426 ( node , pos ) => {
435427 if ( node . type === fromType ) {
436- // Create new attributes by copying the current ones
437- const newAttrs = { ...node . attrs } ;
438-
439- // Handle attribute differences:
440- if (
441- fromType . name === 'ordered_list' &&
442- toType . name === 'bullet_list'
443- ) {
444- // Bullet lists generally do not need an "order" attribute
445- delete newAttrs . order ;
446- } else if (
447- fromType . name === 'bullet_list' &&
448- toType . name === 'ordered_list'
449- ) {
450- // For ordered lists, set a default start if not present
451- newAttrs . order = newAttrs . order || 1 ;
452- }
453- // You can add more attribute merging logic here if needed.
454-
455- // Replace the current list node with one of the target type
428+ const newAttrs = convertAttributes (
429+ fromType ,
430+ toType ,
431+ node . attrs ,
432+ ) ;
456433 const newNode = toType . create (
457434 newAttrs ,
458435 node . content ,
@@ -461,8 +438,7 @@ const convertAllListNodes = (state, fromType, toType, dispatch) => {
461438 tr = tr . replaceWith ( pos , pos + node . nodeSize , newNode ) ;
462439 converted = true ;
463440
464- // Skip the subtree to avoid reprocessing nested nodes already converted.
465- return false ;
441+ return false ; // Skip the subtree.
466442 }
467443
468444 return true ;
@@ -476,26 +452,57 @@ const convertAllListNodes = (state, fromType, toType, dispatch) => {
476452 return converted ;
477453} ;
478454
455+ /**
456+ * Adjusts the current selection to include only fully selected blocks.
457+ *
458+ * @param EditorState - state - The current editor state.
459+ * @returns Object - An object with properties "from" and "to" representing
460+ * the new selection boundaries.
461+ */
462+ const adjustSelectionToFullBlocks = ( state ) => {
463+ const { $from, $to } = state . selection ;
464+ // If the selection start is at the beginning of its block, keep it.
465+ // Otherwise, move to the end of that block (i.e. skip the partial block).
466+ const from = $from . pos === $from . start ( ) ? $from . pos : $from . end ( ) ;
467+ // Similarly, if the selection end is at the end of its block, keep it.
468+ // Otherwise, use the start of that block.
469+ const to = $to . pos === $to . end ( ) ? $to . pos : $to . start ( ) ;
470+
471+ return { from : from , to : to } ;
472+ } ;
473+
479474export const createListCommand = ( schema , listTypeName ) => {
480475 const type = schema . nodes [ listTypeName ] ;
481476 if ( ! type ) {
482477 throw new Error ( `List type "${ listTypeName } " not found in schema` ) ;
483478 }
484479
485480 const command = ( state , dispatch ) => {
486- // (a) If selection is already in the target list type, remove the list.
481+ // If selection is already in the target list type, remove the list.
487482 if ( isInListOfType ( state , type ) ) {
488483 return removeListNodes ( state , type , schema , dispatch ) ;
489484 }
490485
491- // (b) If the selection is in the "other" list type, convert it.
492- const otherType = getOtherListType ( schema , listTypeName ) ;
493- if ( otherType && isInListOfType ( state , otherType ) ) {
494- return convertAllListNodes ( state , otherType , type , dispatch ) ;
486+ // If the selection is in another list type, convert it.
487+ const isOtherListType = getOtherListType ( schema , listTypeName ) ;
488+ if ( isOtherListType && isInListOfType ( state , isOtherListType ) ) {
489+ return convertAllListNodes ( state , isOtherListType , type , dispatch ) ;
490+ }
491+
492+ // Adjust the selection to include only fully selected blocks.
493+ const { from, to } = adjustSelectionToFullBlocks ( state ) ;
494+ if ( from >= to ) {
495+ return false ;
495496 }
496497
497- // (c) Otherwise, wrap the selection in the target list type.
498- return toggleList ( type ) ( state , dispatch ) ;
498+ // Create a new transaction with the adjusted selection.
499+ const adjustedTr = state . tr . setSelection (
500+ new TextSelection ( state . doc . resolve ( from ) , state . doc . resolve ( to ) ) ,
501+ ) ;
502+ // Apply the transaction to get a new state.
503+ const newState = state . apply ( adjustedTr ) ;
504+
505+ return wrapInList ( type ) ( newState , dispatch ) ;
499506 } ;
500507
501508 command . active = ( state ) => {
0 commit comments