Skip to content

Commit

Permalink
feat: add back <svelte:document> (#7149)
Browse files Browse the repository at this point in the history
Closes #3310

---------

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
  • Loading branch information
3 people committed Mar 15, 2023
1 parent c19d088 commit 4b0b471
Show file tree
Hide file tree
Showing 36 changed files with 188 additions and 25 deletions.
7 changes: 1 addition & 6 deletions generate-type-definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 21 additions & 2 deletions site/content/docs/03-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,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
Expand All @@ -1756,7 +1775,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
Expand All @@ -1777,7 +1796,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>
Expand Down
14 changes: 0 additions & 14 deletions site/content/tutorial/16-special-elements/06-svelte-body/text.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
let selection = '';
const handleSelectionChange = (e) => selection = document.getSelection();
</script>

<svelte:body />

<p>Select this text to fire events</p>
<p>Selection: {selection}</p>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions site/content/tutorial/16-special-elements/07-svelte-body/text.md
Original file line number Diff line number Diff line change
@@ -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}
/>
```
6 changes: 5 additions & 1 deletion src/compiler/compile/compiler_warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,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'
}
};
41 changes: 41 additions & 0 deletions src/compiler/compile/nodes/Document.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,6 +48,7 @@ export type INode = Action
| Comment
| ConstTag
| DebugTag
| Document
| EachBlock
| Element
| ElseBlock
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/shared/map_children.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down
25 changes: 25 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Document.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 Head from './Head';
Expand All @@ -28,6 +29,7 @@ const wrappers = {
Body,
Comment: null,
DebugTag,
Document,
EachBlock,
Element,
Head,
Expand Down
1 change: 1 addition & 0 deletions src/compiler/compile/render_ssr/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const handlers: Record<string, Handler> = {
Body: noop,
Comment,
DebugTag,
Document: noop,
EachBlock,
Element,
Head,
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/parse/state/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const meta_tags = new Map([
['svelte:head', 'Head'],
['svelte:options', 'Options'],
['svelte:window', 'Window'],
['svelte:document', 'Document'],
['svelte:body', 'Body']
]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"code": "invalid-tag-name",
"message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:body, svelte:self, svelte:component, svelte:fragment or svelte:element",
"message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:document, svelte:body, svelte:self, svelte:component, svelte:fragment or svelte:element",
"pos": 10,
"start": {
"character": 10,
Expand Down
14 changes: 14 additions & 0 deletions test/runtime/samples/action-document/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
html: '<div></div>',

async test({ assert, target, window }) {
const visibility = new window.Event('visibilitychange');

await window.document.dispatchEvent(visibility);
assert.htmlEqual(target.innerHTML, `
<div>
<div class="tooltip">Perform an Action</div>
</div>
`);
}
};
24 changes: 24 additions & 0 deletions test/runtime/samples/action-document/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script>
let container;
function tooltip(node, text) {
let tooltip = null;
function onVisibilityChange() {
tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.textContent = text;
container.appendChild(tooltip);
}
node.addEventListener('visibilitychange', onVisibilityChange);
return {
destroy() {
node.removeEventListener('visibilitychange', onVisibilityChange);
}
}
}
</script>

<svelte:document use:tooltip="{'Perform an Action'}" />
<div bind:this={container} />

0 comments on commit 4b0b471

Please sign in to comment.