Przeglądaj źródła

Remove list items when list is toggled off (#13580)

GitOrigin-RevId: 19c63b26798fcf3c8e631090c614f0a693d4f071
Alf Eaton 3 lat temu
rodzic
commit
7e20d41c4c

+ 34 - 21
services/web/frontend/js/features/source-editor/extensions/toolbar/lists.ts

@@ -229,39 +229,52 @@ const toggleListForRange = (
           // toggle list off
           // toggle list off
           const changeSpec: ChangeSpec[] = [
           const changeSpec: ChangeSpec[] = [
             {
             {
-              from: emptyBeginLine ? beginLine.from - 1 : beginEnvNode.from,
-              to: emptyBeginLine ? beginLine.to : beginEnvNode.to,
+              from: emptyBeginLine ? beginLine.from : beginEnvNode.from,
+              to: emptyBeginLine
+                ? Math.min(beginLine.to + 1, view.state.doc.length)
+                : beginEnvNode.to,
               insert: '',
               insert: '',
             },
             },
             {
             {
-              from: emptyEndLine ? endLine.from : endEnvNode.from,
-              to: emptyEndLine ? endLine.to + 1 : endEnvNode.to,
+              from: emptyEndLine
+                ? Math.max(endLine.from - 1, 0)
+                : endEnvNode.from,
+              to: emptyEndLine ? endLine.to : endEnvNode.to,
               insert: '',
               insert: '',
             },
             },
           ]
           ]
 
 
-          const commandNodes = descendantsOfNodeWithType(
+          // items that aren't within nested list environments
+          const itemNodes = descendantsOfNodeWithType(
             ancestorNode,
             ancestorNode,
-            'Item'
-          ).filter(
-            commandNode =>
-              view.state.sliceDoc(commandNode.from, commandNode.to) === '\\item'
+            'Item',
+            ListEnvironment
           )
           )
 
 
-          if (commandNodes.length > 0) {
-            // whether the command is the only content on this line, apart from whitespace
-            const emptyLineBeforeItem = /^\s*\\item\{/.test(beginLine.text)
+          if (itemNodes.length > 0) {
+            const indentUnit = getIndentUnit(view.state)
 
 
-            const indentUnit = emptyLineBeforeItem
-              ? getIndentUnit(view.state)
-              : 0
-
-            for (const commandNode of commandNodes) {
-              changeSpec.push({
-                from: commandNode.from - indentUnit,
-                to: commandNode.to + 1,
+            for (const itemNode of itemNodes) {
+              const change: ChangeSpec = {
+                from: itemNode.from,
+                to: itemNode.to,
                 insert: '',
                 insert: '',
-              })
+              }
+
+              const line = view.state.doc.lineAt(itemNode.from)
+
+              const lineBeforeCommand = view.state.sliceDoc(
+                line.from,
+                itemNode.from
+              )
+
+              // if the line before the command is empty, remove one unit of indentation
+              if (lineBeforeCommand.trim().length === 0) {
+                const indentation = getIndentation(view.state, itemNode.from)
+                change.from -= Math.min(indentation ?? 0, indentUnit)
+              }
+
+              changeSpec.push(change)
             }
             }
           }
           }
 
 

+ 4 - 4
services/web/frontend/js/features/source-editor/utils/tree-operations/ancestors.ts

@@ -138,16 +138,16 @@ export const lastAncestorAtEndPosition = (
 export const descendantsOfNodeWithType = (
 export const descendantsOfNodeWithType = (
   node: SyntaxNode,
   node: SyntaxNode,
   type: string | number,
   type: string | number,
-  nested = false
+  leaveType?: string | number
 ): SyntaxNode[] => {
 ): SyntaxNode[] => {
   const children: SyntaxNode[] = []
   const children: SyntaxNode[] = []
 
 
   node.cursor().iterate(nodeRef => {
   node.cursor().iterate(nodeRef => {
     if (nodeRef.type.is(type)) {
     if (nodeRef.type.is(type)) {
       children.push(nodeRef.node)
       children.push(nodeRef.node)
-      if (!nested) {
-        return false
-      }
+    }
+    if (leaveType && nodeRef.type.is(leaveType) && nodeRef.node !== node) {
+      return false
     }
     }
   })
   })
 
 

+ 160 - 0
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx

@@ -147,4 +147,164 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
     cy.get('.cm-line').eq(0).type('ing')
     cy.get('.cm-line').eq(0).type('ing')
     cy.get('.cm-line').eq(0).should('have.text', ' testing')
     cy.get('.cm-line').eq(0).should('have.text', ' testing')
   })
   })
+
+  it('should toggle between list types', function () {
+    mountEditor('test')
+    selectAll()
+
+    clickToolbarButton('Numbered List')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        '\\begin{enumerate}',
+        ' test',
+        '\\end{enumerate}',
+      ].join('')
+    )
+
+    cy.get('.cm-line').eq(1).click()
+
+    clickToolbarButton('Bullet List')
+
+    cy.get('.cm-line').eq(1).click()
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        '\\begin{itemize}',
+        ' test',
+        '\\end{itemize}',
+      ].join('')
+    )
+  })
+
+  it('should remove a list', function () {
+    mountEditor('test')
+    selectAll()
+
+    clickToolbarButton('Numbered List')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        '\\begin{enumerate}',
+        ' test',
+        '\\end{enumerate}',
+      ].join('')
+    )
+
+    cy.get('.cm-line').eq(0).click()
+
+    clickToolbarButton('Numbered List')
+
+    cy.get('.cm-line').eq(0).click()
+
+    cy.get('.cm-content').should('have.text', 'test')
+  })
+
+  it('should not remove a parent list', function () {
+    mountEditor('test\ntest')
+    selectAll()
+
+    clickToolbarButton('Numbered List')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        '\\begin{enumerate}',
+        ' test',
+        ' test',
+        '\\end{enumerate}',
+      ].join('')
+    )
+
+    cy.get('.cm-line').eq(2).click()
+
+    clickToolbarButton('Increase Indent')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        '\\begin{enumerate}',
+        ' test',
+        '    \\begin{enumerate}',
+        ' test',
+        '    \\end{enumerate}',
+        '\\end{enumerate}',
+      ].join('')
+    )
+
+    cy.get('.cm-line').eq(2).click()
+
+    clickToolbarButton('Numbered List')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        '\\begin{enumerate}',
+        ' test',
+        '    test',
+        '\\end{enumerate}',
+      ].join('')
+    )
+  })
+
+  it('should not remove a nested list', function () {
+    mountEditor('test\ntest')
+    selectAll()
+
+    clickToolbarButton('Numbered List')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        '\\begin{enumerate}',
+        ' test',
+        ' test',
+        '\\end{enumerate}',
+      ].join('')
+    )
+
+    cy.get('.cm-line').eq(2).click()
+
+    clickToolbarButton('Increase Indent')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        '\\begin{enumerate}',
+        ' test',
+        '    \\begin{enumerate}',
+        ' test',
+        '    \\end{enumerate}',
+        '\\end{enumerate}',
+      ].join('')
+    )
+
+    cy.get('.cm-line').eq(1).click()
+
+    clickToolbarButton('Numbered List')
+
+    cy.get('.cm-line').eq(1).click()
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [
+        //
+        'test',
+        '    \\begin{enumerate}',
+        ' test',
+        '    \\end{enumerate}',
+      ].join('')
+    )
+  })
 })
 })