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

GitOrigin-RevId: 15e91ef6807433c5fb0a9bedbd5fea42ac35a5f0
This commit is contained in:
Alf Eaton 2024-05-08 11:37:29 +01:00 committed by Copybot
parent a2ff44d7d4
commit cb297e342a
2 changed files with 82 additions and 11 deletions

View file

@ -522,13 +522,13 @@ const tabular = (element: HTMLTableElement) => {
.join(' ') .join(' ')
} }
const listDepth = ( const listDepth = (element: HTMLElement): number =>
element: HTMLOListElement | HTMLUListElement | HTMLLIElement Math.max(0, matchingParents(element, 'ul,ol').length)
): number => Math.max(0, matchingParents(element, 'ul,ol').length - 1)
const listIndent = ( const indentUnit = ' ' // TODO: replace hard-coded indent unit?
element: HTMLOListElement | HTMLUListElement | HTMLLIElement
): string => '\t'.repeat(listDepth(element)) 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}`, start: element => {
end: element => `\n${listIndent(element)}\\end{itemize}\n`, 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}`, start: element => {
end: element => `\n${listIndent(element)}\\end{enumerate}\n`, 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',

View file

@ -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()