import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler.js'; import { current_component, set_current_component } from './lifecycle.js'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils.js'; import { children, detach, start_hydrating, end_hydrating, get_custom_elements_slots, insert, element, attr } from './dom.js'; import { transition_in } from './transitions.js'; /** @returns {void} */ export function bind(component, name, callback) { 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(); } /** @returns {void} */ export function claim_component(block, 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); } /** @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 = []; } } /** @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; } // TODO: Document the other params /** * @param {SvelteComponent} component * @param {import('./public.js').ComponentConstructorOptions} options * * @param {import('./utils.js')['not_equal']} not_equal Used to compare props and state values. * @param {(target: Element | ShadowRoot) => void} [append_styles] Function that appends styles to the DOM when the component is first initialised. * This will be the `add_css` function from the compiled component. * * @returns {void} */ export function init( component, options, instance, create_fragment, not_equal, props, append_styles = null, dirty = [-1] ) { const parent_component = current_component; set_current_component(component); /** @type {import('./private.js').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(); // TODO: what is the correct type here? // @ts-expect-error const nodes = children(options.target); $$.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 { /** The Svelte component constructor */ $$ctor; /** Slots */ $$s; /** The Svelte component instance */ $$c; /** Whether or not the custom element is connected */ $$cn = false; /** Component props data */ $$d = {}; /** `true` if currently in the process of reflecting component props back to attributes */ $$r = false; /** @type {Record} Props definition (name, reflected, type etc) */ $$p_d = {}; /** @type {Record} Event listeners */ $$l = {}; /** @type {Map} Event listener unsubscribe functions */ $$l_u = new Map(); constructor($$componentCtor, $$slots, use_shadow_dom) { super(); this.$$ctor = $$componentCtor; this.$$s = $$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.$$l[type] = this.$$l[type] || []; this.$$l[type].push(listener); if (this.$$c) { const unsub = this.$$c.$on(type, listener); this.$$l_u.set(listener, unsub); } super.addEventListener(type, listener, options); } removeEventListener(type, listener, options) { super.removeEventListener(type, listener, options); if (this.$$c) { const unsub = this.$$l_u.get(listener); if (unsub) { unsub(); this.$$l_u.delete(listener); } } } async connectedCallback() { this.$$cn = true; if (!this.$$c) { // We wait one tick to let possible child slot elements be created/mounted await Promise.resolve(); if (!this.$$cn || this.$$c) { return; } function create_slot(name) { return () => { let node; const obj = { c: function create() { node = element('slot'); if (name !== 'default') { attr(node, 'name', name); } }, /** * @param {HTMLElement} target * @param {HTMLElement} [anchor] */ 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.$$s) { 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.$$g_p(attribute.name); if (!(name in this.$$d)) { this.$$d[name] = get_custom_element_value(name, attribute.value, this.$$p_d, 'toProp'); } } // Port over props that were set programmatically before ce was initialized for (const key in this.$$p_d) { if (!(key in this.$$d) && this[key] !== undefined) { this.$$d[key] = this[key]; // don't transform, these were set through JavaScript delete this[key]; // remove the property that shadows the getter/setter } } this.$$c = new this.$$ctor({ target: this.shadowRoot || this, props: { ...this.$$d, $$slots, $$scope: { ctx: [] } } }); // Reflect component props as attributes const reflect_attributes = () => { this.$$r = true; for (const key in this.$$p_d) { this.$$d[key] = this.$$c.$$.ctx[this.$$c.$$.props[key]]; if (this.$$p_d[key].reflect) { const attribute_value = get_custom_element_value( key, this.$$d[key], this.$$p_d, 'toAttribute' ); if (attribute_value == null) { this.removeAttribute(this.$$p_d[key].attribute || key); } else { this.setAttribute(this.$$p_d[key].attribute || key, attribute_value); } } } this.$$r = false; }; this.$$c.$$.after_update.push(reflect_attributes); reflect_attributes(); // once initially because after_update is added too late for first render for (const type in this.$$l) { for (const listener of this.$$l[type]) { const unsub = this.$$c.$on(type, listener); this.$$l_u.set(listener, unsub); } } this.$$l = {}; } } // 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.$$r) return; attr = this.$$g_p(attr); this.$$d[attr] = get_custom_element_value(attr, newValue, this.$$p_d, 'toProp'); this.$$c?.$set({ [attr]: this.$$d[attr] }); } disconnectedCallback() { this.$$cn = false; // In a microtask, because this could be a move within the DOM Promise.resolve().then(() => { if (!this.$$cn && this.$$c) { this.$$c.$destroy(); this.$$c = undefined; } }); } $$g_p(attribute_name) { return ( Object.keys(this.$$p_d).find( (key) => this.$$p_d[key].attribute === attribute_name || (!this.$$p_d[key].attribute && key.toLowerCase() === attribute_name) ) || attribute_name ); } }; } /** * @param {string} prop * @param {any} value * @param {Record} props_definition * @param {'toAttribute' | 'toProp'} [transform] */ 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; } } } /** * @internal * * Turn a Svelte component into a custom element. * @param {import('./public.js').ComponentType} Component A Svelte component constructor * @param {Record} props_definition The props to observe * @param {string[]} slots The slots to create * @param {string[]} accessors Other accessors besides the ones for props the component has * @param {boolean} use_shadow_dom Whether to use shadow DOM * @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend] */ export function create_custom_element( Component, props_definition, slots, accessors, use_shadow_dom, extend ) { let Class = class extends SvelteElement { constructor() { super(Component, slots, use_shadow_dom); this.$$p_d = 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.$$c && prop in this.$$c ? this.$$c[prop] : this.$$d[prop]; }, set(value) { value = get_custom_element_value(prop, value, props_definition); this.$$d[prop] = value; this.$$c?.$set({ [prop]: value }); } }); }); accessors.forEach((accessor) => { Object.defineProperty(Class.prototype, accessor, { get() { return this.$$c?.[accessor]; } }); }); if (extend) { // @ts-expect-error - assigning here is fine Class = extend(Class); } Component.element = /** @type {any} */ (Class); return Class; } /** * Base class for Svelte components. Used when dev=false. * * @template {Record} [Props=any] * @template {Record} [Events=any] */ export class SvelteComponent { /** * ### PRIVATE API * * Do not use, may change at any time * * @type {any} */ $$ = undefined; /** * ### PRIVATE API * * Do not use, may change at any time * * @type {any} */ $$set = undefined; /** @returns {void} */ $destroy() { destroy_component(this, 1); this.$destroy = noop; } /** * @template {Extract} K * @param {K} type * @param {((e: Events[K]) => void) | null | undefined} callback * @returns {() => void} */ $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); }; } /** * @param {Partial} props * @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] */