Ver Fonte

[visual] Improve handling of pasted lists (#14912)

GitOrigin-RevId: 15e91ef6807433c5fb0a9bedbd5fea42ac35a5f0
Alf Eaton há 2 anos atrás
pai
commit
cb297e342a

+ 52 - 11
services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts

@@ -522,13 +522,13 @@ const tabular = (element: HTMLTableElement) => {
     .join(' ')
     .join(' ')
 }
 }
 
 
-const listDepth = (
-  element: HTMLOListElement | HTMLUListElement | HTMLLIElement
-): number => Math.max(0, matchingParents(element, 'ul,ol').length - 1)
+const listDepth = (element: HTMLElement): number =>
+  Math.max(0, matchingParents(element, 'ul,ol').length)
 
 
-const listIndent = (
-  element: HTMLOListElement | HTMLUListElement | HTMLLIElement
-): string => '\t'.repeat(listDepth(element))
+const indentUnit = '    ' // TODO: replace hard-coded indent unit?
+
+const listIndent = (element: HTMLElement | null): string =>
+  element ? indentUnit.repeat(listDepth(element)) : ''
 
 
 type ElementSelector<T extends string, E extends HTMLElement = HTMLElement> = {
 type ElementSelector<T extends string, E extends HTMLElement = HTMLElement> = {
   selector: T
   selector: T
@@ -610,6 +610,37 @@ const startMultirow = (element: HTMLTableCellElement): string => {
   return `\\multirow{${rowspan}}{*}{`
   return `\\multirow{${rowspan}}{*}{`
 }
 }
 
 
+const listPrefix = (element: HTMLOListElement | HTMLUListElement) => {
+  if (isListOrListItemElement(element.parentElement)) {
+    // within a list = newline
+    return '\n'
+  }
+  // outside a list = double newline
+  return '\n\n'
+}
+
+const listSuffix = (element: HTMLOListElement | HTMLUListElement) => {
+  if (listDepth(element) === 0) {
+    // a top-level list => newline
+    return '\n'
+  } else {
+    // a nested list => no extra newline
+    return ''
+  }
+}
+
+const isListElement = (
+  element: Element | null
+): element is HTMLOListElement | HTMLUListElement =>
+  element !== null && listNodeNames.includes(element.nodeName)
+
+const isListOrListItemElement = (
+  element: Element | null
+): element is HTMLOListElement | HTMLUListElement =>
+  element !== null && (isListElement(element) || element.nodeName === 'LI')
+
+const listNodeNames = ['OL', 'UL']
+
 const selectors = [
 const selectors = [
   createSelector({
   createSelector({
     selector: 'b',
     selector: 'b',
@@ -826,18 +857,28 @@ const selectors = [
   createSelector({
   createSelector({
     // selector: 'ul:has(> li:nth-child(2))', // only select lists with at least 2 items (once Firefox supports :has())
     // selector: 'ul:has(> li:nth-child(2))', // only select lists with at least 2 items (once Firefox supports :has())
     selector: 'ul',
     selector: 'ul',
-    start: element => `\n\n${listIndent(element)}\\begin{itemize}`,
-    end: element => `\n${listIndent(element)}\\end{itemize}\n`,
+    start: element => {
+      return `${listPrefix(element)}${listIndent(element)}\\begin{itemize}`
+    },
+    end: element => {
+      return `\n${listIndent(element)}\\end{itemize}${listSuffix(element)}`
+    },
   }),
   }),
   createSelector({
   createSelector({
     // selector: 'ol:has(> li:nth-child(2))', // only select lists with at least 2 items (once Firefox supports :has())
     // selector: 'ol:has(> li:nth-child(2))', // only select lists with at least 2 items (once Firefox supports :has())
     selector: 'ol',
     selector: 'ol',
-    start: element => `\n\n${listIndent(element)}\\begin{enumerate}`,
-    end: element => `\n${listIndent(element)}\\end{enumerate}\n`,
+    start: element => {
+      return `${listPrefix(element)}${listIndent(element)}\\begin{enumerate}`
+    },
+    end: element => {
+      return `\n${listIndent(element)}\\end{enumerate}${listSuffix(element)}`
+    },
   }),
   }),
   createSelector({
   createSelector({
     selector: 'li',
     selector: 'li',
-    start: element => `\n${listIndent(element)}\t\\item `,
+    start: element => {
+      return `\n${listIndent(element.parentElement)}${indentUnit}\\item `
+    },
   }),
   }),
   createSelector({
   createSelector({
     selector: 'p',
     selector: 'p',

+ 30 - 0
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx

@@ -69,6 +69,36 @@ describe('<CodeMirrorEditor/> paste HTML in Visual mode', function () {
     cy.get('.ol-cm-item').should('have.length', 2)
     cy.get('.ol-cm-item').should('have.length', 2)
   })
   })
 
 
+  it('handles a pasted nested bullet list', function () {
+    mountEditor()
+
+    const data =
+      '<ul><li>foo</li><li><ul><li>bar</li><li>baz</li></ul></li></ul>'
+
+    const clipboardData = new DataTransfer()
+    clipboardData.setData('text/html', data)
+    cy.get('@content').trigger('paste', { clipboardData })
+
+    cy.get('@content').should('have.text', ' foo  bar baz')
+    cy.get('.ol-cm-item').should('have.length', 4)
+    cy.get('.cm-line').should('have.length', 6)
+  })
+
+  it('handles a pasted nested numbered list', function () {
+    mountEditor()
+
+    const data =
+      '<ol><li>foo</li><li><ol><li>bar</li><li>baz</li></ol></li></ol>'
+
+    const clipboardData = new DataTransfer()
+    clipboardData.setData('text/html', data)
+    cy.get('@content').trigger('paste', { clipboardData })
+
+    cy.get('@content').should('have.text', ' foo  bar baz')
+    cy.get('.ol-cm-item').should('have.length', 4)
+    cy.get('.cm-line').should('have.length', 6)
+  })
+
   it('removes a solitary item from a list', function () {
   it('removes a solitary item from a list', function () {
     mountEditor()
     mountEditor()