Skip to content

Commit 14f84a3

Browse files
authored
Merge pull request #1386 from sveltejs/gh-984
width and height bindings
2 parents 4dcde4b + 8f8b130 commit 14f84a3

File tree

14 files changed

+426
-11
lines changed

14 files changed

+426
-11
lines changed

src/compile/nodes/Binding.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import flattenReference from '../../utils/flattenReference';
66
import Compiler from '../Compiler';
77
import Block from '../dom/Block';
88
import Expression from './shared/Expression';
9+
import { dimensions } from '../../utils/patterns';
910

1011
const readOnlyMediaAttributes = new Set([
1112
'duration',
@@ -14,6 +15,9 @@ const readOnlyMediaAttributes = new Set([
1415
'played'
1516
]);
1617

18+
// TODO a lot of this element-specific stuff should live in Element —
19+
// Binding should ideally be agnostic between Element and Component
20+
1721
export default class Binding extends Node {
1822
name: string;
1923
value: Expression;
@@ -57,7 +61,10 @@ export default class Binding extends Node {
5761
const node: Element = this.parent;
5862

5963
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type'));
60-
const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name);
64+
const isReadOnly = (
65+
(node.isMediaNode() && readOnlyMediaAttributes.has(this.name)) ||
66+
dimensions.test(this.name)
67+
);
6168

6269
let updateCondition: string;
6370

@@ -103,8 +110,7 @@ export default class Binding extends Node {
103110
if (this.name === 'currentTime' || this.name === 'volume') {
104111
updateCondition = `!isNaN(${snippet})`;
105112

106-
if (this.name === 'currentTime')
107-
initialUpdate = null;
113+
if (this.name === 'currentTime') initialUpdate = null;
108114
}
109115

110116
if (this.name === 'paused') {
@@ -117,6 +123,12 @@ export default class Binding extends Node {
117123
initialUpdate = null;
118124
}
119125

126+
// bind:offsetWidth and bind:offsetHeight
127+
if (dimensions.test(this.name)) {
128+
initialUpdate = null;
129+
updateDom = null;
130+
}
131+
120132
return {
121133
name: this.name,
122134
object: name,

src/compile/nodes/Element.ts

+36-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Action from './Action';
1717
import Text from './Text';
1818
import * as namespaces from '../../utils/namespaces';
1919
import mapChildren from './shared/mapChildren';
20+
import { dimensions } from '../../utils/patterns';
2021

2122
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
2223
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 {
262263
parentNode;
263264

264265
block.addVariable(name);
265-
const renderStatement = getRenderStatement(this.compiler, this.namespace, this.name);
266+
const renderStatement = getRenderStatement(this.namespace, this.name);
266267
block.builders.create.addLine(
267268
`${name} = ${renderStatement};`
268269
);
@@ -489,13 +490,27 @@ export default class Element extends Node {
489490
`);
490491

491492
group.events.forEach(name => {
492-
block.builders.hydrate.addLine(
493-
`@addListener(${this.var}, "${name}", ${handler});`
494-
);
493+
if (name === 'resize') {
494+
// special case
495+
const resize_listener = block.getUniqueName(`${this.var}_resize_listener`);
496+
block.addVariable(resize_listener);
495497

496-
block.builders.destroy.addLine(
497-
`@removeListener(${this.var}, "${name}", ${handler});`
498-
);
498+
block.builders.mount.addLine(
499+
`${resize_listener} = @addResizeListener(${this.var}, ${handler});`
500+
);
501+
502+
block.builders.unmount.addLine(
503+
`${resize_listener}.cancel();`
504+
);
505+
} else {
506+
block.builders.hydrate.addLine(
507+
`@addListener(${this.var}, "${name}", ${handler});`
508+
);
509+
510+
block.builders.destroy.addLine(
511+
`@removeListener(${this.var}, "${name}", ${handler});`
512+
);
513+
}
499514
});
500515

501516
const allInitialStateIsDefined = group.bindings
@@ -509,6 +524,14 @@ export default class Element extends Node {
509524
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
510525
);
511526
}
527+
528+
if (group.events[0] === 'resize') {
529+
this.compiler.target.hasComplexBindings = true;
530+
531+
block.builders.hydrate.addLine(
532+
`#component.root._beforecreate.push(${handler});`
533+
);
534+
}
512535
});
513536

514537
this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
@@ -916,7 +939,6 @@ export default class Element extends Node {
916939
}
917940

918941
function getRenderStatement(
919-
compiler: Compiler,
920942
namespace: string,
921943
name: string
922944
) {
@@ -971,6 +993,12 @@ const events = [
971993
node.name === 'input' && /radio|checkbox|range/.test(node.getStaticAttributeValue('type'))
972994
},
973995

996+
{
997+
eventNames: ['resize'],
998+
filter: (node: Element, name: string) =>
999+
dimensions.test(name)
1000+
},
1001+
9741002
// media events
9751003
{
9761004
eventNames: ['timeupdate'],

src/shared/dom.js

+29
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,32 @@ export function selectMultipleValue(select) {
193193
return option.__value;
194194
});
195195
}
196+
197+
export function addResizeListener(element, fn) {
198+
if (getComputedStyle(element).position === 'static') {
199+
element.style.position = 'relative';
200+
}
201+
202+
const object = document.createElement('object');
203+
object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
204+
object.type = 'text/html';
205+
206+
object.onload = () => {
207+
object.contentDocument.defaultView.addEventListener('resize', fn);
208+
};
209+
210+
if (/Trident/.test(navigator.userAgent)) {
211+
element.appendChild(object);
212+
object.data = 'about:blank';
213+
} else {
214+
object.data = 'about:blank';
215+
element.appendChild(object);
216+
}
217+
218+
return {
219+
cancel: () => {
220+
object.contentDocument.defaultView.removeEventListener('resize', fn);
221+
element.removeChild(object);
222+
}
223+
};
224+
}

src/utils/patterns.ts

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

src/validate/html/validateElement.ts

+19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as namespaces from '../../utils/namespaces';
22
import validateEventHandler from './validateEventHandler';
33
import validate, { Validator } from '../index';
44
import { Node } from '../../interfaces';
5+
import { dimensions } from '../../utils/patterns';
6+
import isVoidElementName from '../../utils/isVoidElementName';
57

68
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)$/;
79

@@ -157,6 +159,23 @@ export default function validateElement(
157159
message: `'${name}' binding can only be used with <audio> or <video>`
158160
});
159161
}
162+
} else if (dimensions.test(name)) {
163+
if (node.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
164+
validator.error(attribute, {
165+
code: 'invalid-binding',
166+
message: `'${attribute.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
167+
});
168+
} else if (svg.test(node.name)) {
169+
validator.error(attribute, {
170+
code: 'invalid-binding',
171+
message: `'${attribute.name}' is not a valid binding on SVG elements`
172+
});
173+
} else if (isVoidElementName(node.name)) {
174+
validator.error(attribute, {
175+
code: 'invalid-binding',
176+
message: `'${attribute.name}' is not a valid binding on void elements like <${node.name}>. Use a wrapper element instead`
177+
});
178+
}
160179
} else {
161180
validator.error(attribute, {
162181
code: `invalid-binding`,

0 commit comments

Comments
 (0)