mirror of https://github.com/sveltejs/svelte
commit
a825d457ab
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve bind:this support around proxyied state
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: add reactive Set class to svelte/reactivity
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
perf: bail early when traversing non-state
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: better ownership mutation validation
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: bump specificity on all members of a selector list
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
breaking: preserve slots inside templates with a shadowrootmode attribute
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
breaking: make `$props()` rune non-generic
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve namespace inference when having `{@render}` and `{@html}` tags
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: don't collapse whitespace within text nodes
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: improve ssr html mismatch validation
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: invalidate store when mutated inside each block
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore: custom elements validation
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve event delegation with shadowed bindings
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: prevent reactive statement reruns when they have indirect cyclic dependencies
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: add reactive Date object to svelte/reactivity
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve ssr output of dynamic textarea elements
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: make `set.has(...)` granular for existing properties'
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: ensure performance.now() and requestAnimationFrame() are polyfilled in ssr
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: handle TypeScript's optional parameter syntax in snippets
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve ssr code generation for class property $derived
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: use getters for derived class state fields, with memoisation
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: adjust keyed each block equality handling
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: eagerly unsubscribe when store is changed
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: warn when `$props` rune not called
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: error when exporting reassigned state from module context
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve internal render effect sequencing
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve indexed each equality
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve derived rune destructuring support
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: adjust render effect ordering
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: allow arbitrary call expressions and optional chaining for snippets
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: add `$set` and `$on` methods in legacy compat mode
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: deduplicate generated props and action arg names
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: prevent snippet children conflict
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve deep_read performance
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: handle component binding mutation
|
||||||
@ -1,93 +0,0 @@
|
|||||||
import {
|
|
||||||
ROOT_BLOCK,
|
|
||||||
HEAD_BLOCK,
|
|
||||||
DYNAMIC_ELEMENT_BLOCK,
|
|
||||||
DYNAMIC_COMPONENT_BLOCK,
|
|
||||||
SNIPPET_BLOCK
|
|
||||||
} from './constants.js';
|
|
||||||
import { current_block } from './runtime.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} intro
|
|
||||||
* @returns {import('./types.js').RootBlock}
|
|
||||||
*/
|
|
||||||
export function create_root_block(intro) {
|
|
||||||
return {
|
|
||||||
// dom
|
|
||||||
d: null,
|
|
||||||
// effect
|
|
||||||
e: null,
|
|
||||||
// intro
|
|
||||||
i: intro,
|
|
||||||
// parent
|
|
||||||
p: null,
|
|
||||||
// transition
|
|
||||||
r: null,
|
|
||||||
// type
|
|
||||||
t: ROOT_BLOCK
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {import('./types.js').HeadBlock} */
|
|
||||||
export function create_head_block() {
|
|
||||||
return {
|
|
||||||
// dom
|
|
||||||
d: null,
|
|
||||||
// effect
|
|
||||||
e: null,
|
|
||||||
// parent
|
|
||||||
p: /** @type {import('./types.js').Block} */ (current_block),
|
|
||||||
// transition
|
|
||||||
r: null,
|
|
||||||
// type
|
|
||||||
t: HEAD_BLOCK
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {import('./types.js').DynamicElementBlock} */
|
|
||||||
export function create_dynamic_element_block() {
|
|
||||||
return {
|
|
||||||
// dom
|
|
||||||
d: null,
|
|
||||||
// effect
|
|
||||||
e: null,
|
|
||||||
// parent
|
|
||||||
p: /** @type {import('./types.js').Block} */ (current_block),
|
|
||||||
// transition
|
|
||||||
r: null,
|
|
||||||
// type
|
|
||||||
t: DYNAMIC_ELEMENT_BLOCK
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {import('./types.js').DynamicComponentBlock} */
|
|
||||||
export function create_dynamic_component_block() {
|
|
||||||
return {
|
|
||||||
// dom
|
|
||||||
d: null,
|
|
||||||
// effect
|
|
||||||
e: null,
|
|
||||||
// parent
|
|
||||||
p: /** @type {import('./types.js').Block} */ (current_block),
|
|
||||||
// transition
|
|
||||||
r: null,
|
|
||||||
// type
|
|
||||||
t: DYNAMIC_COMPONENT_BLOCK
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {import('./types.js').SnippetBlock} */
|
|
||||||
export function create_snippet_block() {
|
|
||||||
return {
|
|
||||||
// dom
|
|
||||||
d: null,
|
|
||||||
// parent
|
|
||||||
p: /** @type {import('./types.js').Block} */ (current_block),
|
|
||||||
// effect
|
|
||||||
e: null,
|
|
||||||
// transition
|
|
||||||
r: null,
|
|
||||||
// type
|
|
||||||
t: SNIPPET_BLOCK
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { namespace_svg } from '../../../../constants.js';
|
||||||
|
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
|
||||||
|
import { empty } from '../operations.js';
|
||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { insert, remove } from '../reconciler.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element | Text | Comment} anchor
|
||||||
|
* @param {boolean} is_html
|
||||||
|
* @param {() => Record<string, string>} props
|
||||||
|
* @param {(anchor: Element | Text | Comment) => any} component
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function css_props(anchor, is_html, props, component) {
|
||||||
|
hydrate_block_anchor(anchor);
|
||||||
|
|
||||||
|
/** @type {HTMLElement | SVGElement} */
|
||||||
|
let tag;
|
||||||
|
|
||||||
|
/** @type {Text | Comment} */
|
||||||
|
let component_anchor;
|
||||||
|
|
||||||
|
if (hydrating) {
|
||||||
|
// Hydration: css props element is surrounded by a ssr comment ...
|
||||||
|
tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]);
|
||||||
|
// ... and the child(ren) of the css props element is also surround by a ssr comment
|
||||||
|
component_anchor = /** @type {Comment} */ (tag.firstChild);
|
||||||
|
} else {
|
||||||
|
if (is_html) {
|
||||||
|
tag = document.createElement('div');
|
||||||
|
tag.style.display = 'contents';
|
||||||
|
} else {
|
||||||
|
tag = document.createElementNS(namespace_svg, 'g');
|
||||||
|
}
|
||||||
|
|
||||||
|
insert(tag, null, anchor);
|
||||||
|
component_anchor = empty();
|
||||||
|
tag.appendChild(component_anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
component(component_anchor);
|
||||||
|
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
let current_props = {};
|
||||||
|
|
||||||
|
const effect = render_effect(() => {
|
||||||
|
const next_props = props();
|
||||||
|
|
||||||
|
for (const key in current_props) {
|
||||||
|
if (!(key in next_props)) {
|
||||||
|
tag.style.removeProperty(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in next_props) {
|
||||||
|
tag.style.setProperty(key, next_props[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
current_props = next_props;
|
||||||
|
});
|
||||||
|
|
||||||
|
effect.ondestroy = () => {
|
||||||
|
remove(tag);
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { reconcile_html, remove } from '../reconciler.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element | Text | Comment} dom
|
||||||
|
* @param {() => string} get_value
|
||||||
|
* @param {boolean} svg
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function html(dom, get_value, svg) {
|
||||||
|
/** @type {import('#client').TemplateNode | import('#client').TemplateNode[]} */
|
||||||
|
let html_dom;
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
let value;
|
||||||
|
|
||||||
|
const effect = render_effect(() => {
|
||||||
|
if (value !== (value = get_value())) {
|
||||||
|
if (html_dom) remove(html_dom);
|
||||||
|
html_dom = reconcile_html(dom, value, svg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
effect.ondestroy = () => {
|
||||||
|
if (html_dom) {
|
||||||
|
remove(html_dom);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { SNIPPET_BLOCK } from '../../constants.js';
|
||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { remove } from '../reconciler.js';
|
||||||
|
import { current_block, untrack } from '../../runtime.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {() => Function | null | undefined} get_snippet
|
||||||
|
* @param {Node} node
|
||||||
|
* @param {(() => any)[]} args
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function snippet(get_snippet, node, ...args) {
|
||||||
|
/** @type {import('#client').SnippetBlock} */
|
||||||
|
const block = {
|
||||||
|
// dom
|
||||||
|
d: null,
|
||||||
|
// parent
|
||||||
|
p: /** @type {import('#client').Block} */ (current_block),
|
||||||
|
// effect
|
||||||
|
e: null,
|
||||||
|
// transition
|
||||||
|
r: null,
|
||||||
|
// type
|
||||||
|
t: SNIPPET_BLOCK
|
||||||
|
};
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
// Only rerender when the snippet function itself changes,
|
||||||
|
// not when an eagerly-read prop inside the snippet function changes
|
||||||
|
const snippet = get_snippet();
|
||||||
|
if (snippet) {
|
||||||
|
untrack(() => snippet(node, ...args));
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (block.d !== null) {
|
||||||
|
remove(block.d);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, block);
|
||||||
|
}
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
import { DYNAMIC_COMPONENT_BLOCK } from '../../constants.js';
|
||||||
|
import { hydrate_block_anchor } from '../hydration.js';
|
||||||
|
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { remove } from '../reconciler.js';
|
||||||
|
import { current_block, execute_effect } from '../../runtime.js';
|
||||||
|
import { trigger_transitions } from '../elements/transitions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template P
|
||||||
|
* @param {Comment} anchor_node
|
||||||
|
* @param {() => (props: P) => void} component_fn
|
||||||
|
* @param {(component: (props: P) => void) => void} render_fn
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function component(anchor_node, component_fn, render_fn) {
|
||||||
|
/** @type {import('#client').DynamicComponentBlock} */
|
||||||
|
const block = {
|
||||||
|
// dom
|
||||||
|
d: null,
|
||||||
|
// effect
|
||||||
|
e: null,
|
||||||
|
// parent
|
||||||
|
p: /** @type {import('#client').Block} */ (current_block),
|
||||||
|
// transition
|
||||||
|
r: null,
|
||||||
|
// type
|
||||||
|
t: DYNAMIC_COMPONENT_BLOCK
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {null | import('#client').Render} */
|
||||||
|
let current_render = null;
|
||||||
|
hydrate_block_anchor(anchor_node);
|
||||||
|
|
||||||
|
/** @type {null | ((props: P) => void)} */
|
||||||
|
let component = null;
|
||||||
|
|
||||||
|
block.r =
|
||||||
|
/**
|
||||||
|
* @param {import('#client').Transition} transition
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
(transition) => {
|
||||||
|
const render = /** @type {import('#client').Render} */ (current_render);
|
||||||
|
const transitions = render.s;
|
||||||
|
transitions.add(transition);
|
||||||
|
transition.f(() => {
|
||||||
|
transitions.delete(transition);
|
||||||
|
if (transitions.size === 0) {
|
||||||
|
// If the current render has changed since, then we can remove the old render
|
||||||
|
// effect as it's stale.
|
||||||
|
if (current_render !== render && render.e !== null) {
|
||||||
|
if (render.d !== null) {
|
||||||
|
remove(render.d);
|
||||||
|
render.d = null;
|
||||||
|
}
|
||||||
|
destroy_effect(render.e);
|
||||||
|
render.e = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const create_render_effect = () => {
|
||||||
|
/** @type {import('#client').Render} */
|
||||||
|
const render = {
|
||||||
|
d: null,
|
||||||
|
e: null,
|
||||||
|
s: new Set(),
|
||||||
|
p: current_render
|
||||||
|
};
|
||||||
|
|
||||||
|
// Managed effect
|
||||||
|
render.e = render_effect(
|
||||||
|
() => {
|
||||||
|
const current = block.d;
|
||||||
|
if (current !== null) {
|
||||||
|
remove(current);
|
||||||
|
block.d = null;
|
||||||
|
}
|
||||||
|
if (component) {
|
||||||
|
render_fn(component);
|
||||||
|
}
|
||||||
|
render.d = block.d;
|
||||||
|
block.d = null;
|
||||||
|
},
|
||||||
|
block,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
current_render = render;
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
const render = current_render;
|
||||||
|
|
||||||
|
if (render === null) {
|
||||||
|
create_render_effect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transitions = render.s;
|
||||||
|
|
||||||
|
if (transitions.size === 0) {
|
||||||
|
if (render.d !== null) {
|
||||||
|
remove(render.d);
|
||||||
|
render.d = null;
|
||||||
|
}
|
||||||
|
if (render.e) {
|
||||||
|
execute_effect(render.e);
|
||||||
|
} else {
|
||||||
|
create_render_effect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
create_render_effect();
|
||||||
|
trigger_transitions(transitions, 'out');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const component_effect = render_effect(
|
||||||
|
() => {
|
||||||
|
const next_component = component_fn();
|
||||||
|
if (component !== next_component) {
|
||||||
|
component = next_component;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
block,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
component_effect.ondestroy = () => {
|
||||||
|
let render = current_render;
|
||||||
|
while (render !== null) {
|
||||||
|
const dom = render.d;
|
||||||
|
if (dom !== null) {
|
||||||
|
remove(dom);
|
||||||
|
}
|
||||||
|
const effect = render.e;
|
||||||
|
if (effect !== null) {
|
||||||
|
destroy_effect(effect);
|
||||||
|
}
|
||||||
|
render = render.p;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
block.e = component_effect;
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
import { namespace_svg } from '../../../../constants.js';
|
||||||
|
import { DYNAMIC_ELEMENT_BLOCK } from '../../constants.js';
|
||||||
|
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
|
||||||
|
import { empty } from '../operations.js';
|
||||||
|
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { insert, remove } from '../reconciler.js';
|
||||||
|
import { current_block, execute_effect } from '../../runtime.js';
|
||||||
|
import { is_array } from '../../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('#client').Block} block
|
||||||
|
* @param {Element} from
|
||||||
|
* @param {Element} to
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function swap_block_dom(block, from, to) {
|
||||||
|
const dom = block.d;
|
||||||
|
if (is_array(dom)) {
|
||||||
|
for (let i = 0; i < dom.length; i++) {
|
||||||
|
if (dom[i] === from) {
|
||||||
|
dom[i] = to;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (dom === from) {
|
||||||
|
block.d = to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Comment} anchor_node
|
||||||
|
* @param {() => string} tag_fn
|
||||||
|
* @param {boolean | null} is_svg `null` == not statically known
|
||||||
|
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function element(anchor_node, tag_fn, is_svg, render_fn) {
|
||||||
|
/** @type {import('#client').DynamicElementBlock} */
|
||||||
|
const block = {
|
||||||
|
// dom
|
||||||
|
d: null,
|
||||||
|
// effect
|
||||||
|
e: null,
|
||||||
|
// parent
|
||||||
|
p: /** @type {import('#client').Block} */ (current_block),
|
||||||
|
// transition
|
||||||
|
r: null,
|
||||||
|
// type
|
||||||
|
t: DYNAMIC_ELEMENT_BLOCK
|
||||||
|
};
|
||||||
|
|
||||||
|
hydrate_block_anchor(anchor_node);
|
||||||
|
let has_mounted = false;
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
let tag;
|
||||||
|
|
||||||
|
/** @type {null | Element} */
|
||||||
|
let element = null;
|
||||||
|
|
||||||
|
const element_effect = render_effect(
|
||||||
|
() => {
|
||||||
|
tag = tag_fn();
|
||||||
|
if (has_mounted) {
|
||||||
|
execute_effect(render_effect_signal);
|
||||||
|
}
|
||||||
|
has_mounted = true;
|
||||||
|
},
|
||||||
|
block,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Managed effect
|
||||||
|
const render_effect_signal = render_effect(
|
||||||
|
() => {
|
||||||
|
// We try our best infering the namespace in case it's not possible to determine statically,
|
||||||
|
// but on the first render on the client (without hydration) the parent will be undefined,
|
||||||
|
// since the anchor is not attached to its parent / the dom yet.
|
||||||
|
const ns =
|
||||||
|
is_svg || tag === 'svg'
|
||||||
|
? namespace_svg
|
||||||
|
: is_svg === false || anchor_node.parentElement?.tagName === 'foreignObject'
|
||||||
|
? null
|
||||||
|
: anchor_node.parentElement?.namespaceURI ?? null;
|
||||||
|
|
||||||
|
const next_element = tag
|
||||||
|
? hydrating
|
||||||
|
? /** @type {Element} */ (current_hydration_fragment[0])
|
||||||
|
: ns
|
||||||
|
? document.createElementNS(ns, tag)
|
||||||
|
: document.createElement(tag)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const prev_element = element;
|
||||||
|
if (prev_element !== null) {
|
||||||
|
block.d = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
element = next_element;
|
||||||
|
if (element !== null && render_fn !== undefined) {
|
||||||
|
let anchor;
|
||||||
|
if (hydrating) {
|
||||||
|
// Use the existing ssr comment as the anchor so that the inner open and close
|
||||||
|
// methods can pick up the existing nodes correctly
|
||||||
|
anchor = /** @type {Comment} */ (element.firstChild);
|
||||||
|
} else {
|
||||||
|
anchor = empty();
|
||||||
|
element.appendChild(anchor);
|
||||||
|
}
|
||||||
|
render_fn(element, anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const has_prev_element = prev_element !== null;
|
||||||
|
if (has_prev_element) {
|
||||||
|
remove(prev_element);
|
||||||
|
}
|
||||||
|
if (element !== null) {
|
||||||
|
insert(element, null, anchor_node);
|
||||||
|
if (has_prev_element) {
|
||||||
|
const parent_block = block.p;
|
||||||
|
swap_block_dom(parent_block, prev_element, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
block,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
element_effect.ondestroy = () => {
|
||||||
|
if (element !== null) {
|
||||||
|
remove(element);
|
||||||
|
block.d = null;
|
||||||
|
element = null;
|
||||||
|
}
|
||||||
|
destroy_effect(render_effect_signal);
|
||||||
|
};
|
||||||
|
|
||||||
|
block.e = element_effect;
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { HEAD_BLOCK } from '../../constants.js';
|
||||||
|
import {
|
||||||
|
current_hydration_fragment,
|
||||||
|
get_hydration_fragment,
|
||||||
|
hydrating,
|
||||||
|
set_current_hydration_fragment
|
||||||
|
} from '../hydration.js';
|
||||||
|
import { empty } from '../operations.js';
|
||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { remove } from '../reconciler.js';
|
||||||
|
import { current_block } from '../../runtime.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(anchor: Node | null) => void} render_fn
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function head(render_fn) {
|
||||||
|
/** @type {import('#client').HeadBlock} */
|
||||||
|
const block = {
|
||||||
|
// dom
|
||||||
|
d: null,
|
||||||
|
// effect
|
||||||
|
e: null,
|
||||||
|
// parent
|
||||||
|
p: /** @type {import('#client').Block} */ (current_block),
|
||||||
|
// transition
|
||||||
|
r: null,
|
||||||
|
// type
|
||||||
|
t: HEAD_BLOCK
|
||||||
|
};
|
||||||
|
|
||||||
|
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
|
||||||
|
// therefore we need to skip that when we detect that we're not in hydration mode.
|
||||||
|
let hydration_fragment = null;
|
||||||
|
let previous_hydration_fragment = null;
|
||||||
|
|
||||||
|
let is_hydrating = hydrating;
|
||||||
|
if (is_hydrating) {
|
||||||
|
hydration_fragment = get_hydration_fragment(document.head.firstChild);
|
||||||
|
previous_hydration_fragment = current_hydration_fragment;
|
||||||
|
set_current_hydration_fragment(hydration_fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const head_effect = render_effect(
|
||||||
|
() => {
|
||||||
|
const current = block.d;
|
||||||
|
if (current !== null) {
|
||||||
|
remove(current);
|
||||||
|
block.d = null;
|
||||||
|
}
|
||||||
|
let anchor = null;
|
||||||
|
if (!hydrating) {
|
||||||
|
anchor = empty();
|
||||||
|
document.head.appendChild(anchor);
|
||||||
|
}
|
||||||
|
render_fn(anchor);
|
||||||
|
},
|
||||||
|
block,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
head_effect.ondestroy = () => {
|
||||||
|
const current = block.d;
|
||||||
|
if (current !== null) {
|
||||||
|
remove(current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
block.e = head_effect;
|
||||||
|
} finally {
|
||||||
|
if (is_hydrating) {
|
||||||
|
set_current_hydration_fragment(previous_hydration_fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { effect } from '../../reactivity/effects.js';
|
||||||
|
import { deep_read_state, untrack } from '../../runtime.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template P
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {(dom: Element, value?: P) => import('#client').ActionPayload<P>} action
|
||||||
|
* @param {() => P} [value_fn]
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function action(dom, action, value_fn) {
|
||||||
|
/** @type {undefined | import('#client').ActionPayload<P>} */
|
||||||
|
var payload = undefined;
|
||||||
|
var needs_deep_read = false;
|
||||||
|
|
||||||
|
// Action could come from a prop, therefore could be a signal, therefore untrack
|
||||||
|
// TODO we could take advantage of this and enable https://github.com/sveltejs/svelte/issues/6942
|
||||||
|
effect(() => {
|
||||||
|
if (value_fn) {
|
||||||
|
var value = value_fn();
|
||||||
|
untrack(() => {
|
||||||
|
if (payload === undefined) {
|
||||||
|
payload = action(dom, value) || {};
|
||||||
|
needs_deep_read = !!payload?.update;
|
||||||
|
} else {
|
||||||
|
var update = payload.update;
|
||||||
|
if (typeof update === 'function') {
|
||||||
|
update(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Action's update method is coarse-grained, i.e. when anything in the passed value changes, update.
|
||||||
|
// This works in legacy mode because of mutable_source being updated as a whole, but when using $state
|
||||||
|
// together with actions and mutation, it wouldn't notice the change without a deep read.
|
||||||
|
if (needs_deep_read) {
|
||||||
|
deep_read_state(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
untrack(() => (payload = action(dom)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
if (payload !== undefined) {
|
||||||
|
var destroy = payload.destroy;
|
||||||
|
if (typeof destroy === 'function') {
|
||||||
|
return () => {
|
||||||
|
/** @type {Function} */ (destroy)();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,362 @@
|
|||||||
|
import { DEV } from 'esm-env';
|
||||||
|
import { hydrating } from '../hydration.js';
|
||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { get_descriptors, map_get, map_set, object_assign } from '../../utils.js';
|
||||||
|
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
|
||||||
|
import { delegate } from './events.js';
|
||||||
|
import { autofocus } from './misc.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
|
||||||
|
* to remove it upon hydration to avoid a bug when someone resets the form value.
|
||||||
|
* @param {HTMLInputElement | HTMLSelectElement} dom
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function remove_input_attr_defaults(dom) {
|
||||||
|
if (hydrating) {
|
||||||
|
attr(dom, 'value', null);
|
||||||
|
attr(dom, 'checked', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {string} attribute
|
||||||
|
* @param {() => string} value
|
||||||
|
*/
|
||||||
|
export function attr_effect(dom, attribute, value) {
|
||||||
|
render_effect(() => {
|
||||||
|
attr(dom, attribute, value());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {string} attribute
|
||||||
|
* @param {string | null} value
|
||||||
|
*/
|
||||||
|
export function attr(dom, attribute, value) {
|
||||||
|
value = value == null ? null : value + '';
|
||||||
|
|
||||||
|
if (DEV) {
|
||||||
|
check_src_in_dev_hydration(dom, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hydrating ||
|
||||||
|
(dom.getAttribute(attribute) !== value &&
|
||||||
|
// If we reset those, they would result in another network request, which we want to avoid.
|
||||||
|
// We assume they are the same between client and server as checking if they are equal is expensive
|
||||||
|
// (we can't just compare the strings as they can be different between client and server but result in the
|
||||||
|
// same url, so we would need to create hidden anchor elements to compare them)
|
||||||
|
attribute !== 'src' &&
|
||||||
|
attribute !== 'href' &&
|
||||||
|
attribute !== 'srcset')
|
||||||
|
) {
|
||||||
|
if (value === null) {
|
||||||
|
dom.removeAttribute(attribute);
|
||||||
|
} else {
|
||||||
|
dom.setAttribute(attribute, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {string} attribute
|
||||||
|
* @param {() => string} value
|
||||||
|
*/
|
||||||
|
export function xlink_attr_effect(dom, attribute, value) {
|
||||||
|
render_effect(() => {
|
||||||
|
xlink_attr(dom, attribute, value());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {string} attribute
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
export function xlink_attr(dom, attribute, value) {
|
||||||
|
dom.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} node
|
||||||
|
* @param {string} prop
|
||||||
|
* @param {() => any} value
|
||||||
|
*/
|
||||||
|
export function set_custom_element_data_effect(node, prop, value) {
|
||||||
|
render_effect(() => {
|
||||||
|
set_custom_element_data(node, prop, value());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} node
|
||||||
|
* @param {string} prop
|
||||||
|
* @param {any} value
|
||||||
|
*/
|
||||||
|
export function set_custom_element_data(node, prop, value) {
|
||||||
|
if (prop in node) {
|
||||||
|
node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value;
|
||||||
|
} else {
|
||||||
|
attr(node, prop, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `spread_attributes` but self-contained
|
||||||
|
* @param {Element & ElementCSSInlineStyle} dom
|
||||||
|
* @param {() => Record<string, unknown>[]} attrs
|
||||||
|
* @param {boolean} lowercase_attributes
|
||||||
|
* @param {string} css_hash
|
||||||
|
*/
|
||||||
|
export function spread_attributes_effect(dom, attrs, lowercase_attributes, css_hash) {
|
||||||
|
/** @type {Record<string, any> | undefined} */
|
||||||
|
var current;
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
current = spread_attributes(dom, current, attrs(), lowercase_attributes, css_hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spreads attributes onto a DOM element, taking into account the currently set attributes
|
||||||
|
* @param {Element & ElementCSSInlineStyle} dom
|
||||||
|
* @param {Record<string, unknown> | undefined} prev
|
||||||
|
* @param {Record<string, unknown>[]} attrs
|
||||||
|
* @param {boolean} lowercase_attributes
|
||||||
|
* @param {string} css_hash
|
||||||
|
* @returns {Record<string, unknown>}
|
||||||
|
*/
|
||||||
|
export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_hash) {
|
||||||
|
var next = object_assign({}, ...attrs);
|
||||||
|
var has_hash = css_hash.length !== 0;
|
||||||
|
|
||||||
|
for (var key in prev) {
|
||||||
|
if (!(key in next)) {
|
||||||
|
next[key] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_hash && !next.class) {
|
||||||
|
next.class = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
var setters = map_get(setters_cache, dom.nodeName);
|
||||||
|
if (!setters) map_set(setters_cache, dom.nodeName, (setters = get_setters(dom)));
|
||||||
|
|
||||||
|
for (key in next) {
|
||||||
|
var value = next[key];
|
||||||
|
if (value === prev?.[key]) continue;
|
||||||
|
|
||||||
|
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
|
||||||
|
if (prefix === '$$') continue;
|
||||||
|
|
||||||
|
if (prefix === 'on') {
|
||||||
|
/** @type {{ capture?: true }} */
|
||||||
|
var opts = {};
|
||||||
|
var event_name = key.slice(2);
|
||||||
|
var delegated = DelegatedEvents.includes(event_name);
|
||||||
|
|
||||||
|
if (
|
||||||
|
event_name.endsWith('capture') &&
|
||||||
|
event_name !== 'ongotpointercapture' &&
|
||||||
|
event_name !== 'onlostpointercapture'
|
||||||
|
) {
|
||||||
|
event_name = event_name.slice(0, -7);
|
||||||
|
opts.capture = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!delegated && prev?.[key]) {
|
||||||
|
dom.removeEventListener(event_name, /** @type {any} */ (prev[key]), opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
if (!delegated) {
|
||||||
|
dom.addEventListener(event_name, value, opts);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
dom[`__${event_name}`] = value;
|
||||||
|
delegate([event_name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (value == null) {
|
||||||
|
dom.removeAttribute(key);
|
||||||
|
} else if (key === 'style') {
|
||||||
|
dom.style.cssText = value + '';
|
||||||
|
} else if (key === 'autofocus') {
|
||||||
|
autofocus(/** @type {HTMLElement} */ (dom), Boolean(value));
|
||||||
|
} else if (key === '__value' || key === 'value') {
|
||||||
|
// @ts-ignore
|
||||||
|
dom.value = dom[key] = dom.__value = value;
|
||||||
|
} else {
|
||||||
|
var name = key;
|
||||||
|
if (lowercase_attributes) {
|
||||||
|
name = name.toLowerCase();
|
||||||
|
name = AttributeAliases[name] || name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setters.includes(name)) {
|
||||||
|
if (DEV) {
|
||||||
|
check_src_in_dev_hydration(dom, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hydrating ||
|
||||||
|
// @ts-ignore see attr method for an explanation of src/srcset
|
||||||
|
(dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
dom[name] = value;
|
||||||
|
}
|
||||||
|
} else if (typeof value !== 'function') {
|
||||||
|
if (has_hash && name === 'class') {
|
||||||
|
if (value) value += ' ';
|
||||||
|
value += css_hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
attr(dom, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} node
|
||||||
|
* @param {() => Record<string, unknown>[]} attrs
|
||||||
|
* @param {string} css_hash
|
||||||
|
*/
|
||||||
|
export function spread_dynamic_element_attributes_effect(node, attrs, css_hash) {
|
||||||
|
/** @type {Record<string, any> | undefined} */
|
||||||
|
var current;
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
current = spread_dynamic_element_attributes(node, current, attrs(), css_hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} node
|
||||||
|
* @param {Record<string, unknown> | undefined} prev
|
||||||
|
* @param {Record<string, unknown>[]} attrs
|
||||||
|
* @param {string} css_hash
|
||||||
|
*/
|
||||||
|
export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
|
||||||
|
if (node.tagName.includes('-')) {
|
||||||
|
var next = object_assign({}, ...attrs);
|
||||||
|
|
||||||
|
for (var key in prev) {
|
||||||
|
if (!(key in next)) {
|
||||||
|
next[key] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key in next) {
|
||||||
|
set_custom_element_data(node, key, next[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
} else {
|
||||||
|
return spread_attributes(
|
||||||
|
/** @type {Element & ElementCSSInlineStyle} */ (node),
|
||||||
|
prev,
|
||||||
|
attrs,
|
||||||
|
node.namespaceURI !== namespace_svg,
|
||||||
|
css_hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of attributes that should always be set through the attr method,
|
||||||
|
* because updating them through the property setter doesn't work reliably.
|
||||||
|
* In the example of `width`/`height`, the problem is that the setter only
|
||||||
|
* accepts numeric values, but the attribute can also be set to a string like `50%`.
|
||||||
|
* If this list becomes too big, rethink this approach.
|
||||||
|
*/
|
||||||
|
var always_set_through_set_attribute = ['width', 'height'];
|
||||||
|
|
||||||
|
/** @type {Map<string, string[]>} */
|
||||||
|
var setters_cache = new Map();
|
||||||
|
|
||||||
|
/** @param {Element} element */
|
||||||
|
function get_setters(element) {
|
||||||
|
/** @type {string[]} */
|
||||||
|
var setters = [];
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
var descriptors = get_descriptors(element.__proto__);
|
||||||
|
|
||||||
|
for (var key in descriptors) {
|
||||||
|
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) {
|
||||||
|
setters.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return setters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} dom
|
||||||
|
* @param {string} attribute
|
||||||
|
* @param {string | null} value
|
||||||
|
*/
|
||||||
|
function check_src_in_dev_hydration(dom, attribute, value) {
|
||||||
|
if (!hydrating) return;
|
||||||
|
if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return;
|
||||||
|
|
||||||
|
if (attribute === 'srcset' && srcset_url_equal(dom, value)) return;
|
||||||
|
if (src_url_equal(dom.getAttribute(attribute) ?? '', value ?? '')) return;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
`Detected a ${attribute} attribute value change during hydration. This will not be repaired during hydration, ` +
|
||||||
|
`the ${attribute} value that came from the server will be used. Related element:`,
|
||||||
|
dom,
|
||||||
|
' Differing value:',
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} element_src
|
||||||
|
* @param {string} url
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function src_url_equal(element_src, url) {
|
||||||
|
if (element_src === url) return true;
|
||||||
|
return new URL(element_src, document.baseURI).href === new URL(url, document.baseURI).href;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} srcset */
|
||||||
|
function split_srcset(srcset) {
|
||||||
|
return srcset.split(',').map((src) => src.trim().split(' ').filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLSourceElement | HTMLImageElement} element
|
||||||
|
* @param {string | undefined | null} srcset
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function srcset_url_equal(element, srcset) {
|
||||||
|
var element_urls = split_srcset(element.srcset);
|
||||||
|
var urls = split_srcset(srcset ?? '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
urls.length === element_urls.length &&
|
||||||
|
urls.every(
|
||||||
|
([url, width], i) =>
|
||||||
|
width === element_urls[i][1] &&
|
||||||
|
// We need to test both ways because Vite will create an a full URL with
|
||||||
|
// `new URL(asset, import.meta.url).href` for the client when `base: './'`, and the
|
||||||
|
// relative URLs inside srcset are not automatically resolved to absolute URLs by
|
||||||
|
// browsers (in contrast to img.src). This means both SSR and DOM code could
|
||||||
|
// contain relative or absolute URLs.
|
||||||
|
(src_url_equal(element_urls[i][0], url) || src_url_equal(url, element_urls[i][0]))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
import { DEV } from 'esm-env';
|
||||||
|
import { render_effect } from '../../../reactivity/effects.js';
|
||||||
|
import { stringify } from '../../../render.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLInputElement} input
|
||||||
|
* @param {() => unknown} get_value
|
||||||
|
* @param {(value: unknown) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_value(input, get_value, update) {
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
if (DEV && input.type === 'checkbox') {
|
||||||
|
throw new Error(
|
||||||
|
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(is_numberlike_input(input) ? to_number(input.value) : input.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
if (DEV && input.type === 'checkbox') {
|
||||||
|
throw new Error(
|
||||||
|
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = get_value();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
input.__value = value;
|
||||||
|
|
||||||
|
if (is_numberlike_input(input) && value === to_number(input.value)) {
|
||||||
|
// handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.type === 'date' && !value && !input.value) {
|
||||||
|
// Handles the case where a temporarily invalid date is set (while typing, for example with a leading 0 for the day)
|
||||||
|
// and prevents this state from clearing the other parts of the date input (see https://github.com/sveltejs/svelte/issues/7897)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = stringify(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<HTMLInputElement>} inputs
|
||||||
|
* @param {null | [number]} group_index
|
||||||
|
* @param {HTMLInputElement} input
|
||||||
|
* @param {() => unknown} get_value
|
||||||
|
* @param {(value: unknown) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_group(inputs, group_index, input, get_value, update) {
|
||||||
|
var is_checkbox = input.getAttribute('type') === 'checkbox';
|
||||||
|
var binding_group = inputs;
|
||||||
|
|
||||||
|
if (group_index !== null) {
|
||||||
|
for (var index of group_index) {
|
||||||
|
var group = binding_group;
|
||||||
|
// @ts-ignore
|
||||||
|
binding_group = group[index];
|
||||||
|
if (binding_group === undefined) {
|
||||||
|
// @ts-ignore
|
||||||
|
binding_group = group[index] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding_group.push(input);
|
||||||
|
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
// @ts-ignore
|
||||||
|
var value = input.__value;
|
||||||
|
|
||||||
|
if (is_checkbox) {
|
||||||
|
value = get_binding_group_value(binding_group, value, input.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
var value = get_value();
|
||||||
|
|
||||||
|
if (is_checkbox) {
|
||||||
|
value = value || [];
|
||||||
|
// @ts-ignore
|
||||||
|
input.checked = value.includes(input.__value);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
input.checked = input.__value === value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
return () => {
|
||||||
|
var index = binding_group.indexOf(input);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
binding_group.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLInputElement} input
|
||||||
|
* @param {() => unknown} get_value
|
||||||
|
* @param {(value: unknown) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_checked(input, get_value, update) {
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
var value = input.checked;
|
||||||
|
update(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (get_value() == undefined) {
|
||||||
|
update(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
var value = get_value();
|
||||||
|
input.checked = Boolean(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template V
|
||||||
|
* @param {Array<HTMLInputElement>} group
|
||||||
|
* @param {V} __value
|
||||||
|
* @param {boolean} checked
|
||||||
|
* @returns {V[]}
|
||||||
|
*/
|
||||||
|
function get_binding_group_value(group, __value, checked) {
|
||||||
|
var value = new Set();
|
||||||
|
|
||||||
|
for (var i = 0; i < group.length; i += 1) {
|
||||||
|
if (group[i].checked) {
|
||||||
|
// @ts-ignore
|
||||||
|
value.add(group[i].__value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checked) {
|
||||||
|
value.delete(__value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLInputElement} input
|
||||||
|
*/
|
||||||
|
function is_numberlike_input(input) {
|
||||||
|
var type = input.type;
|
||||||
|
return type === 'number' || type === 'range';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
function to_number(value) {
|
||||||
|
return value === '' ? null : +value;
|
||||||
|
}
|
||||||
@ -0,0 +1,278 @@
|
|||||||
|
import { hydrating } from '../../hydration.js';
|
||||||
|
import { destroy_effect, managed_effect, render_effect } from '../../../reactivity/effects.js';
|
||||||
|
import { listen } from './shared.js';
|
||||||
|
|
||||||
|
/** @param {TimeRanges} ranges */
|
||||||
|
function time_ranges_to_array(ranges) {
|
||||||
|
var array = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < ranges.length; i += 1) {
|
||||||
|
array.push({ start: ranges.start(i), end: ranges.end(i) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {() => number | undefined} get_value
|
||||||
|
* @param {(value: number) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_current_time(media, get_value, update) {
|
||||||
|
/** @type {number} */
|
||||||
|
var raf_id;
|
||||||
|
var updating = false;
|
||||||
|
|
||||||
|
// Ideally, listening to timeupdate would be enough, but it fires too infrequently for the currentTime
|
||||||
|
// binding, which is why we use a raf loop, too. We additionally still listen to timeupdate because
|
||||||
|
// the user could be scrubbing through the video using the native controls when the media is paused.
|
||||||
|
var callback = () => {
|
||||||
|
cancelAnimationFrame(raf_id);
|
||||||
|
|
||||||
|
if (!media.paused) {
|
||||||
|
raf_id = requestAnimationFrame(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
updating = true;
|
||||||
|
update(media.currentTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
raf_id = requestAnimationFrame(callback);
|
||||||
|
media.addEventListener('timeupdate', callback);
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
var value = get_value();
|
||||||
|
|
||||||
|
// through isNaN we also allow number strings, which is more robust
|
||||||
|
if (!updating && !isNaN(/** @type {any} */ (value))) {
|
||||||
|
media.currentTime = /** @type {number} */ (value);
|
||||||
|
}
|
||||||
|
|
||||||
|
updating = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
render_effect(() => () => cancelAnimationFrame(raf_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {(array: Array<{ start: number; end: number }>) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_buffered(media, update) {
|
||||||
|
listen(media, ['loadedmetadata', 'progress'], () => update(time_ranges_to_array(media.buffered)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {(array: Array<{ start: number; end: number }>) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_seekable(media, update) {
|
||||||
|
listen(media, ['loadedmetadata'], () => update(time_ranges_to_array(media.seekable)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {(array: Array<{ start: number; end: number }>) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_played(media, update) {
|
||||||
|
listen(media, ['timeupdate'], () => update(time_ranges_to_array(media.played)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {(seeking: boolean) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_seeking(media, update) {
|
||||||
|
listen(media, ['seeking', 'seeked'], () => update(media.seeking));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {(seeking: boolean) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_ended(media, update) {
|
||||||
|
listen(media, ['timeupdate', 'ended'], () => update(media.ended));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {(ready_state: number) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_ready_state(media, update) {
|
||||||
|
listen(
|
||||||
|
media,
|
||||||
|
['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied'],
|
||||||
|
() => update(media.readyState)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {() => number | undefined} get_value
|
||||||
|
* @param {(playback_rate: number) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_playback_rate(media, get_value, update) {
|
||||||
|
var updating = false;
|
||||||
|
|
||||||
|
var callback = () => {
|
||||||
|
if (!updating) {
|
||||||
|
update(media.playbackRate);
|
||||||
|
}
|
||||||
|
updating = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser.
|
||||||
|
// For hydration we could do it immediately but the additional code is not worth the lost microtask.
|
||||||
|
|
||||||
|
/** @type {import('#client').Effect | undefined} */
|
||||||
|
var render;
|
||||||
|
var destroyed = false;
|
||||||
|
|
||||||
|
var effect = managed_effect(() => {
|
||||||
|
destroy_effect(effect);
|
||||||
|
|
||||||
|
if (destroyed) return;
|
||||||
|
|
||||||
|
if (get_value() == null) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(media, ['ratechange'], callback, false);
|
||||||
|
|
||||||
|
render = render_effect(() => {
|
||||||
|
var value = get_value();
|
||||||
|
|
||||||
|
// through isNaN we also allow number strings, which is more robust
|
||||||
|
if (!isNaN(/** @type {any} */ (value)) && value !== media.playbackRate) {
|
||||||
|
updating = true;
|
||||||
|
media.playbackRate = /** @type {number} */ (value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
render_effect(() => () => {
|
||||||
|
destroyed = true;
|
||||||
|
if (render) {
|
||||||
|
destroy_effect(render);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {() => boolean | undefined} get_value
|
||||||
|
* @param {(paused: boolean) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_paused(media, get_value, update) {
|
||||||
|
var mounted = hydrating;
|
||||||
|
var paused = get_value();
|
||||||
|
|
||||||
|
var callback = () => {
|
||||||
|
if (paused !== media.paused) {
|
||||||
|
paused = media.paused;
|
||||||
|
update((paused = media.paused));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (paused == null) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer listening if not mounted yet so that the first canplay event doesn't cause a potentially wrong update
|
||||||
|
if (mounted) {
|
||||||
|
// If someone switches the src while media is playing, the player will pause.
|
||||||
|
// Listen to the canplay event to get notified of this situation.
|
||||||
|
listen(media, ['play', 'pause', 'canplay'], callback, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
paused = !!get_value();
|
||||||
|
|
||||||
|
if (paused !== media.paused) {
|
||||||
|
var toggle = () => {
|
||||||
|
mounted = true;
|
||||||
|
if (paused) {
|
||||||
|
media.pause();
|
||||||
|
} else {
|
||||||
|
media.play().catch(() => {
|
||||||
|
update((paused = true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
toggle();
|
||||||
|
} else {
|
||||||
|
// If this is the first invocation in dom mode, the media element isn't mounted yet,
|
||||||
|
// and therefore its resource isn't loaded yet. We need to wait for the canplay event
|
||||||
|
// in this case or else we'll get a "The play() request was interrupted by a new load request" error.
|
||||||
|
media.addEventListener(
|
||||||
|
'canplay',
|
||||||
|
() => {
|
||||||
|
listen(media, ['play', 'pause', 'canplay'], callback, false);
|
||||||
|
toggle();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {() => number | undefined} get_value
|
||||||
|
* @param {(volume: number) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_volume(media, get_value, update) {
|
||||||
|
var updating = false;
|
||||||
|
var callback = () => {
|
||||||
|
updating = true;
|
||||||
|
update(media.volume);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (get_value() == null) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(media, ['volumechange'], callback, false);
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
var value = get_value();
|
||||||
|
|
||||||
|
// through isNaN we also allow number strings, which is more robust
|
||||||
|
if (!updating && !isNaN(/** @type {any} */ (value))) {
|
||||||
|
media.volume = /** @type {number} */ (value);
|
||||||
|
}
|
||||||
|
|
||||||
|
updating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLVideoElement | HTMLAudioElement} media
|
||||||
|
* @param {() => boolean | undefined} get_value
|
||||||
|
* @param {(muted: boolean) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_muted(media, get_value, update) {
|
||||||
|
var updating = false;
|
||||||
|
|
||||||
|
var callback = () => {
|
||||||
|
updating = true;
|
||||||
|
update(media.muted);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (get_value() == null) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(media, ['volumechange'], callback, false);
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
var value = get_value();
|
||||||
|
|
||||||
|
if (!updating) media.muted = !!value;
|
||||||
|
updating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { listen } from './shared.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(online: boolean) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_online(update) {
|
||||||
|
listen(window, ['online', 'offline'], () => {
|
||||||
|
update(navigator.onLine);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { render_effect } from '../../../reactivity/effects.js';
|
||||||
|
import { get_descriptor } from '../../../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an `export`ed (non-prop) variable available on the `$$props` object
|
||||||
|
* so that consumers can do `bind:x` on the component.
|
||||||
|
* @template V
|
||||||
|
* @param {Record<string, unknown>} props
|
||||||
|
* @param {string} prop
|
||||||
|
* @param {V} value
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_prop(props, prop, value) {
|
||||||
|
var desc = get_descriptor(props, prop);
|
||||||
|
|
||||||
|
if (desc && desc.set) {
|
||||||
|
props[prop] = value;
|
||||||
|
render_effect(() => () => {
|
||||||
|
props[prop] = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
import { effect } from '../../../reactivity/effects.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the correct option(s) (depending on whether this is a multiple select)
|
||||||
|
* @template V
|
||||||
|
* @param {HTMLSelectElement} select
|
||||||
|
* @param {V} value
|
||||||
|
* @param {boolean} [mounting]
|
||||||
|
*/
|
||||||
|
export function select_option(select, value, mounting) {
|
||||||
|
if (select.multiple) {
|
||||||
|
return select_options(select, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var option of select.options) {
|
||||||
|
var option_value = get_option_value(option);
|
||||||
|
if (option_value === value) {
|
||||||
|
option.selected = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounting || value !== undefined) {
|
||||||
|
select.selectedIndex = -1; // no option should be selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the containing `<select>` element and potentially updates its `selected` state.
|
||||||
|
* @param {HTMLOptionElement} option
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function selected(option) {
|
||||||
|
// Inside an effect because the element might not be connected
|
||||||
|
// to the parent <select> yet when this is called
|
||||||
|
effect(() => {
|
||||||
|
var select = option.parentNode;
|
||||||
|
|
||||||
|
while (select != null) {
|
||||||
|
if (select.nodeName === 'SELECT') break;
|
||||||
|
select = select.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (select != null && option.__value === select.__value) {
|
||||||
|
// never set to false, since this causes browser to select default option
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLSelectElement} select
|
||||||
|
* @param {() => unknown} get_value
|
||||||
|
* @param {(value: unknown) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_select_value(select, get_value, update) {
|
||||||
|
var mounting = true;
|
||||||
|
|
||||||
|
select.addEventListener('change', () => {
|
||||||
|
/** @type {unknown} */
|
||||||
|
var value;
|
||||||
|
|
||||||
|
if (select.multiple) {
|
||||||
|
value = [].map.call(select.querySelectorAll(':checked'), get_option_value);
|
||||||
|
} else {
|
||||||
|
/** @type {HTMLOptionElement | null} */
|
||||||
|
var selected_option = select.querySelector(':checked');
|
||||||
|
value = selected_option && get_option_value(selected_option);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated
|
||||||
|
effect(() => {
|
||||||
|
var value = get_value();
|
||||||
|
select_option(select, value, mounting);
|
||||||
|
|
||||||
|
if (mounting && value === undefined) {
|
||||||
|
/** @type {HTMLOptionElement | null} */
|
||||||
|
var selected_option = select.querySelector(':checked');
|
||||||
|
if (selected_option !== null) {
|
||||||
|
value = get_option_value(selected_option);
|
||||||
|
update(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
select.__value = value;
|
||||||
|
mounting = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template V
|
||||||
|
* @param {HTMLSelectElement} select
|
||||||
|
* @param {V} value
|
||||||
|
*/
|
||||||
|
function select_options(select, value) {
|
||||||
|
for (var option of select.options) {
|
||||||
|
// @ts-ignore
|
||||||
|
option.selected = ~value.indexOf(get_option_value(option));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {HTMLOptionElement} option */
|
||||||
|
function get_option_value(option) {
|
||||||
|
// __value only exists if the <option> has a value attribute
|
||||||
|
if ('__value' in option) {
|
||||||
|
return option.__value;
|
||||||
|
} else {
|
||||||
|
return option.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { render_effect } from '../../../reactivity/effects.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires the handler once immediately (unless corresponding arg is set to `false`),
|
||||||
|
* then listens to the given events until the render effect context is destroyed
|
||||||
|
* @param {Element | Window} target
|
||||||
|
* @param {Array<string>} events
|
||||||
|
* @param {() => void} handler
|
||||||
|
* @param {any} call_handler_immediately
|
||||||
|
*/
|
||||||
|
export function listen(target, events, handler, call_handler_immediately = true) {
|
||||||
|
if (call_handler_immediately) {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var name of events) {
|
||||||
|
target.addEventListener(name, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
return () => {
|
||||||
|
for (var name of events) {
|
||||||
|
target.removeEventListener(name, handler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
import { effect, render_effect } from '../../../reactivity/effects.js';
|
||||||
|
import { untrack } from '../../../runtime.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize observer singleton.
|
||||||
|
* One listener per element only!
|
||||||
|
* https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ
|
||||||
|
*/
|
||||||
|
class ResizeObserverSingleton {
|
||||||
|
/** */
|
||||||
|
#listeners = new WeakMap();
|
||||||
|
|
||||||
|
/** @type {ResizeObserver | undefined} */
|
||||||
|
#observer;
|
||||||
|
|
||||||
|
/** @type {ResizeObserverOptions} */
|
||||||
|
#options;
|
||||||
|
|
||||||
|
/** @static */
|
||||||
|
static entries = new WeakMap();
|
||||||
|
|
||||||
|
/** @param {ResizeObserverOptions} options */
|
||||||
|
constructor(options) {
|
||||||
|
this.#options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {(entry: ResizeObserverEntry) => any} listener
|
||||||
|
*/
|
||||||
|
observe(element, listener) {
|
||||||
|
var listeners = this.#listeners.get(element) || new Set();
|
||||||
|
listeners.add(listener);
|
||||||
|
|
||||||
|
this.#listeners.set(element, listeners);
|
||||||
|
this.#getObserver().observe(element, this.#options);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
var listeners = this.#listeners.get(element);
|
||||||
|
listeners.delete(listener);
|
||||||
|
|
||||||
|
if (listeners.size === 0) {
|
||||||
|
this.#listeners.delete(element);
|
||||||
|
/** @type {ResizeObserver} */ (this.#observer).unobserve(element);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#getObserver() {
|
||||||
|
return (
|
||||||
|
this.#observer ??
|
||||||
|
(this.#observer = new ResizeObserver(
|
||||||
|
/** @param {any} entries */ (entries) => {
|
||||||
|
for (var entry of entries) {
|
||||||
|
ResizeObserverSingleton.entries.set(entry.target, entry);
|
||||||
|
for (var listener of this.#listeners.get(entry.target) || []) {
|
||||||
|
listener(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resize_observer_content_box = /* @__PURE__ */ new ResizeObserverSingleton({
|
||||||
|
box: 'content-box'
|
||||||
|
});
|
||||||
|
|
||||||
|
var resize_observer_border_box = /* @__PURE__ */ new ResizeObserverSingleton({
|
||||||
|
box: 'border-box'
|
||||||
|
});
|
||||||
|
|
||||||
|
var resize_observer_device_pixel_content_box = /* @__PURE__ */ new ResizeObserverSingleton({
|
||||||
|
box: 'device-pixel-content-box'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {'contentRect' | 'contentBoxSize' | 'borderBoxSize' | 'devicePixelContentBoxSize'} type
|
||||||
|
* @param {(entry: keyof ResizeObserverEntry) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_resize_observer(element, type, update) {
|
||||||
|
var observer =
|
||||||
|
type === 'contentRect' || type === 'contentBoxSize'
|
||||||
|
? resize_observer_content_box
|
||||||
|
: type === 'borderBoxSize'
|
||||||
|
? resize_observer_border_box
|
||||||
|
: resize_observer_device_pixel_content_box;
|
||||||
|
|
||||||
|
var unsub = observer.observe(element, /** @param {any} entry */ (entry) => update(entry[type]));
|
||||||
|
render_effect(() => unsub);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
* @param {'clientWidth' | 'clientHeight' | 'offsetWidth' | 'offsetHeight'} type
|
||||||
|
* @param {(size: number) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_element_size(element, type, update) {
|
||||||
|
var unsub = resize_observer_border_box.observe(element, () => update(element[type]));
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
untrack(() => update(element[type]));
|
||||||
|
return unsub;
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import { STATE_SYMBOL } from '../../../constants.js';
|
||||||
|
import { effect } from '../../../reactivity/effects.js';
|
||||||
|
import { untrack } from '../../../runtime.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} bound_value
|
||||||
|
* @param {Element} element_or_component
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function is_bound_this(bound_value, element_or_component) {
|
||||||
|
// Find the original target if the value is proxied.
|
||||||
|
var proxy_target = bound_value && bound_value[STATE_SYMBOL]?.t;
|
||||||
|
return bound_value === element_or_component || proxy_target === element_or_component;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} element_or_component
|
||||||
|
* @param {(value: unknown, ...parts: unknown[]) => void} update
|
||||||
|
* @param {(...parts: unknown[]) => unknown} get_value
|
||||||
|
* @param {() => unknown[]} [get_parts] Set if the this binding is used inside an each block,
|
||||||
|
* returns all the parts of the each block context that are used in the expression
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_this(element_or_component, update, get_value, get_parts) {
|
||||||
|
/** @type {unknown[]} */
|
||||||
|
var old_parts;
|
||||||
|
|
||||||
|
/** @type {unknown[]} */
|
||||||
|
var parts;
|
||||||
|
|
||||||
|
var e = effect(() => {
|
||||||
|
old_parts = parts;
|
||||||
|
// We only track changes to the parts, not the value itself to avoid unnecessary reruns.
|
||||||
|
parts = get_parts?.() || [];
|
||||||
|
|
||||||
|
untrack(() => {
|
||||||
|
if (element_or_component !== get_value(...parts)) {
|
||||||
|
update(element_or_component, ...parts);
|
||||||
|
// If this is an effect rerun (cause: each block context changes), then nullfiy the binding at
|
||||||
|
// the previous position if it isn't already taken over by a different effect.
|
||||||
|
if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) {
|
||||||
|
update(null, ...old_parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add effect teardown (likely causes: if block became false, each item removed, component unmounted).
|
||||||
|
// In these cases we need to nullify the binding only if we detect that the value is still the same.
|
||||||
|
// If not, that means that another effect has now taken over the binding.
|
||||||
|
e.ondestroy = () => {
|
||||||
|
// Defer to the next tick so that all updates can be reconciled first.
|
||||||
|
// This solves the case where one variable is shared across multiple this-bindings.
|
||||||
|
effect(() => {
|
||||||
|
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
|
||||||
|
update(null, ...parts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { render_effect } from '../../../reactivity/effects.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {'innerHTML' | 'textContent' | 'innerText'} property
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
* @param {() => unknown} get_value
|
||||||
|
* @param {(value: unknown) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_content_editable(property, element, get_value, update) {
|
||||||
|
element.addEventListener('input', () => {
|
||||||
|
// @ts-ignore
|
||||||
|
update(element[property]);
|
||||||
|
});
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
var value = get_value();
|
||||||
|
|
||||||
|
if (element[property] !== value) {
|
||||||
|
if (value === null) {
|
||||||
|
// @ts-ignore
|
||||||
|
var non_null_value = element[property];
|
||||||
|
update(non_null_value);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
element[property] = value + '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} property
|
||||||
|
* @param {string} event_name
|
||||||
|
* @param {'get' | 'set'} type
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {() => unknown} get_value
|
||||||
|
* @param {(value: unknown) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_property(property, event_name, type, element, get_value, update) {
|
||||||
|
var target_handler = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
update(element[property]);
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener(event_name, target_handler);
|
||||||
|
|
||||||
|
if (type === 'set') {
|
||||||
|
render_effect(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
element[property] = get_value();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'get') {
|
||||||
|
// @ts-ignore
|
||||||
|
update(element[property]);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (element === document.body || element === window || element === document) {
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(event_name, target_handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { render_effect } from '../../../reactivity/effects.js';
|
||||||
|
import { listen } from './shared.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {'x' | 'y'} type
|
||||||
|
* @param {() => number} get_value
|
||||||
|
* @param {(value: number) => void} update
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bind_window_scroll(type, get_value, update) {
|
||||||
|
var is_scrolling_x = type === 'x';
|
||||||
|
|
||||||
|
var target_handler = () => {
|
||||||
|
scrolling = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(clear, 100); // TODO use scrollend event if supported (or when supported everywhere?)
|
||||||
|
|
||||||
|
update(window[is_scrolling_x ? 'scrollX' : 'scrollY']);
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventListener('scroll', target_handler, {
|
||||||
|
passive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var latest_value = 0;
|
||||||
|
var scrolling = false;
|
||||||
|
|
||||||
|
/** @type {ReturnType<typeof setTimeout>} */
|
||||||
|
var timeout;
|
||||||
|
var clear = () => {
|
||||||
|
scrolling = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
latest_value = get_value() || 0;
|
||||||
|
if (!scrolling) {
|
||||||
|
scrolling = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (is_scrolling_x) {
|
||||||
|
scrollTo(latest_value, window.scrollY);
|
||||||
|
} else {
|
||||||
|
scrollTo(window.scrollX, latest_value);
|
||||||
|
}
|
||||||
|
timeout = setTimeout(clear, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
render_effect(() => {
|
||||||
|
return () => {
|
||||||
|
removeEventListener('scroll', target_handler);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {'innerWidth' | 'innerHeight' | 'outerWidth' | 'outerHeight'} type
|
||||||
|
* @param {(size: number) => void} update
|
||||||
|
*/
|
||||||
|
export function bind_window_size(type, update) {
|
||||||
|
listen(window, ['resize'], () => update(window[type]));
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { hydrating } from '../hydration.js';
|
||||||
|
import { set_class_name } from '../operations.js';
|
||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {() => string} value
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function class_name_effect(dom, value) {
|
||||||
|
render_effect(() => {
|
||||||
|
class_name(dom, value());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function class_name(dom, value) {
|
||||||
|
// @ts-expect-error need to add __className to patched prototype
|
||||||
|
var prev_class_name = dom.__className;
|
||||||
|
var next_class_name = to_class(value);
|
||||||
|
|
||||||
|
if (hydrating && dom.className === next_class_name) {
|
||||||
|
// In case of hydration don't reset the class as it's already correct.
|
||||||
|
// @ts-expect-error need to add __className to patched prototype
|
||||||
|
dom.__className = next_class_name;
|
||||||
|
} else if (
|
||||||
|
prev_class_name !== next_class_name ||
|
||||||
|
(hydrating && dom.className !== next_class_name)
|
||||||
|
) {
|
||||||
|
if (next_class_name === '') {
|
||||||
|
dom.removeAttribute('class');
|
||||||
|
} else {
|
||||||
|
set_class_name(dom, next_class_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error need to add __className to patched prototype
|
||||||
|
dom.__className = next_class_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template V
|
||||||
|
* @param {V} value
|
||||||
|
* @returns {string | V}
|
||||||
|
*/
|
||||||
|
export function to_class(value) {
|
||||||
|
return value == null ? '' : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {string} class_name
|
||||||
|
* @param {boolean} value
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function class_toggle(dom, class_name, value) {
|
||||||
|
if (value) {
|
||||||
|
dom.classList.add(class_name);
|
||||||
|
} else {
|
||||||
|
dom.classList.remove(class_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {string} class_name
|
||||||
|
* @param {() => boolean} value
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function class_toggle_effect(dom, class_name, value) {
|
||||||
|
render_effect(() => {
|
||||||
|
class_toggle(dom, class_name, value());
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { all_registered_events, root_event_handles } from '../../render.js';
|
||||||
|
import { define_property, is_array } from '../../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} event_name
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {EventListener} handler
|
||||||
|
* @param {boolean} capture
|
||||||
|
* @param {boolean} [passive]
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function event(event_name, dom, handler, capture, passive) {
|
||||||
|
var options = { capture, passive };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @this {EventTarget}
|
||||||
|
*/
|
||||||
|
function target_handler(/** @type {Event} */ event) {
|
||||||
|
handle_event_propagation(dom, event);
|
||||||
|
if (!event.cancelBubble) {
|
||||||
|
return handler.call(this, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.addEventListener(event_name, target_handler, options);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (dom === document.body || dom === window || dom === document) {
|
||||||
|
render_effect(() => {
|
||||||
|
return () => {
|
||||||
|
dom.removeEventListener(event_name, target_handler, options);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<string>} events
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function delegate(events) {
|
||||||
|
for (var i = 0; i < events.length; i++) {
|
||||||
|
all_registered_events.add(events[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var fn of root_event_handles) {
|
||||||
|
fn(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} handler_element
|
||||||
|
* @param {Event} event
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function handle_event_propagation(handler_element, event) {
|
||||||
|
var owner_document = handler_element.ownerDocument;
|
||||||
|
var event_name = event.type;
|
||||||
|
var path = event.composedPath?.() || [];
|
||||||
|
var current_target = /** @type {null | Element} */ (path[0] || event.target);
|
||||||
|
|
||||||
|
if (event.target !== current_target) {
|
||||||
|
define_property(event, 'target', {
|
||||||
|
configurable: true,
|
||||||
|
value: current_target
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// composedPath contains list of nodes the event has propagated through.
|
||||||
|
// We check __root to skip all nodes below it in case this is a
|
||||||
|
// parent of the __root node, which indicates that there's nested
|
||||||
|
// mounted apps. In this case we don't want to trigger events multiple times.
|
||||||
|
var path_idx = 0;
|
||||||
|
|
||||||
|
// @ts-expect-error is added below
|
||||||
|
var handled_at = event.__root;
|
||||||
|
|
||||||
|
if (handled_at) {
|
||||||
|
var at_idx = path.indexOf(handled_at);
|
||||||
|
if (
|
||||||
|
at_idx !== -1 &&
|
||||||
|
(handler_element === document || handler_element === /** @type {any} */ (window))
|
||||||
|
) {
|
||||||
|
// This is the fallback document listener or a window listener, but the event was already handled
|
||||||
|
// -> ignore, but set handle_at to document/window so that we're resetting the event
|
||||||
|
// chain in case someone manually dispatches the same event object again.
|
||||||
|
// @ts-expect-error
|
||||||
|
event.__root = handler_element;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're deliberately not skipping if the index is higher, because
|
||||||
|
// someone could create an event programmatically and emit it multiple times,
|
||||||
|
// in which case we want to handle the whole propagation chain properly each time.
|
||||||
|
// (this will only be a false negative if the event is dispatched multiple times and
|
||||||
|
// the fallback document listener isn't reached in between, but that's super rare)
|
||||||
|
var handler_idx = path.indexOf(handler_element);
|
||||||
|
if (handler_idx === -1) {
|
||||||
|
// handle_idx can theoretically be -1 (happened in some JSDOM testing scenarios with an event listener on the window object)
|
||||||
|
// so guard against that, too, and assume that everything was handled at this point.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (at_idx <= handler_idx) {
|
||||||
|
// +1 because at_idx is the element which was already handled, and there can only be one delegated event per element.
|
||||||
|
// Avoids on:click and onclick on the same event resulting in onclick being fired twice.
|
||||||
|
path_idx = at_idx + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_target = /** @type {Element} */ (path[path_idx] || event.target);
|
||||||
|
|
||||||
|
// Proxy currentTarget to correct target
|
||||||
|
define_property(event, 'currentTarget', {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return current_target || owner_document;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (current_target !== null) {
|
||||||
|
/** @type {null | Element} */
|
||||||
|
var parent_element =
|
||||||
|
current_target.parentNode || /** @type {any} */ (current_target).host || null;
|
||||||
|
var internal_prop_name = '__' + event_name;
|
||||||
|
// @ts-ignore
|
||||||
|
var delegated = current_target[internal_prop_name];
|
||||||
|
|
||||||
|
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
|
||||||
|
if (is_array(delegated)) {
|
||||||
|
var [fn, ...data] = delegated;
|
||||||
|
fn.apply(current_target, [event, ...data]);
|
||||||
|
} else {
|
||||||
|
delegated.call(current_target, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.cancelBubble ||
|
||||||
|
parent_element === handler_element ||
|
||||||
|
current_target === handler_element
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_target = parent_element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error is used above
|
||||||
|
event.__root = handler_element;
|
||||||
|
// @ts-expect-error is used above
|
||||||
|
current_target = handler_element;
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { hydrating } from '../hydration.js';
|
||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
import { current_block } from '../../runtime.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} dom
|
||||||
|
* @param {boolean} value
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function autofocus(dom, value) {
|
||||||
|
if (value) {
|
||||||
|
const body = document.body;
|
||||||
|
dom.autofocus = true;
|
||||||
|
render_effect(
|
||||||
|
() => {
|
||||||
|
if (document.activeElement === body) {
|
||||||
|
dom.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
current_block,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The child of a textarea actually corresponds to the defaultValue property, so we need
|
||||||
|
* to remove it upon hydration to avoid a bug when someone resets the form value.
|
||||||
|
* @param {HTMLTextAreaElement} dom
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function remove_textarea_child(dom) {
|
||||||
|
if (hydrating && dom.firstChild !== null) {
|
||||||
|
dom.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { render_effect } from '../../reactivity/effects.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} dom
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} value
|
||||||
|
* @param {boolean} [important]
|
||||||
|
*/
|
||||||
|
export function style(dom, key, value, important) {
|
||||||
|
const style = dom.style;
|
||||||
|
const prev_value = style.getPropertyValue(key);
|
||||||
|
if (value == null) {
|
||||||
|
if (prev_value !== '') {
|
||||||
|
style.removeProperty(key);
|
||||||
|
}
|
||||||
|
} else if (prev_value !== value) {
|
||||||
|
style.setProperty(key, value, important ? 'important' : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} dom
|
||||||
|
* @param {string} key
|
||||||
|
* @param {() => string} value
|
||||||
|
* @param {boolean} [important]
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function style_effect(dom, key, value, important) {
|
||||||
|
render_effect(() => {
|
||||||
|
const string = value();
|
||||||
|
style(dom, key, string, important);
|
||||||
|
});
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue