@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
123 lines (121 loc) • 5.01 kB
JavaScript
/**
* Clone a `Range` into a self-contained wrapper element with computed
* styles inlined onto every cloned descendant, so that pasting into
* rich-text targets (email, Word, Notion, etc.) preserves the source's
* visual styling without depending on the source page's stylesheet.
*
* The wrapper defaults to `<pre>` so monospace + whitespace context
* survives a copy/paste round-trip. The original ancestor chain
* between `range.commonAncestorContainer` and `root` is reconstructed
* inside the wrapper so a selection living entirely inside a styled
* descendant (e.g. one token of a syntax-highlighted line) keeps that
* wrapper in the clipboard payload.
*/
const asElement = node => node instanceof Element ? node : null;
const nextElement = walker => asElement(walker.nextNode());
const inlineComputedStyles = (target, computed, props) => {
let inline = target.getAttribute('style') ?? '';
for (const prop of props) {
const value = computed.getPropertyValue(prop);
if (value && value !== 'normal' && value !== 'none' && value !== 'auto') {
inline += `${prop}:${value};`;
}
}
if (inline) {
target.setAttribute('style', inline);
}
};
export const cloneRangeWithInlineStyles = (root, range, options = {}) => {
const {
wrapperTag = 'pre',
className = root.className,
elementStyleProps = [],
rootStyleProps = [],
rootStaticStyles = ''
} = options;
const doc = root.ownerDocument;
const view = doc.defaultView;
const fragment = range.cloneContents();
const container = doc.createElement(wrapperTag);
if (className) {
container.className = className;
}
// `Range.cloneContents` returns the descendants of the
// `commonAncestorContainer` but never the ancestor itself, so any
// selection that lives entirely inside a styled wrapper (a single
// text node inside a token, or multiple children of the same token)
// loses that wrapper in the clipboard payload. The computed-style
// inlining pass below has nothing to inline onto in that case.
// Reconstruct the ancestor chain up to (but not including) `root`
// and inline styles onto each rebuilt wrapper so rich-text paste
// targets keep the original highlighting.
const cac = range.commonAncestorContainer;
const anchor = asElement(cac) ?? cac.parentElement;
let rootContent = fragment;
// The innermost reconstructed wrapper, if any. The style-inlining
// pass below walks from here so the clone walker stays aligned with
// the source walker (which starts from the CAC's descendants).
let cloneStylingRoot = container;
if (anchor && anchor !== root && root.contains(anchor)) {
let current = anchor;
let innermost = null;
while (current && current !== root) {
const cloned = current.cloneNode(false);
// `Element.cloneNode` returns an Element; the runtime check
// exists purely to satisfy the DOM lib's `Node` return type.
if (!(cloned instanceof Element)) {
current = current.parentElement;
continue;
}
if (view && elementStyleProps.length > 0) {
inlineComputedStyles(cloned, view.getComputedStyle(current), elementStyleProps);
}
cloned.appendChild(rootContent);
rootContent = cloned;
if (innermost === null) {
innermost = cloned;
}
current = current.parentElement;
}
if (innermost) {
cloneStylingRoot = innermost;
}
}
container.appendChild(rootContent);
if (view && elementStyleProps.length > 0) {
// Walk the CAC's descendants and mirror them onto the cloned
// descendants of the innermost reconstructed wrapper. Both
// walkers exclude their root, so as long as the roots correspond
// (CAC ↔ innermost reconstructed wrapper, or CAC ↔ wrapper when
// there is no reconstruction) the per-step pairing is correct.
const sourceWalker = doc.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, {
acceptNode: node => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
});
const cloneWalker = doc.createTreeWalker(cloneStylingRoot, NodeFilter.SHOW_ELEMENT);
let source = nextElement(sourceWalker);
let clone = nextElement(cloneWalker);
while (source && clone) {
if (source.tagName === clone.tagName) {
inlineComputedStyles(clone, view.getComputedStyle(source), elementStyleProps);
}
source = nextElement(sourceWalker);
clone = nextElement(cloneWalker);
}
}
if (view && (rootStyleProps.length > 0 || rootStaticStyles)) {
let rootInline = rootStaticStyles;
if (rootStyleProps.length > 0) {
const rootComputed = view.getComputedStyle(root);
for (const prop of rootStyleProps) {
const value = rootComputed.getPropertyValue(prop);
if (value) {
rootInline += `${prop}:${value};`;
}
}
}
if (rootInline) {
container.setAttribute('style', rootInline);
}
}
return container;
};