From b4be4875412cc0d64dd330e1417d7383c560a4a8 Mon Sep 17 00:00:00 2001 From: adam-soltech <138262921+adam-soltech@users.noreply.github.com> Date: Wed, 2 Aug 2023 09:59:48 -0700 Subject: [PATCH] Extend ms word pasting to handle ordered lists and combined text (#1461) Co-authored-by: adam-soltech Co-authored-by: Carson Full --- src/components/RichText/RichTextField.tsx | 4 +- src/components/RichText/ms-word-helpers.ts | 85 +++++++++++++++++----- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/components/RichText/RichTextField.tsx b/src/components/RichText/RichTextField.tsx index 4899de4226..08cfbbfa19 100644 --- a/src/components/RichText/RichTextField.tsx +++ b/src/components/RichText/RichTextField.tsx @@ -36,7 +36,7 @@ import { getHelperText, showError } from '../form/util'; import { FormattedNumber } from '../Formatters'; import { EditorJsTheme } from './EditorJsTheme'; import type { ToolKey } from './editorJsTools'; -import { handleMsUnorderedList } from './ms-word-helpers'; +import { handleMsPasteFormatting } from './ms-word-helpers'; import { RichTextView } from './RichTextView'; declare module '@editorjs/editorjs/types/data-formats/output-data' { @@ -138,7 +138,7 @@ export function RichTextField({ 'paste', (event: ClipboardEvent) => { const formattedUl: RichTextData | undefined = - handleMsUnorderedList(event); + handleMsPasteFormatting(event); if (formattedUl) { event.preventDefault(); input.onChange(formattedUl); diff --git a/src/components/RichText/ms-word-helpers.ts b/src/components/RichText/ms-word-helpers.ts index 9676a9dd19..3886b73659 100644 --- a/src/components/RichText/ms-word-helpers.ts +++ b/src/components/RichText/ms-word-helpers.ts @@ -1,31 +1,78 @@ import { OutputData as RichTextData } from '@editorjs/editorjs'; -export const handleMsUnorderedList = ( +export const handleMsPasteFormatting = ( event: ClipboardEvent ): RichTextData | undefined => { const text = event.clipboardData?.getData('text'); + // If there is no text, or the text has no list return undefined and continue native paste propagation + if (!text || !hasWordListMarkers(text)) { + return; + } - // Match any line that starts with "•\t" followed by anything until the end of the line - const matches = text?.match(/•\t(.*)/g); + const parsedLines = text.split('\n').map((line) => { + if (isUnorderedList(line)) { + return { type: 'ul', text: line.replace(/•\t/, '') }; + } + if (isOrderedList(line)) { + return { type: 'ol', text: line.replace(/^\d+\.\s/, '') }; + } + if (line === '\r') { + return { type: 'break', text: line }; + } + return { type: 'p', text: line }; + }); - if (!matches) { - return undefined; - } - // If there are matches, prevent default actions, then create an array of strings without the "•\t" prefix - const listItems = matches.map((item) => item.replace(/•\t/, '')); + const groupedLines = groupSiblingsBy(parsedLines, (line) => line.type); + + const blocks = groupedLines.map((lines) => { + const type = lines[0]!.type; + const textLines = lines.map((line) => line.text); + + if (type === 'ol') { + return createListBlock(textLines, 'ordered'); + } + if (type === 'ul') { + return createListBlock(textLines, 'unordered'); + } + return createParagraphBlock(textLines); + }); - // Now create a new object that conforms to the structure of RichTextData - // Assumption is that all the list items should be part of a single unordered list return { time: Date.now(), - blocks: [ - { - type: 'list', - data: { - style: 'unordered', - items: listItems, - }, - }, - ], + blocks, }; }; + +const groupSiblingsBy = (items: readonly T[], by: (item: T) => unknown) => + items.reduce((acc: T[][], cur: T) => { + // If it's the first item or different from the last, start a new group + if (!acc.length || by(acc.at(-1)![0]!) !== by(cur)) { + acc.push([cur]); + } else { + // Otherwise, add it to the current group + acc.at(-1)!.push(cur); + } + return acc; + }, []); + +const createParagraphBlock = (text: string[]) => ({ + type: 'paragraph', + data: { + text: text.join(' '), + }, +}); + +const createListBlock = (items: string[], style: 'unordered' | 'ordered') => ({ + type: 'list', + data: { + style: style, + items: items, + }, +}); + +const hasWordListMarkers = (text: string) => + isUnorderedList(text) || isOrderedList(text); + +const isUnorderedList = (text: string) => /•\t(.*)/.test(text); + +const isOrderedList = (text: string) => /^\d+\.\s(.*)/.test(text);