Merge remote-tracking branch 'origin/master' into sites

pull/8453/head
Puru Vijay 2 years ago
commit b45fe8020b

@ -2,43 +2,61 @@
## Unreleased
* Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311))
## 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:
* `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))
* 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))
* Don't throw when calling `unsubscribe` twice ([#8186](https://github.com/sveltejs/svelte/pull/8186))
* Detect unused empty attribute CSS selectors ([#8042](https://github.com/sveltejs/svelte/issues/8042))
* 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))
* Compute node dimensions immediately before crossfading ([#4111](https://github.com/sveltejs/svelte/issues/4111))
* Fix potential infinite invalidate loop with `<svelte:component>` ([#4129](https://github.com/sveltejs/svelte/issues/4129))
* 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))
* Ensure `<input>` value persists when swapping elements with spread attributes in an `#each` block ([#7578](https://github.com/sveltejs/svelte/issues/7578))
* Select first enabled option by default when initial value is undefined ([#7041](https://github.com/sveltejs/svelte/issues/7041))
* Fix race condition on `svelte:element` with transitions ([#7948](https://github.com/sveltejs/svelte/issues/7948))
* Optimise `<svelte:element>` output code for static tag and static attribute ([#8161](https://github.com/sveltejs/svelte/pull/8161))
* Decode html entities correctly ([#8026](https://github.com/sveltejs/svelte/issues/8026))
* Validate component `:global()` selectors ([#6272](https://github.com/sveltejs/svelte/issues/6272))
* Improve warnings:
* Make `noreferrer` warning less zealous ([#6289](https://github.com/sveltejs/svelte/issues/6289))
* Omit a11y warnings on `<video aria-hidden="true">` ([#7874](https://github.com/sveltejs/svelte/issues/7874))
* 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))
* Introduce parameter to allow for horizontal slide transition ([#6182](https://github.com/sveltejs/svelte/issues/6182))
* Add `naturalWidth` and `naturalHeight` bindings ([#7771](https://github.com/sveltejs/svelte/issues/7771))
* make `<!-- svelte-ignore ... -->` work above components ([#8082](https://github.com/sveltejs/svelte/issues/8082))
* add global compound selector validation ([#6272](https://github.com/sveltejs/svelte/issues/6272))
* add `stopImmediatePropagation` event modifier ([#5085](https://github.com/sveltejs/svelte/issues/5085))
* add `readyState` binding for media elements ([#6666](https://github.com/sveltejs/svelte/issues/6666))
* call `<svelte:component>` update to `this` only when it's dirty ([#4129](https://github.com/sveltejs/svelte/issues/4129))
* support exclusively special characters in component filenames ([#7143](https://github.com/sveltejs/svelte/issues/7143))
* Ensure `afterUpdate` is not called after `onDestroy` ([#7476](https://github.com/sveltejs/svelte/issues/7476))
* Improve handling of `inert` attribute ([#7500](https://github.com/sveltejs/svelte/issues/7500))
* Reduce use of template literals in SSR output for better performance ([#7539](https://github.com/sveltejs/svelte/pull/7539))
* Ensure `<input>` value persists when swapping elements with spread attributes in an `{#each}` block ([#7578](https://github.com/sveltejs/svelte/issues/7578))
* Simplify generated code for reactive statements if dependencies are all static ([#7942](https://github.com/sveltejs/svelte/pull/7942))
* Fix race condition on `<svelte:element>` with transitions ([#7948](https://github.com/sveltejs/svelte/issues/7948))
* Allow assigning to a property of a `const` when destructuring ([#7964](https://github.com/sveltejs/svelte/issues/7964))
* 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

@ -534,13 +534,17 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
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;
/**
* 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;
/**
* Elements with the contenteditable attribute support `innerHTML`, `textContent` and `innerText` bindings.
*/
'bind:innerText'?: string | undefined | null;
// SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;

@ -3,12 +3,7 @@
const { execSync } = require('child_process');
const { readFileSync, writeFileSync } = require('fs');
try {
execSync('tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly');
} catch (err) {
console.error(err.stderr.toString());
throw err;
}
execSync('tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly', { stdio: 'inherit' });
// 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.
// We can't use `export type` syntax either because the TS version we're on doesn't have this feature yet.

32
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "svelte",
"version": "3.55.1",
"version": "3.57.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "svelte",
"version": "3.55.1",
"version": "3.57.0",
"license": "MIT",
"devDependencies": {
"@ampproject/remapping": "^0.3.0",
@ -19,14 +19,14 @@
"@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^3.0.1",
"@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/node": "^8.10.53",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"acorn": "^8.8.1",
"agadoo": "^3.0.0",
"aria-query": "^5.1.1",
"aria-query": "^5.1.3",
"axobject-query": "^3.1.1",
"code-red": "^1.0.0",
"css-tree": "^2.3.1",
@ -505,9 +505,9 @@
"dev": true
},
"node_modules/@types/aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-P+dkdFu0n08PDIvw+9nT9ByQnd+Udc8DaWPb9HKfaPwCvWvQpC5XaMRx2xLWECm9x1VKNps6vEAlirjA6+uNrQ==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
"integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==",
"dev": true
},
"node_modules/@types/estree": {
@ -955,9 +955,9 @@
}
},
"node_modules/aria-query": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.1.tgz",
"integrity": "sha512-4cPQjOYM2mqq7mZG8CSxkUvL2Yv/x29VhGq5LKehTsxRnoVQps1YGt9NyjcNQsznEsD4rr8a6zGxqeNTqJWjpA==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
"integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
"dev": true,
"dependencies": {
"deep-equal": "^2.0.5"
@ -5736,9 +5736,9 @@
"from": "@sveltejs/eslint-config@github:sveltejs/eslint-config#v5.8.0"
},
"@types/aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-P+dkdFu0n08PDIvw+9nT9ByQnd+Udc8DaWPb9HKfaPwCvWvQpC5XaMRx2xLWECm9x1VKNps6vEAlirjA6+uNrQ==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
"integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==",
"dev": true
},
"@types/estree": {
@ -6042,9 +6042,9 @@
}
},
"aria-query": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.1.tgz",
"integrity": "sha512-4cPQjOYM2mqq7mZG8CSxkUvL2Yv/x29VhGq5LKehTsxRnoVQps1YGt9NyjcNQsznEsD4rr8a6zGxqeNTqJWjpA==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
"integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
"dev": true,
"requires": {
"deep-equal": "^2.0.5"

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "3.55.1",
"version": "3.57.0",
"description": "Cybernetically enhanced web apps",
"module": "index.mjs",
"main": "index",
@ -129,14 +129,14 @@
"@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^3.0.1",
"@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/node": "^8.10.53",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"acorn": "^8.8.1",
"agadoo": "^3.0.0",
"aria-query": "^5.1.1",
"aria-query": "^5.1.3",
"axobject-query": "^3.1.1",
"code-red": "^1.0.0",
"css-tree": "^2.3.1",

@ -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
<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()`.
### `<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>`
```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.
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
<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.
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
<svelte:head>

@ -693,7 +693,7 @@ Animates a `blur` filter alongside an element's opacity.
* `duration` (`number`, default 400) — milliseconds the transition lasts
* `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
* `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
<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
* `duration` (`number`, default 400) — milliseconds the transition lasts
* `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
* `y` (`number`, default 0) - the y 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 | 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
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).
```sv

@ -277,6 +277,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`
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,11 +2,16 @@
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
<div
contenteditable="true"
bind:innerHTML={html}
></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:body />
<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 { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap';
import is_dynamic from './render_dom/wrappers/shared/is_dynamic';
interface ComponentOptions {
namespace?: string;
@ -1392,11 +1391,12 @@ export default class Component {
module_dependencies.add(name);
}
}
const is_writable_or_mutated =
variable && (variable.writable || variable.mutated);
if (
should_add_as_dependency &&
(!owner || owner === component.instance_scope) &&
(name[0] === '$' || variable)
(name[0] === '$' || is_writable_or_mutated)
) {
dependencies.add(name);
}
@ -1420,19 +1420,6 @@ export default class Component {
const { expression } = node.body as ExpressionStatement;
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({
assignees,
dependencies,

@ -66,7 +66,7 @@ export default {
},
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: {
code: 'dynamic-contenteditable-attribute',

@ -119,6 +119,10 @@ export default {
code: 'a11y-no-interactive-element-to-noninteractive-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[]) => ({
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(', ')}`
@ -213,5 +217,9 @@ export default {
invalid_rest_eachblock_binding: (rest_element_name: string) => ({
code: 'invalid-rest-eachblock-binding',
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'
}
};

@ -17,6 +17,7 @@ export default class CatchBlock extends AbstractBlock {
this.scope = scope.child();
if (parent.catch_node) {
parent.catch_contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
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.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
const owner = this.scope.get_owner(context.key.name);
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) {
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);
}
}
}

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

@ -11,6 +11,7 @@ import StyleDirective from './StyleDirective';
import Text from './Text';
import { namespaces } from '../../utils/namespaces';
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 fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
@ -23,15 +24,14 @@ import { string_literal } from '../utils/stringify';
import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors';
import { ARIARoleDefintionKey, 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 { ARIARoleDefinitionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query';
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 } 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_attribute_set = new Set(aria_attributes);
const aria_roles = roles.keys();
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 = {
a: ['href'],
@ -566,8 +566,8 @@ export default class Element extends Node {
const value = attribute.get_static_value();
if (typeof value === 'string') {
value.split(regex_any_repeated_whitespaces).forEach((current_role: ARIARoleDefintionKey) => {
if (current_role && aria_role_abstract_set.has(current_role)) {
value.split(regex_any_repeated_whitespaces).forEach((current_role: ARIARoleDefinitionKey) => {
if (current_role && is_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)) {
const match = fuzzymatch(current_role, aria_roles);
@ -607,8 +607,12 @@ export default class Element extends Node {
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));
}
});
// no-noninteractive-element-to-interactive-role
if (is_non_interactive_element(this.name, attribute_map) && is_interactive_roles(current_role)) {
component.warn(this, compiler_warnings.a11y_no_noninteractive_element_to_interactive_role(current_role, this.name));
}
});
}
}
@ -640,7 +644,7 @@ export default class Element extends Node {
// click-events-have-key-events
if (handlers_map.has('click')) {
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 (
!this.is_dynamic_element &&
@ -664,7 +668,7 @@ export default class Element extends Node {
}
// 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');
if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) {
component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex);
@ -673,7 +677,7 @@ export default class Element extends Node {
// role-supports-aria-props
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)) {
const { props } = roles.get(role_value);
const invalid_aria_props = new Set(aria.keys().filter(attribute => !(attribute in props)));
@ -1008,14 +1012,8 @@ export default class Element extends Node {
if (this.name !== 'img') {
return component.error(binding, compiler_errors.invalid_binding_element_with('<img>', name));
}
} else if (
name === 'textContent' ||
name === 'innerHTML'
) {
const contenteditable = this.attributes.find(
(attribute: Attribute) => attribute.name === 'contenteditable'
);
} else if (is_name_contenteditable(name)) {
const contenteditable = get_contenteditable_attr(this);
if (!contenteditable) {
return component.error(binding, compiler_errors.missing_contenteditable_attribute);
} else if (contenteditable && !contenteditable.is_static) {

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

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

@ -1,5 +1,5 @@
import { x } from 'code-red';
import { Node, Identifier, Expression } from 'estree';
import { Node, Identifier, Expression, PrivateIdentifier } from 'estree';
import { walk } from 'estree-walker';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import { clone } from '../../../utils/clone';
@ -7,7 +7,16 @@ import Component from '../../Component';
import flatten_reference from '../../utils/flatten_reference';
import TemplateScope from './TemplateScope';
export interface Context {
export type Context = DestructuredVariable | ComputedProperty;
interface ComputedProperty {
type: 'ComputedProperty';
property_name: string;
key: Expression | PrivateIdentifier;
}
interface DestructuredVariable {
type: 'DestructuredVariable'
key: Identifier;
name?: string;
modifier: (node: Node) => Node;
@ -21,26 +30,33 @@ export function unpack_destructuring({
default_modifier = (node) => node,
scope,
component,
context_rest_properties
context_rest_properties,
number_of_computed_props = { n: 0 }
}: {
contexts: Context[];
node: Node;
modifier?: Context['modifier'];
default_modifier?: Context['default_modifier'];
modifier?: DestructuredVariable['modifier'];
default_modifier?: DestructuredVariable['default_modifier'];
scope: TemplateScope;
component: Component;
context_rest_properties: Map<string, Node>;
// we want to pass this by reference, as a sort of global variable, because
// if we pass this by value, we could get computed_property_# variable collisions
// when we deal with nested object destructuring
number_of_computed_props?: { n: number };
}) {
if (!node) return;
if (node.type === 'Identifier') {
contexts.push({
type: 'DestructuredVariable',
key: node as Identifier,
modifier,
default_modifier
});
} else if (node.type === 'RestElement') {
contexts.push({
type: 'DestructuredVariable',
key: node.argument as Identifier,
modifier,
default_modifier
@ -56,7 +72,8 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties
context_rest_properties,
number_of_computed_props
});
context_rest_properties.set((element.argument as Identifier).name, element);
} else if (element && element.type === 'AssignmentPattern') {
@ -76,7 +93,8 @@ export function unpack_destructuring({
)}` as Node,
scope,
component,
context_rest_properties
context_rest_properties,
number_of_computed_props
});
} else {
unpack_destructuring({
@ -86,7 +104,8 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties
context_rest_properties,
number_of_computed_props
});
}
});
@ -105,15 +124,43 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties
context_rest_properties,
number_of_computed_props
});
context_rest_properties.set((property.argument as Identifier).name, property);
} else {
const key = property.key as Identifier;
} else if (property.type === 'Property') {
const key = property.key;
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 = `computed_property_${number_of_computed_props.n}`;
number_of_computed_props.n += 1;
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') {
// e.g. { property = default } or { property: newName = default }
const n = contexts.length;
mark_referenced(value.right, scope, component);
@ -121,7 +168,7 @@ export function unpack_destructuring({
unpack_destructuring({
contexts,
node: value.left,
modifier: (node) => x`${modifier(node)}.${key.name}`,
modifier: new_modifier,
default_modifier: (node, to_ctx) =>
x`${node} !== undefined ? ${node} : ${update_reference(
contexts,
@ -131,17 +178,20 @@ export function unpack_destructuring({
)}` as Node,
scope,
component,
context_rest_properties
context_rest_properties,
number_of_computed_props
});
} else {
// e.g. { property } or { property: newName }
unpack_destructuring({
contexts,
node: value,
modifier: (node) => x`${modifier(node)}.${key.name}` as Node,
modifier: new_modifier,
default_modifier,
scope,
component,
context_rest_properties
context_rest_properties,
number_of_computed_props
});
}
}
@ -157,7 +207,9 @@ function update_reference(
): Node {
const find_from_context = (node: Identifier) => {
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) {
throw new Error(`Cannot access '${node.name}' before initialization`);
}

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

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

@ -505,7 +505,7 @@ export default class Block {
render_binding_groups() {
for (const binding_group of this.binding_groups) {
binding_group.render();
binding_group.render(this);
}
}
}

@ -27,8 +27,8 @@ export interface BindingGroup {
contexts: string[];
list_dependencies: Set<string>;
keypath: string;
elements: Identifier[];
render: () => void;
add_element: (block: Block, element: Identifier) => void;
render: (block: Block) => void;
}
export default class Renderer {

@ -390,13 +390,13 @@ export default function dom(
const resubscribable_reactive_store_unsubscribers = reactive_stores
.filter(store => {
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)}`}());`);
if (has_definition) {
const reactive_declarations: Node[] = [];
const fixed_reactive_declarations: Array<Node | Node[]> = []; // not really 'reactive' but whatever
const reactive_declarations: (Node | Node[]) = [];
const fixed_reactive_declarations: Node[] = []; // not really 'reactive' but whatever
component.reactive_declarations.forEach(d => {
const dependencies = Array.from(d.dependencies);
@ -417,15 +417,6 @@ export default function dom(
reactive_declarations.push(statement);
} else {
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 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 subscribe = `$$subscribe_${name}`;
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.global &&
!variable.module &&
!variable.is_reactive_static &&
(
variable.referenced ||
variable.subscribable ||

@ -11,6 +11,7 @@ import CatchBlock from '../../nodes/CatchBlock';
import { Context } from '../../nodes/shared/Context';
import { Identifier, Literal, Node } from 'estree';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import Expression from '../../nodes/shared/Expression';
type Status = 'pending' | 'then' | 'catch';
@ -69,6 +70,7 @@ class AwaitBlockBranch extends Wrapper {
this.renderer.add_to_context(this.value, true);
} else {
contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.renderer.add_to_context(context.key.name, true);
});
this.value = this.block.parent.get_unique_name('value').name;
@ -96,7 +98,15 @@ class AwaitBlockBranch extends Wrapper {
}
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;

@ -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 get_object from '../../utils/get_object';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import Expression from '../../nodes/shared/Expression';
export class ElseBlockWrapper extends Wrapper {
node: ElseBlock;
@ -86,6 +87,7 @@ export default class EachBlockWrapper extends Wrapper {
block.add_dependencies(dependencies);
this.node.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
renderer.add_to_context(context.key.name, true);
});
add_const_tags_context(renderer, this.node.const_tags);
@ -147,6 +149,7 @@ export default class EachBlockWrapper extends Wrapper {
const store = object.type === 'Identifier' && object.name[0] === '$' ? object.name.slice(1) : null;
node.contexts.forEach(prop => {
if (prop.type !== 'DestructuredVariable') return;
this.block.bindings.set(prop.key.name, {
object: this.vars.each_block_value,
property: this.index_name,
@ -361,7 +364,15 @@ export default class EachBlockWrapper extends Wrapper {
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.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);

@ -145,15 +145,15 @@ export default class BindingWrapper {
}
// model to view
let update_dom = get_dom_updater(parent, this);
let mount_dom = update_dom;
let update_dom = get_dom_updater(parent, this, false);
let mount_dom = get_dom_updater(parent, this, true);
// special cases
switch (this.node.name) {
case 'group':
{
block.renderer.add_to_context('$$binding_groups');
this.binding_group.elements.push(this.parent.var);
this.binding_group.add_element(block, this.parent.var);
if ((this.parent as ElementWrapper).has_dynamic_value) {
update_or_condition = (this.parent as ElementWrapper).dynamic_value_condition;
@ -165,6 +165,11 @@ export default class BindingWrapper {
update_conditions.push(x`${this.snippet} !== ${parent.var}.textContent`);
mount_conditions.push(x`${this.snippet} !== void 0`);
break;
case 'innerText':
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerText`);
mount_conditions.push(x`${this.snippet} !== void 0`);
break;
case 'innerHTML':
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerHTML`);
@ -234,7 +239,8 @@ export default class BindingWrapper {
function get_dom_updater(
element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper
binding: BindingWrapper,
mounting: boolean
) {
const { node } = element;
@ -249,6 +255,7 @@ function get_dom_updater(
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
b`@select_options(${element.var}, ${binding.snippet})` :
mounting ? b`@select_option(${element.var}, ${binding.snippet}, true)` :
b`@select_option(${element.var}, ${binding.snippet})`;
}
@ -321,7 +328,11 @@ function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: B
parent = parent.parent;
}
const elements = [];
/**
* 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 => {
renderer.add_to_context(context, true);
@ -341,8 +352,13 @@ function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: B
contexts,
list_dependencies,
keypath,
elements,
render() {
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);
@ -360,7 +376,7 @@ function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: B
);
}
block.chunks.hydrate.push(
b`${local_name}.p(${elements})`
b`${local_name}.p(${elements.get(block)})`
);
block.chunks.destroy.push(
b`${local_name}.r()`
@ -439,7 +455,7 @@ function get_value_from_dom(
return x`$$value`;
}
// <select bind:value='selected>
// <select bind:value='selected'>
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
x`@select_multiple_value(this)` :

@ -26,6 +26,7 @@ import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag';
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 { push_array } from '../../../../utils/push_array';
@ -48,8 +49,8 @@ const events = [
{
event_names: ['input'],
filter: (node: Element, name: string) =>
(name === 'textContent' || name === 'innerHTML') &&
node.attributes.some(attribute => attribute.name === 'contenteditable')
is_name_contenteditable(name) &&
has_contenteditable_attr(node)
},
{
event_names: ['change'],
@ -160,6 +161,7 @@ export default class ElementWrapper extends Wrapper {
bindings: Binding[];
event_handlers: EventHandler[];
class_dependencies: string[];
dynamic_style_dependencies: Set<string>;
has_dynamic_attribute: boolean;
select_binding_dependencies?: Set<string>;
@ -214,6 +216,8 @@ export default class ElementWrapper extends Wrapper {
return;
}
this.dynamic_style_dependencies = new Set();
if (this.node.children.length) {
this.node.lets.forEach(l => {
extract_names(l.value || l.name).forEach(name => {
@ -763,14 +767,11 @@ export default class ElementWrapper extends Wrapper {
const should_initialise = (
this.node.name === 'select' ||
binding_group.bindings.find(binding => {
return (
binding.node.name === 'indeterminate' ||
binding.node.name === 'textContent' ||
binding.node.name === 'innerHTML' ||
binding.is_readonly_media_attribute()
);
})
binding_group.bindings.find(binding => (
binding.node.name === 'indeterminate' ||
is_name_contenteditable(binding.node.name) ||
binding.is_readonly_media_attribute()
))
);
if (should_initialise) {
@ -801,11 +802,13 @@ export default class ElementWrapper extends Wrapper {
}
add_attributes(block: Block) {
// Get all the class dependencies first
// Get all the class and style dependencies first
this.attributes.forEach((attribute) => {
if (attribute.node.name === 'class') {
const dependencies = attribute.node.get_dependencies();
push_array(this.class_dependencies, dependencies);
} else if (attribute.node.name === 'style') {
add_to_set(this.dynamic_style_dependencies, attribute.node.get_dependencies());
}
});
@ -962,6 +965,7 @@ export default class ElementWrapper extends Wrapper {
const intro_block = b`
@add_render_callback(() => {
if (!#current) return;
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true);
${name}.run(1);
});
@ -1007,6 +1011,7 @@ export default class ElementWrapper extends Wrapper {
if (outro) {
intro_block = b`
@add_render_callback(() => {
if (!#current) return;
if (${outro_name}) ${outro_name}.end(1);
${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
${intro_name}.start();
@ -1170,8 +1175,18 @@ export default class ElementWrapper extends Wrapper {
add_styles(block: Block) {
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) => {
const { name, expression, should_cache, important } = style_directive;
const { name, expression, important, should_cache } = style_directive;
const snippet = expression.manipulate(block);
let cached_snippet: Identifier | undefined;
@ -1184,24 +1199,40 @@ export default class ElementWrapper extends Wrapper {
block.chunks.hydrate.push(updater);
const dependencies = expression.dynamic_dependencies();
// Assume that style has changed through the spread attribute
if (has_spread) {
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) {
block.chunks.update.push(b`
if (${block.renderer.dirty(dependencies)} && (${cached_snippet} !== (${cached_snippet} = ${snippet}))) {
${updater}
}
`);
} else {
block.chunks.update.push(b`
if (${block.renderer.dirty(dependencies)}) {
${updater}
}
`);
condition = x`${condition} && ${cached_snippet} !== (${cached_snippet} = ${snippet})`;
}
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`
if (${condition}) {
${updater}
}
`);
}
});
}

@ -2,6 +2,7 @@ import Wrapper from './shared/Wrapper';
import AwaitBlock from './AwaitBlock';
import Body from './Body';
import DebugTag from './DebugTag';
import Document from './Document';
import EachBlock from './EachBlock';
import Element from './Element/index';
import Head from './Head';
@ -28,6 +29,7 @@ const wrappers = {
Body,
Comment: null,
DebugTag,
Document,
EachBlock,
Element,
Head,

@ -1,24 +1,33 @@
import ConstTag from '../../../nodes/ConstTag';
import Block from '../../Block';
import { b, x } from 'code-red';
import { b, Node, x } from 'code-red';
import Renderer from '../../Renderer';
import Expression from '../../../nodes/shared/Expression';
export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string) {
const const_tags_props = [];
const_tags.forEach((const_tag, i) => {
const name = `#constants_${i}`;
const_tags_props.push(b`const ${name} = ${const_tag.expression.manipulate(block, ctx)}`);
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 })};`);
});
});
return const_tags_props;
const const_tags_props = [];
const_tags.forEach((const_tag, i) => {
const name = `#constants_${i}`;
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 => {
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;
}
export function add_const_tags_context(renderer: Renderer, const_tags: ConstTag[]) {
const_tags.forEach(const_tag => {
const_tag.contexts.forEach(context => {
renderer.add_to_context(context.key.name, true);
});
});
const_tags.forEach(const_tag => {
const_tag.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
renderer.add_to_context(context.key.name, true);
});
});
}

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

@ -1,7 +1,9 @@
import { is_void } from '../../../../shared/utils/names';
import { get_attribute_expression, get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value';
import { boolean_attributes } from '../../../../shared/boolean_attributes';
import { is_name_contenteditable, is_contenteditable } from '../../utils/contenteditable';
import Renderer, { RenderOptions } from '../Renderer';
import Binding from '../../nodes/Binding';
import Element from '../../nodes/Element';
import { p, x } from 'code-red';
import Expression from '../../nodes/shared/Expression';
@ -18,11 +20,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
// awkward special case
let node_contents;
const contenteditable = (
node.name !== 'textarea' &&
node.name !== 'input' &&
node.attributes.some((attribute) => attribute.name === 'contenteditable')
);
const contenteditable = is_contenteditable(node);
if (node.is_dynamic_element) {
renderer.push();
@ -128,7 +126,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
}
}
node.bindings.forEach(binding => {
node.bindings.forEach((binding: Binding) => {
const { name, expression } = binding;
if (binding.is_readonly) {
@ -144,7 +142,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
const condition = type === 'checkbox' ? x`~${bound}.indexOf(${value})` : x`${value} === ${bound}`;
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;
// TODO where was this used?

@ -1,5 +1,14 @@
import * as assert from 'assert';
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', () => {
it('uses the basename', () => {
@ -20,3 +29,61 @@ describe('get_name_from_filename', () => {
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 {
ARIARoleDefintionKey,
ARIARoleDefinitionKey,
roles as roles_map,
elementRoles,
ARIARoleRelationConcept
@ -7,7 +7,9 @@ import {
import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query';
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(
non_abstract_roles
@ -32,17 +34,21 @@ const interactive_roles = new Set(
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);
}
export function is_interactive_roles(role: ARIARoleDefintionKey) {
export function is_interactive_roles(role: ARIARoleDefinitionKey) {
return interactive_roles.has(role);
}
export function is_abstract_role(role: ARIARoleDefinitionKey) {
return abstract_roles.has(role);
}
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);
}
@ -65,7 +71,7 @@ export function is_hidden_from_screen_reader(tag_name: string, attribute_map: Ma
const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = [];
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);
}
});
@ -82,6 +88,10 @@ const interactive_ax_objects = new Set(
[...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[] = [];
elementAXObjects.entries().forEach(([schema, ax_object]) => {
@ -90,6 +100,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(
schema: ARIARoleRelationConcept,
tag_name: string,
@ -110,24 +128,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,
attribute_map: Map<string, Attribute>
): boolean {
): ElementInteractivity {
if (
interactive_element_role_schemas.some((schema) =>
match_schema(schema, tag_name, attribute_map)
)
) {
return true;
return ElementInteractivity.Interactive;
}
if (
tag_name !== 'header' &&
non_interactive_element_role_schemas.some((schema) =>
match_schema(schema, tag_name, attribute_map)
)
) {
return false;
return ElementInteractivity.NonInteractive;
}
if (
@ -135,13 +160,33 @@ export function is_interactive_element(
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()) {
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

@ -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 {
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>;
name: string;
}
@ -223,7 +223,6 @@ export interface Var {
subscribable?: boolean;
is_reactive_dependency?: boolean;
imported?: boolean;
is_reactive_static?: boolean;
}
export interface CssResult {

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

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

@ -19,10 +19,10 @@ export default function get_code_frame(
return lines
.slice(frame_start, frame_end)
.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, ' ');
if (isErrorLine) {
if (is_error_line) {
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
}

@ -359,7 +359,7 @@ export function get_binding_group_value(group, __value, checked) {
return Array.from(value);
}
export function init_binding_group(group) {
export function init_binding_group(group: HTMLInputElement[]) {
let _inputs: HTMLInputElement[];
return {
/* push */ p(...inputs: HTMLInputElement[]) {
@ -606,7 +606,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) {
const option = select.options[i];
@ -616,7 +616,9 @@ export function select_option(select, value) {
}
}
select.selectedIndex = -1; // no option should be selected
if (!mounting || value !== undefined) {
select.selectedIndex = -1; // no option should be selected
}
}
export function select_options(select, value) {
@ -626,16 +628,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) {
const selected_option = select.querySelector(':checked') || first_enabled_option(select);
const selected_option = select.querySelector(':checked');
return selected_option && selected_option.__value;
}

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

@ -189,3 +189,8 @@ export const has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj,
export function action_destroyer(action_result) {
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'];
}

@ -164,7 +164,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
const auto = fn.length < 2;
return readable(initial_value, (set) => {
let inited = false;
let started = false;
const values = [];
let pending = 0;
@ -188,7 +188,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
(value) => {
values[i] = value;
pending &= ~(1 << i);
if (inited) {
if (started) {
sync();
}
},
@ -197,12 +197,16 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
})
);
inited = true;
started = true;
sync();
return function stop() {
run_all(unsubscribers);
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 { assign, is_function } from 'svelte/internal';
import { assign, split_css_unit, is_function } from 'svelte/internal';
export type EasingFunction = (t: number) => number;
@ -15,7 +15,7 @@ export interface BlurParams {
delay?: number;
duration?: number;
easing?: EasingFunction;
amount?: number;
amount?: number | string;
opacity?: number;
}
@ -31,12 +31,12 @@ export function blur(node: Element, {
const f = style.filter === 'none' ? '' : style.filter;
const od = target_opacity * (1 - opacity);
const [value, unit] = split_css_unit(amount);
return {
delay,
duration,
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;
duration?: number;
easing?: EasingFunction;
x?: number;
y?: number;
x?: number | string;
y?: number | string;
opacity?: number;
}
@ -83,13 +83,14 @@ export function fly(node: Element, {
const transform = style.transform === 'none' ? '' : style.transform;
const od = target_opacity * (1 - opacity);
const [xValue, xUnit] = split_css_unit(x);
const [yValue, yUnit] = split_css_unit(y);
return {
delay,
duration,
easing,
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)}`
};
}

@ -0,0 +1,25 @@
/* generated by Svelte vX.Y.Z */
import { SvelteComponent, init, safe_not_equal } from "svelte/internal";
function instance($$self) {
class A {
p1;
p2 = 1;
#p3 = 2;
#getP3() {
return this.#p3;
}
}
return [];
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, null, safe_not_equal, {});
}
}
export default Component;

@ -0,0 +1,12 @@
<script>
class A {
p1;
p2 = 1;
#p3 = 2;
#getP3() {
return this.#p3;
}
}
</script>

@ -9,6 +9,7 @@ import {
noop,
safe_not_equal,
space,
subscribe,
toggle_class
} from "svelte/internal";
@ -132,8 +133,13 @@ let reactiveModuleVar = Math.random();
function instance($$self, $$props, $$invalidate) {
let reactiveDeclaration;
let $reactiveStoreVal;
let $reactiveDeclaration;
let $reactiveDeclaration,
$$unsubscribe_reactiveDeclaration = noop,
$$subscribe_reactiveDeclaration = () => ($$unsubscribe_reactiveDeclaration(), $$unsubscribe_reactiveDeclaration = subscribe(reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value)), reactiveDeclaration);
component_subscribe($$self, reactiveStoreVal, $$value => $$invalidate(2, $reactiveStoreVal = $$value));
$$self.$$.on_destroy.push(() => $$unsubscribe_reactiveDeclaration());
nonReactiveGlobal = Math.random();
const reactiveConst = { x: Math.random() };
reactiveModuleVar += 1;
@ -142,8 +148,7 @@ function instance($$self, $$props, $$invalidate) {
reactiveConst.x += 1;
}
$: reactiveDeclaration = reactiveModuleVar * 2;
component_subscribe($$self, reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value));
$: $$subscribe_reactiveDeclaration($$invalidate(1, reactiveDeclaration = reactiveModuleVar * 2));
return [reactiveConst, reactiveDeclaration, $reactiveStoreVal, $reactiveDeclaration];
}

@ -1,60 +0,0 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
detach,
element,
init,
insert,
noop,
safe_not_equal,
set_data,
space,
text
} from "svelte/internal";
function create_fragment(ctx) {
let h1;
let t3;
let t4;
return {
c() {
h1 = element("h1");
h1.textContent = `Hello ${name}!`;
t3 = space();
t4 = text(/*foo*/ ctx[0]);
},
m(target, anchor) {
insert(target, h1, anchor);
insert(target, t3, anchor);
insert(target, t4, anchor);
},
p(ctx, [dirty]) {
if (dirty & /*foo*/ 1) set_data(t4, /*foo*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(h1);
if (detaching) detach(t3);
if (detaching) detach(t4);
}
};
}
let name = 'world';
function instance($$self) {
let foo;
$: foo = name + name;
return [foo];
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default Component;

@ -1,7 +0,0 @@
<script>
let name = 'world';
$: foo = name + name;
</script>
<h1>Hello {name}!</h1>
{foo}

@ -1,6 +1,6 @@
{
"code": "invalid-tag-name",
"message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:body, svelte:self, svelte:component, svelte:fragment or svelte:element",
"message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:document, svelte:body, svelte:self, svelte:component, svelte:fragment or svelte:element",
"pos": 10,
"start": {
"character": 10,

@ -56,9 +56,7 @@ async function launchPuppeteer() {
const assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8');
describe('runtime (puppeteer)', function() {
// Note: Increase the timeout in preparation for restarting Chromium due to SIGSEGV.
this.timeout(10000);
describe('runtime (puppeteer)', () => {
before(async () => {
svelte = loadSvelte(false);
console.log('[runtime-puppeteer] Loaded Svelte');
@ -75,7 +73,7 @@ describe('runtime (puppeteer)', function() {
const failed = new Set();
function runTest(dir, hydrate) {
function runTest(dir, hydrate, is_first_run) {
if (dir[0] === '.') return;
// MEMO: puppeteer can not execute Chromium properly with Node8,10 on Linux at GitHub actions.
const { version } = process;
@ -254,11 +252,14 @@ describe('runtime (puppeteer)', function() {
prettyPrintPuppeteerAssertionError(err.message);
assertWarnings();
});
});
}).timeout(is_first_run ? 20000 : 10000);
}
// Increase the timeout on the first run in preparation for restarting Chromium due to SIGSEGV.
let first_run = true;
fs.readdirSync(`${__dirname}/samples`).forEach(dir => {
runTest(dir, false);
runTest(dir, true);
runTest(dir, false, first_run);
runTest(dir, true, first_run);
first_run = false;
});
});

@ -0,0 +1,24 @@
export default {
html: `
<p style="font-size: 32px; color: red; background-color: green; border-color: green;"></p>
`,
test({ assert, target, window, component }) {
const p = target.querySelector('p');
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'rgb(255, 0, 0)');
assert.equal(styles.fontSize, '32px');
assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
assert.equal(styles.borderColor, 'rgb(0, 128, 0)');
component.foo = 'font-size: 50px; color: green;'; // Update style attribute
{
const p = target.querySelector('p');
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'rgb(255, 0, 0)');
assert.equal(styles.fontSize, '32px');
assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
assert.equal(styles.borderColor, 'rgb(0, 128, 0)');
}
}
};

@ -0,0 +1,15 @@
<script>
export let foo = "font-size: 20px; color: blue;";
let baz = "red"; // static value
let bar = "32"; // static value interpolated
export let bg = "gre"; // dynamic value interpolated/cached
export let borderColor = "green"; // dynamic value non-cached
</script>
<p
style:font-size="{bar}px"
style:color={baz}
style="{foo}"
style:background-color="{bg}en"
style:border-color={borderColor}
/>

@ -0,0 +1,14 @@
export default {
html: '<div></div>',
async test({ assert, target, window }) {
const visibility = new window.Event('visibilitychange');
await window.document.dispatchEvent(visibility);
assert.htmlEqual(target.innerHTML, `
<div>
<div class="tooltip">Perform an Action</div>
</div>
`);
}
};

@ -0,0 +1,24 @@
<script>
let container;
function tooltip(node, text) {
let tooltip = null;
function onVisibilityChange() {
tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.textContent = text;
container.appendChild(tooltip);
}
node.addEventListener('visibilitychange', onVisibilityChange);
return {
destroy() {
node.removeEventListener('visibilitychange', onVisibilityChange);
}
}
}
</script>
<svelte:document use:tooltip="{'Perform an Action'}" />
<div bind:this={container} />

@ -0,0 +1,32 @@
export default {
async test({ assert, component, target }) {
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<p>propA: 3</p>
<p>propB: 7</p>
<p>num: 3</p>
<p>rest: {"prop3":{"prop9":9,"prop10":10}}</p>
<p>propZ: 5</p>
<p>propY: 6</p>
<p>rest: {"propX":7,"propW":8}</p>
`
);
await (component.object = Promise.resolve({ prop1: 'one', prop2: 'two', prop3: { prop7: 'seven' }, prop4: { prop10: 'ten' }}));
assert.htmlEqual(
target.innerHTML,
`
<p>propA: seven</p>
<p>propB: ten</p>
<p>num: 5</p>
<p>rest: {"prop1":"one","prop2":"two"}</p>
<p>propZ: 5</p>
<p>propY: 6</p>
<p>rest: {"propX":7,"propW":8}</p>
`
);
}
};

@ -0,0 +1,23 @@
<script>
export let object = Promise.resolve({ prop1: { prop4: 2, prop5: 3 }, prop2: { prop6: 5, prop7: 6, prop8: 7 }, prop3: { prop9: 9, prop10: 10 } });
const objectReject = Promise.reject({ propZ: 5, propY: 6, propX: 7, propW: 8 });
let num = 1;
const prop = 'prop';
</script>
{#await object then { [`prop${num++}`]: { [`prop${num + 3}`]: propA }, [`prop${num++}`]: { [`prop${num + 5}`]: propB }, ...rest }}
<p>propA: {propA}</p>
<p>propB: {propB}</p>
<p>num: {num}</p>
<p>rest: {JSON.stringify(rest)}</p>
{/await}
{#await objectReject then value}
resolved
{:catch { [`${prop}Z`]: propZ, [`${prop}Y`]: propY, ...rest }}
<p>propZ: {propZ}</p>
<p>propY: {propY}</p>
<p>rest: {JSON.stringify(rest)}</p>
{/await}

@ -0,0 +1,63 @@
export default {
props: {
thePromise: new Promise(_ => {})
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([10, 11, 12, 13, 14, 15]));
assert.htmlEqual(
target.innerHTML,
`
<p>[1] 11</p>
<p>[3] 13</p>
<p>[4] 14</p>
`
);
await (component.thePromise = Promise.resolve({ 1: 21, 3: 23, 4: 24 }));
assert.htmlEqual(
target.innerHTML,
`
<p>[1] 21</p>
<p>[3] 23</p>
<p>[4] 24</p>
`
);
try {
await (component.thePromise = Promise.reject([30, 31, 32, 33, 34, 35]));
} catch (e) {
// do nothing
}
assert.htmlEqual(
target.innerHTML,
`
<p>[0] 30</p>
<p>[2] 32</p>
<p>[5] 35</p>
`
);
try {
await (component.thePromise = Promise.reject({ 0: 40, 2: 42, 5: 45 }));
} catch (e) {
// do nothing
}
assert.htmlEqual(
target.innerHTML,
`
<p>[0] 40</p>
<p>[2] 42</p>
<p>[5] 45</p>
`
);
}
};

@ -0,0 +1,15 @@
<script>
export let thePromise;
</script>
{#await thePromise}
loading...
{:then { 1: a, 3: b, 4: c }}
<p>[1] {a}</p>
<p>[3] {b}</p>
<p>[4] {c}</p>
{:catch { 0: d, 2: e, 5: f }}
<p>[0] {d}</p>
<p>[2] {e}</p>
<p>[5] {f}</p>
{/await}

@ -0,0 +1,17 @@
export default {
async test({ assert, target }) {
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<p>prop-1: 1</p>
<p>prop4: 4</p>
<p>rest: {"prop2":2,"prop-3":3}</p>
<p>prop-7: 7</p>
<p>prop6: 6</p>
<p>rest: {"prop-5":5,"prop8":8}</p>
`
);
}
};

@ -0,0 +1,19 @@
<script>
const object = Promise.resolve({ 'prop-1': 1, 'prop2': 2, 'prop-3': 3, 'prop4': 4 });
const objectReject = Promise.reject({ 'prop-5': 5, 'prop6': 6, 'prop-7': 7, 'prop8': 8 });
</script>
{#await object then { 'prop-1': prop1, 'prop4': fourthProp, ...rest }}
<p>prop-1: {prop1}</p>
<p>prop4: {fourthProp}</p>
<p>rest: {JSON.stringify(rest)}</p>
{/await}
{#await objectReject then value}
resolved
{:catch { 'prop-7': prop7, 'prop6': sixthProp, ...rest }}
<p>prop-7: {prop7}</p>
<p>prop6: {sixthProp}</p>
<p>rest: {JSON.stringify(rest)}</p>
{/await}

@ -0,0 +1,25 @@
export default {
props: {
name: 'world'
},
ssrHtml: `
<editor contenteditable="true">world</editor>
<p>hello world</p>
`,
async test({ assert, component, target, window }) {
// JSDom doesn't support innerText yet, so the test is not ideal
// https://github.com/jsdom/jsdom/issues/1245
const el = target.querySelector('editor');
assert.equal(el.innerText, 'world');
const event = new window.Event('input');
el.innerText = 'everybody';
await el.dispatchEvent(event);
assert.equal(component.name, 'everybody');
component.name = 'goodbye';
assert.equal(el.innerText, 'goodbye');
}
};

@ -0,0 +1,6 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:innerText={name} />
<p>hello {name}</p>

@ -0,0 +1,40 @@
export default {
async test({ assert, target, component, window }) {
const button = target.querySelector('button');
const clickEvent = new window.Event('click');
const changeEvent = new window.Event('change');
const [input1, input2] = target.querySelectorAll('input[type="checkbox"]');
function validate_inputs(v1, v2) {
assert.equal(input1.checked, v1);
assert.equal(input2.checked, v2);
}
assert.deepEqual(component.test, []);
validate_inputs(false, false);
component.test = ['a', 'b'];
validate_inputs(true, true);
input1.checked = false;
await input1.dispatchEvent(changeEvent);
assert.deepEqual(component.test, ['b']);
input2.checked = false;
await input2.dispatchEvent(changeEvent);
assert.deepEqual(component.test, []);
input1.checked = true;
input2.checked = true;
await input1.dispatchEvent(changeEvent);
await input2.dispatchEvent(changeEvent);
assert.deepEqual(component.test, ['b', 'a']);
await button.dispatchEvent(clickEvent);
assert.deepEqual(component.test, ['b', 'a']); // should it be ['a'] only? valid arguments for both outcomes
input1.checked = false;
await input1.dispatchEvent(changeEvent);
assert.deepEqual(component.test, []);
}
};

@ -0,0 +1,14 @@
<script>
export let test = [];
let hidden = false
</script>
<button on:click={() => hidden = !hidden}>
{hidden ? "show" : "hide"} b
</button>
<label>a <input type="checkbox" bind:group={test} value="a" /></label>
{#if !hidden}
<label>b <input type="checkbox" bind:group={test} value="b" /></label>
{/if}
<label>c <input value="just here, so b is not the last input" /></label>

@ -0,0 +1,34 @@
export default {
async test({ assert, target, component, window }) {
const button = target.querySelector('button');
const clickEvent = new window.Event('click');
const changeEvent = new window.Event('change');
const [input1, input2] = target.querySelectorAll('input[type="radio"]');
function validate_inputs(v1, v2) {
assert.equal(input1.checked, v1);
assert.equal(input2.checked, v2);
}
component.test = 'a';
validate_inputs(true, false);
component.test = 'b';
validate_inputs(false, true);
input1.checked = true;
await input1.dispatchEvent(changeEvent);
assert.deepEqual(component.test, 'a');
input2.checked = true;
await input2.dispatchEvent(changeEvent);
assert.deepEqual(component.test, 'b');
await button.dispatchEvent(clickEvent);
assert.deepEqual(component.test, 'b'); // should it be undefined? valid arguments for both outcomes
input1.checked = true;
await input1.dispatchEvent(changeEvent);
assert.deepEqual(component.test, 'a');
}
};

@ -0,0 +1,14 @@
<script>
export let test;
let hidden = false
</script>
<button on:click={() => hidden = !hidden}>
{hidden ? "show" : "hide"} b
</button>
<label>a <input type="radio" bind:group={test} value="a" /></label>
{#if !hidden}
<label>b <input type="radio" bind:group={test} value="b" /></label>
{/if}
<label>c <input value="just here, so b is not the last input" /></label>

@ -0,0 +1,25 @@
export default {
skip_if_ssr: true, // TODO would be nice to fix this in SSR as well
html: `
<p>selected: b</p>
<select>
<option value='a'>a</option>
<option value='b'>b</option>
<option value='c'>c</option>
</select>
<p>selected: b</p>
`,
test({ assert, component, target }) {
assert.equal(component.selected, 'b');
const select = target.querySelector('select');
const options = [...target.querySelectorAll('option')];
// option with selected attribute should be selected
assert.equal(select.value, 'b');
assert.ok(options[1].selected);
}
};

@ -0,0 +1,13 @@
<script>
export let selected;
</script>
<p>selected: {selected}</p>
<select bind:value={selected}>
<option>a</option>
<option selected>b</option>
<option>c</option>
</select>
<p>selected: {selected}</p>

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

Loading…
Cancel
Save