this repo has no description
at main 524 lines 14 kB view raw
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 */