Merge branch 'master' into tanhauhau/claim-static-html-tag-without-recreating-nested-nodes

pull/7426/head
Simon Holthausen 3 years ago
commit 3a8b6d0abe

@ -1,44 +1,71 @@
# Svelte changelog # Svelte changelog
## Unreleased ## 3.58.0
* Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311))
* Add support for CSS `@container` queries ([#6969](https://github.com/sveltejs/svelte/issues/6969))
* Respect `preserveComments` in DOM output ([#7182](https://github.com/sveltejs/svelte/pull/7182))
* Allow use of `document` for `target` in typings ([#7554](https://github.com/sveltejs/svelte/pull/7554))
* Add `a11y-interactive-supports-focus` warning ([#8392](https://github.com/sveltejs/svelte/pull/8392))
* Fix equality check when updating dynamic text ([#5931](https://github.com/sveltejs/svelte/issues/5931))
* Relax `a11y-no-noninteractive-element-to-interactive-role` warning ([#8402](https://github.com/sveltejs/svelte/pull/8402))
* Properly handle microdata attributes ([#8413](https://github.com/sveltejs/svelte/issues/8413))
* Prevent name collision when using computed destructuring variables ([#8417](https://github.com/sveltejs/svelte/issues/8417))
* Fix escaping `<textarea value={...}>` values in SSR ([#8429](https://github.com/sveltejs/svelte/issues/8429))
## 3.57.0
* Add `<svelte:document>` ([#3310](https://github.com/sveltejs/svelte/issues/3310))
* Add a11y `no-noninteractive-element-to-interactive-role` ([#8167](https://github.com/sveltejs/svelte/pull/8167))
* Stop intro transition from triggering incorrectly ([#6152](https://github.com/sveltejs/svelte/issues/6152), [#6812](https://github.com/sveltejs/svelte/issues/6812))
* Support computed and literal properties when destructuring objects in the template ([#6609](https://github.com/sveltejs/svelte/issues/6609))
* Give `style:` directive precedence over `style=` attribute ([#7475](https://github.com/sveltejs/svelte/issues/7475))
* Select `<option>` with `selected` attribute when initial state is `undefined` ([#8361](https://github.com/sveltejs/svelte/issues/8361))
* Prevent derived store callbacks after store is unsubscribed from ([#8364](https://github.com/sveltejs/svelte/issues/8364))
* Account for `bind:group` members being spread across multiple control flow blocks ([#8372](https://github.com/sveltejs/svelte/issues/8372))
* Revert buggy reactive statement optimization ([#8374](https://github.com/sveltejs/svelte/issues/8374))
* Support CSS units in the `fly` and `blur` transitions ([#7623](https://github.com/sveltejs/svelte/pull/7623))
## 3.56.0
* Add `|stopImmediatePropagation` event modifier ([#5085](https://github.com/sveltejs/svelte/issues/5085))
* Add `axis` parameter to `slide` transition ([#6182](https://github.com/sveltejs/svelte/issues/6182))
* Add `readonly` utility to convert `writable` store to readonly ([#6518](https://github.com/sveltejs/svelte/pull/6518))
* Add `readyState` binding for media elements ([#6666](https://github.com/sveltejs/svelte/issues/6666))
* Generate valid automatic component names when the filename contains only special characters ([#7143](https://github.com/sveltejs/svelte/issues/7143))
* Add `naturalWidth` and `naturalHeight` bindings ([#7771](https://github.com/sveltejs/svelte/issues/7771))
* Support `<!-- svelte-ignore ... -->` on components ([#8082](https://github.com/sveltejs/svelte/issues/8082))
* Add a11y warnings: * Add a11y warnings:
* `aria-activedescendant-has-tabindex`: elements with `aria-activedescendant` need to have a `tabindex` ([#8172](https://github.com/sveltejs/svelte/pull/8172)) * `aria-activedescendant-has-tabindex`: checks that elements with `aria-activedescendant` have a `tabindex` ([#8172](https://github.com/sveltejs/svelte/pull/8172))
* `role-supports-aria-props`: checks that the (implicit) element role supports the given aria attributes ([#8195](https://github.com/sveltejs/svelte/pull/8195)) * `role-supports-aria-props`: checks that the (implicit) element role supports the given aria attributes ([#8195](https://github.com/sveltejs/svelte/pull/8195))
* Omit a11y warning on `<video>` tags with `aria-hidden="true"` ([#7874](https://github.com/sveltejs/svelte/issues/7874))
* Omit a11y "no child content" warning on elements with `aria-label` ([#8299](https://github.com/sveltejs/svelte/pull/8299))
* Omit a11y warnings on `<svelte:element>` ([#7939](https://github.com/sveltejs/svelte/issues/7939))
* Make `noreferrer` warning less zealous ([#6289](https://github.com/sveltejs/svelte/issues/6289))
* `trusted-types` CSP compatibility for Web Components ([#8134](https://github.com/sveltejs/svelte/issues/8134))
* Add `data-sveltekit-replacestate` and `data-sveltekit-keepfocus` attribute typings ([#8281](https://github.com/sveltejs/svelte/issues/8281)) * Add `data-sveltekit-replacestate` and `data-sveltekit-keepfocus` attribute typings ([#8281](https://github.com/sveltejs/svelte/issues/8281))
* Don't throw when calling `unsubscribe` twice ([#8186](https://github.com/sveltejs/svelte/pull/8186)) * Compute node dimensions immediately before crossfading ([#4111](https://github.com/sveltejs/svelte/issues/4111))
* Detect unused empty attribute CSS selectors ([#8042](https://github.com/sveltejs/svelte/issues/8042)) * Fix potential infinite invalidate loop with `<svelte:component>` ([#4129](https://github.com/sveltejs/svelte/issues/4129))
* Simpler output for reactive statements if dependencies are all static ([#7942](https://github.com/sveltejs/svelte/pull/7942))
* Flush remaining `afterUpdate` calls before `onDestroy` ([#7476](https://github.com/sveltejs/svelte/issues/7476))
* Check value equality for `input` types `url` and `search` ([#7027](https://github.com/sveltejs/svelte/issues/7027))
* Compute node dimensions directly before crossfading ([#4111](https://github.com/sveltejs/svelte/issues/4111))
* Add `readonly` method to convert `writable` store to readonly ([#6518](https://github.com/sveltejs/svelte/pull/6518))
* Ensure `bind:offsetHeight` updates initially ([#4233](https://github.com/sveltejs/svelte/issues/4233)) * Ensure `bind:offsetHeight` updates initially ([#4233](https://github.com/sveltejs/svelte/issues/4233))
* Better handling of `inert` attribute ([#7500](https://github.com/sveltejs/svelte/issues/7500))
* Clear inputs when `bind:group` to `undefined` ([#8214](https://github.com/sveltejs/svelte/issues/8214))
* Ensure nested arrays can change at the same time ([#8282](https://github.com/sveltejs/svelte/issues/8282))
* Reduce use of template literals in SSR output for better performance ([#7539](https://github.com/sveltejs/svelte/pull/7539))
* Allow assigning to property of const while destructuring ([#7964](https://github.com/sveltejs/svelte/issues/7964))
* Don't set selected options if value is unbound or not passed ([#5644](https://github.com/sveltejs/svelte/issues/5644)) * Don't set selected options if value is unbound or not passed ([#5644](https://github.com/sveltejs/svelte/issues/5644))
* Ensure `<input>` value persists when swapping elements with spread attributes in an `#each` block ([#7578](https://github.com/sveltejs/svelte/issues/7578)) * Validate component `:global()` selectors ([#6272](https://github.com/sveltejs/svelte/issues/6272))
* Select first enabled option by default when initial value is undefined ([#7041](https://github.com/sveltejs/svelte/issues/7041)) * Improve warnings:
* Fix race condition on `svelte:element` with transitions ([#7948](https://github.com/sveltejs/svelte/issues/7948)) * Make `noreferrer` warning less zealous ([#6289](https://github.com/sveltejs/svelte/issues/6289))
* Optimise `<svelte:element>` output code for static tag and static attribute ([#8161](https://github.com/sveltejs/svelte/pull/8161)) * Omit a11y warnings on `<video aria-hidden="true">` ([#7874](https://github.com/sveltejs/svelte/issues/7874))
* Decode html entities correctly ([#8026](https://github.com/sveltejs/svelte/issues/8026)) * Omit a11y warnings on `<svelte:element>` ([#7939](https://github.com/sveltejs/svelte/issues/7939))
* Detect unused empty attribute CSS selectors ([#8042](https://github.com/sveltejs/svelte/issues/8042))
* Omit "no child content" warning on elements with `aria-label` ([#8296](https://github.com/sveltejs/svelte/issues/8296))
* Check value equality for `<input type="search">` and `<input type="url">` ([#7027](https://github.com/sveltejs/svelte/issues/7027))
* Do not select a disabled `<option>` by default when the initial bound value is undefined ([#7041](https://github.com/sveltejs/svelte/issues/7041))
* Handle `{@html}` tags inside `<template>` tags ([#7364](https://github.com/sveltejs/svelte/pull/7364)) * Handle `{@html}` tags inside `<template>` tags ([#7364](https://github.com/sveltejs/svelte/pull/7364))
* Introduce parameter to allow for horizontal slide transition ([#6182](https://github.com/sveltejs/svelte/issues/6182)) * Ensure `afterUpdate` is not called after `onDestroy` ([#7476](https://github.com/sveltejs/svelte/issues/7476))
* Add `naturalWidth` and `naturalHeight` bindings ([#7771](https://github.com/sveltejs/svelte/issues/7771)) * Improve handling of `inert` attribute ([#7500](https://github.com/sveltejs/svelte/issues/7500))
* make `<!-- svelte-ignore ... -->` work above components ([#8082](https://github.com/sveltejs/svelte/issues/8082)) * Reduce use of template literals in SSR output for better performance ([#7539](https://github.com/sveltejs/svelte/pull/7539))
* add global compound selector validation ([#6272](https://github.com/sveltejs/svelte/issues/6272)) * Ensure `<input>` value persists when swapping elements with spread attributes in an `{#each}` block ([#7578](https://github.com/sveltejs/svelte/issues/7578))
* add `stopImmediatePropagation` event modifier ([#5085](https://github.com/sveltejs/svelte/issues/5085)) * Simplify generated code for reactive statements if dependencies are all static ([#7942](https://github.com/sveltejs/svelte/pull/7942))
* add `readyState` binding for media elements ([#6666](https://github.com/sveltejs/svelte/issues/6666)) * Fix race condition on `<svelte:element>` with transitions ([#7948](https://github.com/sveltejs/svelte/issues/7948))
* call `<svelte:component>` update to `this` only when it's dirty ([#4129](https://github.com/sveltejs/svelte/issues/4129)) * Allow assigning to a property of a `const` when destructuring ([#7964](https://github.com/sveltejs/svelte/issues/7964))
* support exclusively special characters in component filenames ([#7143](https://github.com/sveltejs/svelte/issues/7143)) * Match browser behavior for decoding malformed HTML entities ([#8026](https://github.com/sveltejs/svelte/issues/8026))
* Ensure `trusted-types` CSP compatibility for Web Components ([#8134](https://github.com/sveltejs/svelte/issues/8134))
* Optimise `<svelte:element>` output code for static tag and static attribute ([#8161](https://github.com/sveltejs/svelte/pull/8161))
* Don't throw when calling unsubscribing from a store twice ([#8186](https://github.com/sveltejs/svelte/pull/8186))
* Clear inputs when `bind:group` value is set to `undefined` ([#8214](https://github.com/sveltejs/svelte/issues/8214))
* Fix handling of nested arrays with keyed `{#each}` containing a non-keyed `{#each}` ([#8282](https://github.com/sveltejs/svelte/issues/8282))
## 3.55.1 ## 3.55.1

@ -197,6 +197,9 @@ export interface DOMAttributes<T extends EventTarget> {
'on:message'?: MessageEventHandler<T> | undefined | null; 'on:message'?: MessageEventHandler<T> | undefined | null;
'on:messageerror'?: MessageEventHandler<T> | undefined | null; 'on:messageerror'?: MessageEventHandler<T> | undefined | null;
// Document Events
'on:visibilitychange'?: EventHandler<Event, T> | undefined | null;
// Global Events // Global Events
'on:cancel'?: EventHandler<Event, T> | undefined | null; 'on:cancel'?: EventHandler<Event, T> | undefined | null;
'on:close'?: EventHandler<Event, T> | undefined | null; 'on:close'?: EventHandler<Event, T> | undefined | null;
@ -531,13 +534,17 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
is?: string | undefined | null; is?: string | undefined | null;
/** /**
* Elements with the contenteditable attribute support innerHTML and textContent bindings. * Elements with the contenteditable attribute support `innerHTML`, `textContent` and `innerText` bindings.
*/ */
'bind:innerHTML'?: string | undefined | null; 'bind:innerHTML'?: string | undefined | null;
/** /**
* Elements with the contenteditable attribute support innerHTML and textContent bindings. * Elements with the contenteditable attribute support `innerHTML`, `textContent` and `innerText` bindings.
*/ */
'bind:textContent'?: string | undefined | null; 'bind:textContent'?: string | undefined | null;
/**
* Elements with the contenteditable attribute support `innerHTML`, `textContent` and `innerText` bindings.
*/
'bind:innerText'?: string | undefined | null;
// SvelteKit // SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null; 'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
@ -866,9 +873,9 @@ export interface HTMLMediaAttributes<T extends HTMLMediaElement> extends HTMLAtt
} }
export interface HTMLMetaAttributes extends HTMLAttributes<HTMLMetaElement> { export interface HTMLMetaAttributes extends HTMLAttributes<HTMLMetaElement> {
charSet?: string | undefined | null; charset?: string | undefined | null;
content?: string | undefined | null; content?: string | undefined | null;
httpequiv?: string | undefined | null; 'http-equiv'?: string | undefined | null;
name?: string | undefined | null; name?: string | undefined | null;
media?: string | undefined | null; media?: string | undefined | null;
} }
@ -1579,6 +1586,7 @@ export interface SvelteHTMLElements {
// Svelte specific // Svelte specific
'svelte:window': SvelteWindowAttributes; 'svelte:window': SvelteWindowAttributes;
'svelte:document': HTMLAttributes<Document>;
'svelte:body': HTMLAttributes<HTMLElement>; 'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string }; 'svelte:fragment': { slot?: string };
'svelte:options': { [name: string]: any }; 'svelte:options': { [name: string]: any };

@ -3,12 +3,7 @@
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const { readFileSync, writeFileSync } = require('fs'); const { readFileSync, writeFileSync } = require('fs');
try { execSync('tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly', { stdio: 'inherit' });
execSync('tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly');
} catch (err) {
console.error(err.stderr.toString());
throw err;
}
// We need to add these types to the .d.ts files here because if we add them before building, the build will fail, // We need to add these types to the .d.ts files here because if we add them before building, the build will fail,
// because the TS->JS transformation doesn't know these exports are types and produces code that fails at runtime. // because the TS->JS transformation doesn't know these exports are types and produces code that fails at runtime.
// We can't use `export type` syntax either because the TS version we're on doesn't have this feature yet. // We can't use `export type` syntax either because the TS version we're on doesn't have this feature yet.

1330
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.55.1", "version": "3.58.0",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",
@ -122,41 +122,41 @@
"@ampproject/remapping": "^0.3.0", "@ampproject/remapping": "^0.3.0",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
"@rollup/plugin-commonjs": "^11.0.0", "@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-json": "^4.0.1", "@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-node-resolve": "^11.2.1",
"@rollup/plugin-replace": "^2.3.0", "@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-sucrase": "^3.1.0", "@rollup/plugin-sucrase": "^3.1.0",
"@rollup/plugin-typescript": "^2.0.1", "@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^2.0.0", "@rollup/plugin-virtual": "^3.0.1",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0", "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0",
"@types/aria-query": "^5.0.0", "@types/aria-query": "^5.0.1",
"@types/mocha": "^7.0.0", "@types/mocha": "^7.0.0",
"@types/node": "^8.10.53", "@types/node": "^8.10.53",
"@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.29.0",
"acorn": "^8.8.1", "acorn": "^8.8.1",
"agadoo": "^2.0.0", "agadoo": "^3.0.0",
"aria-query": "^5.1.1", "aria-query": "^5.1.3",
"axobject-query": "^3.1.1", "axobject-query": "^3.1.1",
"code-red": "^0.2.5", "code-red": "^1.0.0",
"css-tree": "^2.2.1", "css-tree": "^2.3.1",
"eslint": "^8.26.0", "eslint": "^8.35.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.27.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"estree-walker": "^3.0.1", "estree-walker": "^3.0.3",
"is-reference": "^3.0.0", "is-reference": "^3.0.1",
"jsdom": "^15.2.1", "jsdom": "^15.2.1",
"kleur": "^4.1.5", "kleur": "^4.1.5",
"locate-character": "^2.0.5", "locate-character": "^2.0.5",
"magic-string": "^0.25.3", "magic-string": "^0.30.0",
"mocha": "^7.0.0", "mocha": "^7.0.0",
"periscopic": "^3.0.4", "periscopic": "^3.1.0",
"puppeteer": "^2.0.0", "puppeteer": "^2.0.0",
"rollup": "^1.27.14", "rollup": "^1.27.14",
"source-map": "^0.7.4", "source-map": "^0.7.4",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tiny-glob": "^0.2.9", "tiny-glob": "^0.2.9",
"tslib": "^2.4.1", "tslib": "^2.5.0",
"typescript": "^3.7.5", "typescript": "^3.7.5",
"util": "^0.12.5" "util": "^0.12.5"
} }

@ -691,7 +691,12 @@ When the value of an `<option>` matches its text content, the attribute can be o
--- ---
Elements with the `contenteditable` attribute support `innerHTML` and `textContent` bindings. Elements with the `contenteditable` attribute support the following bindings:
- [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML)
- [`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText)
- [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
There are slight differences between each of these, read more about them [here](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#Differences_from_innerText).
```sv ```sv
<div contenteditable="true" bind:innerHTML={html}></div> <div contenteditable="true" bind:innerHTML={html}></div>
@ -1746,6 +1751,25 @@ All except `scrollX` and `scrollY` are readonly.
> Note that the page will not be scrolled to the initial value to avoid accessibility issues. Only subsequent changes to the bound variable of `scrollX` and `scrollY` will cause scrolling. However, if the scrolling behaviour is desired, call `scrollTo()` in `onMount()`. > Note that the page will not be scrolled to the initial value to avoid accessibility issues. Only subsequent changes to the bound variable of `scrollX` and `scrollY` will cause scrolling. However, if the scrolling behaviour is desired, call `scrollTo()` in `onMount()`.
### `<svelte:document>`
```sv
<svelte:document on:event={handler}/>
```
---
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document`, such as `visibilitychange`, which don't fire on `window`. It also lets you use [actions](/docs#template-syntax-element-directives-use-action) on `document`.
As with `<svelte:window>`, this element may only appear the top level of your component and must never be inside a block or element.
```sv
<svelte:document
on:visibilitychange={handleVisibilityChange}
use:someAction
/>
```
### `<svelte:body>` ### `<svelte:body>`
```sv ```sv
@ -1756,7 +1780,7 @@ All except `scrollX` and `scrollY` are readonly.
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](/docs#template-syntax-element-directives-use-action) on the `<body>` element. Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](/docs#template-syntax-element-directives-use-action) on the `<body>` element.
As with `<svelte:window>`, this element may only appear the top level of your component and must never be inside a block or element. As with `<svelte:window>` and `<svelte:document>`, this element may only appear the top level of your component and must never be inside a block or element.
```sv ```sv
<svelte:body <svelte:body
@ -1777,7 +1801,7 @@ As with `<svelte:window>`, this element may only appear the top level of your co
This element makes it possible to insert elements into `document.head`. During server-side rendering, `head` content is exposed separately to the main `html` content. This element makes it possible to insert elements into `document.head`. During server-side rendering, `head` content is exposed separately to the main `html` content.
As with `<svelte:window>` and `<svelte:body>`, this element may only appear at the top level of your component and must never be inside a block or element. As with `<svelte:window>`, `<svelte:document>` and `<svelte:body>`, this element may only appear at the top level of your component and must never be inside a block or element.
```sv ```sv
<svelte:head> <svelte:head>

@ -693,7 +693,7 @@ Animates a `blur` filter alongside an element's opacity.
* `duration` (`number`, default 400) — milliseconds the transition lasts * `duration` (`number`, default 400) — milliseconds the transition lasts
* `easing` (`function`, default `cubicInOut`) — an [easing function](/docs#run-time-svelte-easing) * `easing` (`function`, default `cubicInOut`) — an [easing function](/docs#run-time-svelte-easing)
* `opacity` (`number`, default 0) - the opacity value to animate out to and in from * `opacity` (`number`, default 0) - the opacity value to animate out to and in from
* `amount` (`number`, default 5) - the size of the blur in pixels * `amount` (`number | string`, default 5) - the size of the blur. Supports css units (for example: `"4rem"`). The default unit is `px`
```sv ```sv
<script> <script>
@ -728,10 +728,11 @@ Animates the x and y positions and the opacity of an element. `in` transitions a
* `delay` (`number`, default 0) — milliseconds before starting * `delay` (`number`, default 0) — milliseconds before starting
* `duration` (`number`, default 400) — milliseconds the transition lasts * `duration` (`number`, default 400) — milliseconds the transition lasts
* `easing` (`function`, default `cubicOut`) — an [easing function](/docs#run-time-svelte-easing) * `easing` (`function`, default `cubicOut`) — an [easing function](/docs#run-time-svelte-easing)
* `x` (`number`, default 0) - the x offset to animate out to and in from * `x` (`number | string`, default 0) - the x offset to animate out to and in from
* `y` (`number`, default 0) - the y offset to animate out to and in from * `y` (`number | string`, default 0) - the y offset to animate out to and in from
* `opacity` (`number`, default 0) - the opacity value to animate out to and in from * `opacity` (`number`, default 0) - the opacity value to animate out to and in from
x and y use `px` by default but support css units, for example `x: '100vw'` or `y: '50%'`.
You can see the `fly` transition in action in the [transition tutorial](/tutorial/adding-parameters-to-transitions). You can see the `fly` transition in action in the [transition tutorial](/tutorial/adding-parameters-to-transitions).
```sv ```sv

@ -137,6 +137,17 @@ Enforce that attributes important for accessibility have a valid value. For exam
--- ---
### `a11y-interactive-supports-focus`
Enforce that elements with an interactive role and interactive handlers (mouse or key press) must be focusable or tabbable.
```sv
<!-- A11y: Elements with the 'button' interactive role must have a tabindex value. -->
<div role="button" on:keypress={() => {}} />
```
---
### `a11y-label-has-associated-control` ### `a11y-label-has-associated-control`
Enforce that a label tag has a text label and an associated control. Enforce that a label tag has a text label and an associated control.
@ -277,6 +288,17 @@ Some HTML elements have default ARIA roles. Giving these elements an ARIA role t
--- ---
### `a11y-no-noninteractive-element-to-interactive-role`
[WAI-ARIA](https://www.w3.org/TR/wai-aria-1.1/#usage_intro) roles should not be used to convert a non-interactive element to an interactive element. Interactive ARIA roles include `button`, `link`, `checkbox`, `menuitem`, `menuitemcheckbox`, `menuitemradio`, `option`, `radio`, `searchbox`, `switch` and `textbox`.
```sv
<!-- A11y: Non-interactive element <h3> cannot have interactive role 'searchbox' -->
<h3 role="searchbox">Button</h3>
```
---
### `a11y-no-noninteractive-tabindex` ### `a11y-no-noninteractive-tabindex`
Tab key navigation should be limited to elements on the page that can be interacted with. Tab key navigation should be limited to elements on the page that can be interacted with.

@ -0,0 +1,10 @@
<script>
let selection = '';
const handleSelectionChange = (e) => selection = document.getSelection();
</script>
<svelte:document on:selectionchange={handleSelectionChange} />
<p>Select this text to fire events</p>
<p>Selection: {selection}</p>

@ -2,7 +2,12 @@
title: Contenteditable bindings title: Contenteditable bindings
--- ---
Elements with a `contenteditable="true"` attribute support `textContent` and `innerHTML` bindings: Elements with the `contenteditable` attribute support the following bindings:
- [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML)
- [`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText)
- [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
There are slight differences between each of these, read more about them [here](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#Differences_from_innerText).
```html ```html
<div <div

@ -1,14 +0,0 @@
---
title: <svelte:body>
---
Similar to `<svelte:window>`, the `<svelte:body>` element allows you to listen for events that fire on `document.body`. This is useful with the `mouseenter` and `mouseleave` events, which don't fire on `window`.
Add the `mouseenter` and `mouseleave` handlers to the `<svelte:body>` tag:
```html
<svelte:body
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
/>
```

@ -0,0 +1,10 @@
<script>
let selection = '';
const handleSelectionChange = (e) => selection = document.getSelection();
</script>
<svelte:document />
<p>Select this text to fire events</p>
<p>Selection: {selection}</p>

@ -0,0 +1,10 @@
<script>
let selection = '';
const handleSelectionChange = (e) => selection = document.getSelection();
</script>
<svelte:document on:selectionchange={handleSelectionChange} />
<p>Select this text to fire events</p>
<p>Selection: {selection}</p>

@ -0,0 +1,13 @@
---
title: <svelte:document>
---
Similar to `<svelte:window>`, the `<svelte:document>` element allows you to listen for events that fire on `document`. This is useful with events like `selectionchange`, which doesn't fire on `window`.
Add the `selectionchange` handler to the `<svelte:document>` tag:
```html
<svelte:document on:selectionchange={handleSelectionChange} />
```
> Avoid `mouseenter` and `mouseleave` handlers on this element, these events are not fired on `document` in all browsers. Use `<svelte:body>` for this instead.

@ -0,0 +1,14 @@
---
title: <svelte:body>
---
Similar to `<svelte:window>` and `<svelte:document>`, the `<svelte:body>` element allows you to listen for events that fire on `document.body`. This is useful with the `mouseenter` and `mouseleave` events, which don't fire on `window`.
Add the `mouseenter` and `mouseleave` handlers to the `<svelte:body>` tag:
```html
<svelte:body
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
/>
```

@ -38,7 +38,6 @@ import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors'; import compiler_errors from './compiler_errors';
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap'; import check_enable_sourcemap from './utils/check_enable_sourcemap';
import is_dynamic from './render_dom/wrappers/shared/is_dynamic';
import Tag from './nodes/shared/Tag'; import Tag from './nodes/shared/Tag';
interface ComponentOptions { interface ComponentOptions {
@ -955,7 +954,7 @@ export default class Component {
}); });
} }
warn_on_undefined_store_value_references(node: Node, parent: Node, prop: string, scope: Scope) { warn_on_undefined_store_value_references(node: Node, parent: Node, prop: string | number | symbol, scope: Scope) {
if ( if (
node.type === 'LabeledStatement' && node.type === 'LabeledStatement' &&
node.label.name === '$' && node.label.name === '$' &&
@ -1396,11 +1395,12 @@ export default class Component {
module_dependencies.add(name); module_dependencies.add(name);
} }
} }
const is_writable_or_mutated =
variable && (variable.writable || variable.mutated);
if ( if (
should_add_as_dependency && should_add_as_dependency &&
(!owner || owner === component.instance_scope) && (!owner || owner === component.instance_scope) &&
(name[0] === '$' || variable) (name[0] === '$' || is_writable_or_mutated)
) { ) {
dependencies.add(name); dependencies.add(name);
} }
@ -1424,19 +1424,6 @@ export default class Component {
const { expression } = node.body as ExpressionStatement; const { expression } = node.body as ExpressionStatement;
const declaration = expression && (expression as AssignmentExpression).left; const declaration = expression && (expression as AssignmentExpression).left;
const is_dependency_static = Array.from(dependencies).every(
dependency => dependency !== '$$props' && dependency !== '$$restProps' && !is_dynamic(this.var_lookup.get(dependency))
);
if (is_dependency_static) {
assignees.forEach(assignee => {
const variable = component.var_lookup.get(assignee);
if (variable) {
variable.is_reactive_static = true;
}
});
}
unsorted_reactive_declarations.push({ unsorted_reactive_declarations.push({
assignees, assignees,
dependencies, dependencies,

@ -66,7 +66,7 @@ export default {
}, },
missing_contenteditable_attribute: { missing_contenteditable_attribute: {
code: 'missing-contenteditable-attribute', code: 'missing-contenteditable-attribute',
message: '\'contenteditable\' attribute is required for textContent and innerHTML two-way bindings' message: '\'contenteditable\' attribute is required for textContent, innerHTML and innerText two-way bindings'
}, },
dynamic_contenteditable_attribute: { dynamic_contenteditable_attribute: {
code: 'dynamic-contenteditable-attribute', code: 'dynamic-contenteditable-attribute',

@ -119,6 +119,10 @@ export default {
code: 'a11y-no-interactive-element-to-noninteractive-role', code: 'a11y-no-interactive-element-to-noninteractive-role',
message: `A11y: <${element}> cannot have role '${role}'` message: `A11y: <${element}> cannot have role '${role}'`
}), }),
a11y_no_noninteractive_element_to_interactive_role: (role: string | boolean, element: string) => ({
code: 'a11y-no-noninteractive-element-to-interactive-role',
message: `A11y: Non-interactive element <${element}> cannot have interactive role '${role}'`
}),
a11y_role_has_required_aria_props: (role: string, props: string[]) => ({ a11y_role_has_required_aria_props: (role: string, props: string[]) => ({
code: 'a11y-role-has-required-aria-props', code: 'a11y-role-has-required-aria-props',
message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props.map(name => `"${name}"`).join(', ')}` message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props.map(name => `"${name}"`).join(', ')}`
@ -162,6 +166,10 @@ export default {
code: 'a11y-img-redundant-alt', code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.' message: 'A11y: Screenreaders already announce <img> elements as an image.'
}, },
a11y_interactive_supports_focus: (role: string) => ({
code: 'a11y-interactive-supports-focus',
message: `A11y: Elements with the '${role}' interactive role must have a tabindex value.`
}),
a11y_label_has_associated_control: { a11y_label_has_associated_control: {
code: 'a11y-label-has-associated-control', code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.' message: 'A11y: A form label must be associated with a control.'
@ -213,5 +221,9 @@ export default {
invalid_rest_eachblock_binding: (rest_element_name: string) => ({ invalid_rest_eachblock_binding: (rest_element_name: string) => ({
code: 'invalid-rest-eachblock-binding', code: 'invalid-rest-eachblock-binding',
message: `...${rest_element_name} operator will create a new object and binding propagation with original object will not work` message: `...${rest_element_name} operator will create a new object and binding propagation with original object will not work`
}) }),
avoid_mouse_events_on_document: {
code: 'avoid-mouse-events-on-document',
message: 'Mouse enter/leave events on the document are not supported in all browsers and should be avoided'
}
}; };

@ -76,7 +76,7 @@ export default class Selector {
this.blocks.forEach((block, i) => { this.blocks.forEach((block, i) => {
if (i > 0) { if (i > 0) {
if (block.start - c > 1) { if (block.start - c > 1) {
code.overwrite(c, block.start, block.combinator.name || ' '); code.update(c, block.start, block.combinator.name || ' ');
} }
} }
@ -112,7 +112,7 @@ export default class Selector {
} }
if (selector.type === 'TypeSelector' && selector.name === '*') { if (selector.type === 'TypeSelector' && selector.name === '*') {
code.overwrite(selector.start, selector.end, attr); code.update(selector.start, selector.end, attr);
} else { } else {
code.appendLeft(selector.end, attr); code.appendLeft(selector.end, attr);
} }

@ -35,7 +35,7 @@ function minify_declarations(
declarations.forEach((declaration, i) => { declarations.forEach((declaration, i) => {
const separator = i > 0 ? ';' : ''; const separator = i > 0 ? ';' : '';
if ((declaration.node.start - c) > separator.length) { if ((declaration.node.start - c) > separator.length) {
code.overwrite(c, declaration.node.start, separator); code.update(c, declaration.node.start, separator);
} }
declaration.minify(code); declaration.minify(code);
c = declaration.node.end; c = declaration.node.end;
@ -75,7 +75,7 @@ class Rule {
if (selector.used) { if (selector.used) {
const separator = started ? ',' : ''; const separator = started ? ',' : '';
if ((selector.node.start - c) > separator.length) { if ((selector.node.start - c) > separator.length) {
code.overwrite(c, selector.node.start, separator); code.update(c, selector.node.start, separator);
} }
selector.minify(code); selector.minify(code);
@ -133,7 +133,7 @@ class Declaration {
if (block.type === 'Identifier') { if (block.type === 'Identifier') {
const name = block.name; const name = block.name;
if (keyframes.has(name)) { if (keyframes.has(name)) {
code.overwrite(block.start, block.end, keyframes.get(name)); code.update(block.start, block.end, keyframes.get(name));
} }
} }
}); });
@ -156,7 +156,7 @@ class Declaration {
while (regex_whitespace.test(code.original[start])) start += 1; while (regex_whitespace.test(code.original[start])) start += 1;
if (start - c > 1) { if (start - c > 1) {
code.overwrite(c, start, ':'); code.update(c, start, ':');
} }
} }
} }
@ -173,7 +173,7 @@ class Atrule {
} }
apply(node: Element) { apply(node: Element) {
if (this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') { if (this.node.name === 'container' || this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') {
this.children.forEach(child => { this.children.forEach(child => {
child.apply(node); child.apply(node);
}); });
@ -204,7 +204,7 @@ class Atrule {
code.remove(c, this.node.block.start); code.remove(c, this.node.block.start);
} else if (this.node.name === 'supports') { } else if (this.node.name === 'supports') {
let c = this.node.start + 9; let c = this.node.start + 9;
if (this.node.prelude.start - c > 1) code.overwrite(c, this.node.prelude.start, ' '); if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' ');
this.node.prelude.children.forEach((query: CssNode) => { this.node.prelude.children.forEach((query: CssNode) => {
// TODO minify queries // TODO minify queries
c = query.end; c = query.end;
@ -213,7 +213,7 @@ class Atrule {
} else { } else {
let c = this.node.start + this.node.name.length + 1; let c = this.node.start + this.node.name.length + 1;
if (this.node.prelude) { if (this.node.prelude) {
if (this.node.prelude.start - c > 1) code.overwrite(c, this.node.prelude.start, ' '); if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' ');
c = this.node.prelude.end; c = this.node.prelude.end;
} }
if (this.node.block && this.node.block.start - c > 0) { if (this.node.block && this.node.block.start - c > 0) {
@ -255,7 +255,7 @@ class Atrule {
}); });
}); });
} else { } else {
code.overwrite(start, end, keyframes.get(name)); code.update(start, end, keyframes.get(name));
} }
} }
}); });

@ -17,6 +17,7 @@ export default class CatchBlock extends AbstractBlock {
this.scope = scope.child(); this.scope = scope.child();
if (parent.catch_node) { if (parent.catch_node) {
parent.catch_contexts.forEach(context => { parent.catch_contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, parent.expression.dependencies, this); this.scope.add(context.key.name, parent.expression.dependencies, this);
}); });
} }

@ -65,6 +65,7 @@ export default class ConstTag extends Node {
}); });
this.expression = new Expression(this.component, this, this.scope, this.node.expression.right); this.expression = new Expression(this.component, this, this.scope, this.node.expression.right);
this.contexts.forEach(context => { this.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
const owner = this.scope.get_owner(context.key.name); const owner = this.scope.get_owner(context.key.name);
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) { if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) {
this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name)); this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name));

@ -0,0 +1,41 @@
import Node from './shared/Node';
import EventHandler from './EventHandler';
import Action from './Action';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { Element } from '../../interfaces';
import compiler_warnings from '../compiler_warnings';
export default class Document extends Node {
type: 'Document';
handlers: EventHandler[] = [];
actions: Action[] = [];
constructor(component: Component, parent: Node, scope: TemplateScope, info: Element) {
super(component, parent, scope, info);
info.attributes.forEach((node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {
// TODO there shouldn't be anything else here...
}
});
this.validate();
}
private validate() {
const handlers_map = new Set();
this.handlers.forEach(handler => (
handlers_map.add(handler.name)
));
if (handlers_map.has('mouseenter') || handlers_map.has('mouseleave')) {
this.component.warn(this, compiler_warnings.avoid_mouse_events_on_document);
}
}
}

@ -47,6 +47,7 @@ export default class EachBlock extends AbstractBlock {
unpack_destructuring({ contexts: this.contexts, node: info.context, scope, component, context_rest_properties: this.context_rest_properties }); unpack_destructuring({ contexts: this.contexts, node: info.context, scope, component, context_rest_properties: this.context_rest_properties });
this.contexts.forEach(context => { this.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, this.expression.dependencies, this); this.scope.add(context.key.name, this.expression.dependencies, this);
}); });

@ -11,6 +11,7 @@ import StyleDirective from './StyleDirective';
import Text from './Text'; import Text from './Text';
import { namespaces } from '../../utils/namespaces'; import { namespaces } from '../../utils/namespaces';
import map_children from './shared/map_children'; import map_children from './shared/map_children';
import { is_name_contenteditable, get_contenteditable_attr } from '../utils/contenteditable';
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character } from '../../utils/patterns'; import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch'; import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list'; import list from '../../utils/list';
@ -24,15 +25,14 @@ import { string_literal } from '../utils/stringify';
import { Literal } from 'estree'; import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; import { ARIARoleDefinitionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query';
import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element } from '../utils/a11y'; import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element, is_abstract_role, is_static_element, has_disabled_attribute } from '../utils/a11y';
const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const aria_attribute_set = new Set(aria_attributes); const aria_attribute_set = new Set(aria_attributes);
const aria_roles = roles.keys(); const aria_roles = roles.keys();
const aria_role_set = new Set(aria_roles); const aria_role_set = new Set(aria_roles);
const aria_role_abstract_set = new Set(roles.keys().filter(role => roles.get(role).abstract));
const a11y_required_attributes = { const a11y_required_attributes = {
a: ['href'], a: ['href'],
@ -76,6 +76,33 @@ const a11y_labelable = new Set([
'textarea' 'textarea'
]); ]);
const a11y_interactive_handlers = new Set([
// Keyboard events
'keypress',
'keydown',
'keyup',
// Click events
'click',
'contextmenu',
'dblclick',
'drag',
'dragend',
'dragenter',
'dragexit',
'dragleave',
'dragover',
'dragstart',
'drop',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup'
]);
const a11y_nested_implicit_semantics = new Map([ const a11y_nested_implicit_semantics = new Map([
['header', 'banner'], ['header', 'banner'],
['footer', 'contentinfo'] ['footer', 'contentinfo']
@ -146,6 +173,35 @@ const input_type_to_implicit_role = new Map([
['url', 'textbox'] ['url', 'textbox']
]); ]);
/**
* Exceptions to the rule which follows common A11y conventions
* TODO make this configurable by the user
*/
const a11y_non_interactive_element_to_interactive_role_exceptions = {
ul: [
'listbox',
'menu',
'menubar',
'radiogroup',
'tablist',
'tree',
'treegrid'
],
ol: [
'listbox',
'menu',
'menubar',
'radiogroup',
'tablist',
'tree',
'treegrid'
],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],
fieldset: ['radiogroup', 'presentation']
};
const combobox_if_list = new Set(['email', 'search', 'tel', 'text', 'url']); const combobox_if_list = new Set(['email', 'search', 'tel', 'text', 'url']);
function input_implicit_role(attribute_map: Map<string, Attribute>) { function input_implicit_role(attribute_map: Map<string, Attribute>) {
@ -586,8 +642,8 @@ export default class Element extends Node {
const value = attribute.get_static_value(); const value = attribute.get_static_value();
if (typeof value === 'string') { if (typeof value === 'string') {
value.split(regex_any_repeated_whitespaces).forEach((current_role: ARIARoleDefintionKey) => { value.split(regex_any_repeated_whitespaces).forEach((current_role: ARIARoleDefinitionKey) => {
if (current_role && aria_role_abstract_set.has(current_role)) { if (current_role && is_abstract_role(current_role)) {
component.warn(attribute, compiler_warnings.a11y_no_abstract_role(current_role)); component.warn(attribute, compiler_warnings.a11y_no_abstract_role(current_role));
} else if (current_role && !aria_role_set.has(current_role)) { } else if (current_role && !aria_role_set.has(current_role)) {
const match = fuzzymatch(current_role, aria_roles); const match = fuzzymatch(current_role, aria_roles);
@ -623,12 +679,31 @@ export default class Element extends Node {
} }
} }
// interactive-supports-focus
if (
!has_disabled_attribute(attribute_map) &&
!is_hidden_from_screen_reader(this.name, attribute_map) &&
!is_presentation_role(current_role) &&
is_interactive_roles(current_role) &&
is_static_element(this.name, attribute_map) &&
!attribute_map.get('tabindex')
) {
const has_interactive_handlers = handlers.some((handler) => a11y_interactive_handlers.has(handler.name));
if (has_interactive_handlers) {
component.warn(this, compiler_warnings.a11y_interactive_supports_focus(current_role));
}
}
// no-interactive-element-to-noninteractive-role // no-interactive-element-to-noninteractive-role
if (is_interactive_element(this.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role))) { if (is_interactive_element(this.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role))) {
component.warn(this, compiler_warnings.a11y_no_interactive_element_to_noninteractive_role(current_role, this.name)); component.warn(this, compiler_warnings.a11y_no_interactive_element_to_noninteractive_role(current_role, this.name));
} }
});
// no-noninteractive-element-to-interactive-role
if (is_non_interactive_element(this.name, attribute_map) && is_interactive_roles(current_role) && !a11y_non_interactive_element_to_interactive_role_exceptions[this.name]?.includes(current_role)) {
component.warn(this, compiler_warnings.a11y_no_noninteractive_element_to_interactive_role(current_role, this.name));
}
});
} }
} }
@ -660,7 +735,7 @@ export default class Element extends Node {
// click-events-have-key-events // click-events-have-key-events
if (handlers_map.has('click')) { if (handlers_map.has('click')) {
const role = attribute_map.get('role'); const role = attribute_map.get('role');
const is_non_presentation_role = role?.is_static && !is_presentation_role(role.get_static_value() as ARIARoleDefintionKey); const is_non_presentation_role = role?.is_static && !is_presentation_role(role.get_static_value() as ARIARoleDefinitionKey);
if ( if (
!this.is_dynamic_element && !this.is_dynamic_element &&
@ -684,7 +759,7 @@ export default class Element extends Node {
} }
// no-noninteractive-tabindex // no-noninteractive-tabindex
if (!this.is_dynamic_element && !is_interactive_element(this.name, attribute_map) && !is_interactive_roles(attribute_map.get('role')?.get_static_value() as ARIARoleDefintionKey)) { if (!this.is_dynamic_element && !is_interactive_element(this.name, attribute_map) && !is_interactive_roles(attribute_map.get('role')?.get_static_value() as ARIARoleDefinitionKey)) {
const tab_index = attribute_map.get('tabindex'); const tab_index = attribute_map.get('tabindex');
if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) { if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) {
component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex); component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex);
@ -693,7 +768,7 @@ export default class Element extends Node {
// role-supports-aria-props // role-supports-aria-props
const role = attribute_map.get('role'); const role = attribute_map.get('role');
const role_value = (role ? role.get_static_value() : get_implicit_role(this.name, attribute_map)) as ARIARoleDefintionKey; const role_value = (role ? role.get_static_value() : get_implicit_role(this.name, attribute_map)) as ARIARoleDefinitionKey;
if (typeof role_value === 'string' && roles.has(role_value)) { if (typeof role_value === 'string' && roles.has(role_value)) {
const { props } = roles.get(role_value); const { props } = roles.get(role_value);
const invalid_aria_props = new Set(aria.keys().filter(attribute => !(attribute in props))); const invalid_aria_props = new Set(aria.keys().filter(attribute => !(attribute in props)));
@ -1028,14 +1103,8 @@ export default class Element extends Node {
if (this.name !== 'img') { if (this.name !== 'img') {
return component.error(binding, compiler_errors.invalid_binding_element_with('<img>', name)); return component.error(binding, compiler_errors.invalid_binding_element_with('<img>', name));
} }
} else if ( } else if (is_name_contenteditable(name)) {
name === 'textContent' || const contenteditable = get_contenteditable_attr(this);
name === 'innerHTML'
) {
const contenteditable = this.attributes.find(
(attribute: Attribute) => attribute.name === 'contenteditable'
);
if (!contenteditable) { if (!contenteditable) {
return component.error(binding, compiler_errors.missing_contenteditable_attribute); return component.error(binding, compiler_errors.missing_contenteditable_attribute);
} else if (contenteditable && !contenteditable.is_static) { } else if (contenteditable && !contenteditable.is_static) {

@ -17,6 +17,7 @@ export default class ThenBlock extends AbstractBlock {
this.scope = scope.child(); this.scope = scope.child();
if (parent.then_node) { if (parent.then_node) {
parent.then_contexts.forEach(context => { parent.then_contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, parent.expression.dependencies, this); this.scope.add(context.key.name, parent.expression.dependencies, this);
}); });
} }

@ -12,6 +12,7 @@ import StyleDirective from './StyleDirective';
import Comment from './Comment'; import Comment from './Comment';
import ConstTag from './ConstTag'; import ConstTag from './ConstTag';
import DebugTag from './DebugTag'; import DebugTag from './DebugTag';
import Document from './Document';
import EachBlock from './EachBlock'; import EachBlock from './EachBlock';
import Element from './Element'; import Element from './Element';
import ElseBlock from './ElseBlock'; import ElseBlock from './ElseBlock';
@ -47,6 +48,7 @@ export type INode = Action
| Comment | Comment
| ConstTag | ConstTag
| DebugTag | DebugTag
| Document
| EachBlock | EachBlock
| Element | Element
| ElseBlock | ElseBlock

@ -1,5 +1,5 @@
import { x } from 'code-red'; import { x } from 'code-red';
import { Node, Identifier, Expression } from 'estree'; import { Node, Identifier, Expression, PrivateIdentifier } from 'estree';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference'; import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import { clone } from '../../../utils/clone'; import { clone } from '../../../utils/clone';
@ -7,7 +7,16 @@ import Component from '../../Component';
import flatten_reference from '../../utils/flatten_reference'; import flatten_reference from '../../utils/flatten_reference';
import TemplateScope from './TemplateScope'; import TemplateScope from './TemplateScope';
export interface Context { export type Context = DestructuredVariable | ComputedProperty;
interface ComputedProperty {
type: 'ComputedProperty';
property_name: Identifier;
key: Expression | PrivateIdentifier;
}
interface DestructuredVariable {
type: 'DestructuredVariable'
key: Identifier; key: Identifier;
name?: string; name?: string;
modifier: (node: Node) => Node; modifier: (node: Node) => Node;
@ -25,8 +34,8 @@ export function unpack_destructuring({
}: { }: {
contexts: Context[]; contexts: Context[];
node: Node; node: Node;
modifier?: Context['modifier']; modifier?: DestructuredVariable['modifier'];
default_modifier?: Context['default_modifier']; default_modifier?: DestructuredVariable['default_modifier'];
scope: TemplateScope; scope: TemplateScope;
component: Component; component: Component;
context_rest_properties: Map<string, Node>; context_rest_properties: Map<string, Node>;
@ -35,12 +44,14 @@ export function unpack_destructuring({
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
contexts.push({ contexts.push({
type: 'DestructuredVariable',
key: node as Identifier, key: node as Identifier,
modifier, modifier,
default_modifier default_modifier
}); });
} else if (node.type === 'RestElement') { } else if (node.type === 'RestElement') {
contexts.push({ contexts.push({
type: 'DestructuredVariable',
key: node.argument as Identifier, key: node.argument as Identifier,
modifier, modifier,
default_modifier default_modifier
@ -108,12 +119,38 @@ export function unpack_destructuring({
context_rest_properties context_rest_properties
}); });
context_rest_properties.set((property.argument as Identifier).name, property); context_rest_properties.set((property.argument as Identifier).name, property);
} else { } else if (property.type === 'Property') {
const key = property.key as Identifier; const key = property.key;
const value = property.value; const value = property.value;
used_properties.push(x`"${key.name}"`); let new_modifier: (node: Node) => Node;
if (property.computed) {
// e.g { [computedProperty]: ... }
const property_name = component.get_unique_name('computed_property');
contexts.push({
type: 'ComputedProperty',
property_name,
key
});
new_modifier = (node) => x`${modifier(node)}[${property_name}]`;
used_properties.push(x`${property_name}`);
} else if (key.type === 'Identifier') {
// e.g. { someProperty: ... }
const property_name = key.name;
new_modifier = (node) => x`${modifier(node)}.${property_name}`;
used_properties.push(x`"${property_name}"`);
} else if (key.type === 'Literal') {
// e.g. { "property-in-quotes": ... } or { 14: ... }
const property_name = key.value;
new_modifier = (node) => x`${modifier(node)}["${property_name}"]`;
used_properties.push(x`"${property_name}"`);
}
if (value.type === 'AssignmentPattern') { if (value.type === 'AssignmentPattern') {
// e.g. { property = default } or { property: newName = default }
const n = contexts.length; const n = contexts.length;
mark_referenced(value.right, scope, component); mark_referenced(value.right, scope, component);
@ -121,7 +158,7 @@ export function unpack_destructuring({
unpack_destructuring({ unpack_destructuring({
contexts, contexts,
node: value.left, node: value.left,
modifier: (node) => x`${modifier(node)}.${key.name}`, modifier: new_modifier,
default_modifier: (node, to_ctx) => default_modifier: (node, to_ctx) =>
x`${node} !== undefined ? ${node} : ${update_reference( x`${node} !== undefined ? ${node} : ${update_reference(
contexts, contexts,
@ -134,10 +171,11 @@ export function unpack_destructuring({
context_rest_properties context_rest_properties
}); });
} else { } else {
// e.g. { property } or { property: newName }
unpack_destructuring({ unpack_destructuring({
contexts, contexts,
node: value, node: value,
modifier: (node) => x`${modifier(node)}.${key.name}` as Node, modifier: new_modifier,
default_modifier, default_modifier,
scope, scope,
component, component,
@ -157,7 +195,9 @@ function update_reference(
): Node { ): Node {
const find_from_context = (node: Identifier) => { const find_from_context = (node: Identifier) => {
for (let i = n; i < contexts.length; i++) { for (let i = n; i < contexts.length; i++) {
const { key } = contexts[i]; const cur_context = contexts[i];
if (cur_context.type !== 'DestructuredVariable') continue;
const { key } = cur_context;
if (node.name === key.name) { if (node.name === key.name) {
throw new Error(`Cannot access '${node.name}' before initialization`); throw new Error(`Cannot access '${node.name}' before initialization`);
} }

@ -382,6 +382,7 @@ export default class Expression {
// add to get_xxx_context // add to get_xxx_context
// child_ctx[x] = function () { ... } // child_ctx[x] = function () { ... }
(template_scope.get_owner(deps[0]) as EachBlock).contexts.push({ (template_scope.get_owner(deps[0]) as EachBlock).contexts.push({
type: 'DestructuredVariable',
key: func_id, key: func_id,
modifier: () => func_expression, modifier: () => func_expression,
default_modifier: node => node default_modifier: node => node

@ -3,6 +3,7 @@ import Body from '../Body';
import ConstTag from '../ConstTag'; import ConstTag from '../ConstTag';
import Comment from '../Comment'; import Comment from '../Comment';
import EachBlock from '../EachBlock'; import EachBlock from '../EachBlock';
import Document from '../Document';
import Element from '../Element'; import Element from '../Element';
import Head from '../Head'; import Head from '../Head';
import IfBlock from '../IfBlock'; import IfBlock from '../IfBlock';
@ -28,6 +29,7 @@ function get_constructor(type) {
case 'Body': return Body; case 'Body': return Body;
case 'Comment': return Comment; case 'Comment': return Comment;
case 'ConstTag': return ConstTag; case 'ConstTag': return ConstTag;
case 'Document': return Document;
case 'EachBlock': return EachBlock; case 'EachBlock': return EachBlock;
case 'Element': return Element; case 'Element': return Element;
case 'Head': return Head; case 'Head': return Head;

@ -1,4 +1,4 @@
import Renderer from './Renderer'; import Renderer, { BindingGroup } from './Renderer';
import Wrapper from './wrappers/shared/Wrapper'; import Wrapper from './wrappers/shared/Wrapper';
import { b, x } from 'code-red'; import { b, x } from 'code-red';
import { Node, Identifier, ArrayPattern } from 'estree'; import { Node, Identifier, ArrayPattern } from 'estree';
@ -40,6 +40,7 @@ export default class Block {
bindings: Map<string, Bindings>; bindings: Map<string, Bindings>;
binding_group_initialised: Set<string> = new Set(); binding_group_initialised: Set<string> = new Set();
binding_groups: Set<BindingGroup> = new Set();
chunks: { chunks: {
declarations: Array<Node | Node[]>; declarations: Array<Node | Node[]>;
@ -249,6 +250,7 @@ export default class Block {
} }
} }
this.render_binding_groups();
this.render_listeners(); this.render_listeners();
const properties: Record<string, any> = {}; const properties: Record<string, any> = {};
@ -500,4 +502,10 @@ export default class Block {
} }
} }
} }
render_binding_groups() {
for (const binding_group of this.binding_groups) {
binding_group.render(this);
}
}
} }

@ -22,6 +22,15 @@ type BitMasks = Array<{
names: string[]; names: string[];
}>; }>;
export interface BindingGroup {
binding_group: (to_reference?: boolean) => Node;
contexts: string[];
list_dependencies: Set<string>;
keypath: string;
add_element: (block: Block, element: Identifier) => void;
render: (block: Block) => void;
}
export default class Renderer { export default class Renderer {
component: Component; // TODO Maybe Renderer shouldn't know about Component? component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions; options: CompileOptions;
@ -33,7 +42,7 @@ export default class Renderer {
blocks: Array<Block | Node | Node[]> = []; blocks: Array<Block | Node | Node[]> = [];
readonly: Set<string> = new Set(); readonly: Set<string> = new Set();
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number; keypath: string }> = new Map(); binding_groups: Map<string, BindingGroup> = new Map();
block: Block; block: Block;
fragment: FragmentWrapper; fragment: FragmentWrapper;
@ -64,10 +73,6 @@ export default class Renderer {
this.add_to_context('#slots'); this.add_to_context('#slots');
} }
if (this.binding_groups.size > 0) {
this.add_to_context('$$binding_groups');
}
// main block // main block
this.block = new Block({ this.block = new Block({
renderer: this, renderer: this,

@ -390,13 +390,13 @@ export default function dom(
const resubscribable_reactive_store_unsubscribers = reactive_stores const resubscribable_reactive_store_unsubscribers = reactive_stores
.filter(store => { .filter(store => {
const variable = component.var_lookup.get(store.name.slice(1)); const variable = component.var_lookup.get(store.name.slice(1));
return variable && (variable.reassigned || variable.export_name) && !variable.is_reactive_static; return variable && (variable.reassigned || variable.export_name);
}) })
.map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`); .map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`);
if (has_definition) { if (has_definition) {
const reactive_declarations: Node[] = []; const reactive_declarations: (Node | Node[]) = [];
const fixed_reactive_declarations: Array<Node | Node[]> = []; // not really 'reactive' but whatever const fixed_reactive_declarations: Node[] = []; // not really 'reactive' but whatever
component.reactive_declarations.forEach(d => { component.reactive_declarations.forEach(d => {
const dependencies = Array.from(d.dependencies); const dependencies = Array.from(d.dependencies);
@ -417,15 +417,6 @@ export default function dom(
reactive_declarations.push(statement); reactive_declarations.push(statement);
} else { } else {
fixed_reactive_declarations.push(statement); fixed_reactive_declarations.push(statement);
for (const assignee of d.assignees) {
const variable = component.var_lookup.get(assignee);
if (variable && variable.subscribable) {
fixed_reactive_declarations.push(b`
${component.compile_options.dev && b`@validate_store(${assignee}, '${assignee}');`}
@component_subscribe($$self, ${assignee}, $$value => $$invalidate(${renderer.context_lookup.get('$' + assignee).index}, ${'$' + assignee} = $$value));
`);
}
}
} }
}); });
@ -439,7 +430,7 @@ export default function dom(
const name = $name.slice(1); const name = $name.slice(1);
const store = component.var_lookup.get(name); const store = component.var_lookup.get(name);
if (store && (store.reassigned || store.export_name) && !store.is_reactive_static) { if (store && (store.reassigned || store.export_name)) {
const unsubscribe = `$$unsubscribe_${name}`; const unsubscribe = `$$unsubscribe_${name}`;
const subscribe = `$$subscribe_${name}`; const subscribe = `$$subscribe_${name}`;
const i = renderer.context_lookup.get($name).index; const i = renderer.context_lookup.get($name).index;

@ -19,7 +19,6 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
!variable.hoistable && !variable.hoistable &&
!variable.global && !variable.global &&
!variable.module && !variable.module &&
!variable.is_reactive_static &&
( (
variable.referenced || variable.referenced ||
variable.subscribable || variable.subscribable ||

@ -11,6 +11,7 @@ import CatchBlock from '../../nodes/CatchBlock';
import { Context } from '../../nodes/shared/Context'; import { Context } from '../../nodes/shared/Context';
import { Identifier, Literal, Node } from 'estree'; import { Identifier, Literal, Node } from 'estree';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags'; import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import Expression from '../../nodes/shared/Expression';
type Status = 'pending' | 'then' | 'catch'; type Status = 'pending' | 'then' | 'catch';
@ -69,6 +70,7 @@ class AwaitBlockBranch extends Wrapper {
this.renderer.add_to_context(this.value, true); this.renderer.add_to_context(this.value, true);
} else { } else {
contexts.forEach(context => { contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.renderer.add_to_context(context.key.name, true); this.renderer.add_to_context(context.key.name, true);
}); });
this.value = this.block.parent.get_unique_name('value').name; this.value = this.block.parent.get_unique_name('value').name;
@ -96,7 +98,15 @@ class AwaitBlockBranch extends Wrapper {
} }
render_get_context() { render_get_context() {
const props = this.is_destructured ? this.value_contexts.map(prop => b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`#ctx[${this.value_index}]`), name => this.renderer.reference(name))};`) : null; const props = this.is_destructured ? this.value_contexts.map(prop => {
if (prop.type === 'ComputedProperty') {
const expression = new Expression(this.renderer.component, this.node, this.has_consts(this.node) ? this.node.scope : null, prop.key);
return b`const ${prop.property_name} = ${expression.manipulate(this.block, '#ctx')};`;
} else {
const to_ctx = name => this.renderer.reference(name);
return b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`#ctx[${this.value_index}]`), to_ctx)};`;
}
}) : null;
const const_tags_props = this.has_consts(this.node) ? add_const_tags(this.block, this.node.const_tags, '#ctx') : null; const const_tags_props = this.has_consts(this.node) ? add_const_tags(this.block, this.node.const_tags, '#ctx') : null;

@ -0,0 +1,41 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Comment from '../../nodes/Comment';
import Wrapper from './shared/Wrapper';
import { x } from 'code-red';
import { Identifier } from 'estree';
export default class CommentWrapper extends Wrapper {
node: Comment;
var: Identifier;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Comment
) {
super(renderer, block, parent, node);
this.var = x`c` as Identifier;
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (!this.renderer.options.preserveComments) return;
const string_literal = {
type: 'Literal',
value: this.node.data,
loc: {
start: this.renderer.locate(this.node.start),
end: this.renderer.locate(this.node.end)
}
};
block.add_element(
this.var,
x`@comment(${string_literal})`,
parent_nodes && x`@claim_comment(${parent_nodes}, ${string_literal})`,
parent_node
);
}
}

@ -0,0 +1,25 @@
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import { x } from 'code-red';
import Document from '../../nodes/Document';
import { Identifier } from 'estree';
import EventHandler from './Element/EventHandler';
import add_event_handlers from './shared/add_event_handlers';
import { TemplateNode } from '../../../interfaces';
import Renderer from '../Renderer';
import add_actions from './shared/add_actions';
export default class DocumentWrapper extends Wrapper {
node: Document;
handlers: EventHandler[];
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: TemplateNode) {
super(renderer, block, parent, node);
this.handlers = this.node.handlers.map(handler => new EventHandler(handler, this));
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
add_event_handlers(block, x`@_document`, this.handlers);
add_actions(block, x`@_document`, this.node.actions);
}
}

@ -9,6 +9,7 @@ import ElseBlock from '../../nodes/ElseBlock';
import { Identifier, Node } from 'estree'; import { Identifier, Node } from 'estree';
import get_object from '../../utils/get_object'; import get_object from '../../utils/get_object';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags'; import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import Expression from '../../nodes/shared/Expression';
export class ElseBlockWrapper extends Wrapper { export class ElseBlockWrapper extends Wrapper {
node: ElseBlock; node: ElseBlock;
@ -84,6 +85,7 @@ export default class EachBlockWrapper extends Wrapper {
block.add_dependencies(dependencies); block.add_dependencies(dependencies);
this.node.contexts.forEach(context => { this.node.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
renderer.add_to_context(context.key.name, true); renderer.add_to_context(context.key.name, true);
}); });
add_const_tags_context(renderer, this.node.const_tags); add_const_tags_context(renderer, this.node.const_tags);
@ -145,6 +147,7 @@ export default class EachBlockWrapper extends Wrapper {
const store = object.type === 'Identifier' && object.name[0] === '$' ? object.name.slice(1) : null; const store = object.type === 'Identifier' && object.name[0] === '$' ? object.name.slice(1) : null;
node.contexts.forEach(prop => { node.contexts.forEach(prop => {
if (prop.type !== 'DestructuredVariable') return;
this.block.bindings.set(prop.key.name, { this.block.bindings.set(prop.key.name, {
object: this.vars.each_block_value, object: this.vars.each_block_value,
property: this.index_name, property: this.index_name,
@ -359,7 +362,15 @@ export default class EachBlockWrapper extends Wrapper {
this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier); this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier);
} }
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`list[i]`), name => renderer.context_lookup.has(name) ? x`child_ctx[${renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name })};`); this.context_props = this.node.contexts.map(prop => {
if (prop.type === 'DestructuredVariable') {
const to_ctx = (name: string) => renderer.context_lookup.has(name) ? x`child_ctx[${renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name } as Node;
return b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`list[i]`), to_ctx)};`;
} else {
const expression = new Expression(this.renderer.component, this.node, this.node.scope, prop.key);
return b`const ${prop.property_name} = ${expression.manipulate(block, 'child_ctx')};`;
}
});
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`); if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`); if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);

@ -82,6 +82,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
if (node.name === 'value') { if (node.name === 'value') {
handle_select_value_binding(this, node.dependencies); handle_select_value_binding(this, node.dependencies);
this.parent.has_dynamic_value = true;
} }
} }
@ -177,6 +178,17 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
`; `;
} }
if (this.node.name === 'value' && dependencies.length > 0) {
if (this.parent.bindings.some(binding => binding.node.name === 'group')) {
this.parent.dynamic_value_condition = block.get_unique_name('value_has_changed');
block.add_variable(this.parent.dynamic_value_condition, x`false`);
updater = b`
${updater}
${this.parent.dynamic_value_condition} = true;
`;
}
}
if (dependencies.length > 0) { if (dependencies.length > 0) {
const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies)); const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies));
@ -349,7 +361,6 @@ const attribute_lookup: { [key in BooleanAttributes]: AttributeMetadata } & { [k
indeterminate: { applies_to: ['input'] }, indeterminate: { applies_to: ['input'] },
inert: {}, inert: {},
ismap: { property_name: 'isMap', applies_to: ['img'] }, ismap: { property_name: 'isMap', applies_to: ['img'] },
itemscope: {},
loop: { applies_to: ['audio', 'bgsound', 'video'] }, loop: { applies_to: ['audio', 'bgsound', 'video'] },
multiple: { applies_to: ['input', 'select'] }, multiple: { applies_to: ['input', 'select'] },
muted: { applies_to: ['audio', 'video'] }, muted: { applies_to: ['audio', 'video'] },

@ -5,7 +5,7 @@ import InlineComponentWrapper from '../InlineComponent';
import get_object from '../../../utils/get_object'; import get_object from '../../../utils/get_object';
import replace_object from '../../../utils/replace_object'; import replace_object from '../../../utils/replace_object';
import Block from '../../Block'; import Block from '../../Block';
import Renderer from '../../Renderer'; import Renderer, { BindingGroup } from '../../Renderer';
import flatten_reference from '../../../utils/flatten_reference'; import flatten_reference from '../../../utils/flatten_reference';
import { Node, Identifier } from 'estree'; import { Node, Identifier } from 'estree';
import add_to_set from '../../../utils/add_to_set'; import add_to_set from '../../../utils/add_to_set';
@ -26,6 +26,7 @@ export default class BindingWrapper {
snippet: Node; snippet: Node;
is_readonly: boolean; is_readonly: boolean;
needs_lock: boolean; needs_lock: boolean;
binding_group: BindingGroup;
constructor(block: Block, node: Binding, parent: ElementWrapper | InlineComponentWrapper) { constructor(block: Block, node: Binding, parent: ElementWrapper | InlineComponentWrapper) {
this.node = node; this.node = node;
@ -45,6 +46,10 @@ export default class BindingWrapper {
this.object = get_object(this.node.expression.node).name; this.object = get_object(this.node.expression.node).name;
if (this.node.name === 'group') {
this.binding_group = get_binding_group(parent.renderer, this, block);
}
// view to model // view to model
this.handler = get_event_handler(this, parent.renderer, block, this.object, this.node.raw_expression); this.handler = get_event_handler(this, parent.renderer, block, this.object, this.node.raw_expression);
@ -67,6 +72,10 @@ export default class BindingWrapper {
} }
}); });
if (this.binding_group) {
this.binding_group.list_dependencies.forEach(dep => dependencies.add(dep));
}
return dependencies; return dependencies;
} }
@ -105,6 +114,7 @@ export default class BindingWrapper {
const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : []; const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : [];
const mount_conditions: any[] = []; const mount_conditions: any[] = [];
let update_or_condition: any = null;
const dependency_array = Array.from(this.get_dependencies()); const dependency_array = Array.from(this.get_dependencies());
@ -135,40 +145,19 @@ export default class BindingWrapper {
} }
// model to view // model to view
let update_dom = get_dom_updater(parent, this); let update_dom = get_dom_updater(parent, this, false);
let mount_dom = update_dom; let mount_dom = get_dom_updater(parent, this, true);
// special cases // special cases
switch (this.node.name) { switch (this.node.name) {
case 'group': case 'group':
{ {
const { binding_group, is_context, contexts, index, keypath } = get_binding_group(parent.renderer, this.node, block);
block.renderer.add_to_context('$$binding_groups'); block.renderer.add_to_context('$$binding_groups');
this.binding_group.add_element(block, this.parent.var);
if (is_context && !block.binding_group_initialised.has(keypath)) { if ((this.parent as ElementWrapper).has_dynamic_value) {
if (contexts.length > 1) { update_or_condition = (this.parent as ElementWrapper).dynamic_value_condition;
let binding_group = x`${block.renderer.reference('$$binding_groups')}[${index}]`;
for (const name of contexts.slice(0, -1)) {
binding_group = x`${binding_group}[${block.renderer.reference(name)}]`;
block.chunks.init.push(
b`${binding_group} = ${binding_group} || [];`
);
}
}
block.chunks.init.push(
b`${binding_group(true)} = [];`
);
block.binding_group_initialised.add(keypath);
} }
block.chunks.hydrate.push(
b`${binding_group(true)}.push(${parent.var});`
);
block.chunks.destroy.push(
b`${binding_group(true)}.splice(${binding_group(true)}.indexOf(${parent.var}), 1);`
);
break; break;
} }
@ -177,6 +166,11 @@ export default class BindingWrapper {
mount_conditions.push(x`${this.snippet} !== void 0`); mount_conditions.push(x`${this.snippet} !== void 0`);
break; break;
case 'innerText':
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerText`);
mount_conditions.push(x`${this.snippet} !== void 0`);
break;
case 'innerHTML': case 'innerHTML':
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerHTML`); update_conditions.push(x`${this.snippet} !== ${parent.var}.innerHTML`);
mount_conditions.push(x`${this.snippet} !== void 0`); mount_conditions.push(x`${this.snippet} !== void 0`);
@ -214,7 +208,8 @@ export default class BindingWrapper {
if (update_dom) { if (update_dom) {
if (update_conditions.length > 0) { if (update_conditions.length > 0) {
const condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`); let condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`);
if (update_or_condition) condition = x`${update_or_condition} || (${condition})`;
block.chunks.update.push(b` block.chunks.update.push(b`
if (${condition}) { if (${condition}) {
@ -244,7 +239,8 @@ export default class BindingWrapper {
function get_dom_updater( function get_dom_updater(
element: ElementWrapper | InlineComponentWrapper, element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper binding: BindingWrapper,
mounting: boolean
) { ) {
const { node } = element; const { node } = element;
@ -259,6 +255,7 @@ function get_dom_updater(
if (node.name === 'select') { if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ? return node.get_static_attribute_value('multiple') === true ?
b`@select_options(${element.var}, ${binding.snippet})` : b`@select_options(${element.var}, ${binding.snippet})` :
mounting ? b`@select_option(${element.var}, ${binding.snippet}, true)` :
b`@select_option(${element.var}, ${binding.snippet})`; b`@select_option(${element.var}, ${binding.snippet})`;
} }
@ -279,7 +276,8 @@ function get_dom_updater(
return b`${element.var}.${binding.node.name} = ${binding.snippet};`; return b`${element.var}.${binding.node.name} = ${binding.snippet};`;
} }
function get_binding_group(renderer: Renderer, value: Binding, block: Block) { function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: Block) {
const value = binding.node;
const { parts } = flatten_reference(value.raw_expression); const { parts } = flatten_reference(value.raw_expression);
let keypath = parts.join('.'); let keypath = parts.join('.');
@ -314,41 +312,84 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
contexts.push(name); contexts.push(name);
} }
// create a global binding_group across blocks
if (!renderer.binding_groups.has(keypath)) { if (!renderer.binding_groups.has(keypath)) {
const index = renderer.binding_groups.size; const index = renderer.binding_groups.size;
// the bind:group depends on the list in the {#each} block as well
// as reordering (removing and adding back to the DOM) may affect the value
const list_dependencies = new Set<string>();
let parent = value.parent;
while (parent) {
if (parent.type === 'EachBlock') {
for (const dep of parent.expression.dynamic_dependencies()) {
list_dependencies.add(dep);
}
}
parent = parent.parent;
}
/**
* When using bind:group with logic blocks, the inputs with bind:group may be scattered across different blocks.
* This therefore keeps track of all the <input> elements that have the same bind:group within the same block.
*/
const elements = new Map<Block, any>();
contexts.forEach(context => { contexts.forEach(context => {
renderer.add_to_context(context, true); renderer.add_to_context(context, true);
}); });
renderer.binding_groups.set(keypath, { renderer.binding_groups.set(keypath, {
binding_group: (to_reference: boolean = false) => { binding_group: () => {
let binding_group = '$$binding_groups'; let obj = x`$$binding_groups[${index}]`;
let _secondary_indexes = contexts;
if (to_reference) {
binding_group = block.renderer.reference(binding_group);
_secondary_indexes = _secondary_indexes.map(name => block.renderer.reference(name));
}
if (_secondary_indexes.length > 0) { if (contexts.length > 0) {
let obj = x`${binding_group}[${index}]`; contexts.forEach(secondary_index => {
_secondary_indexes.forEach(secondary_index => {
obj = x`${obj}[${secondary_index}]`; obj = x`${obj}[${secondary_index}]`;
}); });
return obj;
} else {
return x`${binding_group}[${index}]`;
} }
return obj;
}, },
is_context: contexts.length > 0,
contexts, contexts,
index, list_dependencies,
keypath keypath,
add_element(block, element) {
if (!elements.has(block)) {
elements.set(block, []);
}
elements.get(block).push(element);
},
render(block) {
const local_name = block.get_unique_name('binding_group');
const binding_group = block.renderer.reference('$$binding_groups');
block.add_variable(local_name);
if (contexts.length > 0) {
const indexes = { type: 'ArrayExpression', elements: contexts.map(name => block.renderer.reference(name)) };
block.chunks.init.push(
b`${local_name} = @init_binding_group_dynamic(${binding_group}[${index}], ${indexes})`
);
block.chunks.update.push(
b`if (${block.renderer.dirty(Array.from(list_dependencies))}) ${local_name}.u(${indexes})`
);
} else {
block.chunks.init.push(
b`${local_name} = @init_binding_group(${binding_group}[${index}])`
);
}
block.chunks.hydrate.push(
b`${local_name}.p(${elements.get(block)})`
);
block.chunks.destroy.push(
b`${local_name}.r()`
);
}
}); });
} }
return renderer.binding_groups.get(keypath); // register the binding_group for the block
const binding_group = renderer.binding_groups.get(keypath);
block.binding_groups.add(binding_group);
return binding_group;
} }
function get_event_handler( function get_event_handler(
@ -386,7 +427,7 @@ function get_event_handler(
} }
} }
const value = get_value_from_dom(renderer, binding.parent, binding, block, contextual_dependencies); const value = get_value_from_dom(renderer, binding.parent, binding, contextual_dependencies);
const mutation = b` const mutation = b`
${lhs} = ${value}; ${lhs} = ${value};
@ -402,10 +443,9 @@ function get_event_handler(
} }
function get_value_from_dom( function get_value_from_dom(
renderer: Renderer, _renderer: Renderer,
element: ElementWrapper | InlineComponentWrapper, element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper, binding: BindingWrapper,
block: Block,
contextual_dependencies: Set<string> contextual_dependencies: Set<string>
) { ) {
const { node } = element; const { node } = element;
@ -415,7 +455,7 @@ function get_value_from_dom(
return x`$$value`; return x`$$value`;
} }
// <select bind:value='selected> // <select bind:value='selected'>
if (node.name === 'select') { if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ? return node.get_static_attribute_value('multiple') === true ?
x`@select_multiple_value(this)` : x`@select_multiple_value(this)` :
@ -427,7 +467,7 @@ function get_value_from_dom(
// <input type='checkbox' bind:group='foo'> // <input type='checkbox' bind:group='foo'>
if (name === 'group') { if (name === 'group') {
if (type === 'checkbox') { if (type === 'checkbox') {
const { binding_group, contexts } = get_binding_group(renderer, binding.node, block); const { binding_group, contexts } = binding.binding_group;
add_to_set(contextual_dependencies, contexts); add_to_set(contextual_dependencies, contexts);
return x`@get_binding_group_value(${binding_group()}, this.__value, this.checked)`; return x`@get_binding_group_value(${binding_group()}, this.__value, this.checked)`;
} }

@ -26,6 +26,7 @@ import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag'; import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag'; import RawMustacheTagWrapper from '../RawMustacheTag';
import is_dynamic from '../shared/is_dynamic'; import is_dynamic from '../shared/is_dynamic';
import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable';
import create_debugging_comment from '../shared/create_debugging_comment'; import create_debugging_comment from '../shared/create_debugging_comment';
import { push_array } from '../../../../utils/push_array'; import { push_array } from '../../../../utils/push_array';
@ -48,8 +49,8 @@ const events = [
{ {
event_names: ['input'], event_names: ['input'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) =>
(name === 'textContent' || name === 'innerHTML') && is_name_contenteditable(name) &&
node.attributes.some(attribute => attribute.name === 'contenteditable') has_contenteditable_attr(node)
}, },
{ {
event_names: ['change'], event_names: ['change'],
@ -160,9 +161,12 @@ export default class ElementWrapper extends Wrapper {
bindings: Binding[]; bindings: Binding[];
event_handlers: EventHandler[]; event_handlers: EventHandler[];
class_dependencies: string[]; class_dependencies: string[];
dynamic_style_dependencies: Set<string>;
has_dynamic_attribute: boolean; has_dynamic_attribute: boolean;
select_binding_dependencies?: Set<string>; select_binding_dependencies?: Set<string>;
has_dynamic_value: boolean;
dynamic_value_condition: any;
var: any; var: any;
void: boolean; void: boolean;
@ -170,6 +174,8 @@ export default class ElementWrapper extends Wrapper {
child_dynamic_element_block?: Block = null; child_dynamic_element_block?: Block = null;
child_dynamic_element?: ElementWrapper = null; child_dynamic_element?: ElementWrapper = null;
element_data_name = null;
constructor( constructor(
renderer: Renderer, renderer: Renderer,
block: Block, block: Block,
@ -212,6 +218,8 @@ export default class ElementWrapper extends Wrapper {
return; return;
} }
this.dynamic_style_dependencies = new Set();
if (this.node.children.length) { if (this.node.children.length) {
this.node.lets.forEach(l => { this.node.lets.forEach(l => {
extract_names(l.value || l.name).forEach(name => { extract_names(l.value || l.name).forEach(name => {
@ -263,6 +271,8 @@ export default class ElementWrapper extends Wrapper {
}); });
this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling); this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);
this.element_data_name = block.get_unique_name(`${this.var.name}_data`);
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
@ -509,7 +519,8 @@ export default class ElementWrapper extends Wrapper {
child.render( child.render(
block, block,
this.node.name === 'template' ? x`${node}.content` : node, this.node.name === 'template' ? x`${node}.content` : node,
nodes nodes,
{ element_data_name: this.element_data_name }
); );
}); });
} }
@ -760,14 +771,11 @@ export default class ElementWrapper extends Wrapper {
const should_initialise = ( const should_initialise = (
this.node.name === 'select' || this.node.name === 'select' ||
binding_group.bindings.find(binding => { binding_group.bindings.find(binding => (
return (
binding.node.name === 'indeterminate' || binding.node.name === 'indeterminate' ||
binding.node.name === 'textContent' || is_name_contenteditable(binding.node.name) ||
binding.node.name === 'innerHTML' ||
binding.is_readonly_media_attribute() binding.is_readonly_media_attribute()
); ))
})
); );
if (should_initialise) { if (should_initialise) {
@ -798,11 +806,13 @@ export default class ElementWrapper extends Wrapper {
} }
add_attributes(block: Block) { add_attributes(block: Block) {
// Get all the class dependencies first // Get all the class and style dependencies first
this.attributes.forEach((attribute) => { this.attributes.forEach((attribute) => {
if (attribute.node.name === 'class') { if (attribute.node.name === 'class') {
const dependencies = attribute.node.get_dependencies(); const dependencies = attribute.node.get_dependencies();
push_array(this.class_dependencies, dependencies); push_array(this.class_dependencies, dependencies);
} else if (attribute.node.name === 'style') {
add_to_set(this.dynamic_style_dependencies, attribute.node.get_dependencies());
} }
}); });
@ -818,7 +828,6 @@ export default class ElementWrapper extends Wrapper {
add_spread_attributes(block: Block) { add_spread_attributes(block: Block) {
const levels = block.get_unique_name(`${this.var.name}_levels`); const levels = block.get_unique_name(`${this.var.name}_levels`);
const data = block.get_unique_name(`${this.var.name}_data`);
const initial_props = []; const initial_props = [];
const updates = []; const updates = [];
@ -849,9 +858,9 @@ export default class ElementWrapper extends Wrapper {
block.chunks.init.push(b` block.chunks.init.push(b`
let ${levels} = [${initial_props}]; let ${levels} = [${initial_props}];
let ${data} = {}; let ${this.element_data_name} = {};
for (let #i = 0; #i < ${levels}.length; #i += 1) { for (let #i = 0; #i < ${levels}.length; #i += 1) {
${data} = @assign(${data}, ${levels}[#i]); ${this.element_data_name} = @assign(${this.element_data_name}, ${levels}[#i]);
} }
`); `);
@ -863,12 +872,12 @@ export default class ElementWrapper extends Wrapper {
: x`@set_attributes`; : x`@set_attributes`;
block.chunks.hydrate.push( block.chunks.hydrate.push(
b`${fn}(${this.var}, ${data});` b`${fn}(${this.var}, ${this.element_data_name});`
); );
if (this.has_dynamic_attribute) { if (this.has_dynamic_attribute) {
block.chunks.update.push(b` block.chunks.update.push(b`
${fn}(${this.var}, ${data} = @get_spread_update(${levels}, [ ${fn}(${this.var}, ${this.element_data_name} = @get_spread_update(${levels}, [
${updates} ${updates}
])); ]));
`); `);
@ -884,23 +893,23 @@ export default class ElementWrapper extends Wrapper {
} }
block.chunks.mount.push(b` block.chunks.mount.push(b`
'value' in ${data} && (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); 'value' in ${this.element_data_name} && (${this.element_data_name}.multiple ? @select_options : @select_option)(${this.var}, ${this.element_data_name}.value);
`); `);
block.chunks.update.push(b` block.chunks.update.push(b`
if (${block.renderer.dirty(Array.from(dependencies))} && 'value' in ${data}) (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); if (${block.renderer.dirty(Array.from(dependencies))} && 'value' in ${this.element_data_name}) (${this.element_data_name}.multiple ? @select_options : @select_option)(${this.var}, ${this.element_data_name}.value);
`); `);
} else if (this.node.name === 'input' && this.attributes.find(attr => attr.node.name === 'value')) { } else if (this.node.name === 'input' && this.attributes.find(attr => attr.node.name === 'value')) {
const type = this.node.get_static_attribute_value('type'); const type = this.node.get_static_attribute_value('type');
if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') { if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') {
block.chunks.mount.push(b` block.chunks.mount.push(b`
if ('value' in ${data}) { if ('value' in ${this.element_data_name}) {
${this.var}.value = ${data}.value; ${this.var}.value = ${this.element_data_name}.value;
} }
`); `);
block.chunks.update.push(b` block.chunks.update.push(b`
if ('value' in ${data}) { if ('value' in ${this.element_data_name}) {
${this.var}.value = ${data}.value; ${this.var}.value = ${this.element_data_name}.value;
} }
`); `);
} }
@ -959,6 +968,7 @@ export default class ElementWrapper extends Wrapper {
const intro_block = b` const intro_block = b`
@add_render_callback(() => { @add_render_callback(() => {
if (!#current) return;
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true); if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true);
${name}.run(1); ${name}.run(1);
}); });
@ -1004,6 +1014,7 @@ export default class ElementWrapper extends Wrapper {
if (outro) { if (outro) {
intro_block = b` intro_block = b`
@add_render_callback(() => { @add_render_callback(() => {
if (!#current) return;
if (${outro_name}) ${outro_name}.end(1); if (${outro_name}) ${outro_name}.end(1);
${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet}); ${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
${intro_name}.start(); ${intro_name}.start();
@ -1167,8 +1178,18 @@ export default class ElementWrapper extends Wrapper {
add_styles(block: Block) { add_styles(block: Block) {
const has_spread = this.node.attributes.some(attr => attr.is_spread); const has_spread = this.node.attributes.some(attr => attr.is_spread);
let style_changed_var: Identifier | undefined;
const maybe_create_style_changed_var = () => {
if (!style_changed_var && this.dynamic_style_dependencies.size) {
style_changed_var = block.get_unique_name('style_changed');
const style_attr_dirty = block.renderer.dirty([...this.dynamic_style_dependencies]);
block.chunks.update.push(b`const ${style_changed_var} = ${style_attr_dirty};`);
}
};
this.node.styles.forEach((style_directive) => { this.node.styles.forEach((style_directive) => {
const { name, expression, should_cache, important } = style_directive; const { name, expression, important, should_cache } = style_directive;
const snippet = expression.manipulate(block); const snippet = expression.manipulate(block);
let cached_snippet: Identifier | undefined; let cached_snippet: Identifier | undefined;
@ -1181,24 +1202,39 @@ export default class ElementWrapper extends Wrapper {
block.chunks.hydrate.push(updater); block.chunks.hydrate.push(updater);
const dependencies = expression.dynamic_dependencies(); // Assume that style has changed through the spread attribute
if (has_spread) { if (has_spread) {
block.chunks.update.push(updater); block.chunks.update.push(updater);
} else if (dependencies.length > 0) { } else {
const self_deps = expression.dynamic_dependencies();
const all_deps = new Set([
...self_deps,
...this.dynamic_style_dependencies
]);
if (all_deps.size === 0) return;
let condition = block.renderer.dirty([...all_deps]);
if (should_cache) { if (should_cache) {
block.chunks.update.push(b` condition = x`${condition} && ${cached_snippet} !== (${cached_snippet} = ${snippet})`;
if (${block.renderer.dirty(dependencies)} && (${cached_snippet} !== (${cached_snippet} = ${snippet}))) {
${updater}
} }
`);
} else { if (this.dynamic_style_dependencies.size > 0) {
maybe_create_style_changed_var();
// If all dependencies are same as the style attribute dependencies, then we can skip the dirty check
condition =
all_deps.size === this.dynamic_style_dependencies.size
? style_changed_var
: x`${style_changed_var} || ${condition}`;
}
block.chunks.update.push(b` block.chunks.update.push(b`
if (${block.renderer.dirty(dependencies)}) { if (${condition}) {
${updater} ${updater}
} }
`); `);
} }
}
}); });
} }

@ -2,8 +2,9 @@ import Wrapper from './shared/Wrapper';
import AwaitBlock from './AwaitBlock'; import AwaitBlock from './AwaitBlock';
import Body from './Body'; import Body from './Body';
import DebugTag from './DebugTag'; import DebugTag from './DebugTag';
import Document from './Document';
import EachBlock from './EachBlock'; import EachBlock from './EachBlock';
import Element from './Element/index'; import Element from './Element';
import Head from './Head'; import Head from './Head';
import IfBlock from './IfBlock'; import IfBlock from './IfBlock';
import KeyBlock from './KeyBlock'; import KeyBlock from './KeyBlock';
@ -13,6 +14,7 @@ import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot'; import Slot from './Slot';
import SlotTemplate from './SlotTemplate'; import SlotTemplate from './SlotTemplate';
import Text from './Text'; import Text from './Text';
import Comment from './Comment';
import Title from './Title'; import Title from './Title';
import Window from './Window'; import Window from './Window';
import { INode } from '../../nodes/interfaces'; import { INode } from '../../nodes/interfaces';
@ -26,8 +28,9 @@ import { regex_starts_with_whitespace } from '../../../utils/patterns';
const wrappers = { const wrappers = {
AwaitBlock, AwaitBlock,
Body, Body,
Comment: null, Comment,
DebugTag, DebugTag,
Document,
EachBlock, EachBlock,
Element, Element,
Head, Head,
@ -116,7 +119,7 @@ export default class FragmentWrapper {
link(last_child, last_child = wrapper); link(last_child, last_child = wrapper);
} else { } else {
const Wrapper = wrappers[child.type]; const Wrapper = wrappers[child.type];
if (!Wrapper) continue; if (!Wrapper || (child.type === 'Comment' && !renderer.options.preserveComments)) continue;
const wrapper = new Wrapper(renderer, block, parent, child, strip_whitespace, last_child || next_sibling); const wrapper = new Wrapper(renderer, block, parent, child, strip_whitespace, last_child || next_sibling);
this.nodes.unshift(wrapper); this.nodes.unshift(wrapper);

@ -5,7 +5,9 @@ import Wrapper from './shared/Wrapper';
import MustacheTag from '../../nodes/MustacheTag'; import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag'; import RawMustacheTag from '../../nodes/RawMustacheTag';
import { x } from 'code-red'; import { x } from 'code-red';
import { Identifier } from 'estree'; import { Identifier, Expression } from 'estree';
import ElementWrapper from './Element';
import AttributeWrapper from './Element/Attribute';
export default class MustacheTagWrapper extends Tag { export default class MustacheTagWrapper extends Tag {
var: Identifier = { type: 'Identifier', name: 't' }; var: Identifier = { type: 'Identifier', name: 't' };
@ -14,10 +16,40 @@ export default class MustacheTagWrapper extends Tag {
super(renderer, block, parent, node); super(renderer, block, parent, node);
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier, data: Record<string, unknown> | undefined) {
const contenteditable_attributes =
this.parent instanceof ElementWrapper &&
this.parent.attributes.filter((a) => a.node.name === 'contenteditable');
const spread_attributes =
this.parent instanceof ElementWrapper &&
this.parent.attributes.filter((a) => a.node.is_spread);
let contenteditable_attr_value: Expression | true | undefined = undefined;
if (contenteditable_attributes.length > 0) {
const attribute = contenteditable_attributes[0] as AttributeWrapper;
if ([true, 'true', ''].includes(attribute.node.get_static_value())) {
contenteditable_attr_value = true;
} else {
contenteditable_attr_value = x`${attribute.get_value(block)}`;
}
} else if (spread_attributes.length > 0 && data.element_data_name) {
contenteditable_attr_value = x`${data.element_data_name}['contenteditable']`;
}
const { init } = this.rename_this_method( const { init } = this.rename_this_method(
block, block,
value => x`@set_data(${this.var}, ${value})` value => {
if (contenteditable_attr_value) {
if (contenteditable_attr_value === true) {
return x`@set_data_contenteditable(${this.var}, ${value})`;
} else {
return x`@set_data_maybe_contenteditable(${this.var}, ${value}, ${contenteditable_attr_value})`;
}
} else {
return x`@set_data(${this.var}, ${value})`;
}
}
); );
block.add_element( block.add_element(

@ -70,7 +70,7 @@ export default class Wrapper {
); );
} }
render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier, _data: Record<string, any> = undefined) {
throw Error('Wrapper class is not renderable'); throw Error('Wrapper class is not renderable');
} }
} }

@ -1,15 +1,23 @@
import ConstTag from '../../../nodes/ConstTag'; import ConstTag from '../../../nodes/ConstTag';
import Block from '../../Block'; import Block from '../../Block';
import { b, x } from 'code-red'; import { b, Node, x } from 'code-red';
import Renderer from '../../Renderer'; import Renderer from '../../Renderer';
import Expression from '../../../nodes/shared/Expression';
export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string) { export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string) {
const const_tags_props = []; const const_tags_props = [];
const_tags.forEach((const_tag, i) => { const_tags.forEach((const_tag, i) => {
const name = `#constants_${i}`; const name = `#constants_${i}`;
const_tags_props.push(b`const ${name} = ${const_tag.expression.manipulate(block, ctx)}`); const_tags_props.push(b`const ${name} = ${const_tag.expression.manipulate(block, ctx)}`);
const to_ctx = (name: string) => block.renderer.context_lookup.has(name) ? x`${ctx}[${block.renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name } as Node;
const_tag.contexts.forEach(context => { const_tag.contexts.forEach(context => {
const_tags_props.push(b`${ctx}[${block.renderer.context_lookup.get(context.key.name).index}] = ${context.default_modifier(context.modifier({ type: 'Identifier', name }), name => block.renderer.context_lookup.has(name) ? x`${ctx}[${block.renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name })};`); if (context.type === 'DestructuredVariable') {
const_tags_props.push(b`${ctx}[${block.renderer.context_lookup.get(context.key.name).index}] = ${context.default_modifier(context.modifier({ type: 'Identifier', name }), to_ctx)}`);
} else {
const expression = new Expression(block.renderer.component, const_tag, const_tag.scope, context.key);
const_tags_props.push(b`const ${context.property_name} = ${expression.manipulate(block, ctx)}`);
}
}); });
}); });
return const_tags_props; return const_tags_props;
@ -18,6 +26,7 @@ export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string
export function add_const_tags_context(renderer: Renderer, const_tags: ConstTag[]) { export function add_const_tags_context(renderer: Renderer, const_tags: ConstTag[]) {
const_tags.forEach(const_tag => { const_tags.forEach(const_tag => {
const_tag.contexts.forEach(context => { const_tag.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
renderer.add_to_context(context.key.name, true); renderer.add_to_context(context.key.name, true);
}); });
}); });

@ -28,6 +28,7 @@ const handlers: Record<string, Handler> = {
Body: noop, Body: noop,
Comment, Comment,
DebugTag, DebugTag,
Document: noop,
EachBlock, EachBlock,
Element, Element,
Head, Head,

@ -1,7 +1,9 @@
import { is_void } from '../../../../shared/utils/names'; import { is_void } from '../../../../shared/utils/names';
import { get_attribute_expression, get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value'; import { get_attribute_expression, get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value';
import { boolean_attributes } from '../../../../shared/boolean_attributes'; import { boolean_attributes } from '../../../../shared/boolean_attributes';
import { is_name_contenteditable, is_contenteditable } from '../../utils/contenteditable';
import Renderer, { RenderOptions } from '../Renderer'; import Renderer, { RenderOptions } from '../Renderer';
import Binding from '../../nodes/Binding';
import Element from '../../nodes/Element'; import Element from '../../nodes/Element';
import { p, x } from 'code-red'; import { p, x } from 'code-red';
import Expression from '../../nodes/shared/Expression'; import Expression from '../../nodes/shared/Expression';
@ -19,11 +21,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
// awkward special case // awkward special case
let node_contents; let node_contents;
const contenteditable = ( const contenteditable = is_contenteditable(node);
node.name !== 'textarea' &&
node.name !== 'input' &&
node.attributes.some((attribute) => attribute.name === 'contenteditable')
);
if (node.is_dynamic_element) { if (node.is_dynamic_element) {
renderer.push(); renderer.push();
@ -129,7 +127,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
} }
} }
node.bindings.forEach(binding => { node.bindings.forEach((binding: Binding) => {
const { name, expression } = binding; const { name, expression } = binding;
if (binding.is_readonly) { if (binding.is_readonly) {
@ -145,7 +143,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
const condition = type === 'checkbox' ? x`~${bound}.indexOf(${value})` : x`${value} === ${bound}`; const condition = type === 'checkbox' ? x`~${bound}.indexOf(${value})` : x`${value} === ${bound}`;
renderer.add_expression(x`${condition} ? @add_attribute("checked", true, 1) : ""`); renderer.add_expression(x`${condition} ? @add_attribute("checked", true, 1) : ""`);
} }
} else if (contenteditable && (name === 'textContent' || name === 'innerHTML')) { } else if (contenteditable && is_name_contenteditable(name)) {
node_contents = expression.node; node_contents = expression.node;
// TODO where was this used? // TODO where was this used?

@ -19,11 +19,17 @@ export function get_class_attribute_value(attribute: Attribute): ESTreeExpressio
export function get_attribute_value(attribute: Attribute): ESTreeExpression { export function get_attribute_value(attribute: Attribute): ESTreeExpression {
if (attribute.chunks.length === 0) return x`""`; if (attribute.chunks.length === 0) return x`""`;
/**
* For value attribute of textarea, it will render as child node of `<textarea>` element.
* Therefore, we need to escape as content (not attribute).
*/
const is_textarea_value = attribute.parent.name.toLowerCase() === 'textarea' && attribute.name.toLowerCase() === 'value';
return attribute.chunks return attribute.chunks
.map((chunk) => { .map((chunk) => {
return chunk.type === 'Text' return chunk.type === 'Text'
? string_literal(chunk.data.replace(regex_double_quotes, '&quot;')) as ESTreeExpression ? string_literal(chunk.data.replace(regex_double_quotes, '&quot;')) as ESTreeExpression
: x`@escape(${chunk.node}, true)`; : x`@escape(${chunk.node}, ${is_textarea_value ? 'false' : 'true'})`;
}) })
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`); .reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
} }

@ -1,5 +1,14 @@
import * as assert from 'assert'; import * as assert from 'assert';
import get_name_from_filename from './get_name_from_filename'; import get_name_from_filename from './get_name_from_filename';
import {
is_contenteditable,
has_contenteditable_attr,
is_name_contenteditable,
get_contenteditable_attr,
CONTENTEDITABLE_BINDINGS
} from './contenteditable';
import Element from '../nodes/Element';
import Attribute from '../nodes/Attribute';
describe('get_name_from_filename', () => { describe('get_name_from_filename', () => {
it('uses the basename', () => { it('uses the basename', () => {
@ -20,3 +29,61 @@ describe('get_name_from_filename', () => {
assert.equal(get_name_from_filename('~.svelte'), '_'); assert.equal(get_name_from_filename('~.svelte'), '_');
}); });
}); });
describe('contenteditable', () => {
describe('is_contenteditable', () => {
it('returns false if node is input', () => {
const node = { name: 'input' } as Element;
assert.equal(is_contenteditable(node), false);
});
it('returns false if node is textarea', () => {
const node = { name: 'textarea' } as Element;
assert.equal(is_contenteditable(node), false);
});
it('returns false if node is not input or textarea AND it is not contenteditable', () => {
const attr = { name: 'href' } as Attribute;
const node = { name: 'a', attributes: [attr] } as Element;
assert.equal(is_contenteditable(node), false);
});
it('returns true if node is not input or textarea AND it is contenteditable', () => {
const attr = { name: 'contenteditable' } as Attribute;
const node = { name: 'a', attributes: [attr] } as Element;
assert.equal(is_contenteditable(node), true);
});
});
describe('has_contenteditable_attr', () => {
it('returns true if attribute is contenteditable', () => {
const attr = { name: 'contenteditable' } as Attribute;
const node = { attributes: [attr] } as Element;
assert.equal(has_contenteditable_attr(node), true);
});
it('returns false if attribute is not contenteditable', () => {
const attr = { name: 'href' } as Attribute;
const node = { attributes: [attr] } as Element;
assert.equal(has_contenteditable_attr(node), false);
});
});
describe('is_name_contenteditable', () => {
it('returns true if name is a contenteditable type', () => {
assert.equal(is_name_contenteditable(CONTENTEDITABLE_BINDINGS[0]), true);
});
it('returns false if name is not contenteditable type', () => {
assert.equal(is_name_contenteditable('value'), false);
});
});
describe('get_contenteditable_attr', () => {
it('returns the contenteditable Attribute if it exists', () => {
const attr = { name: 'contenteditable' } as Attribute;
const node = { name: 'div', attributes: [attr] } as Element;
assert.equal(get_contenteditable_attr(node), attr);
});
it('returns undefined if contenteditable attribute cannot be found', () => {
const node = { name: 'div', attributes: [] } as Element;
assert.equal(get_contenteditable_attr(node), undefined);
});
});
});

@ -1,5 +1,5 @@
import { import {
ARIARoleDefintionKey, ARIARoleDefinitionKey,
roles as roles_map, roles as roles_map,
elementRoles, elementRoles,
ARIARoleRelationConcept ARIARoleRelationConcept
@ -7,7 +7,9 @@ import {
import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query'; import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query';
import Attribute from '../nodes/Attribute'; import Attribute from '../nodes/Attribute';
const non_abstract_roles = [...roles_map.keys()].filter((name) => !roles_map.get(name).abstract); const aria_roles = roles_map.keys();
const abstract_roles = new Set(aria_roles.filter(role => roles_map.get(role).abstract));
const non_abstract_roles = aria_roles.filter((name) => !abstract_roles.has(name));
const non_interactive_roles = new Set( const non_interactive_roles = new Set(
non_abstract_roles non_abstract_roles
@ -32,17 +34,21 @@ const interactive_roles = new Set(
non_abstract_roles.filter((name) => !non_interactive_roles.has(name)) non_abstract_roles.filter((name) => !non_interactive_roles.has(name))
); );
export function is_non_interactive_roles(role: ARIARoleDefintionKey) { export function is_non_interactive_roles(role: ARIARoleDefinitionKey) {
return non_interactive_roles.has(role); return non_interactive_roles.has(role);
} }
export function is_interactive_roles(role: ARIARoleDefintionKey) { export function is_interactive_roles(role: ARIARoleDefinitionKey) {
return interactive_roles.has(role); return interactive_roles.has(role);
} }
export function is_abstract_role(role: ARIARoleDefinitionKey) {
return abstract_roles.has(role);
}
const presentation_roles = new Set(['presentation', 'none']); const presentation_roles = new Set(['presentation', 'none']);
export function is_presentation_role(role: ARIARoleDefintionKey) { export function is_presentation_role(role: ARIARoleDefinitionKey) {
return presentation_roles.has(role); return presentation_roles.has(role);
} }
@ -62,10 +68,28 @@ export function is_hidden_from_screen_reader(tag_name: string, attribute_map: Ma
return aria_hidden_value === true || aria_hidden_value === 'true'; return aria_hidden_value === true || aria_hidden_value === 'true';
} }
export function has_disabled_attribute(attribute_map: Map<string, Attribute>) {
const disabled_attr = attribute_map.get('disabled');
const disabled_attr_value = disabled_attr && disabled_attr.get_static_value();
if (disabled_attr_value) {
return true;
}
const aria_disabled_attr = attribute_map.get('aria-disabled');
if (aria_disabled_attr) {
const aria_disabled_attr_value = aria_disabled_attr.get_static_value();
if (aria_disabled_attr_value === true) {
return true;
}
}
return false;
}
const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = []; const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = [];
elementRoles.entries().forEach(([schema, roles]) => { elementRoles.entries().forEach(([schema, roles]) => {
if ([...roles].every((role) => non_interactive_roles.has(role))) { if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.has(role))) {
non_interactive_element_role_schemas.push(schema); non_interactive_element_role_schemas.push(schema);
} }
}); });
@ -82,6 +106,10 @@ const interactive_ax_objects = new Set(
[...AXObjects.keys()].filter((name) => AXObjects.get(name).type === 'widget') [...AXObjects.keys()].filter((name) => AXObjects.get(name).type === 'widget')
); );
const non_interactive_ax_objects = new Set(
[...AXObjects.keys()].filter((name) => ['windows', 'structure'].includes(AXObjects.get(name).type))
);
const interactive_element_ax_object_schemas: ARIARoleRelationConcept[] = []; const interactive_element_ax_object_schemas: ARIARoleRelationConcept[] = [];
elementAXObjects.entries().forEach(([schema, ax_object]) => { elementAXObjects.entries().forEach(([schema, ax_object]) => {
@ -90,6 +118,14 @@ elementAXObjects.entries().forEach(([schema, ax_object]) => {
} }
}); });
const non_interactive_element_ax_object_schemas: ARIARoleRelationConcept[] = [];
elementAXObjects.entries().forEach(([schema, ax_object]) => {
if ([...ax_object].every((role) => non_interactive_ax_objects.has(role))) {
non_interactive_element_ax_object_schemas.push(schema);
}
});
function match_schema( function match_schema(
schema: ARIARoleRelationConcept, schema: ARIARoleRelationConcept,
tag_name: string, tag_name: string,
@ -110,24 +146,31 @@ function match_schema(
}); });
} }
export function is_interactive_element( export enum ElementInteractivity {
Interactive = 'interactive',
NonInteractive = 'non-interactive',
Static = 'static',
}
export function element_interactivity(
tag_name: string, tag_name: string,
attribute_map: Map<string, Attribute> attribute_map: Map<string, Attribute>
): boolean { ): ElementInteractivity {
if ( if (
interactive_element_role_schemas.some((schema) => interactive_element_role_schemas.some((schema) =>
match_schema(schema, tag_name, attribute_map) match_schema(schema, tag_name, attribute_map)
) )
) { ) {
return true; return ElementInteractivity.Interactive;
} }
if ( if (
tag_name !== 'header' &&
non_interactive_element_role_schemas.some((schema) => non_interactive_element_role_schemas.some((schema) =>
match_schema(schema, tag_name, attribute_map) match_schema(schema, tag_name, attribute_map)
) )
) { ) {
return false; return ElementInteractivity.NonInteractive;
} }
if ( if (
@ -135,13 +178,33 @@ export function is_interactive_element(
match_schema(schema, tag_name, attribute_map) match_schema(schema, tag_name, attribute_map)
) )
) { ) {
return true; return ElementInteractivity.Interactive;
} }
return false; if (
non_interactive_element_ax_object_schemas.some((schema) =>
match_schema(schema, tag_name, attribute_map)
)
) {
return ElementInteractivity.NonInteractive;
}
return ElementInteractivity.Static;
}
export function is_interactive_element(tag_name: string, attribute_map: Map<string, Attribute>): boolean {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Interactive;
}
export function is_non_interactive_element(tag_name: string, attribute_map: Map<string, Attribute>): boolean {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.NonInteractive;
}
export function is_static_element(tag_name: string, attribute_map: Map<string, Attribute>): boolean {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Static;
} }
export function is_semantic_role_element(role: ARIARoleDefintionKey, tag_name: string, attribute_map: Map<string, Attribute>) { export function is_semantic_role_element(role: ARIARoleDefinitionKey, tag_name: string, attribute_map: Map<string, Attribute>) {
for (const [schema, ax_object] of elementAXObjects.entries()) { for (const [schema, ax_object] of elementAXObjects.entries()) {
if (schema.name === tag_name && (!schema.attributes || schema.attributes.every( if (schema.name === tag_name && (!schema.attributes || schema.attributes.every(
(attr) => attribute_map.has(attr.name) && attribute_map.get(attr.name).get_static_value() === attr.value (attr) => attribute_map.has(attr.name) && attribute_map.get(attr.name).get_static_value() === attr.value

@ -0,0 +1,57 @@
// Utilities for managing contenteditable nodes
import Attribute from '../nodes/Attribute';
import Element from '../nodes/Element';
export const CONTENTEDITABLE_BINDINGS = [
'textContent',
'innerHTML',
'innerText'
];
/**
* Returns true if node is an 'input' or 'textarea'.
* @param {Element} node The element to be checked
*/
function is_input_or_textarea(node: Element): boolean {
return node.name === 'textarea' || node.name === 'input';
}
/**
* Check if a given attribute is 'contenteditable'.
* @param {Attribute} attribute A node.attribute
*/
function is_attr_contenteditable(attribute: Attribute): boolean {
return attribute.name === 'contenteditable';
}
/**
* Check if any of a node's attributes are 'contentenditable'.
* @param {Element} node The element to be checked
*/
export function has_contenteditable_attr(node: Element): boolean {
return node.attributes.some(is_attr_contenteditable);
}
/**
* Returns true if node is not textarea or input, but has 'contenteditable' attribute.
* @param {Element} node The element to be tested
*/
export function is_contenteditable(node: Element): boolean {
return !is_input_or_textarea(node) && has_contenteditable_attr(node);
}
/**
* Returns true if a given binding/node is contenteditable.
* @param {string} name A binding or node name to be checked
*/
export function is_name_contenteditable(name: string): boolean {
return CONTENTEDITABLE_BINDINGS.includes(name);
}
/**
* Returns the contenteditable attribute from the node (if it exists).
* @param {Element} node The element to get the attribute from
*/
export function get_contenteditable_attr(node: Element): Attribute | undefined {
return node.attributes.find(is_attr_contenteditable);
}

@ -63,7 +63,7 @@ interface BaseExpressionDirective extends BaseDirective {
} }
export interface Element extends BaseNode { export interface Element extends BaseNode {
type: 'InlineComponent' | 'SlotTemplate' | 'Title' | 'Slot' | 'Element' | 'Head' | 'Options' | 'Window' | 'Body'; type: 'InlineComponent' | 'SlotTemplate' | 'Title' | 'Slot' | 'Element' | 'Head' | 'Options' | 'Window' | 'Document' | 'Body';
attributes: Array<BaseDirective | Attribute | SpreadAttribute>; attributes: Array<BaseDirective | Attribute | SpreadAttribute>;
name: string; name: string;
} }
@ -223,7 +223,6 @@ export interface Var {
subscribable?: boolean; subscribable?: boolean;
is_reactive_dependency?: boolean; is_reactive_dependency?: boolean;
imported?: boolean; imported?: boolean;
is_reactive_static?: boolean;
} }
export interface CssResult { export interface CssResult {

@ -3,12 +3,12 @@ import * as code_red from 'code-red';
export const parse = (source: string): Node => code_red.parse(source, { export const parse = (source: string): Node => code_red.parse(source, {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 12, ecmaVersion: 13,
locations: true locations: true
}); });
export const parse_expression_at = (source: string, index: number): Node => code_red.parseExpressionAt(source, index, { export const parse_expression_at = (source: string, index: number): Node => code_red.parseExpressionAt(source, index, {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 12, ecmaVersion: 13,
locations: true locations: true
}); });

@ -0,0 +1,31 @@
// @ts-nocheck
// Note: Must import from the `css-tree` browser bundled distribution due to `createRequire` usage if importing from
// `css-tree` Node module directly. This allows the production build of Svelte to work correctly.
import { fork } from '../../../../../node_modules/css-tree/dist/csstree.esm.js';
import * as node from './node';
/**
* Extends `css-tree` for container query support by forking and adding new nodes and at-rule support for `@container`.
*
* The new nodes are located in `./node`.
*/
const cqSyntax = fork({
atrule: { // extend or override at-rule dictionary
container: {
parse: {
prelude() {
return this.createSingleNodeList(
this.ContainerQuery()
);
},
block(isStyleBlock = false) {
return this.Block(isStyleBlock);
}
}
}
},
node
});
export const parse = cqSyntax.parse;

@ -0,0 +1,48 @@
// @ts-nocheck
import { Delim } from 'css-tree/tokenizer';
export const name = 'Comparison';
export const structure = {
value: String
};
export function parse() {
const start = this.tokenStart;
const char1 = this.consume(Delim);
// The first character in the comparison operator must match '<', '=', or '>'.
if (char1 !== '<' && char1 !== '>' && char1 !== '=') {
this.error('Malformed comparison operator');
}
let char2;
if (this.tokenType === Delim) {
char2 = this.consume(Delim);
// The second character in the comparison operator must match '='.
if (char2 !== '=') {
this.error('Malformed comparison operator');
}
}
// If the next token is also 'Delim' then it is malformed.
if (this.tokenType === Delim) {
this.error('Malformed comparison operator');
}
const value = char2 ? `${char1}${char2}` : char1;
return {
type: 'Comparison',
loc: this.getLocation(start, this.tokenStart),
value
};
}
export function generate(node) {
for (let index = 0; index < node.value.length; index++) {
this.token(Delim, node.value.charAt(index));
}
}

@ -0,0 +1,85 @@
// @ts-nocheck
import {
Function,
Ident,
Number,
Dimension,
RightParenthesis,
Colon,
Delim
} from 'css-tree/tokenizer';
export const name = 'ContainerFeatureStyle';
export const structure = {
name: String,
value: ['Function', 'Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
export function parse() {
const start = this.tokenStart;
let value = null;
const function_name = this.consumeFunctionName();
if (function_name !== 'style') {
this.error('Unknown container style query identifier; "style" is expected');
}
this.skipSC();
const name = this.consume(Ident);
this.skipSC();
if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
this.skipSC();
switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;
case Dimension:
value = this.Dimension();
break;
case Function:
value = this.QueryCSSFunction();
break;
case Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio, function or identifier is expected');
break;
}
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'ContainerFeatureStyle',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}
export function generate(node) {
this.token(Function, 'style(');
this.token(Ident, node.name);
if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}
this.token(RightParenthesis, ')');
}

@ -0,0 +1,92 @@
// @ts-nocheck
import {
WhiteSpace,
Comment,
Function,
Ident,
LeftParenthesis
} from 'css-tree/tokenizer';
import { lookahead_is_range } from './lookahead_is_range';
const CONTAINER_QUERY_KEYWORDS = new Set(['none', 'and', 'not', 'or']);
export const name = 'ContainerQuery';
export const structure = {
name: 'Identifier',
children: [[
'Identifier',
'QueryFeature',
'QueryFeatureRange',
'ContainerFeatureStyle',
'WhiteSpace'
]]
};
export function parse() {
const start = this.tokenStart;
const children = this.createList();
let child = null;
let name = null;
// Parse potential container name.
if (this.tokenType === Ident) {
const container_name = this.substring(this.tokenStart, this.tokenEnd);
// Container name doesn't match a query keyword, so assign it as container name.
if (!CONTAINER_QUERY_KEYWORDS.has(container_name.toLowerCase())) {
name = container_name;
this.eatIdent(container_name);
}
}
this.skipSC();
scan:
while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
case Ident:
child = this.Identifier();
break;
case Function:
child = this.ContainerFeatureStyle();
break;
case LeftParenthesis:
// Lookahead to determine if range feature.
child = lookahead_is_range.call(this) ? this.QueryFeatureRange() : this.QueryFeature();
break;
default:
break scan;
}
children.push(child);
}
if (child === null) {
this.error('Identifier or parenthesis is expected');
}
return {
type: 'ContainerQuery',
loc: this.getLocation(start, this.tokenStart - 1),
name,
children
};
}
export function generate(node) {
if (typeof node.name === 'string') {
this.token(Ident, node.name);
}
this.children(node);
}

@ -0,0 +1,7 @@
export * as Comparison from './comparison';
export * as ContainerFeatureStyle from './container_feature_style';
export * as ContainerQuery from './container_query';
export * as MediaQuery from './media_query';
export * as QueryFeature from './query_feature';
export * as QueryFeatureRange from './query_feature_range';
export * as QueryCSSFunction from './query_css_function';

@ -0,0 +1,44 @@
// @ts-nocheck
import {
EOF,
WhiteSpace,
Delim,
RightParenthesis,
LeftCurlyBracket,
Colon
} from 'css-tree/tokenizer';
/**
* Looks ahead to determine if query feature is a range query. This involves locating at least one delimiter and no
* colon tokens.
*
* @returns {boolean} Is potential range query.
*/
export function lookahead_is_range() {
let type;
let offset = 0;
let count = 0;
let delim_found = false;
let no_colon = true;
// A range query has maximum 5 tokens when formatted as 'mf-range' /
// '<mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>'. So only look ahead maximum of 6 non-whitespace tokens.
do {
type = this.lookupNonWSType(offset++);
if (type !== WhiteSpace) {
count++;
}
if (type === Delim) {
delim_found = true;
}
if (type === Colon) {
no_colon = false;
}
if (type === LeftCurlyBracket || type === RightParenthesis) {
break;
}
} while (type !== EOF && count <= 6);
return delim_found && no_colon;
}

@ -0,0 +1,64 @@
// @ts-nocheck
import {
WhiteSpace,
Comment,
Ident,
LeftParenthesis
} from 'css-tree/tokenizer';
import { lookahead_is_range } from './lookahead_is_range';
export const name = 'MediaQuery';
export const structure = {
children: [[
'Identifier',
'QueryFeature',
'QueryFeatureRange',
'WhiteSpace'
]]
};
export function parse() {
const children = this.createList();
let child = null;
this.skipSC();
scan:
while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
case Ident:
child = this.Identifier();
break;
case LeftParenthesis:
// Lookahead to determine if range feature.
child = lookahead_is_range.call(this) ? this.QueryFeatureRange() : this.QueryFeature();
break;
default:
break scan;
}
children.push(child);
}
if (child === null) {
this.error('Identifier or parenthesis is expected');
}
return {
type: 'MediaQuery',
loc: this.getLocationFromList(children),
children
};
}
export function generate(node) {
this.children(node);
}

@ -0,0 +1,41 @@
// @ts-nocheck
import {
RightParenthesis
} from 'css-tree/tokenizer';
const QUERY_CSS_FUNCTIONS = new Set(['calc', 'clamp', 'min', 'max']);
export const name = 'QueryCSSFunction';
export const structure = {
name: String,
expression: String
};
export function parse() {
const start = this.tokenStart;
const name = this.consumeFunctionName();
if (!QUERY_CSS_FUNCTIONS.has(name)) {
this.error('Unknown query single value function; expected: "calc", "clamp", "max", min"');
}
const body = this.Raw(this.tokenIndex, null, false);
this.eat(RightParenthesis);
return {
type: 'QueryCSSFunction',
loc: this.getLocation(start, this.tokenStart),
name,
expression: body.value
};
}
export function generate(node) {
this.token(Function, `${node.name}(`);
this.node(node.expression);
this.token(RightParenthesis, ')');
}

@ -0,0 +1,82 @@
// @ts-nocheck
import {
Ident,
Number,
Dimension,
Function,
LeftParenthesis,
RightParenthesis,
Colon,
Delim
} from 'css-tree/tokenizer';
export const name = 'QueryFeature';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
export function parse() {
const start = this.tokenStart;
let value = null;
this.eat(LeftParenthesis);
this.skipSC();
const name = this.consume(Ident);
this.skipSC();
if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
this.skipSC();
switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;
case Dimension:
value = this.Dimension();
break;
case Function:
value = this.QueryCSSFunction();
break;
case Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio, function, or identifier is expected');
break;
}
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'QueryFeature',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}
export function generate(node) {
this.token(LeftParenthesis, '(');
this.token(Ident, node.name);
if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}
this.token(RightParenthesis, ')');
}

@ -0,0 +1,87 @@
// @ts-nocheck
import {
Ident,
Number,
Delim,
Dimension,
Function,
LeftParenthesis,
RightParenthesis,
WhiteSpace
} from 'css-tree/tokenizer';
export const name = 'QueryFeatureRange';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Comparison', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
function lookup_non_WS_type_and_value(offset, type, referenceStr) {
let current_type;
do {
current_type = this.lookupType(offset++);
if (current_type !== WhiteSpace) {
break;
}
} while (current_type !== 0); // NULL -> 0
return current_type === type ? this.lookupValue(offset - 1, referenceStr) : false;
}
export function parse() {
const start = this.tokenStart;
const children = this.createList();
let child = null;
this.eat(LeftParenthesis);
this.skipSC();
while (!this.eof && this.tokenType !== RightParenthesis) {
switch (this.tokenType) {
case Number:
if (lookup_non_WS_type_and_value.call(this, 1, Delim, '/')) {
child = this.Ratio();
} else {
child = this.Number();
}
break;
case Delim:
child = this.Comparison();
break;
case Dimension:
child = this.Dimension();
break;
case Function:
child = this.QueryCSSFunction();
break;
case Ident:
child = this.Identifier();
break;
default:
this.error('Number, dimension, comparison, ratio, function, or identifier is expected');
break;
}
children.push(child);
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'QueryFeatureRange',
loc: this.getLocation(start, this.tokenStart),
children
};
}
export function generate(node) {
this.children(node);
}

@ -1,5 +1,6 @@
// @ts-ignore // @ts-ignore
import parse from 'css-tree/parser'; // import parse from 'css-tree/parser'; // When css-tree supports container queries uncomment.
import { parse } from './css-tree-cq/css_tree_parse'; // Use extended css-tree for container query support.
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { Parser } from '../index'; import { Parser } from '../index';
import { Node } from 'estree'; import { Node } from 'estree';

@ -19,6 +19,7 @@ const meta_tags = new Map([
['svelte:head', 'Head'], ['svelte:head', 'Head'],
['svelte:options', 'Options'], ['svelte:options', 'Options'],
['svelte:window', 'Window'], ['svelte:window', 'Window'],
['svelte:document', 'Document'],
['svelte:body', 'Body'] ['svelte:body', 'Body']
]); ]);

@ -19,10 +19,10 @@ export default function get_code_frame(
return lines return lines
.slice(frame_start, frame_end) .slice(frame_start, frame_end)
.map((str, i) => { .map((str, i) => {
const isErrorLine = frame_start + i === line; const is_error_line = frame_start + i === line;
const line_num = String(i + frame_start + 1).padStart(digits, ' '); const line_num = String(i + frame_start + 1).padStart(digits, ' ');
if (isErrorLine) { if (is_error_line) {
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^'; const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`; return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
} }

@ -1,6 +1,7 @@
import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr } from './dom'; import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr } from './dom';
import { SvelteComponent } from './Component'; import { SvelteComponent } from './Component';
import { is_void } from '../../shared/utils/names'; import { is_void } from '../../shared/utils/names';
import { contenteditable_truthy_values } from './utils';
export function dispatch_dev<T=any>(type: string, detail?: T) { export function dispatch_dev<T=any>(type: string, detail?: T) {
document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail }, { bubbles: true })); document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail }, { bubbles: true }));
@ -83,12 +84,26 @@ export function dataset_dev(node: HTMLElement, property: string, value?: any) {
dispatch_dev('SvelteDOMSetDataset', { node, property, value }); dispatch_dev('SvelteDOMSetDataset', { node, property, value });
} }
export function set_data_dev(text, data) { export function set_data_dev(text: Text, data: unknown) {
data = '' + data; data = '' + data;
if (text.wholeText === data) return; if (text.data === data) return;
dispatch_dev('SvelteDOMSetData', { node: text, data });
text.data = (data as string);
}
export function set_data_contenteditable_dev(text: Text, data: unknown) {
data = '' + data;
if (text.wholeText === data) return;
dispatch_dev('SvelteDOMSetData', { node: text, data }); dispatch_dev('SvelteDOMSetData', { node: text, data });
text.data = data; text.data = (data as string);
}
export function set_data_maybe_contenteditable_dev(text: Text, data: unknown, attr_value: string) {
if (~contenteditable_truthy_values.indexOf(attr_value)) {
set_data_contenteditable_dev(text, data);
} else {
set_data_dev(text, data);
}
} }
export function validate_each_argument(arg) { export function validate_each_argument(arg) {
@ -149,8 +164,9 @@ export interface SvelteComponentDev {
$destroy(): void; $destroy(): void;
[accessor: string]: any; [accessor: string]: any;
} }
export interface ComponentConstructorOptions<Props extends Record<string, any> = Record<string, any>> { export interface ComponentConstructorOptions<Props extends Record<string, any> = Record<string, any>> {
target: Element | ShadowRoot; target: Element | Document | ShadowRoot;
anchor?: Element; anchor?: Element;
props?: Props; props?: Props;
context?: Map<any, any>; context?: Map<any, any>;

@ -1,4 +1,4 @@
import { has_prop } from './utils'; import { contenteditable_truthy_values, has_prop } from './utils';
// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM // Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM
// at the end of hydration without touching the remaining nodes. // at the end of hydration without touching the remaining nodes.
@ -13,7 +13,7 @@ export function end_hydrating() {
type NodeEx = Node & { type NodeEx = Node & {
claim_order?: number, claim_order?: number,
hydrate_init? : true, hydrate_init?: true,
actual_end_child?: NodeEx, actual_end_child?: NodeEx,
childNodes: NodeListOf<NodeEx>, childNodes: NodeListOf<NodeEx>,
}; };
@ -35,7 +35,7 @@ function init_hydrate(target: NodeEx) {
if (target.hydrate_init) return; if (target.hydrate_init) return;
target.hydrate_init = true; target.hydrate_init = true;
type NodeEx2 = NodeEx & {claim_order: number}; type NodeEx2 = NodeEx & { claim_order: number };
// We know that all children have claim_order values since the unclaimed have been detached if target is not <head> // We know that all children have claim_order values since the unclaimed have been detached if target is not <head>
let children: ArrayLike<NodeEx2> = target.childNodes as NodeListOf<NodeEx2>; let children: ArrayLike<NodeEx2> = target.childNodes as NodeListOf<NodeEx2>;
@ -254,13 +254,17 @@ export function empty() {
return text(''); return text('');
} }
export function comment(content: string) {
return document.createComment(content);
}
export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) { export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
node.addEventListener(event, handler, options); node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options); return () => node.removeEventListener(event, handler, options);
} }
export function prevent_default(fn) { export function prevent_default(fn) {
return function(event) { return function (event) {
event.preventDefault(); event.preventDefault();
// @ts-ignore // @ts-ignore
return fn.call(this, event); return fn.call(this, event);
@ -268,7 +272,7 @@ export function prevent_default(fn) {
} }
export function stop_propagation(fn) { export function stop_propagation(fn) {
return function(event) { return function (event) {
event.stopPropagation(); event.stopPropagation();
// @ts-ignore // @ts-ignore
return fn.call(this, event); return fn.call(this, event);
@ -284,14 +288,14 @@ export function stop_immediate_propagation(fn) {
} }
export function self(fn) { export function self(fn) {
return function(event) { return function (event) {
// @ts-ignore // @ts-ignore
if (event.target === this) fn.call(this, event); if (event.target === this) fn.call(this, event);
}; };
} }
export function trusted(fn) { export function trusted(fn) {
return function(event) { return function (event) {
// @ts-ignore // @ts-ignore
if (event.isTrusted) fn.call(this, event); if (event.isTrusted) fn.call(this, event);
}; };
@ -363,6 +367,53 @@ export function get_binding_group_value(group, __value, checked) {
return Array.from(value); return Array.from(value);
} }
export function init_binding_group(group: HTMLInputElement[]) {
let _inputs: HTMLInputElement[];
return {
/* push */ p(...inputs: HTMLInputElement[]) {
_inputs = inputs;
_inputs.forEach(input => group.push(input));
},
/* remove */ r() {
_inputs.forEach(input => group.splice(group.indexOf(input), 1));
}
};
}
export function init_binding_group_dynamic(group, indexes: number[]) {
let _group: HTMLInputElement[] = get_binding_group(group);
let _inputs: HTMLInputElement[];
function get_binding_group(group) {
for (let i = 0; i < indexes.length; i++) {
group = group[indexes[i]] = group[indexes[i]] || [];
}
return group;
}
function push() {
_inputs.forEach(input => _group.push(input));
}
function remove() {
_inputs.forEach(input => _group.splice(_group.indexOf(input), 1));
}
return {
/* update */ u(new_indexes: number[]) {
indexes = new_indexes;
const new_group = get_binding_group(group);
if (new_group !== _group) {
remove();
_group = new_group;
push();
}
},
/* push */ p(...inputs: HTMLInputElement[]) {
_inputs = inputs;
push();
},
/* remove */ r: remove
};
}
export function to_number(value) { export function to_number(value) {
return value === '' ? null : +value; return value === '' ? null : +value;
} }
@ -396,7 +447,7 @@ export function children(element: Element) {
function init_claim_info(nodes: ChildNodeArray) { function init_claim_info(nodes: ChildNodeArray) {
if (nodes.claim_info === undefined) { if (nodes.claim_info === undefined) {
nodes.claim_info = {last_index: 0, total_claimed: 0}; nodes.claim_info = { last_index: 0, total_claimed: 0 };
} }
} }
@ -507,6 +558,19 @@ export function claim_space(nodes) {
return claim_text(nodes, ' '); return claim_text(nodes, ' ');
} }
export function claim_comment(nodes:ChildNodeArray, data) {
return claim_node<Comment>(
nodes,
(node: ChildNode): node is Comment => node.nodeType === 8,
(node: Comment) => {
node.data = '' + data;
return undefined;
},
() => comment(data),
true
);
}
function find_comment(nodes, text, start) { function find_comment(nodes, text, start) {
for (let i = start; i < nodes.length; i += 1) { for (let i = start; i < nodes.length; i += 1) {
const node = nodes[i]; const node = nodes[i];
@ -538,9 +602,24 @@ export function claim_html_tag(nodes, is_svg: boolean) {
return new HtmlTagHydration(claimed_nodes, is_svg); return new HtmlTagHydration(claimed_nodes, is_svg);
} }
export function set_data(text, data) { export function set_data(text: Text, data: unknown) {
data = '' + data;
if (text.data === data) return;
text.data = (data as string);
}
export function set_data_contenteditable(text: Text, data: unknown) {
data = '' + data; data = '' + data;
if (text.wholeText !== data) text.data = data; if (text.wholeText === data) return;
text.data = (data as string);
}
export function set_data_maybe_contenteditable(text: Text, data: unknown, attr_value: string) {
if (~contenteditable_truthy_values.indexOf(attr_value)) {
set_data_contenteditable(text, data);
} else {
set_data(text, data);
}
} }
export function set_input_value(input, value) { export function set_input_value(input, value) {
@ -563,7 +642,7 @@ export function set_style(node, key, value, important) {
} }
} }
export function select_option(select, value) { export function select_option(select, value, mounting) {
for (let i = 0; i < select.options.length; i += 1) { for (let i = 0; i < select.options.length; i += 1) {
const option = select.options[i]; const option = select.options[i];
@ -573,7 +652,9 @@ export function select_option(select, value) {
} }
} }
if (!mounting || value !== undefined) {
select.selectedIndex = -1; // no option should be selected select.selectedIndex = -1; // no option should be selected
}
} }
export function select_options(select, value) { export function select_options(select, value) {
@ -583,16 +664,8 @@ export function select_options(select, value) {
} }
} }
function first_enabled_option(select) {
for (const option of select.options) {
if (!option.disabled) {
return option;
}
}
}
export function select_value(select) { export function select_value(select) {
const selected_option = select.querySelector(':checked') || first_enabled_option(select); const selected_option = select.querySelector(':checked');
return selected_option && selected_option.__value; return selected_option && selected_option.__value;
} }
@ -672,7 +745,7 @@ export function toggle_class(element, name, toggle) {
element.classList[toggle ? 'add' : 'remove'](name); element.classList[toggle ? 'add' : 'remove'](name);
} }
export function custom_event<T=any>(type: string, detail?: T, { bubbles = false, cancelable = false } = {}): CustomEvent<T> { export function custom_event<T = any>(type: string, detail?: T, { bubbles = false, cancelable = false } = {}): CustomEvent<T> {
const e: CustomEvent<T> = document.createEvent('CustomEvent'); const e: CustomEvent<T> = document.createEvent('CustomEvent');
e.initCustomEvent(type, bubbles, cancelable, detail); e.initCustomEvent(type, bubbles, cancelable, detail);
return e; return e;

@ -1,4 +1,5 @@
import { transition_in, transition_out } from './transitions'; import { transition_in, transition_out } from './transitions';
import { run_all } from './utils';
export function destroy_block(block, lookup) { export function destroy_block(block, lookup) {
block.d(1); block.d(1);
@ -32,6 +33,7 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
const new_blocks = []; const new_blocks = [];
const new_lookup = new Map(); const new_lookup = new Map();
const deltas = new Map(); const deltas = new Map();
const updates = [];
i = n; i = n;
while (i--) { while (i--) {
@ -43,7 +45,8 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
block = create_each_block(key, child_ctx); block = create_each_block(key, child_ctx);
block.c(); block.c();
} else if (dynamic) { } else if (dynamic) {
block.p(child_ctx, dirty); // defer updates until all the DOM shuffling is done
updates.push(() => block.p(child_ctx, dirty));
} }
new_lookup.set(key, new_blocks[i] = block); new_lookup.set(key, new_blocks[i] = block);
@ -99,6 +102,8 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
while (n) insert(new_blocks[n - 1]); while (n) insert(new_blocks[n - 1]);
run_all(updates);
return new_blocks; return new_blocks;
} }

@ -8,7 +8,7 @@ export const binding_callbacks = [];
let render_callbacks = []; let render_callbacks = [];
const flush_callbacks = []; const flush_callbacks = [];
const resolved_promise = Promise.resolve(); const resolved_promise = /* @__PURE__ */ Promise.resolve();
let update_scheduled = false; let update_scheduled = false;
export function schedule_update() { export function schedule_update() {

@ -189,3 +189,10 @@ export const has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj,
export function action_destroyer(action_result) { export function action_destroyer(action_result) {
return action_result && is_function(action_result.destroy) ? action_result.destroy : noop; return action_result && is_function(action_result.destroy) ? action_result.destroy : noop;
} }
export function split_css_unit(value: number | string): [number, string] {
const split = typeof value === 'string' && value.match(/^\s*(-?[\d.]+)([^\s]*)\s*$/);
return split ? [parseFloat(split[1]), split[2] || 'px'] : [value as number, 'px'];
}
export const contenteditable_truthy_values = ['', true, 1, 'true', 'contenteditable'];

@ -164,7 +164,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
const auto = fn.length < 2; const auto = fn.length < 2;
return readable(initial_value, (set) => { return readable(initial_value, (set) => {
let inited = false; let started = false;
const values = []; const values = [];
let pending = 0; let pending = 0;
@ -188,7 +188,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
(value) => { (value) => {
values[i] = value; values[i] = value;
pending &= ~(1 << i); pending &= ~(1 << i);
if (inited) { if (started) {
sync(); sync();
} }
}, },
@ -197,12 +197,16 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
}) })
); );
inited = true; started = true;
sync(); sync();
return function stop() { return function stop() {
run_all(unsubscribers); run_all(unsubscribers);
cleanup(); cleanup();
// We need to set this to false because callbacks can still happen despite having unsubscribed:
// Callbacks might already be placed in the queue which doesn't know it should no longer
// invoke this derived store.
started = false;
}; };
}); });
} }

@ -1,5 +1,5 @@
import { cubicOut, cubicInOut, linear } from 'svelte/easing'; import { cubicOut, cubicInOut, linear } from 'svelte/easing';
import { assign, is_function } from 'svelte/internal'; import { assign, split_css_unit, is_function } from 'svelte/internal';
export type EasingFunction = (t: number) => number; export type EasingFunction = (t: number) => number;
@ -15,7 +15,7 @@ export interface BlurParams {
delay?: number; delay?: number;
duration?: number; duration?: number;
easing?: EasingFunction; easing?: EasingFunction;
amount?: number; amount?: number | string;
opacity?: number; opacity?: number;
} }
@ -31,12 +31,12 @@ export function blur(node: Element, {
const f = style.filter === 'none' ? '' : style.filter; const f = style.filter === 'none' ? '' : style.filter;
const od = target_opacity * (1 - opacity); const od = target_opacity * (1 - opacity);
const [value, unit] = split_css_unit(amount);
return { return {
delay, delay,
duration, duration,
easing, easing,
css: (_t, u) => `opacity: ${target_opacity - (od * u)}; filter: ${f} blur(${u * amount}px);` css: (_t, u) => `opacity: ${target_opacity - (od * u)}; filter: ${f} blur(${u * value}${unit});`
}; };
} }
@ -65,8 +65,8 @@ export interface FlyParams {
delay?: number; delay?: number;
duration?: number; duration?: number;
easing?: EasingFunction; easing?: EasingFunction;
x?: number; x?: number | string;
y?: number; y?: number | string;
opacity?: number; opacity?: number;
} }
@ -83,13 +83,14 @@ export function fly(node: Element, {
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
const od = target_opacity * (1 - opacity); const od = target_opacity * (1 - opacity);
const [xValue, xUnit] = split_css_unit(x);
const [yValue, yUnit] = split_css_unit(y);
return { return {
delay, delay,
duration, duration,
easing, easing,
css: (t, u) => ` css: (t, u) => `
transform: ${transform} translate(${(1 - t) * x}px, ${(1 - t) * y}px); transform: ${transform} translate(${(1 - t) * xValue}${xUnit}, ${(1 - t) * yValue}${yUnit});
opacity: ${target_opacity - (od * u)}` opacity: ${target_opacity - (od * u)}`
}; };
} }

@ -13,7 +13,6 @@ const _boolean_attributes = [
'hidden', 'hidden',
'inert', 'inert',
'ismap', 'ismap',
'itemscope',
'loop', 'loop',
'multiple', 'multiple',
'muted', 'muted',

@ -0,0 +1 @@
div.svelte-xyz{container:test-container / inline-size}@container (min-width: 400px){div.svelte-xyz{color:red}}@container test-container (min-width: 410px){div.svelte-xyz{color:green}}@container test-container (width < 400px){div.svelte-xyz{color:blue}}@container test-container (0 <= width < 300px){div.svelte-xyz{color:purple}}@container not (width < 400px){div.svelte-xyz{color:pink}}@container (width > 400px) and (height > 400px){div.svelte-xyz{color:lightgreen}}@container (width > 400px) or (height > 400px){div.svelte-xyz{color:lightblue}}@container (width > 400px) and (width > 800px) or (orientation: portrait){div.svelte-xyz{color:salmon}}@container style(color: blue){div.svelte-xyz{color:tan}}@container test-container (min-width: calc(400px + 1px)){div.svelte-xyz{color:green}}@container test-container (width < clamp(200px, 40%, 400px)){div.svelte-xyz{color:blue}}@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)){div.svelte-xyz{color:purple}}@container style(--var: calc(400px + 1px)){div.svelte-xyz{color:sandybrown}}

@ -0,0 +1,87 @@
<div>container query</div>
<style>
div {
container: test-container / inline-size;
}
/* Most common container query statements. */
@container (min-width: 400px) {
div {
color: red;
}
}
@container test-container (min-width: 410px) {
div {
color: green;
}
}
@container test-container (width < 400px) {
div {
color: blue;
}
}
@container test-container (0 <= width < 300px) {
div {
color: purple;
}
}
@container not (width < 400px) {
div {
color: pink;
}
}
@container (width > 400px) and (height > 400px) {
div {
color: lightgreen;
}
}
@container (width > 400px) or (height > 400px) {
div {
color: lightblue;
}
}
@container (width > 400px) and (width > 800px) or (orientation: portrait) {
div {
color: salmon;
}
}
@container style(color: blue) {
div {
color: tan;
}
}
@container test-container (min-width: calc(400px + 1px)) {
div {
color: green;
}
}
@container test-container (width < clamp(200px, 40%, 400px)) {
div {
color: blue;
}
}
@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)) {
div {
color: purple;
}
}
@container style(--var: calc(400px + 1px)) {
div {
color: sandybrown;
}
}
</style>

@ -1 +1 @@
@media(min-width: 400px){.large-screen.svelte-xyz{display:block}} @media(min-width: 400px){.large-screen.svelte-xyz{display:block}}@media(min-width: calc(400px + 1px)){.large-screen.svelte-xyz{display:block}}@media(width >= 600px){.large-screen.svelte-xyz{display:block}}@media(400px <= width <= 1000px){.large-screen.svelte-xyz{display:block}}@media(width < clamp(200px, 40%, 400px)){.large-screen.svelte-xyz{display:block}}@media(calc(400px + 1px) <= width <= calc(1000px + 1px)){.large-screen.svelte-xyz{display:block}}

@ -6,4 +6,34 @@
display: block; display: block;
} }
} }
@media (min-width: calc(400px + 1px)) {
.large-screen {
display: block;
}
}
@media (width >= 600px) {
.large-screen {
display: block;
}
}
@media (400px <= width <= 1000px) {
.large-screen {
display: block;
}
}
@media (width < clamp(200px, 40%, 400px)) {
.large-screen {
display: block;
}
}
@media (calc(400px + 1px) <= width <= calc(1000px + 1px)) {
.large-screen {
display: block;
}
}
</style> </style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save