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
## 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:
* `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

@ -197,6 +197,9 @@ export interface DOMAttributes<T extends EventTarget> {
'on:message'?: MessageEventHandler<T> | undefined | null;
'on:messageerror'?: MessageEventHandler<T> | undefined | null;
// Document Events
'on:visibilitychange'?: EventHandler<Event, T> | undefined | null;
// Global Events
'on:cancel'?: 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;
/**
* 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;
@ -866,9 +873,9 @@ export interface HTMLMediaAttributes<T extends HTMLMediaElement> extends HTMLAtt
}
export interface HTMLMetaAttributes extends HTMLAttributes<HTMLMetaElement> {
charSet?: string | undefined | null;
charset?: string | undefined | null;
content?: string | undefined | null;
httpequiv?: string | undefined | null;
'http-equiv'?: string | undefined | null;
name?: string | undefined | null;
media?: string | undefined | null;
}
@ -1579,6 +1586,7 @@ export interface SvelteHTMLElements {
// Svelte specific
'svelte:window': SvelteWindowAttributes;
'svelte:document': HTMLAttributes<Document>;
'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string };
'svelte:options': { [name: string]: any };

@ -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.

1330
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "3.55.1",
"version": "3.58.0",
"description": "Cybernetically enhanced web apps",
"module": "index.mjs",
"main": "index",
@ -122,41 +122,41 @@
"@ampproject/remapping": "^0.3.0",
"@jridgewell/sourcemap-codec": "^1.4.14",
"@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-replace": "^2.3.0",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-sucrase": "^3.1.0",
"@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",
"@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.22.0",
"@typescript-eslint/parser": "^5.22.0",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"acorn": "^8.8.1",
"agadoo": "^2.0.0",
"aria-query": "^5.1.1",
"agadoo": "^3.0.0",
"aria-query": "^5.1.3",
"axobject-query": "^3.1.1",
"code-red": "^0.2.5",
"css-tree": "^2.2.1",
"eslint": "^8.26.0",
"eslint-plugin-import": "^2.26.0",
"code-red": "^1.0.0",
"css-tree": "^2.3.1",
"eslint": "^8.35.0",
"eslint-plugin-import": "^2.27.0",
"eslint-plugin-svelte3": "^4.0.0",
"estree-walker": "^3.0.1",
"is-reference": "^3.0.0",
"estree-walker": "^3.0.3",
"is-reference": "^3.0.1",
"jsdom": "^15.2.1",
"kleur": "^4.1.5",
"locate-character": "^2.0.5",
"magic-string": "^0.25.3",
"magic-string": "^0.30.0",
"mocha": "^7.0.0",
"periscopic": "^3.0.4",
"periscopic": "^3.1.0",
"puppeteer": "^2.0.0",
"rollup": "^1.27.14",
"source-map": "^0.7.4",
"source-map-support": "^0.5.21",
"tiny-glob": "^0.2.9",
"tslib": "^2.4.1",
"tslib": "^2.5.0",
"typescript": "^3.7.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
<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

@ -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`
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`
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
---
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

@ -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 { 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';
import Tag from './nodes/shared/Tag';
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 (
node.type === 'LabeledStatement' &&
node.label.name === '$' &&
@ -1396,11 +1395,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);
}
@ -1424,19 +1424,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(', ')}`
@ -162,6 +166,10 @@ export default {
code: 'a11y-img-redundant-alt',
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: {
code: 'a11y-label-has-associated-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) => ({
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'
}
};

@ -76,7 +76,7 @@ export default class Selector {
this.blocks.forEach((block, i) => {
if (i > 0) {
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 === '*') {
code.overwrite(selector.start, selector.end, attr);
code.update(selector.start, selector.end, attr);
} else {
code.appendLeft(selector.end, attr);
}

@ -35,7 +35,7 @@ function minify_declarations(
declarations.forEach((declaration, i) => {
const separator = i > 0 ? ';' : '';
if ((declaration.node.start - c) > separator.length) {
code.overwrite(c, declaration.node.start, separator);
code.update(c, declaration.node.start, separator);
}
declaration.minify(code);
c = declaration.node.end;
@ -75,7 +75,7 @@ class Rule {
if (selector.used) {
const separator = started ? ',' : '';
if ((selector.node.start - c) > separator.length) {
code.overwrite(c, selector.node.start, separator);
code.update(c, selector.node.start, separator);
}
selector.minify(code);
@ -133,7 +133,7 @@ class Declaration {
if (block.type === 'Identifier') {
const name = block.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;
if (start - c > 1) {
code.overwrite(c, start, ':');
code.update(c, start, ':');
}
}
}
@ -173,7 +173,7 @@ class Atrule {
}
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 => {
child.apply(node);
});
@ -204,7 +204,7 @@ class Atrule {
code.remove(c, this.node.block.start);
} else if (this.node.name === 'supports') {
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) => {
// TODO minify queries
c = query.end;
@ -213,7 +213,7 @@ class Atrule {
} else {
let c = this.node.start + this.node.name.length + 1;
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;
}
if (this.node.block && this.node.block.start - c > 0) {
@ -255,7 +255,7 @@ class Atrule {
});
});
} 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();
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);
}
}
}

@ -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 });
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';
@ -24,15 +25,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, 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_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'],
@ -76,6 +76,33 @@ const a11y_labelable = new Set([
'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([
['header', 'banner'],
['footer', 'contentinfo']
@ -146,6 +173,35 @@ const input_type_to_implicit_role = new Map([
['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']);
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();
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);
@ -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
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) && !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
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 &&
@ -684,7 +759,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);
@ -693,7 +768,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)));
@ -1028,14 +1103,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: Identifier;
key: Expression | PrivateIdentifier;
}
interface DestructuredVariable {
type: 'DestructuredVariable'
key: Identifier;
name?: string;
modifier: (node: Node) => Node;
@ -25,8 +34,8 @@ export function unpack_destructuring({
}: {
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>;
@ -35,12 +44,14 @@ export function unpack_destructuring({
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
@ -108,12 +119,38 @@ export function unpack_destructuring({
context_rest_properties
});
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 = 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') {
// e.g. { property = default } or { property: newName = default }
const n = contexts.length;
mark_referenced(value.right, scope, component);
@ -121,7 +158,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,
@ -134,10 +171,11 @@ export function unpack_destructuring({
context_rest_properties
});
} 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,
@ -157,7 +195,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`);
}

@ -382,6 +382,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;

@ -1,4 +1,4 @@
import Renderer from './Renderer';
import Renderer, { BindingGroup } from './Renderer';
import Wrapper from './wrappers/shared/Wrapper';
import { b, x } from 'code-red';
import { Node, Identifier, ArrayPattern } from 'estree';
@ -40,6 +40,7 @@ export default class Block {
bindings: Map<string, Bindings>;
binding_group_initialised: Set<string> = new Set();
binding_groups: Set<BindingGroup> = new Set();
chunks: {
declarations: Array<Node | Node[]>;
@ -249,6 +250,7 @@ export default class Block {
}
}
this.render_binding_groups();
this.render_listeners();
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[];
}>;
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 {
component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions;
@ -33,7 +42,7 @@ export default class Renderer {
blocks: Array<Block | Node | Node[]> = [];
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
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;
fragment: FragmentWrapper;
@ -64,10 +73,6 @@ export default class Renderer {
this.add_to_context('#slots');
}
if (this.binding_groups.size > 0) {
this.add_to_context('$$binding_groups');
}
// main block
this.block = new Block({
renderer: this,

@ -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,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 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;
@ -84,6 +85,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);
@ -145,6 +147,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,
@ -359,7 +362,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;`);

@ -82,6 +82,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
if (node.name === 'value') {
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) {
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'] },
inert: {},
ismap: { property_name: 'isMap', applies_to: ['img'] },
itemscope: {},
loop: { applies_to: ['audio', 'bgsound', 'video'] },
multiple: { applies_to: ['input', 'select'] },
muted: { applies_to: ['audio', 'video'] },

@ -5,7 +5,7 @@ import InlineComponentWrapper from '../InlineComponent';
import get_object from '../../../utils/get_object';
import replace_object from '../../../utils/replace_object';
import Block from '../../Block';
import Renderer from '../../Renderer';
import Renderer, { BindingGroup } from '../../Renderer';
import flatten_reference from '../../../utils/flatten_reference';
import { Node, Identifier } from 'estree';
import add_to_set from '../../../utils/add_to_set';
@ -26,6 +26,7 @@ export default class BindingWrapper {
snippet: Node;
is_readonly: boolean;
needs_lock: boolean;
binding_group: BindingGroup;
constructor(block: Block, node: Binding, parent: ElementWrapper | InlineComponentWrapper) {
this.node = node;
@ -45,6 +46,10 @@ export default class BindingWrapper {
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
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;
}
@ -105,6 +114,7 @@ export default class BindingWrapper {
const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : [];
const mount_conditions: any[] = [];
let update_or_condition: any = null;
const dependency_array = Array.from(this.get_dependencies());
@ -135,40 +145,19 @@ 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':
{
const { binding_group, is_context, contexts, index, keypath } = get_binding_group(parent.renderer, this.node, block);
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 (contexts.length > 1) {
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);
if ((this.parent as ElementWrapper).has_dynamic_value) {
update_or_condition = (this.parent as ElementWrapper).dynamic_value_condition;
}
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;
}
@ -177,6 +166,11 @@ export default class BindingWrapper {
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`);
mount_conditions.push(x`${this.snippet} !== void 0`);
@ -214,7 +208,8 @@ export default class BindingWrapper {
if (update_dom) {
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`
if (${condition}) {
@ -244,7 +239,8 @@ export default class BindingWrapper {
function get_dom_updater(
element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper
binding: BindingWrapper,
mounting: boolean
) {
const { node } = element;
@ -259,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})`;
}
@ -279,7 +276,8 @@ function get_dom_updater(
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);
let keypath = parts.join('.');
@ -314,41 +312,84 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
contexts.push(name);
}
// create a global binding_group across blocks
if (!renderer.binding_groups.has(keypath)) {
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 => {
renderer.add_to_context(context, true);
});
renderer.binding_groups.set(keypath, {
binding_group: (to_reference: boolean = false) => {
let binding_group = '$$binding_groups';
let _secondary_indexes = contexts;
if (to_reference) {
binding_group = block.renderer.reference(binding_group);
_secondary_indexes = _secondary_indexes.map(name => block.renderer.reference(name));
}
binding_group: () => {
let obj = x`$$binding_groups[${index}]`;
if (_secondary_indexes.length > 0) {
let obj = x`${binding_group}[${index}]`;
_secondary_indexes.forEach(secondary_index => {
if (contexts.length > 0) {
contexts.forEach(secondary_index => {
obj = x`${obj}[${secondary_index}]`;
});
return obj;
} else {
return x`${binding_group}[${index}]`;
}
return obj;
},
is_context: contexts.length > 0,
contexts,
index,
keypath
list_dependencies,
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(
@ -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`
${lhs} = ${value};
@ -402,10 +443,9 @@ function get_event_handler(
}
function get_value_from_dom(
renderer: Renderer,
_renderer: Renderer,
element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper,
block: Block,
contextual_dependencies: Set<string>
) {
const { node } = element;
@ -415,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)` :
@ -427,7 +467,7 @@ function get_value_from_dom(
// <input type='checkbox' bind:group='foo'>
if (name === 'group') {
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);
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 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,9 +161,12 @@ 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>;
has_dynamic_value: boolean;
dynamic_value_condition: any;
var: any;
void: boolean;
@ -170,6 +174,8 @@ export default class ElementWrapper extends Wrapper {
child_dynamic_element_block?: Block = null;
child_dynamic_element?: ElementWrapper = null;
element_data_name = null;
constructor(
renderer: Renderer,
block: Block,
@ -212,6 +218,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 => {
@ -263,6 +271,8 @@ export default class ElementWrapper extends Wrapper {
});
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) {
@ -509,7 +519,8 @@ export default class ElementWrapper extends Wrapper {
child.render(
block,
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 = (
this.node.name === 'select' ||
binding_group.bindings.find(binding => {
return (
binding_group.bindings.find(binding => (
binding.node.name === 'indeterminate' ||
binding.node.name === 'textContent' ||
binding.node.name === 'innerHTML' ||
is_name_contenteditable(binding.node.name) ||
binding.is_readonly_media_attribute()
);
})
))
);
if (should_initialise) {
@ -798,11 +806,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());
}
});
@ -818,7 +828,6 @@ export default class ElementWrapper extends Wrapper {
add_spread_attributes(block: Block) {
const levels = block.get_unique_name(`${this.var.name}_levels`);
const data = block.get_unique_name(`${this.var.name}_data`);
const initial_props = [];
const updates = [];
@ -849,9 +858,9 @@ export default class ElementWrapper extends Wrapper {
block.chunks.init.push(b`
let ${levels} = [${initial_props}];
let ${data} = {};
let ${this.element_data_name} = {};
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`;
block.chunks.hydrate.push(
b`${fn}(${this.var}, ${data});`
b`${fn}(${this.var}, ${this.element_data_name});`
);
if (this.has_dynamic_attribute) {
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}
]));
`);
@ -884,23 +893,23 @@ export default class ElementWrapper extends Wrapper {
}
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`
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')) {
const type = this.node.get_static_attribute_value('type');
if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') {
block.chunks.mount.push(b`
if ('value' in ${data}) {
${this.var}.value = ${data}.value;
if ('value' in ${this.element_data_name}) {
${this.var}.value = ${this.element_data_name}.value;
}
`);
block.chunks.update.push(b`
if ('value' in ${data}) {
${this.var}.value = ${data}.value;
if ('value' in ${this.element_data_name}) {
${this.var}.value = ${this.element_data_name}.value;
}
`);
}
@ -959,6 +968,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);
});
@ -1004,6 +1014,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();
@ -1167,8 +1178,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;
@ -1181,24 +1202,39 @@ 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}
condition = x`${condition} && ${cached_snippet} !== (${cached_snippet} = ${snippet})`;
}
`);
} 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`
if (${block.renderer.dirty(dependencies)}) {
if (${condition}) {
${updater}
}
`);
}
}
});
}

@ -2,8 +2,9 @@ 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 Element from './Element';
import Head from './Head';
import IfBlock from './IfBlock';
import KeyBlock from './KeyBlock';
@ -13,6 +14,7 @@ import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import SlotTemplate from './SlotTemplate';
import Text from './Text';
import Comment from './Comment';
import Title from './Title';
import Window from './Window';
import { INode } from '../../nodes/interfaces';
@ -26,8 +28,9 @@ import { regex_starts_with_whitespace } from '../../../utils/patterns';
const wrappers = {
AwaitBlock,
Body,
Comment: null,
Comment,
DebugTag,
Document,
EachBlock,
Element,
Head,
@ -116,7 +119,7 @@ export default class FragmentWrapper {
link(last_child, last_child = wrapper);
} else {
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);
this.nodes.unshift(wrapper);

@ -5,7 +5,9 @@ import Wrapper from './shared/Wrapper';
import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag';
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 {
var: Identifier = { type: 'Identifier', name: 't' };
@ -14,10 +16,40 @@ export default class MustacheTagWrapper extends Tag {
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(
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(

@ -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');
}
}

@ -1,15 +1,23 @@
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 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_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;
@ -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[]) {
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';
@ -19,11 +21,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();
@ -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;
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}`;
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?

@ -19,11 +19,17 @@ export function get_class_attribute_value(attribute: Attribute): ESTreeExpressio
export function get_attribute_value(attribute: Attribute): ESTreeExpression {
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
.map((chunk) => {
return chunk.type === 'Text'
? 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}`);
}

@ -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);
}
@ -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';
}
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[] = [];
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 +106,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 +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(
schema: ARIARoleRelationConcept,
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,
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 +178,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
});

@ -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
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 { Parser } from '../index';
import { Node } from 'estree';

@ -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}`;
}

@ -1,6 +1,7 @@
import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr } from './dom';
import { SvelteComponent } from './Component';
import { is_void } from '../../shared/utils/names';
import { contenteditable_truthy_values } from './utils';
export function dispatch_dev<T=any>(type: string, detail?: T) {
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 });
}
export function set_data_dev(text, data) {
export function set_data_dev(text: Text, data: unknown) {
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 });
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) {
@ -149,8 +164,9 @@ export interface SvelteComponentDev {
$destroy(): void;
[accessor: string]: any;
}
export interface ComponentConstructorOptions<Props extends Record<string, any> = Record<string, any>> {
target: Element | ShadowRoot;
target: Element | Document | ShadowRoot;
anchor?: Element;
props?: Props;
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
// at the end of hydration without touching the remaining nodes.
@ -254,6 +254,10 @@ export function empty() {
return text('');
}
export function comment(content: string) {
return document.createComment(content);
}
export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options);
@ -363,6 +367,53 @@ export function get_binding_group_value(group, __value, checked) {
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) {
return value === '' ? null : +value;
}
@ -507,6 +558,19 @@ export function claim_space(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) {
for (let i = start; i < nodes.length; i += 1) {
const node = nodes[i];
@ -538,9 +602,24 @@ export function claim_html_tag(nodes, is_svg: boolean) {
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;
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) {
@ -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) {
const option = select.options[i];
@ -573,8 +652,10 @@ export function select_option(select, value) {
}
}
if (!mounting || value !== undefined) {
select.selectedIndex = -1; // no option should be selected
}
}
export function select_options(select, value) {
for (let i = 0; i < select.options.length; i += 1) {
@ -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) {
const selected_option = select.querySelector(':checked') || first_enabled_option(select);
const selected_option = select.querySelector(':checked');
return selected_option && selected_option.__value;
}

@ -1,4 +1,5 @@
import { transition_in, transition_out } from './transitions';
import { run_all } from './utils';
export function destroy_block(block, lookup) {
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_lookup = new Map();
const deltas = new Map();
const updates = [];
i = n;
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.c();
} 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);
@ -99,6 +102,8 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
while (n) insert(new_blocks[n - 1]);
run_all(updates);
return new_blocks;
}

@ -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,10 @@ 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'];
}
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;
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)}`
};
}

@ -13,7 +13,6 @@ const _boolean_attributes = [
'hidden',
'inert',
'ismap',
'itemscope',
'loop',
'multiple',
'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;
}
}
@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>

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

Loading…
Cancel
Save