- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 625
 
          fix: setSelection not using MultipleNodeSelection
          #2125
        
          New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -1,8 +1,13 @@ | ||
| import { TextSelection, type Transaction } from "prosemirror-state"; | ||
| import { TableMap } from "prosemirror-tables"; | ||
| import { | ||
| NodeSelection, | ||
| TextSelection, | ||
| type Transaction, | ||
| } from "prosemirror-state"; | ||
| import { CellSelection } from "prosemirror-tables"; | ||
| 
     | 
||
| import { Block } from "../../../blocks/defaultBlocks.js"; | ||
| import { Selection } from "../../../editor/selectionTypes.js"; | ||
| import { MultipleNodeSelection } from "../../../extensions-shared/MultipleNodeSelection.js"; | ||
| import { | ||
| BlockIdentifier, | ||
| BlockSchema, | ||
| 
        
          
        
         | 
    @@ -15,7 +20,7 @@ import { | |
| prosemirrorSliceToSlicedBlocks, | ||
| } from "../../nodeConversions/nodeToBlock.js"; | ||
| import { getNodeById } from "../../nodeUtil.js"; | ||
| import { getBlockNoteSchema, getPmSchema } from "../../pmUtil.js"; | ||
| import { getPmSchema } from "../../pmUtil.js"; | ||
| 
     | 
||
| export function getSelection< | ||
| BSchema extends BlockSchema, | ||
| 
        
          
        
         | 
    @@ -24,16 +29,24 @@ export function getSelection< | |
| >(tr: Transaction): Selection<BSchema, I, S> | undefined { | ||
| const pmSchema = getPmSchema(tr); | ||
| // Return undefined if the selection is collapsed or a node is selected. | ||
| if (tr.selection.empty || "node" in tr.selection) { | ||
| if (tr.selection.empty || tr.selection instanceof NodeSelection) { | ||
| return undefined; | ||
| } | ||
| 
     | 
||
| const $startBlockBeforePos = tr.doc.resolve( | ||
| getNearestBlockPos(tr.doc, tr.selection.from).posBeforeNode, | ||
| ); | ||
| const $endBlockBeforePos = tr.doc.resolve( | ||
| getNearestBlockPos(tr.doc, tr.selection.to).posBeforeNode, | ||
| ); | ||
| const $startBlockBeforePos = | ||
| tr.selection instanceof MultipleNodeSelection | ||
| ? tr.selection.$anchor | ||
| : tr.doc.resolve( | ||
| getNearestBlockPos(tr.doc, tr.selection.from).posBeforeNode, | ||
| ); | ||
| const $endBlockBeforePos = | ||
| tr.selection instanceof MultipleNodeSelection | ||
| ? tr.doc.resolve( | ||
| tr.selection.head - tr.selection.$head.nodeBefore!.nodeSize, | ||
| ) | ||
| : tr.doc.resolve( | ||
| getNearestBlockPos(tr.doc, tr.selection.to).posBeforeNode, | ||
| ); | ||
| 
     | 
||
| // Converts the node at the given index and depth around `$startBlockBeforePos` | ||
| // to a block. Used to get blocks at given indices at the shared depth and | ||
| 
          
            
          
           | 
    @@ -140,84 +153,123 @@ export function setSelection( | |
| const startBlockId = | ||
| typeof startBlock === "string" ? startBlock : startBlock.id; | ||
| const endBlockId = typeof endBlock === "string" ? endBlock : endBlock.id; | ||
| const pmSchema = getPmSchema(tr); | ||
| const schema = getBlockNoteSchema(pmSchema); | ||
| 
     | 
||
| if (startBlockId === endBlockId) { | ||
| throw new Error( | ||
| `Attempting to set selection with the same anchor and head blocks (id ${startBlockId})`, | ||
| ); | ||
| } | ||
| const anchorPosInfo = getNodeById(startBlockId, tr.doc); | ||
| if (!anchorPosInfo) { | ||
| throw new Error(`Block with ID ${startBlockId} not found`); | ||
| } | ||
| const headPosInfo = getNodeById(endBlockId, tr.doc); | ||
| if (!headPosInfo) { | ||
| throw new Error(`Block with ID ${endBlockId} not found`); | ||
| } | ||
| // If the same block is provided for the start and end, its content gets | ||
| // selected. | ||
| const posInfo = getNodeById(startBlockId, tr.doc); | ||
| if (!posInfo) { | ||
| throw new Error(`Block with ID ${startBlockId} not found`); | ||
| } | ||
| 
     | 
||
| const anchorBlockInfo = getBlockInfo(anchorPosInfo); | ||
| const headBlockInfo = getBlockInfo(headPosInfo); | ||
| 
     | 
||
| const anchorBlockConfig = | ||
| schema.blockSchema[ | ||
| anchorBlockInfo.blockNoteType as keyof typeof schema.blockSchema | ||
| ]; | ||
| const headBlockConfig = | ||
| schema.blockSchema[ | ||
| headBlockInfo.blockNoteType as keyof typeof schema.blockSchema | ||
| ]; | ||
| 
     | 
||
| if ( | ||
| !anchorBlockInfo.isBlockContainer || | ||
| anchorBlockConfig.content === "none" | ||
| ) { | ||
| throw new Error( | ||
| `Attempting to set selection anchor in block without content (id ${startBlockId})`, | ||
| ); | ||
| const blockInfo = getBlockInfo(posInfo); | ||
| 
     | 
||
| // Case for regular blocks. | ||
| if (blockInfo.isBlockContainer) { | ||
| const content = blockInfo.blockContent.node.type.spec.content!; | ||
| 
     | 
||
| // Set `NodeSelection` on the `blockContent` node if it has no content. | ||
| if (content === "") { | ||
| tr.setSelection( | ||
| NodeSelection.create(tr.doc, blockInfo.blockContent.beforePos), | ||
| ); | ||
| 
     | 
||
| return; | ||
| } | ||
| 
     | 
||
| // Set a `TextSelection` spanning the block's inline content, if it has | ||
| // inline content. | ||
| if (content === "inline*") { | ||
| tr.setSelection( | ||
| TextSelection.create( | ||
| tr.doc, | ||
| blockInfo.blockContent.beforePos + 1, | ||
| blockInfo.blockContent.afterPos - 1, | ||
| ), | ||
| ); | ||
| 
     | 
||
| return; | ||
| } | ||
| 
     | 
||
| // Set a `CellSelection` spanning all cells in the table, if it has table | ||
| // content. | ||
| if (content === "tableRow+") { | ||
| const firstRowBeforePos = blockInfo.blockContent.beforePos + 1; | ||
| const firstCellBeforePos = firstRowBeforePos + 1; | ||
| const lastRowAfterPos = blockInfo.blockContent.afterPos - 1; | ||
| const lastCellAfterPos = lastRowAfterPos - 1; | ||
| 
     | 
||
| tr.setSelection( | ||
| CellSelection.create( | ||
| tr.doc, | ||
| firstCellBeforePos, | ||
| lastCellAfterPos - | ||
| tr.doc.resolve(lastCellAfterPos).nodeBefore!.nodeSize, | ||
| ), | ||
| ); | ||
| 
         
      Comment on lines
    
      +202
     to 
      +209
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should it be a cell selection or a node selection over the table 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer having less special cases for things like tables & columns, so if we can simplify it. Let's do that instead  | 
||
| 
     | 
||
| return; | ||
| } | ||
| 
     | 
||
| throw new Error( | ||
| `Invalid content type: ${content} for node type ${blockInfo.blockContent.node.type.name}`, | ||
| ); | ||
| } | ||
| 
     | 
||
| // Case for when block is a `columnList`. | ||
| if (blockInfo.blockNoteType === "columnList") { | ||
| const firstColumnBeforePos = blockInfo.bnBlock.beforePos + 1; | ||
| const firstBlockBeforePos = firstColumnBeforePos + 1; | ||
| const lastColumnAfterPos = blockInfo.bnBlock.afterPos - 1; | ||
| const lastBlockAfterPos = lastColumnAfterPos - 1; | ||
| 
     | 
||
| tr.setSelection( | ||
| MultipleNodeSelection.create( | ||
| tr.doc, | ||
| firstBlockBeforePos, | ||
| lastBlockAfterPos - | ||
| tr.doc.resolve(lastBlockAfterPos).nodeBefore!.nodeSize, | ||
| ), | ||
| ); | ||
| } | ||
| 
     | 
||
| // Case for when block is a `column`. | ||
| if (blockInfo.blockNoteType === "column") { | ||
| const firstBlockBeforePos = blockInfo.bnBlock.beforePos + 1; | ||
| const lastBlockAfterPos = blockInfo.bnBlock.afterPos - 1; | ||
| 
     | 
||
| // Run recursively as the column may only have one block. | ||
| setSelection( | ||
| tr, | ||
| tr.doc.resolve(firstBlockBeforePos).nodeAfter!.attrs.id, | ||
| tr.doc.resolve(lastBlockAfterPos).nodeBefore!.attrs.id, | ||
| ); | ||
| } | ||
| 
         
      Comment on lines
    
      +220
     to 
      +247
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of always searching for the correct position, can we use helpers to get this so we don't have to write out this logic every time. In this case specifically though, I think we should be using the getNearestBlock APIs so we don't have to care about the specific strcutures. If we ever change this for columns it will be a hell of a refactor & I'd rather things not be special cased everywhere  | 
||
| 
     | 
||
| throw new Error(`Invalid block node: ${blockInfo.blockNoteType}`); | ||
| } | ||
| if (!headBlockInfo.isBlockContainer || headBlockConfig.content === "none") { | ||
| throw new Error( | ||
| `Attempting to set selection anchor in block without content (id ${endBlockId})`, | ||
| ); | ||
| 
     | 
||
| const startPosInfo = getNodeById(startBlockId, tr.doc); | ||
| if (!startPosInfo) { | ||
| throw new Error(`Block with ID ${startBlockId} not found`); | ||
| } | ||
| 
     | 
||
| let startPos: number; | ||
| let endPos: number; | ||
| const startBlockInfo = getBlockInfo(startPosInfo); | ||
| 
     | 
||
| if (anchorBlockConfig.content === "table") { | ||
| const tableMap = TableMap.get(anchorBlockInfo.blockContent.node); | ||
| const firstCellPos = | ||
| anchorBlockInfo.blockContent.beforePos + | ||
| tableMap.positionAt(0, 0, anchorBlockInfo.blockContent.node) + | ||
| 1; | ||
| startPos = firstCellPos + 2; | ||
| } else { | ||
| startPos = anchorBlockInfo.blockContent.beforePos + 1; | ||
| const endPosInfo = getNodeById(endBlockId, tr.doc); | ||
| if (!endPosInfo) { | ||
| throw new Error(`Block with ID ${endBlockId} not found`); | ||
| } | ||
| 
     | 
||
| if (headBlockConfig.content === "table") { | ||
| const tableMap = TableMap.get(headBlockInfo.blockContent.node); | ||
| const lastCellPos = | ||
| headBlockInfo.blockContent.beforePos + | ||
| tableMap.positionAt( | ||
| tableMap.height - 1, | ||
| tableMap.width - 1, | ||
| headBlockInfo.blockContent.node, | ||
| ) + | ||
| 1; | ||
| const lastCellNodeSize = tr.doc.resolve(lastCellPos).nodeAfter!.nodeSize; | ||
| endPos = lastCellPos + lastCellNodeSize - 2; | ||
| } else { | ||
| endPos = headBlockInfo.blockContent.afterPos - 1; | ||
| } | ||
| const endBlockInfo = getBlockInfo(endPosInfo); | ||
| 
     | 
||
| // TODO: We should polish up the `MultipleNodeSelection` and use that instead. | ||
| // Right now it's missing a few things like a jsonID and styling to show | ||
| // which nodes are selected. `TextSelection` is ok for now, but has the | ||
| // restriction that the start/end blocks must have content. | ||
| tr.setSelection(TextSelection.create(tr.doc, startPos, endPos)); | ||
| tr.setSelection( | ||
| MultipleNodeSelection.create( | ||
| tr.doc, | ||
| startBlockInfo.bnBlock.beforePos, | ||
| endBlockInfo.bnBlock.afterPos, | ||
| ), | ||
| ); | ||
| } | ||
| 
     | 
||
| export function getSelectionCutBlocks(tr: Transaction) { | ||
| 
          
            
          
           | 
    ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anchor can be different than
from, depending on direction, so we need to decide whether we are using$fromor$anchor