Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: document fullscreenElement, pictureInPictureElement and visibilityState binding #8507

Merged
merged 3 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion elements/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,11 @@ export interface SvelteMediaTimeRange {
end: number;
}

export interface SvelteDocumentAttributes extends HTMLAttributes<Document> {
readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null;
readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null;
}

export interface SvelteWindowAttributes extends HTMLAttributes<Window> {
readonly 'bind:innerWidth'?: Window['innerWidth'] | undefined | null;
readonly 'bind:innerHeight'?: Window['innerHeight'] | undefined | null;
Expand Down Expand Up @@ -1591,7 +1596,7 @@ export interface SvelteHTMLElements {

// Svelte specific
'svelte:window': SvelteWindowAttributes;
'svelte:document': HTMLAttributes<Document>;
'svelte:document': SvelteDocumentAttributes;
'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string };
'svelte:options': { [name: string]: any };
Expand Down
12 changes: 12 additions & 0 deletions site/content/docs/03-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,9 @@ All except `scrollX` and `scrollY` are readonly.
```sv
<svelte:document on:event={handler}/>
```
```sv
<svelte:document bind:prop={value}/>
```

---

Expand All @@ -1770,6 +1773,15 @@ As with `<svelte:window>`, this element may only appear the top level of your co
/>
```

---

You can also bind to the following properties:

* `fullscreenElement`
* `visibilityState`

All are readonly.

### `<svelte:body>`

```sv
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/compile/nodes/Binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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') {
Expand Down
21 changes: 21 additions & 0 deletions src/compiler/compile/nodes/Document.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
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 list from '../../utils/list';
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) {
Expand All @@ -17,6 +27,17 @@ 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)) {
const match = fuzzymatch(node.name, valid_bindings);
if (match) {
return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:document>', ` (did you mean '${match}'?)`));
} else {
return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:document>', ` — 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 {
Expand Down
71 changes: 70 additions & 1 deletion src/compiler/compile/render_dom/wrappers/Document.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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[];
Expand All @@ -19,7 +29,66 @@ export default class DocumentWrapper extends Wrapper {
}

render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
const { renderer } = this;
const { component } = renderer;

const events: Record<string, Array<{ name: string; value: string }>> = {};
const bindings: Record<string, string> = {};

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 binding_events = associated_events[binding.name];
const property = binding.name;

binding_events.forEach(associated_event => {
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;
});
}
}
31 changes: 31 additions & 0 deletions test/runtime/samples/document-binding-fullscreen/_config.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
7 changes: 7 additions & 0 deletions test/runtime/samples/document-binding-fullscreen/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
export let fullscreen;
</script>

<svelte:document bind:fullscreenElement={fullscreen}/>

<div />