Merge pull request #1386 from sveltejs/gh-984

width and height bindings
pull/1392/head
Rich Harris 7 years ago committed by GitHub
commit 14f84a3e8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,6 +6,7 @@ import flattenReference from '../../utils/flattenReference';
import Compiler from '../Compiler';
import Block from '../dom/Block';
import Expression from './shared/Expression';
import { dimensions } from '../../utils/patterns';
const readOnlyMediaAttributes = new Set([
'duration',
@ -14,6 +15,9 @@ const readOnlyMediaAttributes = new Set([
'played'
]);
// TODO a lot of this element-specific stuff should live in Element —
// Binding should ideally be agnostic between Element and Component
export default class Binding extends Node {
name: string;
value: Expression;
@ -57,7 +61,10 @@ export default class Binding extends Node {
const node: Element = this.parent;
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type'));
const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name);
const isReadOnly = (
(node.isMediaNode() && readOnlyMediaAttributes.has(this.name)) ||
dimensions.test(this.name)
);
let updateCondition: string;
@ -103,8 +110,7 @@ export default class Binding extends Node {
if (this.name === 'currentTime' || this.name === 'volume') {
updateCondition = `!isNaN(${snippet})`;
if (this.name === 'currentTime')
initialUpdate = null;
if (this.name === 'currentTime') initialUpdate = null;
}
if (this.name === 'paused') {
@ -117,6 +123,12 @@ export default class Binding extends Node {
initialUpdate = null;
}
// bind:offsetWidth and bind:offsetHeight
if (dimensions.test(this.name)) {
initialUpdate = null;
updateDom = null;
}
return {
name: this.name,
object: name,

@ -17,6 +17,7 @@ import Action from './Action';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren';
import { dimensions } from '../../utils/patterns';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const booleanAttributes = new Set('async autocomplete autofocus autoplay border challenge checked compact contenteditable controls default defer disabled formnovalidate frameborder hidden indeterminate ismap loop multiple muted nohref noresize noshade novalidate nowrap open readonly required reversed scoped scrolling seamless selected sortable spellcheck translate'.split(' '));
@ -262,7 +263,7 @@ export default class Element extends Node {
parentNode;
block.addVariable(name);
const renderStatement = getRenderStatement(this.compiler, this.namespace, this.name);
const renderStatement = getRenderStatement(this.namespace, this.name);
block.builders.create.addLine(
`${name} = ${renderStatement};`
);
@ -489,13 +490,27 @@ export default class Element extends Node {
`);
group.events.forEach(name => {
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${name}", ${handler});`
);
if (name === 'resize') {
// special case
const resize_listener = block.getUniqueName(`${this.var}_resize_listener`);
block.addVariable(resize_listener);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${name}", ${handler});`
);
block.builders.mount.addLine(
`${resize_listener} = @addResizeListener(${this.var}, ${handler});`
);
block.builders.unmount.addLine(
`${resize_listener}.cancel();`
);
} else {
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${name}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${name}", ${handler});`
);
}
});
const allInitialStateIsDefined = group.bindings
@ -509,6 +524,14 @@ export default class Element extends Node {
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
);
}
if (group.events[0] === 'resize') {
this.compiler.target.hasComplexBindings = true;
block.builders.hydrate.addLine(
`#component.root._beforecreate.push(${handler});`
);
}
});
this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
@ -916,7 +939,6 @@ export default class Element extends Node {
}
function getRenderStatement(
compiler: Compiler,
namespace: string,
name: string
) {
@ -971,6 +993,12 @@ const events = [
node.name === 'input' && /radio|checkbox|range/.test(node.getStaticAttributeValue('type'))
},
{
eventNames: ['resize'],
filter: (node: Element, name: string) =>
dimensions.test(name)
},
// media events
{
eventNames: ['timeupdate'],

@ -193,3 +193,32 @@ export function selectMultipleValue(select) {
return option.__value;
});
}
export function addResizeListener(element, fn) {
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
const object = document.createElement('object');
object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
object.type = 'text/html';
object.onload = () => {
object.contentDocument.defaultView.addEventListener('resize', fn);
};
if (/Trident/.test(navigator.userAgent)) {
element.appendChild(object);
object.data = 'about:blank';
} else {
object.data = 'about:blank';
element.appendChild(object);
}
return {
cancel: () => {
object.contentDocument.defaultView.removeEventListener('resize', fn);
element.removeChild(object);
}
};
}

@ -1 +1,3 @@
export const whitespace = /[ \t\r\n]/;
export const dimensions = /^(?:offset|client)(?:Width|Height)$/;

@ -2,6 +2,8 @@ import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
import { dimensions } from '../../utils/patterns';
import isVoidElementName from '../../utils/isVoidElementName';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
@ -157,6 +159,23 @@ export default function validateElement(
message: `'${name}' binding can only be used with <audio> or <video>`
});
}
} else if (dimensions.test(name)) {
if (node.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
validator.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
} else if (svg.test(node.name)) {
validator.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on SVG elements`
});
} else if (isVoidElementName(node.name)) {
validator.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on void elements like <${node.name}>. Use a wrapper element instead`
});
}
} else {
validator.error(attribute, {
code: `invalid-binding`,

@ -0,0 +1,218 @@
function noop() {}
function assign(tar, src) {
for (var k in src) tar[k] = src[k];
return tar;
}
function insertNode(node, target, anchor) {
target.insertBefore(node, anchor);
}
function detachNode(node) {
node.parentNode.removeChild(node);
}
function createElement(name) {
return document.createElement(name);
}
function addResizeListener(element, fn) {
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
const object = document.createElement('object');
object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
object.type = 'text/html';
object.onload = () => {
object.contentDocument.defaultView.addEventListener('resize', fn);
};
if (/Trident/.test(navigator.userAgent)) {
element.appendChild(object);
object.data = 'about:blank';
} else {
object.data = 'about:blank';
element.appendChild(object);
}
return {
cancel: () => {
object.contentDocument.defaultView.removeEventListener('resize', fn);
element.removeChild(object);
}
};
}
function blankObject() {
return Object.create(null);
}
function destroy(detach) {
this.destroy = noop;
this.fire('destroy');
this.set = noop;
if (detach !== false) this._fragment.u();
this._fragment.d();
this._fragment = null;
this._state = {};
}
function _differs(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function fire(eventName, data) {
var handlers =
eventName in this._handlers && this._handlers[eventName].slice();
if (!handlers) return;
for (var i = 0; i < handlers.length; i += 1) {
var handler = handlers[i];
if (!handler.__calling) {
handler.__calling = true;
handler.call(this, data);
handler.__calling = false;
}
}
}
function get() {
return this._state;
}
function init(component, options) {
component._handlers = blankObject();
component._bind = options._bind;
component.options = options;
component.root = options.root || component;
component.store = component.root.store || options.store;
}
function on(eventName, handler) {
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
return {
cancel: function() {
var index = handlers.indexOf(handler);
if (~index) handlers.splice(index, 1);
}
};
}
function set(newState) {
this._set(assign({}, newState));
if (this.root._lock) return;
this.root._lock = true;
callAll(this.root._beforecreate);
callAll(this.root._oncreate);
callAll(this.root._aftercreate);
this.root._lock = false;
}
function _set(newState) {
var oldState = this._state,
changed = {},
dirty = false;
for (var key in newState) {
if (this._differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign(assign({}, oldState), newState);
this._recompute(changed, this._state);
if (this._bind) this._bind(changed, this._state);
if (this._fragment) {
this.fire("state", { changed: changed, current: this._state, previous: oldState });
this._fragment.p(changed, this._state);
this.fire("update", { changed: changed, current: this._state, previous: oldState });
}
}
function callAll(fns) {
while (fns && fns.length) fns.shift()();
}
function _mount(target, anchor) {
this._fragment[this._fragment.i ? 'i' : 'm'](target, anchor || null);
}
function _unmount() {
if (this._fragment) this._fragment.u();
}
var proto = {
destroy,
get,
fire,
on,
set,
_recompute: noop,
_set,
_mount,
_unmount,
_differs
};
/* generated by Svelte vX.Y.Z */
function create_main_fragment(component, ctx) {
var div, div_resize_listener;
function div_resize_handler() {
component.set({ w: div.offsetWidth, h: div.offsetHeight });
}
return {
c() {
div = createElement("div");
div.textContent = "some content";
component.root._beforecreate.push(div_resize_handler);
},
m(target, anchor) {
insertNode(div, target, anchor);
div_resize_listener = addResizeListener(div, div_resize_handler);
},
p: noop,
u() {
detachNode(div);
div_resize_listener.cancel();
},
d: noop
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
if (!options.root) {
this._oncreate = [];
this._beforecreate = [];
}
this._fragment = create_main_fragment(this, this._state);
if (options.target) {
this._fragment.c();
this._mount(options.target, options.anchor);
callAll(this._beforecreate);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,54 @@
/* generated by Svelte vX.Y.Z */
import { addResizeListener, assign, callAll, createElement, detachNode, init, insertNode, noop, proto } from "svelte/shared.js";
function create_main_fragment(component, ctx) {
var div, div_resize_listener;
function div_resize_handler() {
component.set({ w: div.offsetWidth, h: div.offsetHeight });
}
return {
c() {
div = createElement("div");
div.textContent = "some content";
component.root._beforecreate.push(div_resize_handler);
},
m(target, anchor) {
insertNode(div, target, anchor);
div_resize_listener = addResizeListener(div, div_resize_handler);
},
p: noop,
u() {
detachNode(div);
div_resize_listener.cancel();
},
d: noop
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
if (!options.root) {
this._oncreate = [];
this._beforecreate = [];
}
this._fragment = create_main_fragment(this, this._state);
if (options.target) {
this._fragment.c();
this._mount(options.target, options.anchor);
callAll(this._beforecreate);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,3 @@
<div bind:offsetWidth=w bind:offsetHeight=h>
some content
</div>

@ -0,0 +1,15 @@
[{
"code": "invalid-binding",
"message": "'offsetWidth' is not a valid binding on SVG elements",
"pos": 13,
"start": {
"line": 2,
"column": 7,
"character": 13
},
"end": {
"line": 2,
"column": 23,
"character": 29
}
}]

@ -0,0 +1,3 @@
<svg>
<text bind:offsetWidth>some text</text>
</svg>

After

Width:  |  Height:  |  Size: 53 B

@ -0,0 +1,15 @@
[{
"code": "invalid-binding",
"message": "'offsetWidth' is not a valid binding on <svg>. Use 'clientWidth' instead",
"pos": 5,
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 21,
"character": 21
}
}]

@ -0,0 +1 @@
<svg bind:offsetWidth></svg>

After

Width:  |  Height:  |  Size: 28 B

@ -0,0 +1,15 @@
[{
"code": "invalid-binding",
"message": "'offsetWidth' is not a valid binding on void elements like <img>. Use a wrapper element instead",
"pos": 22,
"start": {
"line": 1,
"column": 22,
"character": 22
},
"end": {
"line": 1,
"column": 38,
"character": 38
}
}]

@ -0,0 +1 @@
<img src='potato.jpg' bind:offsetWidth>
Loading…
Cancel
Save