diff --git a/elements/index.d.ts b/elements/index.d.ts index c2fe47f848..00450c748d 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 4d85c2b0cf..5436c6d608 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 303506222f..f655554c81 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 90c706b669..90e2b1ab3e 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 e0e60b1eb4..5ec46dda86 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 0000000000..154ec0445a --- /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 0000000000..5b00199821 --- /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 0000000000..b6a64f0cf3 --- /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 0000000000..8b268d27e6 --- /dev/null +++ b/test/runtime/samples/document-binding-pip/main.svelte @@ -0,0 +1,7 @@ + + + + +
\ No newline at end of file