this repo has no description
1import {
2 add_render_callback,
3 flush,
4 flush_render_callbacks,
5 schedule_update,
6 dirty_components
7} from './scheduler.js';
8import { current_component, set_current_component } from './lifecycle.js';
9import { blank_object, is_empty, is_function, run, run_all, noop } from './utils.js';
10import {
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';
20import { transition_in } from './transitions.js';
21
22/** @returns {void} */
23export 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} */
32export function create_component(block) {
33 block && block.c();
34}
35
36/** @returns {void} */
37export function claim_component(block, parent_nodes) {
38 block && block.l(parent_nodes);
39}
40
41/** @returns {void} */
42export 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} */
64export 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} */
78function 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 */
98export 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
169export let SvelteElement;
170
171if (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 */
369function 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 */
412export 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 */
464export 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 */