diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts b/services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts index d4d75cdcf8..bf1fafca2d 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts @@ -522,13 +522,13 @@ const tabular = (element: HTMLTableElement) => { .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 = { selector: T @@ -610,6 +610,37 @@ const startMultirow = (element: HTMLTableCellElement): string => { 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 = [ createSelector({ selector: 'b', @@ -826,18 +857,28 @@ const selectors = [ createSelector({ // selector: 'ul:has(> li:nth-child(2))', // only select lists with at least 2 items (once Firefox supports :has()) 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({ // selector: 'ol:has(> li:nth-child(2))', // only select lists with at least 2 items (once Firefox supports :has()) 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({ selector: 'li', - start: element => `\n${listIndent(element)}\t\\item `, + start: element => { + return `\n${listIndent(element.parentElement)}${indentUnit}\\item ` + }, }), createSelector({ selector: 'p', diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx index 5717e9405d..b0debaf8c9 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx @@ -69,6 +69,36 @@ describe(' paste HTML in Visual mode', function () { cy.get('.ol-cm-item').should('have.length', 2) }) + it('handles a pasted nested bullet list', function () { + mountEditor() + + const data = + '' + + 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 = + '
  1. foo
    1. bar
    2. baz
' + + 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 () { mountEditor()