mirror of https://github.com/sveltejs/svelte
commit
d7f3d45700
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: add support for webkitdirectory DOM boolean attribute
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: don't override instance methods during legacy class creation
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: add reactive Map class to svelte/reactivity
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: adjust scope parent for named slots
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve handling of unowned derived signals
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: improve element class attribute behaviour
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: add types for svelte/reactivity
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
breaking: apply fallback value every time in runes mode
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: ensure select value is updated upon select option removal
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: ensure capture events don't call delegated events
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: ensure arguments are supported on all reactive Date methods
|
@ -1,194 +1,132 @@
|
|||||||
import { is_promise } from '../../../common.js';
|
import { is_promise } from '../../../common.js';
|
||||||
import { hydrate_block_anchor } from '../hydration.js';
|
import { hydrate_block_anchor } from '../hydration.js';
|
||||||
import { remove } from '../reconciler.js';
|
import { remove } from '../reconciler.js';
|
||||||
import { current_block, execute_effect, flushSync } from '../../runtime.js';
|
import {
|
||||||
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
|
current_component_context,
|
||||||
import { trigger_transitions } from '../elements/transitions.js';
|
flushSync,
|
||||||
import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js';
|
set_current_component_context,
|
||||||
|
set_current_effect,
|
||||||
/** @returns {import('../../types.js').AwaitBlock} */
|
set_current_reaction
|
||||||
export function create_await_block() {
|
} from '../../runtime.js';
|
||||||
return {
|
import { destroy_effect, pause_effect, render_effect } from '../../reactivity/effects.js';
|
||||||
// dom
|
import { DESTROYED, INERT } from '../../constants.js';
|
||||||
d: null,
|
import { create_block } from './utils.js';
|
||||||
// effect
|
|
||||||
e: null,
|
|
||||||
// parent
|
|
||||||
p: /** @type {import('../../types.js').Block} */ (current_block),
|
|
||||||
// pending
|
|
||||||
n: true,
|
|
||||||
// transition
|
|
||||||
r: null,
|
|
||||||
// type
|
|
||||||
t: AWAIT_BLOCK
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template V
|
* @template V
|
||||||
* @param {Comment} anchor_node
|
* @param {Comment} anchor
|
||||||
* @param {(() => Promise<V>)} input
|
* @param {(() => Promise<V>)} get_input
|
||||||
* @param {null | ((anchor: Node) => void)} pending_fn
|
* @param {null | ((anchor: Node) => void)} pending_fn
|
||||||
* @param {null | ((anchor: Node, value: V) => void)} then_fn
|
* @param {null | ((anchor: Node, value: V) => void)} then_fn
|
||||||
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
|
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
|
export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
|
||||||
const block = create_await_block();
|
const block = create_block();
|
||||||
|
|
||||||
/** @type {null | import('../../types.js').Render} */
|
const component_context = current_component_context;
|
||||||
let current_render = null;
|
|
||||||
hydrate_block_anchor(anchor_node);
|
hydrate_block_anchor(anchor);
|
||||||
|
|
||||||
/** @type {{}} */
|
/** @type {any} */
|
||||||
let latest_token;
|
let input;
|
||||||
|
|
||||||
/** @type {typeof UNINITIALIZED | V} */
|
/** @type {import('#client').Effect | null} */
|
||||||
let resolved_value = UNINITIALIZED;
|
let pending_effect;
|
||||||
|
|
||||||
/** @type {unknown} */
|
/** @type {import('#client').Effect | null} */
|
||||||
let error = UNINITIALIZED;
|
let then_effect;
|
||||||
let pending = false;
|
|
||||||
block.r =
|
/** @type {import('#client').Effect | null} */
|
||||||
/**
|
let catch_effect;
|
||||||
* @param {import('../../types.js').Transition} transition
|
|
||||||
* @returns {void}
|
/**
|
||||||
*/
|
* @param {(anchor: Comment, value: any) => void} fn
|
||||||
(transition) => {
|
* @param {any} value
|
||||||
const render = /** @type {import('../../types.js').Render} */ (current_render);
|
*/
|
||||||
const transitions = render.s;
|
function create_effect(fn, value) {
|
||||||
transitions.add(transition);
|
set_current_effect(branch);
|
||||||
transition.f(() => {
|
set_current_reaction(branch); // TODO do we need both?
|
||||||
transitions.delete(transition);
|
set_current_component_context(component_context);
|
||||||
if (transitions.size === 0) {
|
var effect = render_effect(() => fn(anchor, value), {}, true);
|
||||||
// If the current render has changed since, then we can remove the old render
|
set_current_component_context(null);
|
||||||
// effect as it's stale.
|
set_current_reaction(null);
|
||||||
if (current_render !== render && render.e !== null) {
|
set_current_effect(null);
|
||||||
if (render.d !== null) {
|
|
||||||
remove(render.d);
|
// without this, the DOM does not update until two ticks after the promise,
|
||||||
render.d = null;
|
// resolves which is unexpected behaviour (and somewhat irksome to test)
|
||||||
}
|
flushSync();
|
||||||
destroy_effect(render.e);
|
|
||||||
render.e = null;
|
return effect;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
/** @param {import('#client').Effect} effect */
|
||||||
};
|
function pause(effect) {
|
||||||
const create_render_effect = () => {
|
if ((effect.f & DESTROYED) !== 0) return;
|
||||||
/** @type {import('../../types.js').Render} */
|
const block = effect.block;
|
||||||
const render = {
|
|
||||||
d: null,
|
pause_effect(effect, () => {
|
||||||
e: null,
|
// TODO make this unnecessary
|
||||||
s: new Set(),
|
const dom = block?.d;
|
||||||
p: current_render
|
if (dom) remove(dom);
|
||||||
};
|
});
|
||||||
const effect = render_effect(
|
}
|
||||||
() => {
|
|
||||||
if (error === UNINITIALIZED) {
|
const branch = render_effect(() => {
|
||||||
if (resolved_value === UNINITIALIZED) {
|
if (input === (input = get_input())) return;
|
||||||
// pending = true
|
|
||||||
block.n = true;
|
if (is_promise(input)) {
|
||||||
if (pending_fn !== null) {
|
const promise = /** @type {Promise<any>} */ (input);
|
||||||
pending_fn(anchor_node);
|
|
||||||
}
|
if (pending_fn) {
|
||||||
} else if (then_fn !== null) {
|
if (pending_effect && (pending_effect.f & INERT) === 0) {
|
||||||
// pending = false
|
if (pending_effect.block?.d) remove(pending_effect.block.d);
|
||||||
block.n = false;
|
destroy_effect(pending_effect);
|
||||||
then_fn(anchor_node, resolved_value);
|
|
||||||
}
|
|
||||||
} else if (catch_fn !== null) {
|
|
||||||
// pending = false
|
|
||||||
block.n = false;
|
|
||||||
catch_fn(anchor_node, error);
|
|
||||||
}
|
}
|
||||||
render.d = block.d;
|
|
||||||
block.d = null;
|
pending_effect = render_effect(() => pending_fn(anchor), {}, true);
|
||||||
},
|
|
||||||
block,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
render.e = effect;
|
|
||||||
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();
|
if (then_effect) pause(then_effect);
|
||||||
trigger_transitions(transitions, 'out');
|
if (catch_effect) pause(catch_effect);
|
||||||
}
|
|
||||||
};
|
promise.then(
|
||||||
const await_effect = render_effect(
|
(value) => {
|
||||||
() => {
|
if (promise !== input) return;
|
||||||
const token = {};
|
if (pending_effect) pause(pending_effect);
|
||||||
latest_token = token;
|
|
||||||
const promise = input();
|
if (then_fn) {
|
||||||
if (is_promise(promise)) {
|
then_effect = create_effect(then_fn, value);
|
||||||
promise.then(
|
}
|
||||||
/** @param {V} v */
|
},
|
||||||
(v) => {
|
(error) => {
|
||||||
if (latest_token === token) {
|
if (promise !== input) return;
|
||||||
// Ensure UI is in sync before resolving value.
|
if (pending_effect) pause(pending_effect);
|
||||||
flushSync();
|
|
||||||
resolved_value = v;
|
if (catch_fn) {
|
||||||
pending = false;
|
catch_effect = create_effect(catch_fn, error);
|
||||||
render();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** @param {unknown} _error */
|
|
||||||
(_error) => {
|
|
||||||
error = _error;
|
|
||||||
pending = false;
|
|
||||||
render();
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
if (resolved_value !== UNINITIALIZED || error !== UNINITIALIZED) {
|
|
||||||
error = UNINITIALIZED;
|
|
||||||
resolved_value = UNINITIALIZED;
|
|
||||||
}
|
}
|
||||||
if (!pending) {
|
);
|
||||||
pending = true;
|
} else {
|
||||||
render();
|
if (pending_effect) pause(pending_effect);
|
||||||
|
if (catch_effect) pause(catch_effect);
|
||||||
|
|
||||||
|
if (then_fn) {
|
||||||
|
if (then_effect) {
|
||||||
|
if (then_effect.block?.d) remove(then_effect.block.d);
|
||||||
|
destroy_effect(then_effect);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
error = UNINITIALIZED;
|
then_effect = render_effect(() => then_fn(anchor, input), {}, true);
|
||||||
resolved_value = promise;
|
|
||||||
pending = false;
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
block,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await_effect.ondestroy = () => {
|
|
||||||
let render = current_render;
|
|
||||||
latest_token = {};
|
|
||||||
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);
|
||||||
|
|
||||||
|
branch.ondestroy = () => {
|
||||||
|
// TODO this sucks, tidy it up
|
||||||
|
if (pending_effect?.block?.d) remove(pending_effect.block.d);
|
||||||
|
if (then_effect?.block?.d) remove(then_effect.block.d);
|
||||||
|
if (catch_effect?.block?.d) remove(catch_effect.block.d);
|
||||||
};
|
};
|
||||||
block.e = await_effect;
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,140 +1,76 @@
|
|||||||
import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js';
|
import { UNINITIALIZED } from '../../constants.js';
|
||||||
import { hydrate_block_anchor } from '../hydration.js';
|
import { hydrate_block_anchor } from '../hydration.js';
|
||||||
import { remove } from '../reconciler.js';
|
import { remove } from '../reconciler.js';
|
||||||
import { current_block, execute_effect } from '../../runtime.js';
|
import { pause_effect, render_effect } from '../../reactivity/effects.js';
|
||||||
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
|
|
||||||
import { trigger_transitions } from '../elements/transitions.js';
|
|
||||||
import { safe_not_equal } from '../../reactivity/equality.js';
|
import { safe_not_equal } from '../../reactivity/equality.js';
|
||||||
|
import { create_block } from './utils.js';
|
||||||
/** @returns {import('../../types.js').KeyBlock} */
|
|
||||||
function create_key_block() {
|
|
||||||
return {
|
|
||||||
// dom
|
|
||||||
d: null,
|
|
||||||
// effect
|
|
||||||
e: null,
|
|
||||||
// parent
|
|
||||||
p: /** @type {import('../../types.js').Block} */ (current_block),
|
|
||||||
// transition
|
|
||||||
r: null,
|
|
||||||
// type
|
|
||||||
t: KEY_BLOCK
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template V
|
* @template V
|
||||||
* @param {Comment} anchor_node
|
* @param {Comment} anchor
|
||||||
* @param {() => V} key
|
* @param {() => V} get_key
|
||||||
* @param {(anchor: Node) => void} render_fn
|
* @param {(anchor: Node) => void} render_fn
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function key_block(anchor_node, key, render_fn) {
|
export function key_block(anchor, get_key, render_fn) {
|
||||||
const block = create_key_block();
|
const block = create_block();
|
||||||
|
|
||||||
/** @type {null | import('../../types.js').Render} */
|
hydrate_block_anchor(anchor);
|
||||||
let current_render = null;
|
|
||||||
hydrate_block_anchor(anchor_node);
|
|
||||||
|
|
||||||
/** @type {V | typeof UNINITIALIZED} */
|
/** @type {V | typeof UNINITIALIZED} */
|
||||||
let key_value = UNINITIALIZED;
|
let key = UNINITIALIZED;
|
||||||
let mounted = false;
|
|
||||||
block.r =
|
/** @type {import('#client').Effect} */
|
||||||
/**
|
let effect;
|
||||||
* @param {import('../../types.js').Transition} transition
|
|
||||||
* @returns {void}
|
/**
|
||||||
*/
|
* Every time `key` changes, we create a new effect. Old effects are
|
||||||
(transition) => {
|
* removed from this set when they have fully transitioned out
|
||||||
const render = /** @type {import('../../types.js').Render} */ (current_render);
|
* @type {Set<import('#client').Effect>}
|
||||||
const transitions = render.s;
|
*/
|
||||||
transitions.add(transition);
|
let effects = new Set();
|
||||||
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('../../types.js').Render} */
|
|
||||||
const render = {
|
|
||||||
d: null,
|
|
||||||
e: null,
|
|
||||||
s: new Set(),
|
|
||||||
p: current_render
|
|
||||||
};
|
|
||||||
const effect = render_effect(
|
|
||||||
() => {
|
|
||||||
render_fn(anchor_node);
|
|
||||||
render.d = block.d;
|
|
||||||
block.d = null;
|
|
||||||
},
|
|
||||||
block,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
render.e = effect;
|
|
||||||
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 {
|
|
||||||
trigger_transitions(transitions, 'out');
|
|
||||||
create_render_effect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const key_effect = render_effect(
|
const key_effect = render_effect(
|
||||||
() => {
|
() => {
|
||||||
const prev_key_value = key_value;
|
if (safe_not_equal(key, (key = get_key()))) {
|
||||||
key_value = key();
|
if (effect) {
|
||||||
if (mounted && safe_not_equal(prev_key_value, key_value)) {
|
var e = effect;
|
||||||
render();
|
pause_effect(e, () => {
|
||||||
|
effects.delete(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
effect = render_effect(
|
||||||
|
() => {
|
||||||
|
render_fn(anchor);
|
||||||
|
|
||||||
|
const dom = block.d;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (dom !== null) {
|
||||||
|
remove(dom);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
block,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-expect-error TODO tidy up
|
||||||
|
effect.d = block.d;
|
||||||
|
|
||||||
|
effects.add(effect);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
block,
|
block,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
// To ensure topological ordering of the key effect to the render effect,
|
|
||||||
// we trigger the effect after.
|
|
||||||
render();
|
|
||||||
mounted = true;
|
|
||||||
key_effect.ondestroy = () => {
|
key_effect.ondestroy = () => {
|
||||||
let render = current_render;
|
for (const e of effects) {
|
||||||
while (render !== null) {
|
// @ts-expect-error TODO tidy up. ondestroy should be totally unnecessary
|
||||||
const dom = render.d;
|
if (e.d) remove(e.d);
|
||||||
if (dom !== null) {
|
|
||||||
remove(dom);
|
|
||||||
}
|
|
||||||
const effect = render.e;
|
|
||||||
if (effect !== null) {
|
|
||||||
destroy_effect(effect);
|
|
||||||
}
|
|
||||||
render = render.p;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
block.e = key_effect;
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
/** @returns {import('#client').Block} */
|
||||||
|
export function create_block() {
|
||||||
|
return {
|
||||||
|
// dom
|
||||||
|
d: null
|
||||||
|
};
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,2 +1,3 @@
|
|||||||
export { ReactiveDate as Date } from './date.js';
|
export { ReactiveDate as Date } from './date.js';
|
||||||
export { ReactiveSet as Set } from './set.js';
|
export { ReactiveSet as Set } from './set.js';
|
||||||
|
export { ReactiveMap as Map } from './map.js';
|
||||||
|
@ -0,0 +1,157 @@
|
|||||||
|
import { DEV } from 'esm-env';
|
||||||
|
import { source, set } from '../internal/client/reactivity/sources.js';
|
||||||
|
import { get } from '../internal/client/runtime.js';
|
||||||
|
import { UNINITIALIZED } from '../internal/client/constants.js';
|
||||||
|
import { map } from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template K
|
||||||
|
* @template V
|
||||||
|
*/
|
||||||
|
export class ReactiveMap extends Map {
|
||||||
|
/** @type {Map<K, import('#client').Source<V>>} */
|
||||||
|
#sources = new Map();
|
||||||
|
#version = source(0);
|
||||||
|
#size = source(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
|
||||||
|
*/
|
||||||
|
constructor(value) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// If the value is invalid then the native exception will fire here
|
||||||
|
if (DEV) new Map(value);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
var sources = this.#sources;
|
||||||
|
|
||||||
|
for (var [key, v] of value) {
|
||||||
|
sources.set(key, source(v));
|
||||||
|
super.set(key, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#size.v = sources.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#increment_version() {
|
||||||
|
set(this.#version, this.#version.v + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {K} key */
|
||||||
|
has(key) {
|
||||||
|
var s = this.#sources.get(key);
|
||||||
|
|
||||||
|
if (s === undefined) {
|
||||||
|
// We should always track the version in case
|
||||||
|
// the Set ever gets this value in the future.
|
||||||
|
get(this.#version);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(s);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(value: V, key: K, map: Map<K, V>) => void} callbackfn
|
||||||
|
* @param {any} [this_arg]
|
||||||
|
*/
|
||||||
|
forEach(callbackfn, this_arg) {
|
||||||
|
get(this.#version);
|
||||||
|
|
||||||
|
return super.forEach(callbackfn, this_arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {K} key */
|
||||||
|
get(key) {
|
||||||
|
var s = this.#sources.get(key);
|
||||||
|
|
||||||
|
if (s === undefined) {
|
||||||
|
// We should always track the version in case
|
||||||
|
// the Set ever gets this value in the future.
|
||||||
|
get(this.#version);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {K} key
|
||||||
|
* @param {V} value
|
||||||
|
* */
|
||||||
|
set(key, value) {
|
||||||
|
var sources = this.#sources;
|
||||||
|
var s = sources.get(key);
|
||||||
|
|
||||||
|
if (s === undefined) {
|
||||||
|
sources.set(key, source(value));
|
||||||
|
set(this.#size, sources.size);
|
||||||
|
this.#increment_version();
|
||||||
|
} else {
|
||||||
|
set(s, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {K} key */
|
||||||
|
delete(key) {
|
||||||
|
var sources = this.#sources;
|
||||||
|
var s = sources.get(key);
|
||||||
|
|
||||||
|
if (s !== undefined) {
|
||||||
|
sources.delete(key);
|
||||||
|
set(this.#size, sources.size);
|
||||||
|
set(s, /** @type {V} */ (UNINITIALIZED));
|
||||||
|
this.#increment_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
var sources = this.#sources;
|
||||||
|
|
||||||
|
if (sources.size !== 0) {
|
||||||
|
set(this.#size, 0);
|
||||||
|
for (var s of sources.values()) {
|
||||||
|
set(s, /** @type {V} */ (UNINITIALIZED));
|
||||||
|
}
|
||||||
|
this.#increment_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.clear();
|
||||||
|
super.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
get(this.#version);
|
||||||
|
return this.#sources.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
values() {
|
||||||
|
get(this.#version);
|
||||||
|
return map(this.#sources.values(), get);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries() {
|
||||||
|
get(this.#version);
|
||||||
|
return map(
|
||||||
|
this.#sources.entries(),
|
||||||
|
([key, source]) => /** @type {[K, V]} */ ([key, get(source)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return get(this.#size);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js';
|
||||||
|
import { flushSync } from '../main/main-client.js';
|
||||||
|
import { ReactiveMap } from './map.js';
|
||||||
|
import { assert, test } from 'vitest';
|
||||||
|
|
||||||
|
test('map.values()', () => {
|
||||||
|
const map = new ReactiveMap([
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
[3, 3],
|
||||||
|
[4, 4],
|
||||||
|
[5, 5]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const log: any = [];
|
||||||
|
|
||||||
|
const cleanup = user_root_effect(() => {
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push(map.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push(map.has(3));
|
||||||
|
});
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push(Array.from(map.values()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
map.delete(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
map.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(log, [5, true, [1, 2, 3, 4, 5], 4, false, [1, 2, 4, 5], 0, [], false]); // TODO update when we fix effect ordering bug
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('map.get(...)', () => {
|
||||||
|
const map = new ReactiveMap([
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
[3, 3]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const log: any = [];
|
||||||
|
|
||||||
|
const cleanup = user_root_effect(() => {
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push('get 1', map.get(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push('get 2', map.get(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push('get 3', map.get(3));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
map.delete(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
map.set(2, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', undefined, 'get 2', 2]);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('map.has(...)', () => {
|
||||||
|
const map = new ReactiveMap([
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
[3, 3]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const log: any = [];
|
||||||
|
|
||||||
|
const cleanup = user_root_effect(() => {
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push('has 1', map.has(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push('has 2', map.has(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push('has 3', map.has(3));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
map.delete(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
map.set(2, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(log, [
|
||||||
|
'has 1',
|
||||||
|
true,
|
||||||
|
'has 2',
|
||||||
|
true,
|
||||||
|
'has 3',
|
||||||
|
true,
|
||||||
|
'has 2',
|
||||||
|
false,
|
||||||
|
'has 2',
|
||||||
|
true
|
||||||
|
]);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('map handling of undefined values', () => {
|
||||||
|
const map = new ReactiveMap();
|
||||||
|
|
||||||
|
const log: any = [];
|
||||||
|
|
||||||
|
const cleanup = user_root_effect(() => {
|
||||||
|
map.set(1, undefined);
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push(map.get(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
map.delete(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
map.set(1, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(log, [undefined, undefined, 1]);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @template U
|
||||||
|
* @param {Iterable<T>} iterable
|
||||||
|
* @param {(value: T) => U} fn
|
||||||
|
* @returns {IterableIterator<U>}
|
||||||
|
*/
|
||||||
|
export function map(iterable, fn) {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: get_this,
|
||||||
|
next() {
|
||||||
|
for (const value of iterable) {
|
||||||
|
return { done: false, value: fn(value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { done: true, value: undefined };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @this {any} */
|
||||||
|
function get_this() {
|
||||||
|
return this;
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
<!--ssr:0--><div class="foo"></div><!--ssr:0-->
|
<!--ssr:0--><div id="foo"></div><!--ssr:0-->
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
export let className;
|
export let id;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={className}></div>
|
<div id={id}></div>
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await new Promise((r) => setTimeout(r, 200)); // wait for await block to resolve
|
||||||
|
|
||||||
|
const options = target.querySelectorAll('option');
|
||||||
|
assert.ok(!options[0].selected);
|
||||||
|
assert.ok(options[1].selected);
|
||||||
|
assert.ok(!options[2].selected);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,19 @@
|
|||||||
|
<script>
|
||||||
|
let promise = getNumbers();
|
||||||
|
let selected = 2;
|
||||||
|
|
||||||
|
async function getNumbers() {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
return [1, 2, 3];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select bind:value={selected}>
|
||||||
|
{#await promise}
|
||||||
|
<option>-1</option>
|
||||||
|
{:then numbers}
|
||||||
|
{#each numbers as number}
|
||||||
|
<option>{number}</option>
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
</select>
|
@ -0,0 +1,35 @@
|
|||||||
|
import { ok, test } from '../../test';
|
||||||
|
|
||||||
|
// test select binding behavior when a selected option is removed
|
||||||
|
export default test({
|
||||||
|
skip_if_ssr: 'permanent',
|
||||||
|
|
||||||
|
html: `<p>selected: a</p><select><option value="a">a</option><option value="b">b</option><option value="c">c</option></select>`,
|
||||||
|
|
||||||
|
async test({ assert, component, target }) {
|
||||||
|
const select = target.querySelector('select');
|
||||||
|
ok(select);
|
||||||
|
const options = target.querySelectorAll('option');
|
||||||
|
|
||||||
|
// first option should be selected by default since no value was bound
|
||||||
|
assert.equal(component.selected, 'a');
|
||||||
|
assert.equal(select.value, 'a');
|
||||||
|
assert.ok(options[0].selected);
|
||||||
|
|
||||||
|
// remove the selected item, so the bound value no longer matches anything
|
||||||
|
component.items = ['b', 'c'];
|
||||||
|
|
||||||
|
// There's a MutationObserver
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
// now no option should be selected
|
||||||
|
assert.equal(select.value, '');
|
||||||
|
assert.equal(select.selectedIndex, -1);
|
||||||
|
|
||||||
|
// model of selected value should be kept around, even if it is not in the list
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`<p>selected: a</p><select><option value="b">b</option><option value="c">c</option></select>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
export let selected;
|
||||||
|
export let items = ['a', 'b', 'c'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>selected: {selected}</p>
|
||||||
|
|
||||||
|
<select bind:value={selected}>
|
||||||
|
{#each items as letter}
|
||||||
|
<option>{letter}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
@ -0,0 +1 @@
|
|||||||
|
<slot name="inner" text="hello world" />
|
@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
export let text;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{text}
|
||||||
|
<hr />
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
@ -0,0 +1,7 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `
|
||||||
|
<div>hello world <hr> <div slot="footer">hello world</div></div>
|
||||||
|
`
|
||||||
|
});
|
@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
import Nested from "./Nested.svelte"
|
||||||
|
import Nested2 from "./Nested2.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Nested>
|
||||||
|
<Nested2 slot="inner" let:text {text}>
|
||||||
|
<div slot="footer">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</Nested2>
|
||||||
|
</Nested>
|
@ -0,0 +1,13 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
skip: true, // failing test for https://github.com/sveltejs/svelte/issues/10787
|
||||||
|
html: `<button>3</button>`,
|
||||||
|
async test({ assert, target }) {
|
||||||
|
target.querySelector('button')?.click();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,16 @@
|
|||||||
|
<script>
|
||||||
|
let x = 1;
|
||||||
|
let y = true;
|
||||||
|
$: array = y ? [1, 2] : [1];
|
||||||
|
$: count = array.length === 2 && x ? 1 : 0;
|
||||||
|
$: sum = count + array.length;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
// order is important here: x must be updated before y
|
||||||
|
// in order to test that $: still runs in the correct order
|
||||||
|
x = 2;
|
||||||
|
y = false;
|
||||||
|
}}>{sum}</button
|
||||||
|
>
|
@ -0,0 +1,16 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await target.querySelector('button')?.click();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
const options = target.querySelectorAll('option');
|
||||||
|
assert.equal(options[0].selected, false);
|
||||||
|
assert.equal(options[1].selected, true);
|
||||||
|
assert.equal(options[2].selected, false);
|
||||||
|
assert.equal(options[3].selected, false);
|
||||||
|
assert.equal(options[4].selected, true);
|
||||||
|
assert.equal(options[5].selected, false);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,33 @@
|
|||||||
|
<script>
|
||||||
|
let value = 'bar';
|
||||||
|
let value_bound = 'bar';
|
||||||
|
let options = {};
|
||||||
|
|
||||||
|
function loadOptions() {
|
||||||
|
options = {
|
||||||
|
foo: 'Foo',
|
||||||
|
bar: 'Bar',
|
||||||
|
baz: 'Baz',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select {value}>
|
||||||
|
{#each Object.entries(options) as [key, value] (key)}
|
||||||
|
<option value={key}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select bind:value={value_bound}>
|
||||||
|
{#each Object.entries(options) as [key, value] (key)}
|
||||||
|
<option value={key}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button on:click={loadOptions}>
|
||||||
|
Load options
|
||||||
|
</button>
|
@ -0,0 +1,21 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
const items = [{ id: 'a' }, { id: 'b' }];
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
get props() {
|
||||||
|
return { foo: [items[0]], items };
|
||||||
|
},
|
||||||
|
|
||||||
|
test({ assert, component, target }) {
|
||||||
|
const options = target.querySelectorAll('option');
|
||||||
|
|
||||||
|
assert.equal(options[0].selected, true);
|
||||||
|
assert.equal(options[1].selected, false);
|
||||||
|
|
||||||
|
component.foo = [component.items[1]];
|
||||||
|
|
||||||
|
assert.equal(options[0].selected, false);
|
||||||
|
assert.equal(options[1].selected, true);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
export let foo;
|
||||||
|
export let items;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select value="{foo}" multiple>
|
||||||
|
<option value='{items[0]}'>a</option>
|
||||||
|
<option value='{items[1]}'>b</option>
|
||||||
|
</select>
|
@ -0,0 +1,30 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
let [btn1, btn2] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn1.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn2.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`<button>Activate</button><button>Toggle</button>\nneighba\nneighba`
|
||||||
|
);
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn2.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`<button>Activate</button><button>Toggle</button>\nzeeba\nzeeba`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,49 @@
|
|||||||
|
<script>
|
||||||
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
|
class Model {
|
||||||
|
data = $state();
|
||||||
|
|
||||||
|
constructor(data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
name = $derived(this.data?.name);
|
||||||
|
source = $derived(this.data?.source);
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.data.name = this.data.name === 'zeeba' ? 'neighba' : 'zeeba';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let model = $state(new Model({ name: 'zeeba', source: 'initial' }));
|
||||||
|
|
||||||
|
let setModel = (source) => {
|
||||||
|
let next = new Model({ name: 'zeeba', source });
|
||||||
|
model = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
let needsSet = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if(needsSet) {
|
||||||
|
setModel('effect');
|
||||||
|
untrack(() => {
|
||||||
|
needsSet = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let setWithEffect = () => {
|
||||||
|
needsSet = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let toggle = () => {
|
||||||
|
model.toggle();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={setWithEffect}>Activate</button>
|
||||||
|
<button onclick={toggle}>Toggle</button>
|
||||||
|
{model.name}
|
||||||
|
{model.data.name}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue