Skip to content

feat: initial support for multi-column; lists and media fixes(Copy/paste project) #2528

Open
VladaHarbour wants to merge 4 commits intomainfrom
sd-148_copy-paste-superdoc
Open

feat: initial support for multi-column; lists and media fixes(Copy/paste project) #2528
VladaHarbour wants to merge 4 commits intomainfrom
sd-148_copy-paste-superdoc

Conversation

@VladaHarbour
Copy link
Copy Markdown
Contributor

This PR introduces fixes to support copy/paste experience within Superdoc:

  1. Preserves multi-column layout
  2. Preserves lists (including nested)
  3. Preserves images and headings

(We have hyperlinks and table support in current stable version)

@linear
Copy link
Copy Markdown

linear bot commented Mar 23, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 547bdff47c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@VladaHarbour VladaHarbour changed the title feat: initial support for multi-column; lists and media fixes feat: initial support for multi-column; lists and media fixes(Copy/paste project) Mar 23, 2026
@VladaHarbour VladaHarbour force-pushed the sd-148_copy-paste-superdoc branch from 3e811a3 to 499b262 Compare March 25, 2026 10:27
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VladaHarbour nice work, Codex bot comments from earlier are all handled.

  • handleCutEvent: preventDefault before the try block means Cmd+X silently fails if anything throws — see inline fix
  • stripWordListConditionalPrefixes: missing [endif] eats all content after it — see inline warning
  • createListIdAllocator is copy-pasted between two files, bodySectPr helpers are duplicated — see inline suggestions
  • missing round-trip tests for embedSliceInHtml/extractSliceFromHtml/stripSliceFromHtml and shouldPreserveSlicePastedListRendering
  • no Playwright tests for copy-paste yet — fine as follow-up


const { from, to } = view.state.selection;
if (from === to) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if anything inside the try block fails, preventDefault already blocked the browser's cut — so the user presses Cmd+X and nothing happens (no clipboard data, no deletion). moving preventDefault to after the clipboard writes means the browser's own cut kicks in as a fallback.

Suggested change
try {
const slice = view.state.doc.slice(from, to);
const fragment = slice.content;
const sliceJson = JSON.stringify(slice.toJSON());
clipboardData.setData(SUPERDOC_SLICE_MIME, sliceJson);
const mediaJson = collectReferencedImageMediaForClipboard(sliceJson, editor);
if (mediaJson) {
clipboardData.setData(SUPERDOC_MEDIA_MIME, mediaJson);
}
const div = document.createElement('div');
const serializer = PmDOMSerializer.fromSchema(view.state.schema);
div.appendChild(serializer.serializeFragment(fragment));
annotateFragmentDomWithClipboardData(div, fragment, editor);
const html = unflattenListsInHtml(div.innerHTML);
const bodySectPr = view.state.doc.attrs?.bodySectPr;
const bodySectPrJson = bodySectPr && bodySectPrShouldEmbed(bodySectPr) ? JSON.stringify(bodySectPr) : '';
clipboardData.setData('text/html', embedSliceInHtml(html, sliceJson, bodySectPrJson));
clipboardData.setData('text/plain', fragment.textBetween(0, fragment.size, '\n\n'));
event.preventDefault();
view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
} catch (error) {
console.warn('Failed to handle cut:', error);
}

Copy link
Copy Markdown
Contributor Author

@VladaHarbour VladaHarbour Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved preventDefault inside try/catch

const current = node.childNodes[i];
if (current?.nodeType === Node.COMMENT_NODE && current.nodeValue?.includes('[if !supportLists]')) {
let j = i + 1;
while (j < node.childNodes.length) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the closing [endif] comment is missing (broken Word HTML), this loop removes every node after it until there's nothing left. a warning would make this easier to debug:

Suggested change
while (j < node.childNodes.length) {
while (j < node.childNodes.length) {
const next = node.childNodes[j];
if (next?.nodeType === Node.COMMENT_NODE && next.nodeValue?.includes('[endif]')) {
node.removeChild(next);
break;
}
node.removeChild(next);
}
if (j >= node.childNodes.length) {
console.warn('[SuperDoc] stripWordListConditionalPrefixes: missing [endif], some content may have been removed');
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created more advanced handling for this corner case + test

}

/** Apply pasted body `sectPr` when target has single-column layout. */
function tryApplyEmbeddedBodySectPr(editor, view, bodySectPr) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tryApplyEmbeddedBodySectPr and applyEmbeddedBodySectPrToTransaction do the same checks and the same work — only difference is who owns the transaction. could be one shared function:

function applyBodySectPrToTr(editor, tr, bodySectPr, currentDoc) {
  if (!bodySectPr || typeof bodySectPr !== 'object') return false;
  const incomingCols = getSectPrColumns(bodySectPr);
  if (!incomingCols?.count || incomingCols.count <= 1) return false;
  const currentCols = getSectPrColumns(currentDoc.attrs?.bodySectPr);
  if (currentCols?.count > 1) return false;
  const clone = JSON.parse(JSON.stringify(bodySectPr));
  tr.setDocAttribute('bodySectPr', clone);
  if (editor?.converter) editor.converter.bodySectPr = clone;
  return true;
}

worth doing?

if (paragraphMeta.length === 0) {
return fragment;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createListIdAllocator is identical to the one in html-helpers.js:53 — moving it to the shared numbering helpers would keep them from drifting apart.

return doc.body.innerHTML;
}

function createListIdAllocator(editor) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same createListIdAllocator as InputRule.js:790 — see comment there.

@VladaHarbour VladaHarbour force-pushed the sd-148_copy-paste-superdoc branch from a5d7065 to d410051 Compare March 27, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants