diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index 24a8753476..38c5f81ec9 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -14,6 +14,11 @@ class DomGenerator extends Generator { this.renderers = []; this.uses = new Set(); + // initial values for e.g. window.innerWidth, if there's a <:Window> meta tag + this.builders = { + metaBindings: new CodeBuilder() + }; + this.importedComponents = new Map(); } @@ -329,6 +334,10 @@ export default function dom ( parsed, source, options, names ) { `this._state = ${templateProperties.data ? `Object.assign( ${generator.alias( 'template' )}.data(), options.data )` : `options.data || {}`};` ); + if ( !generator.builders.metaBindings.isEmpty() ) { + constructorBlock.addBlock( generator.builders.metaBindings ); + } + if ( templateProperties.computed ) { constructorBlock.addLine( `${generator.alias( 'applyComputations' )}( this._state, this._state, {}, true );` diff --git a/src/generators/dom/visitors/Element.js b/src/generators/dom/visitors/Element.js index 0526e5fc71..478bc39e2e 100644 --- a/src/generators/dom/visitors/Element.js +++ b/src/generators/dom/visitors/Element.js @@ -2,10 +2,20 @@ import CodeBuilder from '../../../utils/CodeBuilder.js'; import deindent from '../../../utils/deindent.js'; import addElementAttributes from './attributes/addElementAttributes.js'; import Component from './Component.js'; +import Window from './meta/Window.js'; + +const meta = { + ':Window': Window +}; export default { enter ( generator, node ) { + if ( node.name in meta ) { + return meta[ node.name ].enter( generator, node ); + } + const isComponent = generator.components.has( node.name ) || node.name === ':Self'; + if ( isComponent ) { return Component.enter( generator, node ); } @@ -100,7 +110,13 @@ export default { }, leave ( generator, node ) { + if ( node.name in meta ) { + if ( meta[ node.name ].leave ) meta[ node.name ].leave( generator, node ); + return; + } + const isComponent = generator.components.has( node.name ); + if ( isComponent ) { return Component.leave( generator, node ); } diff --git a/src/generators/dom/visitors/meta/Window.js b/src/generators/dom/visitors/meta/Window.js new file mode 100644 index 0000000000..e1d5cb7b43 --- /dev/null +++ b/src/generators/dom/visitors/meta/Window.js @@ -0,0 +1,84 @@ +import flattenReference from '../../../../utils/flattenReference.js'; +import deindent from '../../../../utils/deindent.js'; + +const associatedEvents = { + innerWidth: 'resize', + innerHeight: 'resize', + outerWidth: 'resize', + outerHeight: 'resize', + + scrollX: 'scroll', + scrollY: 'scroll' +}; + +export default { + enter ( generator, node ) { + const events = {}; + + node.attributes.forEach( attribute => { + if ( attribute.type === 'EventHandler' ) { + // TODO verify that it's a valid callee (i.e. built-in or declared method) + generator.addSourcemapLocations( attribute.expression ); + + const flattened = flattenReference( attribute.expression.callee ); + if ( flattened.name !== 'event' && flattened.name !== 'this' ) { + // allow event.stopPropagation(), this.select() etc + generator.code.prependRight( attribute.expression.start, 'component.' ); + } + + const handlerName = generator.current.getUniqueName( `onwindow${attribute.name}` ); + + generator.current.builders.init.addBlock( deindent` + var ${handlerName} = function ( event ) { + [✂${attribute.expression.start}-${attribute.expression.end}✂]; + }; + window.addEventListener( '${attribute.name}', ${handlerName} ); + ` ); + + generator.current.builders.teardown.addBlock( deindent` + window.removeEventListener( '${attribute.name}', ${handlerName} ); + ` ); + } + + if ( attribute.type === 'Binding' ) { + const associatedEvent = associatedEvents[ attribute.name ]; + + if ( !associatedEvent ) { + throw new Error( `Cannot bind to ${attribute.name} on <:Window>` ); + } + + if ( attribute.value.type !== 'Identifier' ) { + const { parts, keypath } = flattenReference( attribute.value ); + throw new Error( `Bindings on <:Window/> must be to top-level properties, e.g. '${parts.pop()}' rather than '${keypath}'` ); + } + + if ( !events[ associatedEvent ] ) events[ associatedEvent ] = []; + events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` ); + + // add initial value + generator.builders.metaBindings.addLine( + `this._state.${attribute.value.name} = window.${attribute.name};` + ); + } + }); + + Object.keys( events ).forEach( event => { + const handlerName = generator.current.getUniqueName( `onwindow${event}` ); + + const props = events[ event ].join( ',\n' ); + + generator.current.builders.init.addBlock( deindent` + var ${handlerName} = function ( event ) { + component.set({ + ${props} + }); + }; + window.addEventListener( '${event}', ${handlerName} ); + ` ); + + generator.current.builders.teardown.addBlock( deindent` + window.removeEventListener( '${event}', ${handlerName} ); + ` ); + }); + } +}; diff --git a/src/generators/server-side-rendering/visitors/Element.js b/src/generators/server-side-rendering/visitors/Element.js index 971b54d658..d50f795b6c 100644 --- a/src/generators/server-side-rendering/visitors/Element.js +++ b/src/generators/server-side-rendering/visitors/Element.js @@ -1,8 +1,17 @@ import Component from './Component.js'; import isVoidElementName from '../../../utils/isVoidElementName.js'; +import Window from './meta/Window.js'; + +const meta = { + ':Window': Window +}; export default { enter ( generator, node ) { + if ( node.name in meta ) { + return meta[ node.name ].enter( generator, node ); + } + if ( generator.components.has( node.name ) || node.name === ':Self' ) { Component.enter( generator, node ); return; @@ -39,6 +48,11 @@ export default { }, leave ( generator, node ) { + if ( node.name in meta ) { + if ( meta[ node.name ].leave ) meta[ node.name ].leave( generator, node ); + return; + } + if ( generator.components.has( node.name ) || node.name === ':Self' ) { Component.leave( generator, node ); return; diff --git a/src/generators/server-side-rendering/visitors/meta/Window.js b/src/generators/server-side-rendering/visitors/meta/Window.js new file mode 100644 index 0000000000..7df7d8bdaf --- /dev/null +++ b/src/generators/server-side-rendering/visitors/meta/Window.js @@ -0,0 +1,9 @@ +export default { + enter () { + // noop + }, + + leave () { + // noop + } +}; diff --git a/src/parse/index.js b/src/parse/index.js index ee0c1d8dfb..29122d69bc 100644 --- a/src/parse/index.js +++ b/src/parse/index.js @@ -32,6 +32,7 @@ export default function parse ( template, options = {} ) { index: 0, template, stack: [], + metaTags: {}, current () { return this.stack[ this.stack.length - 1 ]; diff --git a/src/parse/state/tag.js b/src/parse/state/tag.js index 361f6f49f0..5069c5c85c 100644 --- a/src/parse/state/tag.js +++ b/src/parse/state/tag.js @@ -11,6 +11,10 @@ const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/; const SELF = ':Self'; +const metaTags = { + ':Window': true +}; + const specials = new Map( [ [ 'script', { read: readScript, @@ -80,6 +84,22 @@ export default function tag ( parser ) { const name = readTagName( parser ); + if ( name in metaTags ) { + if ( name in parser.metaTags ) { + if ( isClosingTag && parser.current().children.length ) { + parser.error( `<${name}> cannot have children`, parser.current().children[0].start ); + } + + parser.error( `A component can only have one <${name}> tag`, start ); + } + + parser.metaTags[ name ] = true; + + if ( parser.stack.length > 1 ) { + parser.error( `<${name}> tags cannot be inside elements or blocks`, start ); + } + } + parser.allowWhitespace(); if ( isClosingTag ) { @@ -192,6 +212,9 @@ function readTagName ( parser ) { } const name = parser.readUntil( /(\s|\/|>)/ ); + + if ( name in metaTags ) return name; + if ( !validTagName.test( name ) ) { parser.error( `Expected valid tag name`, start ); } diff --git a/test/generator/index.js b/test/generator/index.js index f52831b7c2..04599c6f2b 100644 --- a/test/generator/index.js +++ b/test/generator/index.js @@ -93,6 +93,8 @@ describe( 'generate', () => { return env() .then( window => { + global.window = window; + // Put the constructor on window for testing window.SvelteComponent = SvelteComponent; diff --git a/test/generator/samples/window-binding-resize/_config.js b/test/generator/samples/window-binding-resize/_config.js new file mode 100644 index 0000000000..ee991552a7 --- /dev/null +++ b/test/generator/samples/window-binding-resize/_config.js @@ -0,0 +1,29 @@ +export default { + html: `
1024x768
`, + + skip: /^v4/.test( process.version ), // node 4 apparently does some dumb stuff + 'skip-ssr': true, // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason + + test ( assert, component, target, window ) { + const event = new window.Event( 'resize' ); + + // JSDOM executes window event listeners with `global` rather than + // `window` (bug?) so we need to do this + Object.defineProperties( global, { + innerWidth: { + value: 567, + configurable: true + }, + innerHeight: { + value: 456, + configurable: true + } + }); + + window.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` +
567x456
+ `); + } +}; \ No newline at end of file diff --git a/test/generator/samples/window-binding-resize/main.html b/test/generator/samples/window-binding-resize/main.html new file mode 100644 index 0000000000..e7e682a10c --- /dev/null +++ b/test/generator/samples/window-binding-resize/main.html @@ -0,0 +1,3 @@ +<:Window bind:innerWidth='width' bind:innerHeight='height'/> + +
{{width}}x{{height}}
\ No newline at end of file diff --git a/test/generator/samples/window-event/_config.js b/test/generator/samples/window-event/_config.js new file mode 100644 index 0000000000..8d805ba59e --- /dev/null +++ b/test/generator/samples/window-event/_config.js @@ -0,0 +1,29 @@ +export default { + html: `
undefinedxundefined
`, + + skip: /^v4/.test( process.version ), // node 4 apparently does some dumb stuff + 'skip-ssr': true, // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason + + test ( assert, component, target, window ) { + const event = new window.Event( 'resize' ); + + // JSDOM executes window event listeners with `global` rather than + // `window` (bug?) so we need to do this + Object.defineProperties( global, { + innerWidth: { + value: 567, + configurable: true + }, + innerHeight: { + value: 456, + configurable: true + } + }); + + window.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` +
567x456
+ `); + } +}; \ No newline at end of file diff --git a/test/generator/samples/window-event/main.html b/test/generator/samples/window-event/main.html new file mode 100644 index 0000000000..b6679a0e44 --- /dev/null +++ b/test/generator/samples/window-event/main.html @@ -0,0 +1,3 @@ +<:Window on:resize='set({ width: this.innerWidth, height: this.innerHeight })'/> + +
{{width}}x{{height}}
\ No newline at end of file diff --git a/test/parser/samples/error-window-children/error.json b/test/parser/samples/error-window-children/error.json new file mode 100644 index 0000000000..8ff51ca8fb --- /dev/null +++ b/test/parser/samples/error-window-children/error.json @@ -0,0 +1,8 @@ +{ + "message": "<:Window> cannot have children", + "loc": { + "line": 1, + "column": 9 + }, + "pos": 9 +} \ No newline at end of file diff --git a/test/parser/samples/error-window-children/input.html b/test/parser/samples/error-window-children/input.html new file mode 100644 index 0000000000..eff4bebe50 --- /dev/null +++ b/test/parser/samples/error-window-children/input.html @@ -0,0 +1 @@ +<:Window>contents \ No newline at end of file diff --git a/test/parser/samples/error-window-duplicate/error.json b/test/parser/samples/error-window-duplicate/error.json new file mode 100644 index 0000000000..1e86fafea2 --- /dev/null +++ b/test/parser/samples/error-window-duplicate/error.json @@ -0,0 +1,8 @@ +{ + "message": "A component can only have one <:Window> tag", + "loc": { + "line": 2, + "column": 0 + }, + "pos": 11 +} \ No newline at end of file diff --git a/test/parser/samples/error-window-duplicate/input.html b/test/parser/samples/error-window-duplicate/input.html new file mode 100644 index 0000000000..683468e436 --- /dev/null +++ b/test/parser/samples/error-window-duplicate/input.html @@ -0,0 +1,2 @@ +<:Window/> +<:Window/> \ No newline at end of file diff --git a/test/parser/samples/error-window-inside-block/error.json b/test/parser/samples/error-window-inside-block/error.json new file mode 100644 index 0000000000..d6f6bd2f33 --- /dev/null +++ b/test/parser/samples/error-window-inside-block/error.json @@ -0,0 +1,8 @@ +{ + "message": "<:Window> tags cannot be inside elements or blocks", + "loc": { + "line": 2, + "column": 1 + }, + "pos": 13 +} \ No newline at end of file diff --git a/test/parser/samples/error-window-inside-block/input.html b/test/parser/samples/error-window-inside-block/input.html new file mode 100644 index 0000000000..b361517056 --- /dev/null +++ b/test/parser/samples/error-window-inside-block/input.html @@ -0,0 +1,3 @@ +{{#if foo}} + <:Window/> +{{/if}} \ No newline at end of file diff --git a/test/parser/samples/error-window-inside-element/error.json b/test/parser/samples/error-window-inside-element/error.json new file mode 100644 index 0000000000..5c28694d61 --- /dev/null +++ b/test/parser/samples/error-window-inside-element/error.json @@ -0,0 +1,8 @@ +{ + "message": "<:Window> tags cannot be inside elements or blocks", + "loc": { + "line": 2, + "column": 1 + }, + "pos": 7 +} \ No newline at end of file diff --git a/test/parser/samples/error-window-inside-element/input.html b/test/parser/samples/error-window-inside-element/input.html new file mode 100644 index 0000000000..edda05bfbb --- /dev/null +++ b/test/parser/samples/error-window-inside-element/input.html @@ -0,0 +1,3 @@ +
+ <:Window/> +
> \ No newline at end of file