Shadow DOM innerHTML 的插槽被 assignedNodes() 替换

Shadow DOM innerHTML with slots replaced by assignedNodes()

我正在使用阴影 DOM 处理自定义元素,例如:

<hello-there><b>S</b>amantha</hello-there>

innerHTML(在我的例子中由 lit/lit-element 生成)类似于:

<span>Hello <slot></slot>!</span>

我知道如果 const ht = document.querySelector('hello-there') 我可以调用 .innerHTML 并获得 <b>S</b>amantha 并且在 ht 的 shadowRoot 上,我可以调用 .innerHTML Hello !`。但是...

浏览器本质上呈现 <span>Hello <b>S</b>amantha!</span> 的等价物。除了遍历所有 .assignedNodes 并用插槽内容替换插槽之外,还有其他方法可以获得此输出吗?类似于 .slotRenderedInnerHTML?

(更新:我现在已经编写了遍历指定节点并执行我想要的代码,但与浏览器原生解决方案相比,它似乎脆弱且缓慢。)

class HelloThere extends HTMLElement {
   constructor() {
      super();
      const shadow = this.attachShadow({mode: 'open'});
      shadow.innerHTML = '<span>Hello <slot></slot>!</span>';
   }
}

customElements.define('hello-there', HelloThere);
<hello-there><b>S</b>amantha</hello-there>
<div>Output: <input type="text" size="200" id="output"></input></div>
<script>
const ht = document.querySelector('hello-there');
const out = document.querySelector('#output');

</script>
<button onclick="out.value = ht.innerHTML">InnerHTML hello-there</button><br>
<button onclick="out.value = ht.outerHTML">OuterHTML hello-there</button><br>
<button onclick="out.value = ht.shadowRoot.innerHTML">InnerHTML hello-there shadow</button><br>
<button onclick="out.value = ht.shadowRoot.outerHTML">OuterHTML hello-there shadow (property does not exist)</button><br>
<button onclick="out.value = '<span>Hello <b>S</b>amantha!</span>'">Desired output</button>

因为似乎没有 browser-native 回答问题的方法(而且浏览器开发人员似乎并不完全理解看到与用户大致看到的内容非常接近的效用在他们的浏览器中)我写了这段代码。

这里是打字稿,片段中有 pure-Javascript:

const MATCH_END = /(<\/[a-zA-Z][a-zA-Z0-9_-]*>)$/;

/**
 * Reconstruct the innerHTML of a shadow element
 */
export function reconstruct_shadow_slot_innerHTML(el: HTMLElement): string {
    return reconstruct_shadow_slotted(el).join('').replace(/\s+/, ' ');
}

export function reconstruct_shadow_slotted(el: Element): string[] {
    const child_nodes = el.shadowRoot ? el.shadowRoot.childNodes : el.childNodes;
    return reconstruct_from_nodeList(child_nodes);
}

function reconstruct_from_nodeList(child_nodes: NodeList|Node[]): string[] {
    const new_values = [];
    for (const child_node of Array.from(child_nodes)) {
        if (!(child_node instanceof Element)) {
            if (child_node.nodeType === Node.TEXT_NODE) {
                // text nodes are typed as Text or CharacterData in TypeScript
                new_values.push((child_node as Text).data);
            } else if (child_node.nodeType === Node.COMMENT_NODE) {
                const new_data = (child_node as Text).data;
                new_values.push('<!--' + new_data + '-->');
            }
            continue;
        } else if (child_node.tagName === 'SLOT') {
            const slot = child_node as HTMLSlotElement;
            new_values.push(...reconstruct_from_nodeList(slot.assignedNodes()));
            continue;
        } else if (child_node.shadowRoot) {
            new_values.push(...reconstruct_shadow_slotted(child_node));
            continue;
        }
        let start_tag: string = '';
        let end_tag: string = '';

        // see @syduki's answer to my Q at
        // 
        // for why cloning the Node is much faster than doing innerHTML;
        const clone = child_node.cloneNode() as Element;  // shallow clone
        const tag_only = clone.outerHTML;
        const match = MATCH_END.exec(tag_only);
        if (match === null) {  // empty tag, like <input>
            start_tag = tag_only;
        } else {
            end_tag = match[1];
            start_tag = tag_only.replace(end_tag, '');
        }
        new_values.push(start_tag);
        const inner_values: string[] = reconstruct_from_nodeList(child_node.childNodes);
        new_values.push(...inner_values);
        new_values.push(end_tag);
    }
    return new_values;
}

根据上下文回答:

const MATCH_END = /(<\/[a-zA-Z][a-zA-Z0-9_-]*>)$/;


/**
 * Reconstruct the innerHTML of a shadow element
 */
function reconstruct_shadow_slot_innerHTML(el) {
    return reconstruct_shadow_slotted(el).join('').replace(/\s+/, ' ');
}

function reconstruct_shadow_slotted(el) {
    const child_nodes = el.shadowRoot ? el.shadowRoot.childNodes : el.childNodes;
    return reconstruct_from_nodeList(child_nodes);
}


function reconstruct_from_nodeList(child_nodes) {
    const new_values = [];
    for (const child_node of Array.from(child_nodes)) {
        if (!(child_node instanceof Element)) {
            if (child_node.nodeType === Node.TEXT_NODE) {
                new_values.push(child_node.data);
            } else if (child_node.nodeType === Node.COMMENT_NODE) {
                const new_data = child_node.data;
                new_values.push('<!--' + new_data + '-->');
            }
            continue;
        } else if (child_node.tagName === 'SLOT') {
            const slot = child_node;
            new_values.push(...reconstruct_from_nodeList(slot.assignedNodes()));
            continue;
        } else if (child_node.shadowRoot) {
            new_values.push(...reconstruct_shadow_slotted(child_node));
            continue;
        }
        let start_tag = '';
        let end_tag = '';

        const clone = child_node.cloneNode();
        // shallow clone
        const tag_only = clone.outerHTML;
        const match = MATCH_END.exec(tag_only);
        if (match === null) {  // empty tag, like <input>
            start_tag = tag_only;
        } else {
            end_tag = match[1];
            start_tag = tag_only.replace(end_tag, '');
        }
        new_values.push(start_tag);
        const inner_values = reconstruct_from_nodeList(child_node.childNodes);
        new_values.push(...inner_values);
        new_values.push(end_tag);
    }

    return new_values;
}

class HelloThere extends HTMLElement {
   constructor() {
      super();
      const shadow = this.attachShadow({mode: 'open'});
      shadow.innerHTML = '<span>Hello <slot></slot>!</span>';
   }
}

customElements.define('hello-there', HelloThere);
<hello-there><b>S</b>amantha</hello-there>
<div>Output: <input type="text" size="200" id="output"></input></div>
<script>
const ht = document.querySelector('hello-there');
const out = document.querySelector('#output');

</script>
<button onclick="out.value = ht.innerHTML">InnerHTML hello-there</button><br>
<button onclick="out.value = ht.outerHTML">OuterHTML hello-there</button><br>
<button onclick="out.value = ht.shadowRoot.innerHTML">InnerHTML hello-there shadow</button><br>
<button onclick="out.value = ht.shadowRoot.outerHTML">OuterHTML hello-there shadow (property does not exist)</button><br>
<button onclick="out.value = reconstruct_shadow_slot_innerHTML(ht)">Desired output</button>