light-slot.js 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. /**
  2. * light-slot.js — light DOM 下替代 <slot> 的内容投影
  3. *
  4. * 用法:
  5. * import { capture, project } from './light-slot.js';
  6. *
  7. * class MyCard extends LightElement {
  8. * connectedCallback() {
  9. * super.connectedCallback();
  10. * capture(this); // ← 渲染前捕获外部子节点
  11. * }
  12. * updated() {
  13. * project(this); // ← 渲染后搬入容器(内部幂等)
  14. * }
  15. * render() {
  16. * return html`
  17. * <div class="card-header">
  18. * <div data-light-slot="actions"></div>
  19. * </div>
  20. * <div class="card-body" data-light-slot="body"></div>
  21. * `;
  22. * }
  23. * }
  24. *
  25. * 关键约束(违反即出 bug):
  26. * 1. capture 必须在渲染前调用。渲染后再扫宿主子节点,会把组件自己刚生成的内容
  27. * 当成「外部传入」搬走,越搬越乱。
  28. * 2. project 必须靠 dataset.projected 做幂等,否则每次重渲染都重复搬、节点来回跳。
  29. * 3. 传入具名内容用 slot="actions" 属性;不写 slot 属性的默认进 body。
  30. */
  31. const KEY = '__lightIncoming';
  32. /**
  33. * 渲染前捕获宿主元素上的外部子节点
  34. * @param {HTMLElement} host
  35. */
  36. export function capture(host) {
  37. const body = [];
  38. const named = {};
  39. for (const n of [...host.childNodes]) {
  40. // 元素节点且带 slot 属性 → 具名插槽
  41. if (n.nodeType === 1 && n.hasAttribute('slot')) {
  42. const name = n.getAttribute('slot');
  43. (named[name] ||= []).push(n);
  44. } else if (n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim())) {
  45. // 元素节点,或非空白文本节点 → 默认 body
  46. body.push(n);
  47. }
  48. }
  49. host[KEY] = { body, named };
  50. }
  51. /**
  52. * 渲染后把捕获的节点搬进对应容器(幂等)
  53. * @param {HTMLElement} host
  54. */
  55. export function project(host) {
  56. const stash = host[KEY];
  57. if (!stash) return;
  58. // 默认插槽 → body
  59. const bodyBox = host.querySelector('[data-light-slot="body"]');
  60. if (bodyBox && !bodyBox.dataset.projected) {
  61. const nodes = [...stash.body, ...(stash.named.body || [])];
  62. for (const n of nodes) {
  63. if (n.parentNode === host) bodyBox.appendChild(n);
  64. }
  65. bodyBox.dataset.projected = '1';
  66. }
  67. // 具名插槽
  68. for (const [name, nodes] of Object.entries(stash.named)) {
  69. if (name === 'body') continue;
  70. const box = host.querySelector(`[data-light-slot="${name}"]`);
  71. if (box && !box.dataset.projected) {
  72. for (const n of nodes) {
  73. if (n.parentNode === host) box.appendChild(n);
  74. }
  75. box.dataset.projected = '1';
  76. }
  77. }
  78. }