/ node_modules / svelte / src / runtime / internal / Component.js
Component.js
  1  import {
  2  	add_render_callback,
  3  	flush,
  4  	flush_render_callbacks,
  5  	schedule_update,
  6  	dirty_components
  7  } from './scheduler.js';
  8  import { current_component, set_current_component } from './lifecycle.js';
  9  import { blank_object, is_empty, is_function, run, run_all, noop } from './utils.js';
 10  import {
 11  	children,
 12  	detach,
 13  	start_hydrating,
 14  	end_hydrating,
 15  	get_custom_elements_slots,
 16  	insert,
 17  	element,
 18  	attr
 19  } from './dom.js';
 20  import { transition_in } from './transitions.js';
 21  
 22  /** @returns {void} */
 23  export function bind(component, name, callback) {
 24  	const index = component.$$.props[name];
 25  	if (index !== undefined) {
 26  		component.$$.bound[index] = callback;
 27  		callback(component.$$.ctx[index]);
 28  	}
 29  }
 30  
 31  /** @returns {void} */
 32  export function create_component(block) {
 33  	block && block.c();
 34  }
 35  
 36  /** @returns {void} */
 37  export function claim_component(block, parent_nodes) {
 38  	block && block.l(parent_nodes);
 39  }
 40  
 41  /** @returns {void} */
 42  export function mount_component(component, target, anchor) {
 43  	const { fragment, after_update } = component.$$;
 44  	fragment && fragment.m(target, anchor);
 45  	// onMount happens before the initial afterUpdate
 46  	add_render_callback(() => {
 47  		const new_on_destroy = component.$$.on_mount.map(run).filter(is_function);
 48  		// if the component was destroyed immediately
 49  		// it will update the `$$.on_destroy` reference to `null`.
 50  		// the destructured on_destroy may still reference to the old array
 51  		if (component.$$.on_destroy) {
 52  			component.$$.on_destroy.push(...new_on_destroy);
 53  		} else {
 54  			// Edge case - component was destroyed immediately,
 55  			// most likely as a result of a binding initialising
 56  			run_all(new_on_destroy);
 57  		}
 58  		component.$$.on_mount = [];
 59  	});
 60  	after_update.forEach(add_render_callback);
 61  }
 62  
 63  /** @returns {void} */
 64  export function destroy_component(component, detaching) {
 65  	const $$ = component.$$;
 66  	if ($$.fragment !== null) {
 67  		flush_render_callbacks($$.after_update);
 68  		run_all($$.on_destroy);
 69  		$$.fragment && $$.fragment.d(detaching);
 70  		// TODO null out other refs, including component.$$ (but need to
 71  		// preserve final state?)
 72  		$$.on_destroy = $$.fragment = null;
 73  		$$.ctx = [];
 74  	}
 75  }
 76  
 77  /** @returns {void} */
 78  function make_dirty(component, i) {
 79  	if (component.$$.dirty[0] === -1) {
 80  		dirty_components.push(component);
 81  		schedule_update();
 82  		component.$$.dirty.fill(0);
 83  	}
 84  	component.$$.dirty[(i / 31) | 0] |= 1 << i % 31;
 85  }
 86  
 87  // TODO: Document the other params
 88  /**
 89   * @param {SvelteComponent} component
 90   * @param {import('./public.js').ComponentConstructorOptions} options
 91   *
 92   * @param {import('./utils.js')['not_equal']} not_equal Used to compare props and state values.
 93   * @param {(target: Element | ShadowRoot) => void} [append_styles] Function that appends styles to the DOM when the component is first initialised.
 94   * This will be the `add_css` function from the compiled component.
 95   *
 96   * @returns {void}
 97   */
 98  export function init(
 99  	component,
100  	options,
101  	instance,
102  	create_fragment,
103  	not_equal,
104  	props,
105  	append_styles = null,
106  	dirty = [-1]
107  ) {
108  	const parent_component = current_component;
109  	set_current_component(component);
110  	/** @type {import('./private.js').T$$} */
111  	const $$ = (component.$$ = {
112  		fragment: null,
113  		ctx: [],
114  		// state
115  		props,
116  		update: noop,
117  		not_equal,
118  		bound: blank_object(),
119  		// lifecycle
120  		on_mount: [],
121  		on_destroy: [],
122  		on_disconnect: [],
123  		before_update: [],
124  		after_update: [],
125  		context: new Map(options.context || (parent_component ? parent_component.$$.context : [])),
126  		// everything else
127  		callbacks: blank_object(),
128  		dirty,
129  		skip_bound: false,
130  		root: options.target || parent_component.$$.root
131  	});
132  	append_styles && append_styles($$.root);
133  	let ready = false;
134  	$$.ctx = instance
135  		? instance(component, options.props || {}, (i, ret, ...rest) => {
136  				const value = rest.length ? rest[0] : ret;
137  				if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) {
138  					if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
139  					if (ready) make_dirty(component, i);
140  				}
141  				return ret;
142  		  })
143  		: [];
144  	$$.update();
145  	ready = true;
146  	run_all($$.before_update);
147  	// `false` as a special case of no DOM component
148  	$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
149  	if (options.target) {
150  		if (options.hydrate) {
151  			start_hydrating();
152  			// TODO: what is the correct type here?
153  			// @ts-expect-error
154  			const nodes = children(options.target);
155  			$$.fragment && $$.fragment.l(nodes);
156  			nodes.forEach(detach);
157  		} else {
158  			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
159  			$$.fragment && $$.fragment.c();
160  		}
161  		if (options.intro) transition_in(component.$$.fragment);
162  		mount_component(component, options.target, options.anchor);
163  		end_hydrating();
164  		flush();
165  	}
166  	set_current_component(parent_component);
167  }
168  
169  export let SvelteElement;
170  
171  if (typeof HTMLElement === 'function') {
172  	SvelteElement = class extends HTMLElement {
173  		/** The Svelte component constructor */
174  		$$ctor;
175  		/** Slots */
176  		$$s;
177  		/** The Svelte component instance */
178  		$$c;
179  		/** Whether or not the custom element is connected */
180  		$$cn = false;
181  		/** Component props data */
182  		$$d = {};
183  		/** `true` if currently in the process of reflecting component props back to attributes */
184  		$$r = false;
185  		/** @type {Record<string, CustomElementPropDefinition>} Props definition (name, reflected, type etc) */
186  		$$p_d = {};
187  		/** @type {Record<string, Function[]>} Event listeners */
188  		$$l = {};
189  		/** @type {Map<Function, Function>} Event listener unsubscribe functions */
190  		$$l_u = new Map();
191  
192  		constructor($$componentCtor, $$slots, use_shadow_dom) {
193  			super();
194  			this.$$ctor = $$componentCtor;
195  			this.$$s = $$slots;
196  			if (use_shadow_dom) {
197  				this.attachShadow({ mode: 'open' });
198  			}
199  		}
200  
201  		addEventListener(type, listener, options) {
202  			// We can't determine upfront if the event is a custom event or not, so we have to
203  			// listen to both. If someone uses a custom event with the same name as a regular
204  			// browser event, this fires twice - we can't avoid that.
205  			this.$$l[type] = this.$$l[type] || [];
206  			this.$$l[type].push(listener);
207  			if (this.$$c) {
208  				const unsub = this.$$c.$on(type, listener);
209  				this.$$l_u.set(listener, unsub);
210  			}
211  			super.addEventListener(type, listener, options);
212  		}
213  
214  		removeEventListener(type, listener, options) {
215  			super.removeEventListener(type, listener, options);
216  			if (this.$$c) {
217  				const unsub = this.$$l_u.get(listener);
218  				if (unsub) {
219  					unsub();
220  					this.$$l_u.delete(listener);
221  				}
222  			}
223  			if (this.$$l[type]) {
224  				const idx = this.$$l[type].indexOf(listener);
225  				if (idx >= 0) {
226  					this.$$l[type].splice(idx, 1);
227  				}
228  			}
229  		}
230  
231  		async connectedCallback() {
232  			this.$$cn = true;
233  			if (!this.$$c) {
234  				// We wait one tick to let possible child slot elements be created/mounted
235  				await Promise.resolve();
236  				if (!this.$$cn || this.$$c) {
237  					return;
238  				}
239  				function create_slot(name) {
240  					return () => {
241  						let node;
242  						const obj = {
243  							c: function create() {
244  								node = element('slot');
245  								if (name !== 'default') {
246  									attr(node, 'name', name);
247  								}
248  							},
249  							/**
250  							 * @param {HTMLElement} target
251  							 * @param {HTMLElement} [anchor]
252  							 */
253  							m: function mount(target, anchor) {
254  								insert(target, node, anchor);
255  							},
256  							d: function destroy(detaching) {
257  								if (detaching) {
258  									detach(node);
259  								}
260  							}
261  						};
262  						return obj;
263  					};
264  				}
265  				const $$slots = {};
266  				const existing_slots = get_custom_elements_slots(this);
267  				for (const name of this.$$s) {
268  					if (name in existing_slots) {
269  						$$slots[name] = [create_slot(name)];
270  					}
271  				}
272  				for (const attribute of this.attributes) {
273  					// this.$$data takes precedence over this.attributes
274  					const name = this.$$g_p(attribute.name);
275  					if (!(name in this.$$d)) {
276  						this.$$d[name] = get_custom_element_value(name, attribute.value, this.$$p_d, 'toProp');
277  					}
278  				}
279  				// Port over props that were set programmatically before ce was initialized
280  				for (const key in this.$$p_d) {
281  					if (!(key in this.$$d) && this[key] !== undefined) {
282  						this.$$d[key] = this[key]; // don't transform, these were set through JavaScript
283  						delete this[key]; // remove the property that shadows the getter/setter
284  					}
285  				}
286  				this.$$c = new this.$$ctor({
287  					target: this.shadowRoot || this,
288  					props: {
289  						...this.$$d,
290  						$$slots,
291  						$$scope: {
292  							ctx: []
293  						}
294  					}
295  				});
296  
297  				// Reflect component props as attributes
298  				const reflect_attributes = () => {
299  					this.$$r = true;
300  					for (const key in this.$$p_d) {
301  						this.$$d[key] = this.$$c.$$.ctx[this.$$c.$$.props[key]];
302  						if (this.$$p_d[key].reflect) {
303  							const attribute_value = get_custom_element_value(
304  								key,
305  								this.$$d[key],
306  								this.$$p_d,
307  								'toAttribute'
308  							);
309  							if (attribute_value == null) {
310  								this.removeAttribute(this.$$p_d[key].attribute || key);
311  							} else {
312  								this.setAttribute(this.$$p_d[key].attribute || key, attribute_value);
313  							}
314  						}
315  					}
316  					this.$$r = false;
317  				};
318  				this.$$c.$$.after_update.push(reflect_attributes);
319  				reflect_attributes(); // once initially because after_update is added too late for first render
320  
321  				for (const type in this.$$l) {
322  					for (const listener of this.$$l[type]) {
323  						const unsub = this.$$c.$on(type, listener);
324  						this.$$l_u.set(listener, unsub);
325  					}
326  				}
327  				this.$$l = {};
328  			}
329  		}
330  
331  		// We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte
332  		// and setting attributes through setAttribute etc, this is helpful
333  		attributeChangedCallback(attr, _oldValue, newValue) {
334  			if (this.$$r) return;
335  			attr = this.$$g_p(attr);
336  			this.$$d[attr] = get_custom_element_value(attr, newValue, this.$$p_d, 'toProp');
337  			this.$$c?.$set({ [attr]: this.$$d[attr] });
338  		}
339  
340  		disconnectedCallback() {
341  			this.$$cn = false;
342  			// In a microtask, because this could be a move within the DOM
343  			Promise.resolve().then(() => {
344  				if (!this.$$cn && this.$$c) {
345  					this.$$c.$destroy();
346  					this.$$c = undefined;
347  				}
348  			});
349  		}
350  
351  		$$g_p(attribute_name) {
352  			return (
353  				Object.keys(this.$$p_d).find(
354  					(key) =>
355  						this.$$p_d[key].attribute === attribute_name ||
356  						(!this.$$p_d[key].attribute && key.toLowerCase() === attribute_name)
357  				) || attribute_name
358  			);
359  		}
360  	};
361  }
362  
363  /**
364   * @param {string} prop
365   * @param {any} value
366   * @param {Record<string, CustomElementPropDefinition>} props_definition
367   * @param {'toAttribute' | 'toProp'} [transform]
368   */
369  function get_custom_element_value(prop, value, props_definition, transform) {
370  	const type = props_definition[prop]?.type;
371  	value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value;
372  	if (!transform || !props_definition[prop]) {
373  		return value;
374  	} else if (transform === 'toAttribute') {
375  		switch (type) {
376  			case 'Object':
377  			case 'Array':
378  				return value == null ? null : JSON.stringify(value);
379  			case 'Boolean':
380  				return value ? '' : null;
381  			case 'Number':
382  				return value == null ? null : value;
383  			default:
384  				return value;
385  		}
386  	} else {
387  		switch (type) {
388  			case 'Object':
389  			case 'Array':
390  				return value && JSON.parse(value);
391  			case 'Boolean':
392  				return value; // conversion already handled above
393  			case 'Number':
394  				return value != null ? +value : value;
395  			default:
396  				return value;
397  		}
398  	}
399  }
400  
401  /**
402   * @internal
403   *
404   * Turn a Svelte component into a custom element.
405   * @param {import('./public.js').ComponentType} Component  A Svelte component constructor
406   * @param {Record<string, CustomElementPropDefinition>} props_definition  The props to observe
407   * @param {string[]} slots  The slots to create
408   * @param {string[]} accessors  Other accessors besides the ones for props the component has
409   * @param {boolean} use_shadow_dom  Whether to use shadow DOM
410   * @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend]
411   */
412  export function create_custom_element(
413  	Component,
414  	props_definition,
415  	slots,
416  	accessors,
417  	use_shadow_dom,
418  	extend
419  ) {
420  	let Class = class extends SvelteElement {
421  		constructor() {
422  			super(Component, slots, use_shadow_dom);
423  			this.$$p_d = props_definition;
424  		}
425  		static get observedAttributes() {
426  			return Object.keys(props_definition).map((key) =>
427  				(props_definition[key].attribute || key).toLowerCase()
428  			);
429  		}
430  	};
431  	Object.keys(props_definition).forEach((prop) => {
432  		Object.defineProperty(Class.prototype, prop, {
433  			get() {
434  				return this.$$c && prop in this.$$c ? this.$$c[prop] : this.$$d[prop];
435  			},
436  			set(value) {
437  				value = get_custom_element_value(prop, value, props_definition);
438  				this.$$d[prop] = value;
439  				this.$$c?.$set({ [prop]: value });
440  			}
441  		});
442  	});
443  	accessors.forEach((accessor) => {
444  		Object.defineProperty(Class.prototype, accessor, {
445  			get() {
446  				return this.$$c?.[accessor];
447  			}
448  		});
449  	});
450  	if (extend) {
451  		// @ts-expect-error - assigning here is fine
452  		Class = extend(Class);
453  	}
454  	Component.element = /** @type {any} */ (Class);
455  	return Class;
456  }
457  
458  /**
459   * Base class for Svelte components. Used when dev=false.
460   *
461   * @template {Record<string, any>} [Props=any]
462   * @template {Record<string, any>} [Events=any]
463   */
464  export class SvelteComponent {
465  	/**
466  	 * ### PRIVATE API
467  	 *
468  	 * Do not use, may change at any time
469  	 *
470  	 * @type {any}
471  	 */
472  	$$ = undefined;
473  	/**
474  	 * ### PRIVATE API
475  	 *
476  	 * Do not use, may change at any time
477  	 *
478  	 * @type {any}
479  	 */
480  	$$set = undefined;
481  
482  	/** @returns {void} */
483  	$destroy() {
484  		destroy_component(this, 1);
485  		this.$destroy = noop;
486  	}
487  
488  	/**
489  	 * @template {Extract<keyof Events, string>} K
490  	 * @param {K} type
491  	 * @param {((e: Events[K]) => void) | null | undefined} callback
492  	 * @returns {() => void}
493  	 */
494  	$on(type, callback) {
495  		if (!is_function(callback)) {
496  			return noop;
497  		}
498  		const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
499  		callbacks.push(callback);
500  		return () => {
501  			const index = callbacks.indexOf(callback);
502  			if (index !== -1) callbacks.splice(index, 1);
503  		};
504  	}
505  
506  	/**
507  	 * @param {Partial<Props>} props
508  	 * @returns {void}
509  	 */
510  	$set(props) {
511  		if (this.$$set && !is_empty(props)) {
512  			this.$$.skip_bound = true;
513  			this.$$set(props);
514  			this.$$.skip_bound = false;
515  		}
516  	}
517  }
518  
519  /**
520   * @typedef {Object} CustomElementPropDefinition
521   * @property {string} [attribute]
522   * @property {boolean} [reflect]
523   * @property {'String'|'Boolean'|'Number'|'Array'|'Object'} [type]
524   */