From 43ab54bc4ed4a389ecc6de67526cd9bc10aede4a Mon Sep 17 00:00:00 2001 From: ThaUnknown <6506529+ThaUnknown@users.noreply.github.com> Date: Sun, 16 Apr 2023 22:21:28 +0200 Subject: [PATCH 1/3] feat: document binding fullscreenElement and visibilityState binding --- elements/index.d.ts | 7 +- site/content/docs/03-template-syntax.md | 12 ++++ src/compiler/compile/nodes/Document.ts | 19 +++++ .../compile/render_dom/wrappers/Document.ts | 69 ++++++++++++++++++- 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/elements/index.d.ts b/elements/index.d.ts index ac32ae94c312..c2fe47f84859 100644 --- a/elements/index.d.ts +++ b/elements/index.d.ts @@ -1078,6 +1078,11 @@ export interface SvelteMediaTimeRange { end: number; } +export interface SvelteDocumentAttributes extends HTMLAttributes { + readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null; + readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null; +} + export interface SvelteWindowAttributes extends HTMLAttributes { readonly 'bind:innerWidth'?: Window['innerWidth'] | undefined | null; readonly 'bind:innerHeight'?: Window['innerHeight'] | undefined | null; @@ -1591,7 +1596,7 @@ export interface SvelteHTMLElements { // Svelte specific 'svelte:window': SvelteWindowAttributes; - 'svelte:document': HTMLAttributes; + 'svelte:document': SvelteDocumentAttributes; 'svelte:body': HTMLAttributes; 'svelte:fragment': { slot?: string }; 'svelte:options': { [name: string]: any }; diff --git a/site/content/docs/03-template-syntax.md b/site/content/docs/03-template-syntax.md index 170c303b8524..4d85c2b0cf4d 100644 --- a/site/content/docs/03-template-syntax.md +++ b/site/content/docs/03-template-syntax.md @@ -1756,6 +1756,9 @@ All except `scrollX` and `scrollY` are readonly. ```sv ``` +```sv + +``` --- @@ -1770,6 +1773,15 @@ As with ``, this element may only appear the top level of your co /> ``` +--- + +You can also bind to the following properties: + +* `visibilityState` +* `fullscreenElement` + +All except are readonly. + ### `` ```sv diff --git a/src/compiler/compile/nodes/Document.ts b/src/compiler/compile/nodes/Document.ts index 653ccb627bf1..90c706b669f8 100644 --- a/src/compiler/compile/nodes/Document.ts +++ b/src/compiler/compile/nodes/Document.ts @@ -1,14 +1,23 @@ import Node from './shared/Node'; +import Binding from './Binding'; import EventHandler from './EventHandler'; +import fuzzymatch from '../../utils/fuzzymatch'; import Action from './Action'; import Component from '../Component'; import TemplateScope from './shared/TemplateScope'; import { Element } from '../../interfaces'; import compiler_warnings from '../compiler_warnings'; +import compiler_errors from '../compiler_errors'; + +const valid_bindings = [ + 'fullscreenElement', + 'visibilityState' +]; export default class Document extends Node { type: 'Document'; handlers: EventHandler[] = []; + bindings: Binding[] = []; actions: Action[] = []; constructor(component: Component, parent: Node, scope: TemplateScope, info: Element) { @@ -17,6 +26,16 @@ export default class Document extends Node { info.attributes.forEach((node) => { if (node.type === 'EventHandler') { this.handlers.push(new EventHandler(component, this, scope, node)); + } else if (node.type === 'Binding') { + if (!~valid_bindings.indexOf(node.name)) { + if (fuzzymatch(node.name, valid_bindings)) { + return component.error(node, compiler_errors.invalid_binding_on(node.name, '', ` (did you mean '${match}'?)`)); + } else { + return component.error(node, compiler_errors.invalid_binding_on(node.name, '', ` — valid bindings are ${list(valid_bindings)}`)); + } + } + + this.bindings.push(new Binding(component, this, scope, node)); } else if (node.type === 'Action') { this.actions.push(new Action(component, this, scope, node)); } else { diff --git a/src/compiler/compile/render_dom/wrappers/Document.ts b/src/compiler/compile/render_dom/wrappers/Document.ts index 4f7c86c54f91..e0e60b1eb425 100644 --- a/src/compiler/compile/render_dom/wrappers/Document.ts +++ b/src/compiler/compile/render_dom/wrappers/Document.ts @@ -1,6 +1,6 @@ import Block from '../Block'; import Wrapper from './shared/Wrapper'; -import { x } from 'code-red'; +import { b, x } from 'code-red'; import Document from '../../nodes/Document'; import { Identifier } from 'estree'; import EventHandler from './Element/EventHandler'; @@ -9,6 +9,16 @@ import { TemplateNode } from '../../../interfaces'; import Renderer from '../Renderer'; import add_actions from './shared/add_actions'; +const associated_events = { + fullscreenElement: 'fullscreenchange', + visibilityState: 'visibilitychange' +}; + +const readonly = new Set([ + 'fullscreenElement', + 'visibilityState' +]); + export default class DocumentWrapper extends Wrapper { node: Document; handlers: EventHandler[]; @@ -19,7 +29,64 @@ export default class DocumentWrapper extends Wrapper { } render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { + const { renderer } = this; + const { component } = renderer; + + const events: Record> = {}; + const bindings: Record = {}; + add_event_handlers(block, x`@_document`, this.handlers); add_actions(block, x`@_document`, this.node.actions); + + this.node.bindings.forEach(binding => { + // TODO: what if it's a MemberExpression? + const binding_name = (binding.expression.node as Identifier).name; + + // in dev mode, throw if read-only values are written to + if (readonly.has(binding.name)) { + renderer.readonly.add(binding_name); + } + + bindings[binding.name] = binding_name; + + const associated_event = associated_events[binding.name]; + const property = binding.name; + + if (!events[associated_event]) events[associated_event] = []; + events[associated_event].push({ + name: binding_name, + value: property + }); + }); + + Object.keys(events).forEach(event => { + const id = block.get_unique_name(`ondocument${event}`); + const props = events[event]; + + renderer.add_to_context(id.name); + const fn = renderer.reference(id.name); + + props.forEach(prop => { + renderer.meta_bindings.push( + b`this._state.${prop.name} = @_document.${prop.value};` + ); + }); + + block.event_listeners.push(x` + @listen(@_document, "${event}", ${fn}) + `); + + component.partly_hoisted.push(b` + function ${id}() { + ${props.map(prop => renderer.invalidate(prop.name, x`${prop.name} = @_document.${prop.value}`))} + } + `); + + block.chunks.init.push(b` + @add_render_callback(${fn}); + `); + + component.has_reactive_assignments = true; + }); } } From 17a3d509be8287ca9c9c70294564e627757cab41 Mon Sep 17 00:00:00 2001 From: ThaUnknown <6506529+ThaUnknown@users.noreply.github.com> Date: Mon, 17 Apr 2023 19:54:54 +0200 Subject: [PATCH 2/3] feat: PiPelement binding fix: undefined properties feat: tests --- elements/index.d.ts | 1 + site/content/docs/03-template-syntax.md | 5 ++- src/compiler/compile/nodes/Binding.ts | 3 +- src/compiler/compile/nodes/Document.ts | 5 ++- .../compile/render_dom/wrappers/Document.ts | 18 +++++--- .../document-binding-fullscreen/_config.js | 31 +++++++++++++ .../document-binding-fullscreen/main.svelte | 7 +++ .../samples/document-binding-pip/_config.js | 44 +++++++++++++++++++ .../samples/document-binding-pip/main.svelte | 7 +++ 9 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 test/runtime/samples/document-binding-fullscreen/_config.js create mode 100644 test/runtime/samples/document-binding-fullscreen/main.svelte create mode 100644 test/runtime/samples/document-binding-pip/_config.js create mode 100644 test/runtime/samples/document-binding-pip/main.svelte diff --git a/elements/index.d.ts b/elements/index.d.ts index c2fe47f84859..00450c748ddb 100644 --- a/elements/index.d.ts +++ b/elements/index.d.ts @@ -1081,6 +1081,7 @@ export interface SvelteMediaTimeRange { export interface SvelteDocumentAttributes extends HTMLAttributes { readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null; readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null; + readonly 'bind:pictureInPictureElement'?: Document['pictureInPictureElement'] | undefined | null; } export interface SvelteWindowAttributes extends HTMLAttributes { diff --git a/site/content/docs/03-template-syntax.md b/site/content/docs/03-template-syntax.md index 4d85c2b0cf4d..5436c6d60852 100644 --- a/site/content/docs/03-template-syntax.md +++ b/site/content/docs/03-template-syntax.md @@ -1777,10 +1777,11 @@ As with ``, this element may only appear the top level of your co You can also bind to the following properties: -* `visibilityState` * `fullscreenElement` +* `pictureInPictureElement` +* `visibilityState` -All except are readonly. +All are readonly. ### `` diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index 303506222fca..f655554c817f 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -9,6 +9,7 @@ import { TemplateNode } from '../../interfaces'; import Element from './Element'; import InlineComponent from './InlineComponent'; import Window from './Window'; +import Document from './Document'; import { clone } from '../../utils/clone'; import compiler_errors from '../compiler_errors'; import compiler_warnings from '../compiler_warnings'; @@ -36,7 +37,7 @@ export default class Binding extends Node { is_contextual: boolean; is_readonly: boolean; - constructor(component: Component, parent: Element | InlineComponent | Window, scope: TemplateScope, info: TemplateNode) { + constructor(component: Component, parent: Element | InlineComponent | Window | Document, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') { diff --git a/src/compiler/compile/nodes/Document.ts b/src/compiler/compile/nodes/Document.ts index 90c706b669f8..90e2b1ab3ec4 100644 --- a/src/compiler/compile/nodes/Document.ts +++ b/src/compiler/compile/nodes/Document.ts @@ -4,6 +4,7 @@ import EventHandler from './EventHandler'; import fuzzymatch from '../../utils/fuzzymatch'; import Action from './Action'; import Component from '../Component'; +import list from '../../utils/list'; import TemplateScope from './shared/TemplateScope'; import { Element } from '../../interfaces'; import compiler_warnings from '../compiler_warnings'; @@ -11,6 +12,7 @@ import compiler_errors from '../compiler_errors'; const valid_bindings = [ 'fullscreenElement', + 'pictureInPictureElement', 'visibilityState' ]; @@ -28,7 +30,8 @@ export default class Document extends Node { this.handlers.push(new EventHandler(component, this, scope, node)); } else if (node.type === 'Binding') { if (!~valid_bindings.indexOf(node.name)) { - if (fuzzymatch(node.name, valid_bindings)) { + const match = fuzzymatch(node.name, valid_bindings); + if (match) { return component.error(node, compiler_errors.invalid_binding_on(node.name, '', ` (did you mean '${match}'?)`)); } else { return component.error(node, compiler_errors.invalid_binding_on(node.name, '', ` — valid bindings are ${list(valid_bindings)}`)); diff --git a/src/compiler/compile/render_dom/wrappers/Document.ts b/src/compiler/compile/render_dom/wrappers/Document.ts index e0e60b1eb425..5ec46dda868f 100644 --- a/src/compiler/compile/render_dom/wrappers/Document.ts +++ b/src/compiler/compile/render_dom/wrappers/Document.ts @@ -10,12 +10,14 @@ import Renderer from '../Renderer'; import add_actions from './shared/add_actions'; const associated_events = { - fullscreenElement: 'fullscreenchange', - visibilityState: 'visibilitychange' + fullscreenElement: ['fullscreenchange'], + pictureInPictureElement: ['enterpictureinpicture', 'leavepictureinpicture'], + visibilityState: ['visibilitychange'] }; const readonly = new Set([ 'fullscreenElement', + 'pictureInPictureElement', 'visibilityState' ]); @@ -49,13 +51,15 @@ export default class DocumentWrapper extends Wrapper { bindings[binding.name] = binding_name; - const associated_event = associated_events[binding.name]; + const binding_events = associated_events[binding.name]; const property = binding.name; - if (!events[associated_event]) events[associated_event] = []; - events[associated_event].push({ - name: binding_name, - value: property + binding_events.forEach(associated_event => { + if (!events[associated_event]) events[associated_event] = []; + events[associated_event].push({ + name: binding_name, + value: property + }); }); }); diff --git a/test/runtime/samples/document-binding-fullscreen/_config.js b/test/runtime/samples/document-binding-fullscreen/_config.js new file mode 100644 index 000000000000..154ec0445ac5 --- /dev/null +++ b/test/runtime/samples/document-binding-fullscreen/_config.js @@ -0,0 +1,31 @@ +export default { + before_test() { + Object.defineProperties(window.document, { + fullscreenElement: { + value: null, + configurable: true + } + }); + }, + + // copied from window-binding + // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason + skip_if_ssr: true, + + async test({ assert, target, window, component }) { + const event = new window.Event('fullscreenchange'); + + const div = target.querySelector('div'); + + Object.defineProperties(window.document, { + fullscreenElement: { + value: div, + configurable: true + } + }); + + window.document.dispatchEvent(event); + + assert.equal(component.fullscreen, div); + } +}; diff --git a/test/runtime/samples/document-binding-fullscreen/main.svelte b/test/runtime/samples/document-binding-fullscreen/main.svelte new file mode 100644 index 000000000000..5b001998211e --- /dev/null +++ b/test/runtime/samples/document-binding-fullscreen/main.svelte @@ -0,0 +1,7 @@ + + + + +
\ No newline at end of file diff --git a/test/runtime/samples/document-binding-pip/_config.js b/test/runtime/samples/document-binding-pip/_config.js new file mode 100644 index 000000000000..b6a64f0cf39e --- /dev/null +++ b/test/runtime/samples/document-binding-pip/_config.js @@ -0,0 +1,44 @@ +export default { + before_test() { + Object.defineProperties(document, { + pictureInPictureElement: { + value: null, + configurable: true + } + }); + }, + + // copied from window-binding + // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason + skip_if_ssr: true, + + async test({ assert, target, window, component }) { + const enter = new window.Event('enterpictureinpicture'); + + const div = target.querySelector('div'); + + Object.defineProperties(window.document, { + pictureInPictureElement: { + value: div, + configurable: true + } + }); + + window.document.dispatchEvent(enter); + + assert.equal(component.pip, div); + + const leave = new window.Event('leavepictureinpicture'); + + Object.defineProperties(window.document, { + pictureInPictureElement: { + value: null, + configurable: true + } + }); + + window.document.dispatchEvent(leave); + + assert.equal(component.pip, null); + } +}; diff --git a/test/runtime/samples/document-binding-pip/main.svelte b/test/runtime/samples/document-binding-pip/main.svelte new file mode 100644 index 000000000000..8b268d27e602 --- /dev/null +++ b/test/runtime/samples/document-binding-pip/main.svelte @@ -0,0 +1,7 @@ + + + + +
\ No newline at end of file From f351e662f189e88f566eadf7528787a2df6963f8 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 28 Apr 2023 10:02:03 +0200 Subject: [PATCH 3/3] remove pictureInPictureElement --- elements/index.d.ts | 1 - site/content/docs/03-template-syntax.md | 1 - src/compiler/compile/nodes/Document.ts | 1 - .../compile/render_dom/wrappers/Document.ts | 2 - .../samples/document-binding-pip/_config.js | 44 ------------------- .../samples/document-binding-pip/main.svelte | 7 --- 6 files changed, 56 deletions(-) delete mode 100644 test/runtime/samples/document-binding-pip/_config.js delete mode 100644 test/runtime/samples/document-binding-pip/main.svelte diff --git a/elements/index.d.ts b/elements/index.d.ts index 00450c748ddb..c2fe47f84859 100644 --- a/elements/index.d.ts +++ b/elements/index.d.ts @@ -1081,7 +1081,6 @@ export interface SvelteMediaTimeRange { export interface SvelteDocumentAttributes extends HTMLAttributes { readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null; readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null; - readonly 'bind:pictureInPictureElement'?: Document['pictureInPictureElement'] | undefined | null; } export interface SvelteWindowAttributes extends HTMLAttributes { diff --git a/site/content/docs/03-template-syntax.md b/site/content/docs/03-template-syntax.md index 5436c6d60852..21dd265f6c66 100644 --- a/site/content/docs/03-template-syntax.md +++ b/site/content/docs/03-template-syntax.md @@ -1778,7 +1778,6 @@ As with ``, this element may only appear the top level of your co You can also bind to the following properties: * `fullscreenElement` -* `pictureInPictureElement` * `visibilityState` All are readonly. diff --git a/src/compiler/compile/nodes/Document.ts b/src/compiler/compile/nodes/Document.ts index 90e2b1ab3ec4..60264aa40e97 100644 --- a/src/compiler/compile/nodes/Document.ts +++ b/src/compiler/compile/nodes/Document.ts @@ -12,7 +12,6 @@ import compiler_errors from '../compiler_errors'; const valid_bindings = [ 'fullscreenElement', - 'pictureInPictureElement', 'visibilityState' ]; diff --git a/src/compiler/compile/render_dom/wrappers/Document.ts b/src/compiler/compile/render_dom/wrappers/Document.ts index 5ec46dda868f..0a9565e64c7a 100644 --- a/src/compiler/compile/render_dom/wrappers/Document.ts +++ b/src/compiler/compile/render_dom/wrappers/Document.ts @@ -11,13 +11,11 @@ import add_actions from './shared/add_actions'; const associated_events = { fullscreenElement: ['fullscreenchange'], - pictureInPictureElement: ['enterpictureinpicture', 'leavepictureinpicture'], visibilityState: ['visibilitychange'] }; const readonly = new Set([ 'fullscreenElement', - 'pictureInPictureElement', 'visibilityState' ]); diff --git a/test/runtime/samples/document-binding-pip/_config.js b/test/runtime/samples/document-binding-pip/_config.js deleted file mode 100644 index b6a64f0cf39e..000000000000 --- a/test/runtime/samples/document-binding-pip/_config.js +++ /dev/null @@ -1,44 +0,0 @@ -export default { - before_test() { - Object.defineProperties(document, { - pictureInPictureElement: { - value: null, - configurable: true - } - }); - }, - - // copied from window-binding - // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason - skip_if_ssr: true, - - async test({ assert, target, window, component }) { - const enter = new window.Event('enterpictureinpicture'); - - const div = target.querySelector('div'); - - Object.defineProperties(window.document, { - pictureInPictureElement: { - value: div, - configurable: true - } - }); - - window.document.dispatchEvent(enter); - - assert.equal(component.pip, div); - - const leave = new window.Event('leavepictureinpicture'); - - Object.defineProperties(window.document, { - pictureInPictureElement: { - value: null, - configurable: true - } - }); - - window.document.dispatchEvent(leave); - - assert.equal(component.pip, null); - } -}; diff --git a/test/runtime/samples/document-binding-pip/main.svelte b/test/runtime/samples/document-binding-pip/main.svelte deleted file mode 100644 index 8b268d27e602..000000000000 --- a/test/runtime/samples/document-binding-pip/main.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - - -
\ No newline at end of file