diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 9822529ece61..3dfb4fd275cf 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -14,7 +14,7 @@ import Stylesheet from './css/Stylesheet'; import { test } from '../config'; import Fragment from './nodes/Fragment'; import internal_exports from './internal_exports'; -import { Ast, CompileOptions, Var, Warning, CssResult } from '../interfaces'; +import { Ast, CompileOptions, Var, Warning, CssResult, ShadowDomMode } from '../interfaces'; import error from '../utils/error'; import get_code_frame from '../utils/get_code_frame'; import flatten_reference from './utils/flatten_reference'; @@ -30,6 +30,7 @@ import check_graph_for_cycles from './utils/check_graph_for_cycles'; import { print, x, b } from 'code-red'; interface ComponentOptions { + shadowdom?: ShadowDomMode; namespace?: string; tag?: string; immutable?: boolean; @@ -158,6 +159,7 @@ export default class Component { }); } this.tag = this.component_options.tag || compile_options.tag; + this.compile_options.shadowDom = this.component_options.shadowdom || "open"; } else { this.tag = this.name.name; } @@ -170,7 +172,7 @@ export default class Component { this.walk_instance_js_post_template(); - if (!compile_options.customElement) this.stylesheet.reify(); + if (!compile_options.customElement || compile_options.shadowDom=="none") this.stylesheet.reify(); this.stylesheet.warn_on_unused_selectors(this); } @@ -1414,7 +1416,16 @@ function process_component_options(component: Component, nodes) { component_options[name] = value; break; } + case 'shadowdom': { + const code = 'invalid-shadowdom-attribute'; + const message = `'shadowdom' must be set to 'open', 'closed or 'none'`; + const value = get_value(attribute, code, message); + if (value != "open" && value != "none" && value != "closed") + component.error(attribute, { code, message }); + component_options[name] = value; + break; + } default: component.error(attribute, { code: `invalid-options-attribute`, diff --git a/src/compiler/compile/index.ts b/src/compiler/compile/index.ts index 12b161aeeb0e..5fe11fd2f5eb 100644 --- a/src/compiler/compile/index.ts +++ b/src/compiler/compile/index.ts @@ -22,6 +22,7 @@ const valid_options = [ 'hydratable', 'legacy', 'customElement', + 'shadowDom', 'tag', 'css', 'loopGuardTimeout', diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 26fa4a70f81f..fddf9373439a 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -20,7 +20,7 @@ export default function dom( block.has_outro_method = true; // prevent fragment being created twice (#1063) - if (options.customElement) block.chunks.create.push(b`this.c = @noop;`); + if (options.customElement && options.shadowDom !== "none") block.chunks.create.push(b`this.c = @noop;`); const body = []; @@ -29,7 +29,7 @@ export default function dom( body.push(b`const ${renderer.file_var} = ${file};`); } - const css = component.stylesheet.render(options.filename, !options.customElement); + const css = component.stylesheet.render(options.filename, (!options.customElement || options.shadowDom === "none")); const styles = component.stylesheet.has_styles && options.dev ? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` : css.code; @@ -37,9 +37,9 @@ export default function dom( const add_css = component.get_unique_name('add_css'); const should_add_css = ( - !options.customElement && + (!options.customElement && !!styles && - options.css !== false + options.css !== false ) || options.shadowDom === "none" ); if (should_add_css) { @@ -437,14 +437,22 @@ export default function dom( } if (options.customElement) { + const lightDom = options.shadowDom === 'none'; const declaration = b` class ${name} extends @SvelteElement { constructor(options) { super(); - - ${css.code && b`this.shadowRoot.innerHTML = \`\`;`} - - @init(this, { target: this.shadowRoot }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); + ${!lightDom && b` + this._root =this.attachShadow({ mode: '${options.shadowDom}' }); + ` || b` + this._copycontent(); + this.slotObserver = new MutationObserver(() => this._slotcontent()); + this.slotObserver.observe(this, {childList: true, subtree: true}); + `} + ${css.code && !lightDom && b`this._root.innerHTML = \`\`;`} + ${should_add_css && lightDom && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`} + + @init(this, { target: ${lightDom ? 'this' : 'this._root'} }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); ${dev_props_check} @@ -476,6 +484,82 @@ export default function dom( }); } + if (lightDom) { + declaration.body.body.push({ + type: 'MethodDefinition', + kind: 'method', + static: false, + computed: false, + key: { type: 'Identifier', name: '_copycontent' }, + value: x`function() { + if(this.children){ + this._content = Array.from(this.childNodes) + while (this.firstChild) { + this.removeChild(this.firstChild) + } + } + }` as FunctionExpression + }); + + declaration.body.body.push({ + type: 'MethodDefinition', + kind: 'method', + static: false, + computed: false, + key: { type: 'Identifier', name: '_slotcontent' }, + value: x`function() { + if(this.slotting) return; // prevent running in parallel + this.slotting = true; + if(this._content){ + let namedslots = Array.from(this.querySelectorAll("slot[name]")) + let defaultslot = this.querySelector("slot:not([name])") + let named = {} + if(!namedslots.length && !defaultslot) return(this.slotting=false); + let slotted = [] + this._content.filter((node)=> node.slot ).forEach((node)=> named[node.slot] = node ) + namedslots.forEach(slot =>{ + this._content.forEach(node =>{ //append all named slots + if(named[node.slot] && slot.getAttribute("name") == node.slot){ + if(!slot.hasAttribute("hasupdated")){ + while (slot.firstChild) { + slot.removeChild(slot.firstChild) + } + slot.appendChild(named[node.slot]); + } + slot.setAttribute("hasupdated","") + slotted.push(node) + } + }) + }) + if(!defaultslot) return(this.slotting=false); + // get all nodes without a slot attribute + let toAppend = this._content.filter(node => node.hasAttribute && !node.hasAttribute("slot")) + // remove default + if(!defaultslot.hasAttribute("hasupdated") && toAppend.length){ + while (defaultslot.firstChild) { + defaultslot.removeChild(defaultslot.firstChild) + } + } + toAppend.forEach(node => !defaultslot.hasAttribute("hasupdated") && defaultslot.appendChild(node) ) + defaultslot.setAttribute("hasupdated","") + } + this.slotting = false + } + }` as FunctionExpression + }); + + declaration.body.body.push({ + type: 'MethodDefinition', + kind: 'method', + static: false, + computed: false, + key: { type: 'Identifier', name: 'disconnectedCallback' }, + value: x`function() { + this.slotObserver.disconnect(); + }` as FunctionExpression + }); + } + declaration.body.body.push(...accessors); body.push(declaration); diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index a5e286462ff3..a7932615fa33 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -120,6 +120,7 @@ export interface CompileOptions { hydratable?: boolean; legacy?: boolean; customElement?: boolean; + shadowDom?: ShadowDomMode; tag?: string; css?: boolean; loopGuardTimeout?: number; @@ -166,4 +167,8 @@ export interface Var { export interface CssResult { code: string; map: SourceMap; -} \ No newline at end of file +} + +export type ShadowDomMode = 'none' + | 'open' + | 'closed' \ No newline at end of file diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 10588a08046b..9426b6a5ecad 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -165,9 +165,10 @@ export let SvelteElement; if (typeof HTMLElement === 'function') { SvelteElement = class extends HTMLElement { $$: T$$; + protected _content; + slotting; constructor() { super(); - this.attachShadow({ mode: 'open' }); } connectedCallback() { diff --git a/test/custom-elements/samples/shadowdom-closed/main.svelte b/test/custom-elements/samples/shadowdom-closed/main.svelte new file mode 100644 index 000000000000..43c2300bc04f --- /dev/null +++ b/test/custom-elements/samples/shadowdom-closed/main.svelte @@ -0,0 +1,8 @@ + + + + +

Hello {name}!

+ diff --git a/test/custom-elements/samples/shadowdom-closed/test.js b/test/custom-elements/samples/shadowdom-closed/test.js new file mode 100644 index 000000000000..cc0ed2ff8ed6 --- /dev/null +++ b/test/custom-elements/samples/shadowdom-closed/test.js @@ -0,0 +1,13 @@ +import * as assert from 'assert'; +import './main.svelte'; + +export default function (target) { + target.innerHTML = ''; + const el = target.querySelector('custom-element'); + + assert.equal(el.name, 'world'); + + const h1 = el._root.querySelector('h1'); + assert.equal(h1.textContent, 'Hello world!'); + assert.equal(el.shadowRoot, null); +} \ No newline at end of file diff --git a/test/custom-elements/samples/shadowdom-none-css/main.svelte b/test/custom-elements/samples/shadowdom-none-css/main.svelte new file mode 100644 index 000000000000..20061da3cbb1 --- /dev/null +++ b/test/custom-elements/samples/shadowdom-none-css/main.svelte @@ -0,0 +1,10 @@ + + + + +

Hello World

+ diff --git a/test/custom-elements/samples/shadowdom-none-css/test.js b/test/custom-elements/samples/shadowdom-none-css/test.js new file mode 100644 index 000000000000..9f6ec3c20ef6 --- /dev/null +++ b/test/custom-elements/samples/shadowdom-none-css/test.js @@ -0,0 +1,10 @@ +import * as assert from 'assert'; +import './main.svelte'; + +export default function (target) { + target.innerHTML = ''; + const el = target.querySelector('custom-element'); + const h1 = el.querySelector('h1'); + const colour = getComputedStyle(h1).color; + assert.equal(colour,"rgb(255, 0, 0)"); +} \ No newline at end of file diff --git a/test/custom-elements/samples/shadowdom-none-slots/main.svelte b/test/custom-elements/samples/shadowdom-none-slots/main.svelte new file mode 100644 index 000000000000..ebe2db4e2d0a --- /dev/null +++ b/test/custom-elements/samples/shadowdom-none-slots/main.svelte @@ -0,0 +1,11 @@ + + +
+ +

default fallback content

+
+ + +

foo fallback content

+
+
diff --git a/test/custom-elements/samples/shadowdom-none-slots/test.js b/test/custom-elements/samples/shadowdom-none-slots/test.js new file mode 100644 index 000000000000..5c79a7b2652e --- /dev/null +++ b/test/custom-elements/samples/shadowdom-none-slots/test.js @@ -0,0 +1,17 @@ +import * as assert from 'assert'; +import './main.svelte'; + +export default function (target) { + target.innerHTML = ` + + slotted + `; + + const el = target.querySelector('custom-element'); + + const div = el.children[0]; + const [slot0] = div.children; + + assert.equal(slot0.children[1], target.querySelector('strong')); + //assert.equal(slot1.assignedNodes().length, 0); +} \ No newline at end of file diff --git a/test/custom-elements/samples/shadowdom-none/main.svelte b/test/custom-elements/samples/shadowdom-none/main.svelte new file mode 100644 index 000000000000..79a40384ec03 --- /dev/null +++ b/test/custom-elements/samples/shadowdom-none/main.svelte @@ -0,0 +1,8 @@ + + + + +

Hello {name}!

+ diff --git a/test/custom-elements/samples/shadowdom-none/test.js b/test/custom-elements/samples/shadowdom-none/test.js new file mode 100644 index 000000000000..7cd82a30eeec --- /dev/null +++ b/test/custom-elements/samples/shadowdom-none/test.js @@ -0,0 +1,12 @@ +import * as assert from 'assert'; +import './main.svelte'; + +export default function (target) { + target.innerHTML = ''; + const el = target.querySelector('custom-element'); + + assert.equal(el.name, 'world'); + + const h1 = el.querySelector('h1'); + assert.equal(h1.textContent, 'Hello world!'); +} \ No newline at end of file diff --git a/test/js/samples/css-shadow-dom-keyframes/expected.js b/test/js/samples/css-shadow-dom-keyframes/expected.js index a0a0ebe0211b..e05a7c02412a 100644 --- a/test/js/samples/css-shadow-dom-keyframes/expected.js +++ b/test/js/samples/css-shadow-dom-keyframes/expected.js @@ -33,8 +33,9 @@ function create_fragment(ctx) { class Component extends SvelteElement { constructor(options) { super(); - this.shadowRoot.innerHTML = ``; - init(this, { target: this.shadowRoot }, null, create_fragment, safe_not_equal, {}); + this._root = this.attachShadow({ mode: "open" }); + this._root.innerHTML = ``; + init(this, { target: this._root }, null, create_fragment, safe_not_equal, {}); if (options) { if (options.target) {