svelte/src/compiler/compile/render_dom/wrappers/Window.ts

190 lines
5.5 KiB

import Renderer from '../Renderer';
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import { b, x } from 'code-red';
import add_event_handlers from './shared/add_event_handlers';
import Window from '../../nodes/Window';
import add_actions from './shared/add_actions';
import { changed } from './shared/changed';
import { Identifier } from 'estree';
import { TemplateNode } from '../../../interfaces';
import EventHandler from './Element/EventHandler';
const associated_events = {
innerWidth: 'resize',
innerHeight: 'resize',
outerWidth: 'resize',
outerHeight: 'resize',
scrollX: 'scroll',
scrollY: 'scroll',
};
const properties = {
scrollX: 'pageXOffset',
scrollY: 'pageYOffset'
};
const readonly = new Set([
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'online',
]);
export default class WindowWrapper extends Wrapper {
node: Window;
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) {
const { renderer } = this;
const { component } = renderer;
const events = {};
const bindings: Record<string, string> = {};
add_actions(component, block, '@_window', this.node.actions);
add_event_handlers(block, '@_window', this.handlers);
this.node.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
renderer.readonly.add(binding.expression.node.name);
}
bindings[binding.name] = binding.expression.node.name;
// bind:online is a special case, we need to listen for two separate events
if (binding.name === 'online') return;
const associated_event = associated_events[binding.name];
const property = properties[binding.name] || binding.name;
if (!events[associated_event]) events[associated_event] = [];
events[associated_event].push({
name: binding.expression.node.name,
value: property
});
});
const scrolling = block.get_unique_name(`scrolling`);
const clear_scrolling = block.get_unique_name(`clear_scrolling`);
const scrolling_timeout = block.get_unique_name(`scrolling_timeout`);
Object.keys(events).forEach(event => {
const id = block.get_unique_name(`onwindow${event}`);
const props = events[event];
if (event === 'scroll') {
// TODO other bidirectional bindings...
block.add_variable(scrolling, x`false`);
block.add_variable(clear_scrolling, x`() => { ${scrolling} = false }`);
block.add_variable(scrolling_timeout);
const condition = bindings.scrollX && bindings.scrollY
? x`"${bindings.scrollX}" in this._state || "${bindings.scrollY}" in this._state`
: x`"${bindings.scrollX || bindings.scrollY}" in this._state`;
const scrollX = bindings.scrollX && x`this._state.${bindings.scrollX}`;
const scrollY = bindings.scrollY && x`this._state.${bindings.scrollY}`;
renderer.meta_bindings.push(b`
if (${condition}) {
@_scrollTo(${scrollX || '@_window.pageXOffset'}, ${scrollY || '@_window.pageYOffset'});
}
${scrollX && `${scrollX} = @_window.pageXOffset;`}
${scrollY && `${scrollY} = @_window.pageYOffset;`}
`);
block.event_listeners.push(x`
@listen(@_window, "${event}", () => {
${scrolling} = true;
@_clearTimeout(${scrolling_timeout});
${scrolling_timeout} = @_setTimeout(${clear_scrolling}, 100);
#ctx.${id}();
})
`);
} else {
props.forEach(prop => {
renderer.meta_bindings.push(
b`this._state.${prop.name} = @_window.${prop.value};`
);
});
block.event_listeners.push(x`
@listen(@_window, "${event}", #ctx.${id})
`);
}
component.add_var({
name: id.name,
internal: true,
referenced: true
});
component.partly_hoisted.push(b`
function ${id}() {
${props.map(prop => component.invalidate(prop.name, x`${prop.name} = @_window.${prop.value}`))}
}
`);
block.chunks.init.push(b`
@add_render_callback(#ctx.${id});
`);
component.has_reactive_assignments = true;
});
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX || bindings.scrollY) {
const condition = changed([bindings.scrollX, bindings.scrollY].filter(Boolean));
const scrollX = bindings.scrollX ? x`#ctx.${bindings.scrollX}` : x`@_window.pageXOffset`;
const scrollY = bindings.scrollY ? x`#ctx.${bindings.scrollY}` : x`@_window.pageYOffset`;
block.chunks.update.push(b`
if (${condition} && !${scrolling}) {
${scrolling} = true;
@_clearTimeout(${scrolling_timeout});
@_scrollTo(${scrollX}, ${scrollY});
${scrolling_timeout} = @_setTimeout(${clear_scrolling}, 100);
}
`);
}
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const id = block.get_unique_name(`onlinestatuschanged`);
const name = bindings.online;
component.add_var({
name: id.name,
internal: true,
referenced: true
});
component.partly_hoisted.push(b`
function ${id}() {
${component.invalidate(name, x`${name} = @_navigator.onLine`)}
}
`);
block.chunks.init.push(b`
@add_render_callback(#ctx.${id});
`);
block.event_listeners.push(
x`@listen(@_window, "online", #ctx.${id})`,
x`@listen(@_window, "offline", #ctx.${id})`
);
component.has_reactive_assignments = true;
}
}
}