diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/svelte/src/runtime/internal/Component.js | |
init commit
Diffstat (limited to 'node_modules/svelte/src/runtime/internal/Component.js')
| -rw-r--r-- | node_modules/svelte/src/runtime/internal/Component.js | 524 |
1 files changed, 524 insertions, 0 deletions
diff --git a/node_modules/svelte/src/runtime/internal/Component.js b/node_modules/svelte/src/runtime/internal/Component.js new file mode 100644 index 0000000..bc2c83d --- /dev/null +++ b/node_modules/svelte/src/runtime/internal/Component.js @@ -0,0 +1,524 @@ +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<string, CustomElementPropDefinition>} Props definition (name, reflected, type etc) */ + $$p_d = {}; + /** @type {Record<string, Function[]>} Event listeners */ + $$l = {}; + /** @type {Map<Function, Function>} 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); + } + } + if (this.$$l[type]) { + const idx = this.$$l[type].indexOf(listener); + if (idx >= 0) { + this.$$l[type].splice(idx, 1); + } + } + } + + 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<string, CustomElementPropDefinition>} 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<string, CustomElementPropDefinition>} 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<string, any>} [Props=any] + * @template {Record<string, any>} [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<keyof Events, string>} 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>} 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] + */ |
