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: add back <svelte:document> #7149

Merged
merged 12 commits into from
Mar 15, 2023
Merged
19 changes: 19 additions & 0 deletions site/content/docs/02-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar instead of Similarly (this typo is originally from svelte:body).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds worse to me. Though they're possibly both a bit awkward. Perhaps we should just remove that part and start with "This element..."


`<svelte:document>` also has to appear at the top level of your component.

```sv
<svelte:document
on:visibilitychange={handleVisibilityChange}
use:someAction
/>
```

### `<svelte:body>`

```sv
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/compile/compiler_warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
export default {
custom_element_no_tag: {
code: 'custom-element-no-tag',
message: 'No custom element \'tag\' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>'
message: 'No custom element \'tag\' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>'
},
unused_export_let: (component: string, property: string) => ({
code: 'unused-export-let',
Expand Down Expand Up @@ -143,5 +143,9 @@ export default {
redundant_event_modifier_passive: {
code: 'redundant-event-modifier',
message: 'The passive modifier only works with wheel and touch events'
},
avoid_mouse_events_on_document: {
code: 'avoid-mouse-events-on-document',
message: 'Mouse events on the document are not supported in all browsers and should be avoided'
}
};
37 changes: 37 additions & 0 deletions src/compiler/compile/nodes/Document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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...
}
});

const handlers_map = new Set();

this.handlers.forEach(handler => (
handlers_map.add(handler.name)
));

if (handlers_map.has('mouseenter') || handlers_map.has('mouseleave')) {
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 @@ -27,6 +28,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 @@ -27,6 +27,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 @@ -16,6 +16,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} />