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