merge version-4 branch

pull/8362/head
Ben McCann 3 years ago
commit 7d3b368e6f

@ -46,7 +46,7 @@ jobs:
timeout-minutes: 15
strategy:
matrix:
node-version: [8, 10, 12, 14, 16, 18]
node-version: [14, 16, 18]
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v3
@ -59,16 +59,6 @@ jobs:
id: download-artifact
with:
name: build-assets
- name: Get Node version ${{ runner.os }}
run: echo "NODE_VERSION=`node --version`" >> $GITHUB_ENV
if: runner.os != 'Windows'
- name: Get Node version ${{ runner.os }}
run: |
chcp 65001
echo ("NODE_VERSION=$(node --version)") >> $env:GITHUB_ENV
if: runner.os == 'Windows'
- run: npm install --save-dev puppeteer@13
if: ${{ runner.os == 'Linux' && (!startsWith(env.NODE_VERSION, 'v8.') && !startsWith(env.NODE_VERSION, 'v10.')) }}
- run: npm install
env:
SKIP_PREPARE: true

@ -1,5 +1,10 @@
# Svelte changelog
## Unreleased
* Handle `width`/`height` attributes when spreading ([#6752](https://github.com/sveltejs/svelte/issues/6752))
* Add support for resize observer bindings (`<div bind:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize>`) ([#8022](https://github.com/sveltejs/svelte/pull/8022))
## 3.58.0
* Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311))

@ -546,6 +546,11 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
*/
'bind:innerText'?: string | undefined | null;
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4
readonly 'bind:borderBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4
readonly 'bind:devicePixelContentBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4
// SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null;

1470
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -120,7 +120,7 @@
"homepage": "https://svelte.dev",
"devDependencies": {
"@ampproject/remapping": "^0.3.0",
"@jridgewell/sourcemap-codec": "^1.4.14",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@jridgewell/trace-mapping": "^0.3.17",
"@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-json": "^6.0.0",
@ -152,7 +152,7 @@
"magic-string": "^0.30.0",
"mocha": "^7.0.0",
"periscopic": "^3.1.0",
"puppeteer": "^2.0.0",
"puppeteer": "^19.8.5",
"rollup": "^1.27.14",
"source-map": "^0.7.4",
"source-map-support": "^0.5.21",

@ -286,7 +286,7 @@ You cannot `export default`, since the default export is the component itself.
<script context="module">
let totalComponents = 0;
// this allows an importer to do e.g.
// the export keyword allows this function to imported with e.g.
// `import Example, { alertTotal } from './Example.svelte'`
export function alertTotal() {
alert(totalComponents);

@ -3,7 +3,7 @@ import get_object from '../utils/get_object';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { regex_dimensions } from '../../utils/patterns';
import { regex_dimensions, regex_box_size } from '../../utils/patterns';
import { Node as ESTreeNode } from 'estree';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
@ -92,6 +92,7 @@ export default class Binding extends Node {
this.is_readonly =
regex_dimensions.test(this.name) ||
regex_box_size.test(this.name) ||
(isElement(parent) &&
((parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
(parent.name === 'input' && type === 'file')) /* TODO others? */);

@ -12,7 +12,7 @@ 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 { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import Let from './Let';
@ -1090,7 +1090,10 @@ export default class Element extends Node {
} else if (contenteditable && !contenteditable.is_static) {
return component.error(contenteditable, compiler_errors.dynamic_contenteditable_attribute);
}
} else if (name !== 'this') {
} else if (
name !== 'this' &&
!regex_box_size.test(name)
) {
return component.error(binding, compiler_errors.invalid_binding(binding.name));
}
});

@ -11,6 +11,7 @@ import { Node, Identifier } from 'estree';
import add_to_set from '../../../utils/add_to_set';
import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import handle_select_value_binding from './handle_select_value_binding';
import { regex_box_size } from '../../../../utils/patterns';
export default class BindingWrapper {
node: Binding;
@ -455,7 +456,12 @@ function get_value_from_dom(
return x`$$value`;
}
// <select bind:value='selected'>
// <div bind:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize>
if (regex_box_size.test(name)) {
return x`@ResizeObserverSingleton.entries.get(this)?.${name}`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
x`@select_multiple_value(this)` :

@ -12,7 +12,7 @@ import { namespaces } from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute';
import SpreadAttributeWrapper from './SpreadAttribute';
import { regex_dimensions, regex_starts_with_newline, regex_backslashes } from '../../../../utils/patterns';
import { regex_dimensions, regex_starts_with_newline, regex_backslashes, regex_border_box_size, regex_content_box_size, regex_device_pixel_content_box_size, regex_content_rect } from '../../../../utils/patterns';
import Binding from './Binding';
import add_to_set from '../../../utils/add_to_set';
import { add_event_handler } from '../shared/add_event_handlers';
@ -64,11 +64,29 @@ const events = [
filter: (node: Element, _name: string) =>
node.name === 'input' && node.get_static_attribute_value('type') === 'range'
},
// resize events
{
event_names: ['elementresize'],
filter: (_node: Element, name: string) =>
regex_dimensions.test(name)
},
{
event_names: ['elementresizecontentbox'],
filter: (_node: Element, name: string) =>
regex_content_rect.test(name) ?? regex_content_box_size.test(name)
},
{
event_names: ['elementresizeborderbox'],
filter: (_node: Element, name: string) =>
regex_border_box_size.test(name)
},
{
event_names: ['elementresizedevicepixelcontentbox'],
filter: (_node: Element, name: string) =>
regex_device_pixel_content_box_size.test(name)
},
// media events
{
event_names: ['timeupdate'],
@ -747,14 +765,33 @@ export default class ElementWrapper extends Wrapper {
`);
binding_group.events.forEach(name => {
if (name === 'elementresize') {
// special case
if (['elementresize', 'elementresizecontentbox', 'elementresizeborderbox', 'elementresizedevicepixelcontentbox'].indexOf(name) !== -1) {
const resize_listener = block.get_unique_name(`${this.var.name}_resize_listener`);
block.add_variable(resize_listener);
block.chunks.mount.push(
b`${resize_listener} = @add_resize_listener(${this.var}, ${callee}.bind(${this.var}));`
);
// Can't dynamically do `@fn[name]`, code-red doesn't know how to resolve it
switch (name) {
case 'elementresize':
block.chunks.mount.push(
b`${resize_listener} = @add_iframe_resize_listener(${this.var}, ${callee}.bind(${this.var}));`
);
break;
case 'elementresizecontentbox':
block.chunks.mount.push(
b`${resize_listener} = @resize_observer_content_box.observe(${this.var}, ${callee}.bind(${this.var}));`
);
break;
case 'elementresizeborderbox':
block.chunks.mount.push(
b`${resize_listener} = @resize_observer_border_box.observe(${this.var}, ${callee}.bind(${this.var}));`
);
break;
case 'elementresizedevicepixelcontentbox':
block.chunks.mount.push(
b`${resize_listener} = @resize_observer_device_pixel_content_box.observe(${this.var}, ${callee}.bind(${this.var}));`
);
break;
}
block.chunks.destroy.push(
b`${resize_listener}();`

@ -4,7 +4,7 @@ export const regex_starts_with_whitespace = /^\s/;
export const regex_starts_with_whitespaces = /^[ \t\r\n]*/;
export const regex_ends_with_whitespace = /\s$/;
export const regex_ends_with_whitespaces = /[ \t\r\n]*$/;
export const regex_only_whitespaces = /^\s+$/;
export const regex_only_whitespaces = /^[ \t\n\r\f]+$/;
export const regex_whitespace_characters = /\s/g;
export const regex_non_whitespace_character = /\S/;
@ -22,3 +22,9 @@ export const regex_ends_with_underscore = /_$/;
export const regex_invalid_variable_identifier_characters = /[^a-zA-Z0-9_$]/g;
export const regex_dimensions = /^(?:offset|client)(?:Width|Height)$/;
export const regex_content_rect = /^(?:contentRect)$/;
export const regex_content_box_size = /^(?:contentBoxSize)$/;
export const regex_border_box_size = /^(?:borderBoxSize)$/;
export const regex_device_pixel_content_box_size = /^(?:devicePixelContentBoxSize)$/;
export const regex_box_size = /^(?:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize)$/;

@ -0,0 +1,69 @@
import { globals } from './globals';
/**
* Resize observer singleton.
* One listener per element only!
* https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ
*/
export class ResizeObserverSingleton {
constructor(readonly options?: ResizeObserverOptions) {}
observe(element: Element, listener: Listener) {
this._listeners.set(element, listener);
this._getObserver().observe(element, this.options);
return () => {
this._listeners.delete(element);
this._observer.unobserve(element); // this line can probably be removed
};
}
private readonly _listeners: WeakMap<Element, Listener> = 'WeakMap' in globals ? new WeakMap() : undefined;
private _observer?: ResizeObserver;
private _getObserver() {
return this._observer ?? (this._observer = new ResizeObserver((entries) => {
for (const entry of entries) {
(ResizeObserverSingleton as any).entries.set(entry.target, entry);
this._listeners.get(entry.target)?.(entry);
}
}));
}
}
// Needs to be written like this to pass the tree-shake-test
(ResizeObserverSingleton as any).entries = 'WeakMap' in globals ? new WeakMap() : undefined;
type Listener = (entry: ResizeObserverEntry)=>any;
// TODO: Remove this
interface ResizeObserverSize {
readonly blockSize: number;
readonly inlineSize: number;
}
interface ResizeObserverEntry {
readonly borderBoxSize: readonly ResizeObserverSize[];
readonly contentBoxSize: readonly ResizeObserverSize[];
readonly contentRect: DOMRectReadOnly;
readonly devicePixelContentBoxSize: readonly ResizeObserverSize[];
readonly target: Element;
}
type ResizeObserverBoxOptions = 'border-box' | 'content-box' | 'device-pixel-content-box';
interface ResizeObserverOptions {
box?: ResizeObserverBoxOptions;
}
interface ResizeObserver {
disconnect(): void;
observe(target: Element, options?: ResizeObserverOptions): void;
unobserve(target: Element): void;
}
interface ResizeObserverCallback {
(entries: ResizeObserverEntry[], observer: ResizeObserver): void;
}
declare let ResizeObserver: {
prototype: ResizeObserver;
new(callback: ResizeObserverCallback): ResizeObserver;
};

@ -1,3 +1,4 @@
import { ResizeObserverSingleton } from './ResizeObserverSingleton';
import { contenteditable_truthy_values, has_prop } from './utils';
// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM
@ -306,6 +307,15 @@ export function attr(node: Element, attribute: string, value?: string) {
else if (node.getAttribute(attribute) !== value) node.setAttribute(attribute, value);
}
/**
* List of attributes that should always be set through the attr method,
* because updating them through the property setter doesn't work reliably.
* In the example of `width`/`height`, the problem is that the setter only
* accepts numeric values, but the attribute can also be set to a string like `50%`.
* If this list becomes too big, rethink this approach.
*/
const always_set_through_set_attribute = ['width', 'height'];
export function set_attributes(node: Element & ElementCSSInlineStyle, attributes: { [x: string]: string }) {
// @ts-ignore
const descriptors = Object.getOwnPropertyDescriptors(node.__proto__);
@ -316,7 +326,7 @@ export function set_attributes(node: Element & ElementCSSInlineStyle, attributes
node.style.cssText = attributes[key];
} else if (key === '__value') {
(node as any).value = node[key] = attributes[key];
} else if (descriptors[key] && descriptors[key].set) {
} else if (descriptors[key] && descriptors[key].set && always_set_through_set_attribute.indexOf(key) === -1) {
node[key] = attributes[key];
} else {
attr(node, key, attributes[key]);
@ -689,7 +699,7 @@ export function is_crossorigin() {
return crossorigin;
}
export function add_resize_listener(node: HTMLElement, fn: () => void) {
export function add_iframe_resize_listener(node: HTMLElement, fn: () => void) {
const computed_style = getComputedStyle(node);
if (computed_style.position === 'static') {
@ -737,6 +747,11 @@ export function add_resize_listener(node: HTMLElement, fn: () => void) {
};
}
export const resize_observer_content_box = /* @__PURE__ */ new ResizeObserverSingleton({ box: 'content-box' });
export const resize_observer_border_box = /* @__PURE__ */ new ResizeObserverSingleton({ box: 'border-box' });
export const resize_observer_device_pixel_content_box = /* @__PURE__ */ new ResizeObserverSingleton({ box: 'device-pixel-content-box' });
export { ResizeObserverSingleton };
export function toggle_class(element, name, toggle) {
element.classList[toggle ? 'add' : 'remove'](name);
}

@ -376,8 +376,10 @@ export function create_bidirectional_transition(node: Element & ElementCSSInline
run(b: INTRO | OUTRO) {
if (is_function(config)) {
wait().then(() => {
const opts: TransitionOptions = { direction: b ? 'in' : 'out' };
// @ts-ignore
config = config(options);
config = config(opts);
go(b);
});
} else {

@ -109,11 +109,11 @@ function cleanChildren(node) {
node.removeChild(child);
}
child.data = child.data.replace(/\s+/g, '\n');
child.data = child.data.replace(/[ \t\n\r\f]+/g, '\n');
if (previous && previous.nodeType === 3) {
previous.data += child.data;
previous.data = previous.data.replace(/\s+/g, '\n');
previous.data = previous.data.replace(/[ \t\n\r\f]+/g, '\n');
node.removeChild(child);
child = previous;
@ -130,13 +130,13 @@ function cleanChildren(node) {
// collapse whitespace
if (node.firstChild && node.firstChild.nodeType === 3) {
node.firstChild.data = node.firstChild.data.replace(/^\s+/, '');
if (!node.firstChild.data) node.removeChild(node.firstChild);
node.firstChild.data = node.firstChild.data.replace(/^[ \t\n\r\f]+/, '');
if (!node.firstChild.data.length) node.removeChild(node.firstChild);
}
if (node.lastChild && node.lastChild.nodeType === 3) {
node.lastChild.data = node.lastChild.data.replace(/\s+$/, '');
if (!node.lastChild.data) node.removeChild(node.lastChild);
node.lastChild.data = node.lastChild.data.replace(/[ \t\n\r\f]+$/, '');
if (!node.lastChild.data.length) node.removeChild(node.lastChild);
}
}
@ -145,7 +145,7 @@ export function normalizeHtml(window, html, preserveComments = false) {
const node = window.document.createElement('div');
node.innerHTML = html
.replace(/(<!--.*?-->)/g, preserveComments ? '$1' : '')
.replace(/>[\s\r\n]+</g, '><')
.replace(/>[ \t\n\r\f]+</g, '><')
.trim();
cleanChildren(node);
return node.innerHTML.replace(/<\/?noscript\/?>/g, '');

@ -1,8 +1,8 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
add_iframe_resize_listener,
add_render_callback,
add_resize_listener,
detach,
element,
init,
@ -23,7 +23,7 @@ function create_fragment(ctx) {
},
m(target, anchor) {
insert(target, div, anchor);
div_resize_listener = add_resize_listener(div, /*div_elementresize_handler*/ ctx[2].bind(div));
div_resize_listener = add_iframe_resize_listener(div, /*div_elementresize_handler*/ ctx[2].bind(div));
},
p: noop,
i: noop,

@ -1,8 +1,8 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
add_iframe_resize_listener,
add_render_callback,
add_resize_listener,
detach,
element,
init,
@ -42,7 +42,7 @@ function create_fragment(ctx) {
},
m(target, anchor) {
insert(target, video, anchor);
video_resize_listener = add_resize_listener(video, /*video_elementresize_handler*/ ctx[7].bind(video));
video_resize_listener = add_iframe_resize_listener(video, /*video_elementresize_handler*/ ctx[7].bind(video));
if (!mounted) {
dispose = [

@ -0,0 +1,11 @@
export default {
html: `
<div>&nbsp;</div>
<div>
<span>&nbsp;</span>
</div>
<div>&nbsp;</div>
`
};

@ -0,0 +1,13 @@
<script>
import Component from './Component.svelte'
</script>
<Component>&nbsp;</Component>
<Component>
<span>&nbsp;</span>
</Component>
<Component>
{@html "&nbsp;"}
</Component>

@ -0,0 +1,4 @@
export default {
// https://github.com/sveltejs/svelte/issues/6752
html: '<img height="100%" width="100%" alt="" />'
};

@ -0,0 +1 @@
<img height="100%" width="100%" alt="" {...$$restProps} />

@ -4,16 +4,19 @@ export default {
const div_in = target.querySelector('#in');
const div_out = target.querySelector('#out');
const div_both = target.querySelector('#both');
const div_bothin = target.querySelector('#both-in');
const div_bothout = target.querySelector('#both-out');
assert.equal(div_in.initial, 'in');
assert.equal(div_out.initial, 'out');
assert.equal(div_both.initial, 'both');
assert.equal(div_in.directions, 'in,in');
assert.equal(div_out.directions, 'out');
assert.equal(div_bothin.directions, 'both');
assert.equal(div_bothout.directions, 'both');
return Promise.resolve().then(() => {
assert.equal(div_in.later, 'in');
assert.equal(div_out.later, 'out');
assert.equal(div_both.later, 'both');
assert.equal(div_in.directions, 'in,in');
assert.equal(div_out.directions, 'out,out');
assert.equal(div_bothin.directions, 'both,in');
assert.equal(div_bothout.directions, 'both,out');
});
}
};

@ -2,10 +2,10 @@
export let visible;
function foo(node, _params, options) {
node.initial = options.direction;
node.directions = options.direction;
return (opts) => {
node.later = opts.direction;
node.directions += "," + opts.direction;
return {
duration: 10
@ -15,10 +15,11 @@
</script>
{#if visible}
<div id="both" transition:foo></div>
<div id="in" in:foo></div>
<div id="both-in" transition:foo />
<div id="in" in:foo />
{/if}
{#if !visible}
<div id="out" out:foo></div>
<div id="out" out:foo />
<div id="both-out" transition:foo={{ duration: 500 }} />
{/if}

Loading…
Cancel
Save