diff --git a/src/runtime/internal/Component.js b/src/runtime/internal/Component.js index 89d533fbff..c7a27b9dfc 100644 --- a/src/runtime/internal/Component.js +++ b/src/runtime/internal/Component.js @@ -1,262 +1,293 @@ -import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler'; +import { + add_render_callback, + flush, + flush_render_callbacks, + schedule_update, + dirty_components +} from './scheduler'; import { current_component, set_current_component } from './lifecycle'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; -import { children, detach, start_hydrating, end_hydrating, get_custom_elements_slots, insert } from './dom'; +import { + children, + detach, + start_hydrating, + end_hydrating, + get_custom_elements_slots, + insert +} from './dom'; import { transition_in } from './transitions'; /** @returns {void} */ export function bind(component, name, callback) { - const index = component.$$.props[name]; - if (index !== undefined) { - component.$$.bound[index] = callback; - callback(component.$$.ctx[index]); - } + const index = component.$$.props[name]; + if (index !== undefined) { + component.$$.bound[index] = callback; + callback(component.$$.ctx[index]); + } } /** @returns {void} */ export function create_component(block) { - block && block.c(); + block && block.c(); } /** @returns {void} */ export function claim_component(block, parent_nodes) { - block && block.l(parent_nodes); + block && block.l(parent_nodes); } /** @returns {void} */ export function mount_component(component, target, anchor) { - const { fragment, after_update } = component.$$; - fragment && fragment.m(target, anchor); - // onMount happens before the initial afterUpdate - add_render_callback(() => { - const new_on_destroy = component.$$.on_mount.map(run).filter(is_function); - // if the component was destroyed immediately - // it will update the `$$.on_destroy` reference to `null`. - // the destructured on_destroy may still reference to the old array - if (component.$$.on_destroy) { - component.$$.on_destroy.push(...new_on_destroy); - } - else { - // Edge case - component was destroyed immediately, - // most likely as a result of a binding initialising - run_all(new_on_destroy); - } - component.$$.on_mount = []; - }); - after_update.forEach(add_render_callback); + const { fragment, after_update } = component.$$; + fragment && fragment.m(target, anchor); + // onMount happens before the initial afterUpdate + add_render_callback(() => { + const new_on_destroy = component.$$.on_mount.map(run).filter(is_function); + // if the component was destroyed immediately + // it will update the `$$.on_destroy` reference to `null`. + // the destructured on_destroy may still reference to the old array + if (component.$$.on_destroy) { + component.$$.on_destroy.push(...new_on_destroy); + } else { + // Edge case - component was destroyed immediately, + // most likely as a result of a binding initialising + run_all(new_on_destroy); + } + component.$$.on_mount = []; + }); + after_update.forEach(add_render_callback); } /** @returns {void} */ export function destroy_component(component, detaching) { - const $$ = component.$$; - if ($$.fragment !== null) { - flush_render_callbacks($$.after_update); - run_all($$.on_destroy); - $$.fragment && $$.fragment.d(detaching); - // TODO null out other refs, including component.$$ (but need to - // preserve final state?) - $$.on_destroy = $$.fragment = null; - $$.ctx = []; - } + const $$ = component.$$; + if ($$.fragment !== null) { + flush_render_callbacks($$.after_update); + run_all($$.on_destroy); + $$.fragment && $$.fragment.d(detaching); + // TODO null out other refs, including component.$$ (but need to + // preserve final state?) + $$.on_destroy = $$.fragment = null; + $$.ctx = []; + } } /** @returns {void} */ function make_dirty(component, i) { - if (component.$$.dirty[0] === -1) { - dirty_components.push(component); - schedule_update(); - component.$$.dirty.fill(0); - } - component.$$.dirty[(i / 31) | 0] |= 1 << i % 31; + if (component.$$.dirty[0] === -1) { + dirty_components.push(component); + schedule_update(); + component.$$.dirty.fill(0); + } + component.$$.dirty[(i / 31) | 0] |= 1 << i % 31; } /** @returns {void} */ -export function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) { - const parent_component = current_component; - set_current_component(component); - /** @type {T$$} */ - const $$ = (component.$$ = { - fragment: null, - ctx: [], - // state - props, - update: noop, - not_equal, - bound: blank_object(), - // lifecycle - on_mount: [], - on_destroy: [], - on_disconnect: [], - before_update: [], - after_update: [], - context: new Map(options.context || (parent_component ? parent_component.$$.context : [])), - // everything else - callbacks: blank_object(), - dirty, - skip_bound: false, - root: options.target || parent_component.$$.root - }); - append_styles && append_styles($$.root); - let ready = false; - $$.ctx = instance - ? instance(component, options.props || {}, (i, ret, ...rest) => { - const value = rest.length ? rest[0] : ret; - if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) { - if (!$$.skip_bound && $$.bound[i]) - $$.bound[i](value); - if (ready) - make_dirty(component, i); - } - return ret; - }) - : []; - $$.update(); - ready = true; - run_all($$.before_update); - // `false` as a special case of no DOM component - $$.fragment = create_fragment ? create_fragment($$.ctx) : false; - if (options.target) { - if (options.hydrate) { - start_hydrating(); - const nodes = children(options.target); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - $$.fragment && $$.fragment.l(nodes); - nodes.forEach(detach); - } - else { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - $$.fragment && $$.fragment.c(); - } - if (options.intro) - transition_in(component.$$.fragment); - mount_component(component, options.target, options.anchor); - end_hydrating(); - flush(); - } - set_current_component(parent_component); +export function init( + component, + options, + instance, + create_fragment, + not_equal, + props, + append_styles, + dirty = [-1] +) { + const parent_component = current_component; + set_current_component(component); + /** @type {T$$} */ + const $$ = (component.$$ = { + fragment: null, + ctx: [], + // state + props, + update: noop, + not_equal, + bound: blank_object(), + // lifecycle + on_mount: [], + on_destroy: [], + on_disconnect: [], + before_update: [], + after_update: [], + context: new Map(options.context || (parent_component ? parent_component.$$.context : [])), + // everything else + callbacks: blank_object(), + dirty, + skip_bound: false, + root: options.target || parent_component.$$.root + }); + append_styles && append_styles($$.root); + let ready = false; + $$.ctx = instance + ? instance(component, options.props || {}, (i, ret, ...rest) => { + const value = rest.length ? rest[0] : ret; + if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) { + if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value); + if (ready) make_dirty(component, i); + } + return ret; + }) + : []; + $$.update(); + ready = true; + run_all($$.before_update); + // `false` as a special case of no DOM component + $$.fragment = create_fragment ? create_fragment($$.ctx) : false; + if (options.target) { + if (options.hydrate) { + start_hydrating(); + const nodes = children(options.target); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + $$.fragment && $$.fragment.l(nodes); + nodes.forEach(detach); + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + $$.fragment && $$.fragment.c(); + } + if (options.intro) transition_in(component.$$.fragment); + mount_component(component, options.target, options.anchor); + end_hydrating(); + flush(); + } + set_current_component(parent_component); } export let SvelteElement; if (typeof HTMLElement === 'function') { - SvelteElement = class extends HTMLElement { - $$componentCtor; - $$slots; - $$component; - $$connected = false; - $$data = {}; - $$reflecting = false; - $$props_definition = {}; - $$listeners = {}; - $$listener_unsubscribe_fns = new Map(); - constructor($$componentCtor, $$slots, use_shadow_dom) { - super(); - this.$$componentCtor = $$componentCtor; - this.$$slots = $$slots; - if (use_shadow_dom) { - this.attachShadow({ mode: 'open' }); - } - } - addEventListener(type, listener, options) { - // We can't determine upfront if the event is a custom event or not, so we have to - // listen to both. If someone uses a custom event with the same name as a regular - // browser event, this fires twice - we can't avoid that. - this.$$listeners[type] = this.$$listeners[type] || []; - this.$$listeners[type].push(listener); - if (this.$$component) { - const unsub = this.$$component.$on(type, listener); - this.$$listener_unsubscribe_fns.set(listener, unsub); - } - super.addEventListener(type, listener, options); - } - removeEventListener(type, listener, options) { - super.removeEventListener(type, listener, options); - if (this.$$component) { - const unsub = this.$$listener_unsubscribe_fns.get(listener); - if (unsub) { - unsub(); - this.$$listener_unsubscribe_fns.delete(listener); - } - } - } - async connectedCallback() { - this.$$connected = true; - if (!this.$$component) { - // We wait one tick to let possible child slot elements be created/mounted - await Promise.resolve(); - if (!this.$$connected) { - return; - } - function create_slot(name) { - return () => { - let node; - const obj = { - c: function create() { - node = document.createElement('slot'); - if (name !== 'default') { - node.setAttribute('name', name); - } - }, - m: function mount(target, anchor) { - insert(target, node, anchor); - }, - d: function destroy(detaching) { - if (detaching) { - detach(node); - } - } - }; - return obj; - }; - } - const $$slots = {}; - const existing_slots = get_custom_elements_slots(this); - for (const name of this.$$slots) { - if (name in existing_slots) { - $$slots[name] = [create_slot(name)]; - } - } - for (const attribute of this.attributes) { - // this.$$data takes precedence over this.attributes - const name = this.$$get_prop_name(attribute.name); - if (!(name in this.$$data)) { - this.$$data[name] = get_custom_element_value(name, attribute.value, this.$$props_definition, 'toProp'); - } - } - this.$$component = new this.$$componentCtor({ - target: this.shadowRoot || this, - props: { - ...this.$$data, - $$slots, - $$scope: { - ctx: [] - } - } - }); - for (const type in this.$$listeners) { - for (const listener of this.$$listeners[type]) { - const unsub = this.$$component.$on(type, listener); - this.$$listener_unsubscribe_fns.set(listener, unsub); - } - } - this.$$listeners = {}; - } - } - // We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte - // and setting attributes through setAttribute etc, this is helpful - attributeChangedCallback(attr, _oldValue, newValue) { - if (this.$$reflecting) - return; - attr = this.$$get_prop_name(attr); - this.$$data[attr] = get_custom_element_value(attr, newValue, this.$$props_definition, 'toProp'); - this.$$component.$set({ [attr]: this.$$data[attr] }); - } - disconnectedCallback() { - this.$$connected = false; - // In a microtask, because this could be a move within the DOM - Promise.resolve().then(() => { - if (!this.$$connected) { - this.$$component.$destroy(); - this.$$component = undefined; - } - }); - } - $$get_prop_name(attribute_name) { - return (Object.keys(this.$$props_definition).find((key) => this.$$props_definition[key].attribute === attribute_name || - (!this.$$props_definition[key].attribute && key.toLowerCase() === attribute_name)) || attribute_name); - } - }; + SvelteElement = class extends HTMLElement { + $$componentCtor; + $$slots; + $$component; + $$connected = false; + $$data = {}; + $$reflecting = false; + $$props_definition = {}; + $$listeners = {}; + $$listener_unsubscribe_fns = new Map(); + constructor($$componentCtor, $$slots, use_shadow_dom) { + super(); + this.$$componentCtor = $$componentCtor; + this.$$slots = $$slots; + if (use_shadow_dom) { + this.attachShadow({ mode: 'open' }); + } + } + addEventListener(type, listener, options) { + // We can't determine upfront if the event is a custom event or not, so we have to + // listen to both. If someone uses a custom event with the same name as a regular + // browser event, this fires twice - we can't avoid that. + this.$$listeners[type] = this.$$listeners[type] || []; + this.$$listeners[type].push(listener); + if (this.$$component) { + const unsub = this.$$component.$on(type, listener); + this.$$listener_unsubscribe_fns.set(listener, unsub); + } + super.addEventListener(type, listener, options); + } + removeEventListener(type, listener, options) { + super.removeEventListener(type, listener, options); + if (this.$$component) { + const unsub = this.$$listener_unsubscribe_fns.get(listener); + if (unsub) { + unsub(); + this.$$listener_unsubscribe_fns.delete(listener); + } + } + } + async connectedCallback() { + this.$$connected = true; + if (!this.$$component) { + // We wait one tick to let possible child slot elements be created/mounted + await Promise.resolve(); + if (!this.$$connected) { + return; + } + function create_slot(name) { + return () => { + let node; + const obj = { + c: function create() { + node = document.createElement('slot'); + if (name !== 'default') { + node.setAttribute('name', name); + } + }, + m: function mount(target, anchor) { + insert(target, node, anchor); + }, + d: function destroy(detaching) { + if (detaching) { + detach(node); + } + } + }; + return obj; + }; + } + const $$slots = {}; + const existing_slots = get_custom_elements_slots(this); + for (const name of this.$$slots) { + if (name in existing_slots) { + $$slots[name] = [create_slot(name)]; + } + } + for (const attribute of this.attributes) { + // this.$$data takes precedence over this.attributes + const name = this.$$get_prop_name(attribute.name); + if (!(name in this.$$data)) { + this.$$data[name] = get_custom_element_value( + name, + attribute.value, + this.$$props_definition, + 'toProp' + ); + } + } + this.$$component = new this.$$componentCtor({ + target: this.shadowRoot || this, + props: { + ...this.$$data, + $$slots, + $$scope: { + ctx: [] + } + } + }); + for (const type in this.$$listeners) { + for (const listener of this.$$listeners[type]) { + const unsub = this.$$component.$on(type, listener); + this.$$listener_unsubscribe_fns.set(listener, unsub); + } + } + this.$$listeners = {}; + } + } + // We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte + // and setting attributes through setAttribute etc, this is helpful + attributeChangedCallback(attr, _oldValue, newValue) { + if (this.$$reflecting) return; + attr = this.$$get_prop_name(attr); + this.$$data[attr] = get_custom_element_value( + attr, + newValue, + this.$$props_definition, + 'toProp' + ); + this.$$component.$set({ [attr]: this.$$data[attr] }); + } + disconnectedCallback() { + this.$$connected = false; + // In a microtask, because this could be a move within the DOM + Promise.resolve().then(() => { + if (!this.$$connected) { + this.$$component.$destroy(); + this.$$component = undefined; + } + }); + } + $$get_prop_name(attribute_name) { + return ( + Object.keys(this.$$props_definition).find( + (key) => + this.$$props_definition[key].attribute === attribute_name || + (!this.$$props_definition[key].attribute && key.toLowerCase() === attribute_name) + ) || attribute_name + ); + } + }; } /** @param {string} prop * @param {any} value @@ -265,37 +296,35 @@ if (typeof HTMLElement === 'function') { * @returns {any} */ function get_custom_element_value(prop, value, props_definition, transform) { - const type = props_definition[prop]?.type; - value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value; - if (!transform || !props_definition[prop]) { - return value; - } - else if (transform === 'toAttribute') { - switch (type) { - case 'Object': - case 'Array': - return value == null ? null : JSON.stringify(value); - case 'Boolean': - return value ? '' : null; - case 'Number': - return value == null ? null : value; - default: - return value; - } - } - else { - switch (type) { - case 'Object': - case 'Array': - return value && JSON.parse(value); - case 'Boolean': - return value; // conversion already handled above - case 'Number': - return value != null ? +value : value; - default: - return value; - } - } + const type = props_definition[prop]?.type; + value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value; + if (!transform || !props_definition[prop]) { + return value; + } else if (transform === 'toAttribute') { + switch (type) { + case 'Object': + case 'Array': + return value == null ? null : JSON.stringify(value); + case 'Boolean': + return value ? '' : null; + case 'Number': + return value == null ? null : value; + default: + return value; + } + } else { + switch (type) { + case 'Object': + case 'Array': + return value && JSON.parse(value); + case 'Boolean': + return value; // conversion already handled above + case 'Number': + return value != null ? +value : value; + default: + return value; + } + } } /** * @internal @@ -308,92 +337,100 @@ function get_custom_element_value(prop, value, props_definition, transform) { * @param {boolean} use_shadow_dom Whether to use shadow DOM * @returns {Class} A custom element class */ -export function create_custom_element(Component, props_definition, slots, accessors, use_shadow_dom) { - const Class = class extends SvelteElement { - constructor() { - super(Component, slots, use_shadow_dom); - this.$$props_definition = props_definition; - } - static get observedAttributes() { - return Object.keys(props_definition).map((key) => (props_definition[key].attribute || key).toLowerCase()); - } - }; - Object.keys(props_definition).forEach((prop) => { - Object.defineProperty(Class.prototype, prop, { - get() { - return this.$$component && prop in this.$$component - ? this.$$component[prop] - : this.$$data[prop]; - }, - set(value) { - value = get_custom_element_value(prop, value, props_definition); - this.$$data[prop] = value; - this.$$component?.$set({ [prop]: value }); - if (props_definition[prop].reflect) { - this.$$reflecting = true; - const attribute_value = get_custom_element_value(prop, value, props_definition, 'toAttribute'); - if (attribute_value == null) { - this.removeAttribute(prop); - } - else { - this.setAttribute(props_definition[prop].attribute || prop, attribute_value); - } - this.$$reflecting = false; - } - } - }); - }); - accessors.forEach((accessor) => { - Object.defineProperty(Class.prototype, accessor, { - get() { - return this.$$component?.[accessor]; - } - }); - }); - Component.element = Class; - return Class; +export function create_custom_element( + Component, + props_definition, + slots, + accessors, + use_shadow_dom +) { + const Class = class extends SvelteElement { + constructor() { + super(Component, slots, use_shadow_dom); + this.$$props_definition = props_definition; + } + static get observedAttributes() { + return Object.keys(props_definition).map((key) => + (props_definition[key].attribute || key).toLowerCase() + ); + } + }; + Object.keys(props_definition).forEach((prop) => { + Object.defineProperty(Class.prototype, prop, { + get() { + return this.$$component && prop in this.$$component + ? this.$$component[prop] + : this.$$data[prop]; + }, + set(value) { + value = get_custom_element_value(prop, value, props_definition); + this.$$data[prop] = value; + this.$$component?.$set({ [prop]: value }); + if (props_definition[prop].reflect) { + this.$$reflecting = true; + const attribute_value = get_custom_element_value( + prop, + value, + props_definition, + 'toAttribute' + ); + if (attribute_value == null) { + this.removeAttribute(prop); + } else { + this.setAttribute(props_definition[prop].attribute || prop, attribute_value); + } + this.$$reflecting = false; + } + } + }); + }); + accessors.forEach((accessor) => { + Object.defineProperty(Class.prototype, accessor, { + get() { + return this.$$component?.[accessor]; + } + }); + }); + Component.element = Class; + return Class; } /** * Base class for Svelte components. Used when dev=false. */ export class SvelteComponent { - /** */ - $$ = undefined; - /** */ - $$set = undefined; - /** @returns {void} */ - $destroy() { - destroy_component(this, 1); - this.$destroy = noop; - } - /** @returns {any} */ - $on(type, callback) { - if (!is_function(callback)) { - return noop; - } - const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []); - callbacks.push(callback); - return () => { - const index = callbacks.indexOf(callback); - if (index !== -1) - callbacks.splice(index, 1); - }; - } - /** @returns {void} */ - $set($$props) { - if (this.$$set && !is_empty($$props)) { - this.$$.skip_bound = true; - this.$$set($$props); - this.$$.skip_bound = false; - } - } + /** */ + $$ = undefined; + /** */ + $$set = undefined; + /** @returns {void} */ + $destroy() { + destroy_component(this, 1); + this.$destroy = noop; + } + /** @returns {any} */ + $on(type, callback) { + if (!is_function(callback)) { + return noop; + } + const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []); + callbacks.push(callback); + return () => { + const index = callbacks.indexOf(callback); + if (index !== -1) callbacks.splice(index, 1); + }; + } + /** @returns {void} */ + $set($$props) { + if (this.$$set && !is_empty($$props)) { + this.$$.skip_bound = true; + this.$$set($$props); + this.$$.skip_bound = false; + } + } } - - - /** @typedef {Object} CustomElementPropDefinition * @property {string} [attribute] * @property {boolean} [reflect] - * @property {'String'|'Boolean'|'Number'|'Array'|'Object'} [type] - */ \ No newline at end of file + * @property {'String'|'Boolean'|'Number'|'Array'|'Object'} [type] + */