Added support for resize observer bindings

pull/8019/head
TheCymaera 4 years ago
parent 380583804c
commit 25dd06e791

@ -3,7 +3,7 @@ import get_object from '../utils/get_object';
import Expression from './shared/Expression'; import Expression from './shared/Expression';
import Component from '../Component'; import Component from '../Component';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { regex_dimensions } from '../../utils/patterns'; import { regex_dimensions, regex_box_size } from '../../utils/patterns';
import { Node as ESTreeNode } from 'estree'; import { Node as ESTreeNode } from 'estree';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import Element from './Element'; import Element from './Element';
@ -89,6 +89,7 @@ export default class Binding extends Node {
this.is_readonly = this.is_readonly =
regex_dimensions.test(this.name) || regex_dimensions.test(this.name) ||
regex_box_size.test(this.name) ||
(isElement(parent) && (isElement(parent) &&
((parent.is_media_node() && read_only_media_attributes.has(this.name)) || ((parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
(parent.name === 'input' && type === 'file')) /* TODO others? */); (parent.name === 'input' && type === 'file')) /* TODO others? */);

@ -11,7 +11,7 @@ import StyleDirective from './StyleDirective';
import Text from './Text'; import Text from './Text';
import { namespaces } from '../../utils/namespaces'; import { namespaces } from '../../utils/namespaces';
import map_children from './shared/map_children'; import map_children from './shared/map_children';
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character } from '../../utils/patterns'; import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size, regex_border_box_size, regex_content_box_size } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch'; import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list'; import list from '../../utils/list';
import Let from './Let'; import Let from './Let';
@ -907,7 +907,10 @@ export default class Element extends Node {
} else if (contenteditable && !contenteditable.is_static) { } else if (contenteditable && !contenteditable.is_static) {
return component.error(contenteditable, compiler_errors.dynamic_contenteditable_attribute); return component.error(contenteditable, compiler_errors.dynamic_contenteditable_attribute);
} }
} else if (name !== 'this') { } else if (
name !== 'this' &&
!regex_box_size.test(name)
) {
return component.error(binding, compiler_errors.invalid_binding(binding.name)); return component.error(binding, compiler_errors.invalid_binding(binding.name));
} }
}); });

@ -11,6 +11,7 @@ import { Node, Identifier } from 'estree';
import add_to_set from '../../../utils/add_to_set'; import add_to_set from '../../../utils/add_to_set';
import mark_each_block_bindings from '../shared/mark_each_block_bindings'; import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import handle_select_value_binding from './handle_select_value_binding'; import handle_select_value_binding from './handle_select_value_binding';
import { regex_box_size } from '../../../../utils/patterns.js';
export default class BindingWrapper { export default class BindingWrapper {
node: Binding; node: Binding;
@ -413,6 +414,17 @@ function get_value_from_dom(
return x`$$value`; return x`$$value`;
} }
// <div bind:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize>
if (regex_box_size.test(name)) {
const functionName = {
"contentRect": "get_content_rect",
"contentBoxSize": "get_content_box_size",
"borderBoxSize": "get_border_box_size",
"devicePixelContentBoxSize": "get_device_pixel_content_box_size",
}
return x`@${functionName}(this)`;
}
// <select bind:value='selected> // <select bind:value='selected>
if (node.name === 'select') { if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ? return node.get_static_attribute_value('multiple') === true ?

@ -12,7 +12,7 @@ import { namespaces } from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute'; import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute'; import StyleAttributeWrapper from './StyleAttribute';
import SpreadAttributeWrapper from './SpreadAttribute'; import SpreadAttributeWrapper from './SpreadAttribute';
import { regex_dimensions, regex_starts_with_newline, regex_backslashes } from '../../../../utils/patterns'; import { regex_dimensions, regex_starts_with_newline, regex_backslashes, regex_border_box_size, regex_content_box_size, regex_device_pixel_content_box_size } from '../../../../utils/patterns';
import Binding from './Binding'; import Binding from './Binding';
import add_to_set from '../../../utils/add_to_set'; import add_to_set from '../../../utils/add_to_set';
import { add_event_handler } from '../shared/add_event_handlers'; import { add_event_handler } from '../shared/add_event_handlers';
@ -70,6 +70,22 @@ const events = [
regex_dimensions.test(name) regex_dimensions.test(name)
}, },
{
event_names: ['elementresizeobserveborderbox'],
filter: (_node: Element, name: string) =>
regex_border_box_size.test(name)
},
{
event_names: ['elementresizeobservecontentbox'],
filter: (_node: Element, name: string) =>
regex_content_box_size.test(name)
},
{
event_names: ['elementresizeobservedevicepixelcontentbox'],
filter: (_node: Element, name: string) =>
regex_device_pixel_content_box_size.test(name)
},
// media events // media events
{ {
event_names: ['timeupdate'], event_names: ['timeupdate'],
@ -688,13 +704,19 @@ export default class ElementWrapper extends Wrapper {
`); `);
binding_group.events.forEach(name => { binding_group.events.forEach(name => {
if (name === 'elementresize') { if (name.startsWith('elementresize')) {
// special case
const resize_listener = block.get_unique_name(`${this.var.name}_resize_listener`); const resize_listener = block.get_unique_name(`${this.var.name}_resize_listener`);
block.add_variable(resize_listener); block.add_variable(resize_listener);
const functionName = ({
"elementresize": "add_iframe_resize_listener",
"elementresizeobservecontentbox": "add_content_box_observer",
"elementresizeobserveborderbox": "add_border_box_observer",
"elementresizeobservedevicepixelcontentbox": "add_device_pixel_content_box_observer",
})[name]
block.chunks.mount.push( block.chunks.mount.push(
b`${resize_listener} = @add_resize_listener(${this.var}, ${callee}.bind(${this.var}));` b`${resize_listener} = @${functionName}(${this.var}, ${callee}.bind(${this.var}));`
); );
block.chunks.destroy.push( block.chunks.destroy.push(

@ -22,3 +22,8 @@ export const regex_ends_with_underscore = /_$/;
export const regex_invalid_variable_identifier_characters = /[^a-zA-Z0-9_$]/g; export const regex_invalid_variable_identifier_characters = /[^a-zA-Z0-9_$]/g;
export const regex_dimensions = /^(?:offset|client)(?:Width|Height)$/; export const regex_dimensions = /^(?:offset|client)(?:Width|Height)$/;
export const regex_content_box_size = /^(?:contentRect|contentBoxSize)$/;
export const regex_border_box_size = /^(?:borderBoxSize)$/;
export const regex_device_pixel_content_box_size = /^(?:devicePixelContentBoxSize)$/;
export const regex_box_size = /^(?:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize)$/

@ -7,40 +7,54 @@ const MapImplementation = "WeakMap" in window ? WeakMap : Map;
export class ResizeObserverSingleton { export class ResizeObserverSingleton {
constructor(readonly options?: ResizeObserverOptions) {} constructor(readonly options?: ResizeObserverOptions) {}
addListener(element: Element, callback: (element: Element)=>any) { addListener(element: Element, callback: Callback) {
if (!this._callbacks.has(element)) { if (!this._subscriptions.has(element)) {
this._callbacks.set(element, new Set([callback])); this._subscriptions.set(element, new Subscription(new Set([callback])));
this._getObserver().observe(element, this.options); this._getObserver().observe(element, this.options);
} else { } else {
this._callbacks.get(element)!.add(callback); const subscription = this._subscriptions.get(element)!;
callback(element); subscription.listeners.add(callback);
callback.call(element, subscription.lastEntry!);
} }
} }
removeListener(element: Element, callback: (element: Element)=>any) { removeListener(element: Element, callback: Callback) {
const list = this._callbacks.get(element); const subscription = this._subscriptions.get(element);
if (!list) return; if (!subscription) return;
if (list.size > 1) { if (subscription.listeners.size > 1) {
list.delete(callback); subscription.listeners.delete(callback);
} else { } else {
this._callbacks.delete(element); this._subscriptions.delete(element);
this._getObserver().unobserve(element); this._getObserver().unobserve(element);
} }
} }
// TODO: consider using Array instead of Set? getLastEntry(element: Element) {
// If we can guarantee one listener per element, we can simplify even more. return this._subscriptions.get(element)?.lastEntry;
private readonly _callbacks: WeakMap<Element, Set<(element: Element)=>any>> = new MapImplementation(); }
private readonly _subscriptions: WeakMap<Element, Subscription> = new MapImplementation();
private _observer: ResizeObserver|undefined = undefined; private _observer: ResizeObserver|undefined = undefined;
private _getObserver() { private _getObserver() {
return this._observer ?? (this._observer = new ResizeObserver((entries)=>{ return this._observer ?? (this._observer = new ResizeObserver((entries)=>{
for (const entry of entries) { for (const entry of entries) {
const element = entry.target; const element = entry.target;
const callbacks = this._callbacks.get(element); const subscription = this._subscriptions.get(element);
if (callbacks) { if (!subscription) continue;
for (const callback of callbacks) callback(element); subscription.lastEntry = entry;
} for (const callback of subscription.listeners) callback.call(element, entry);
} }
})); }));
} }
}
type Callback = (this: Element, entry: ResizeObserverEntry)=>any;
class Subscription {
// TODO: consider using Array instead of Set?
// If we can guarantee one listener per element, we can simplify even more.
constructor(
readonly listeners: Set<Callback>,
public lastEntry?: ResizeObserverEntry,
) {}
} }

@ -597,7 +597,7 @@ export function is_crossorigin() {
return crossorigin; return crossorigin;
} }
function add_iframe_resize_listener(node: HTMLElement, fn: () => void) { export function add_iframe_resize_listener(node: HTMLElement, fn: () => void) {
const computed_style = getComputedStyle(node); const computed_style = getComputedStyle(node);
if (computed_style.position === 'static') { if (computed_style.position === 'static') {
@ -641,30 +641,41 @@ function add_iframe_resize_listener(node: HTMLElement, fn: () => void) {
}; };
} }
const resize_observer_content_box = new ResizeObserverSingleton({ box: "content-box" });
const resize_observer_border_box = new ResizeObserverSingleton({ box: "border-box" });
const resize_observer_device_pixel_content_box = new ResizeObserverSingleton({ box: "device-pixel-content-box" });
export const resize_observer_content_box = new ResizeObserverSingleton({ box: "content-box" }); export function add_content_box_observer(node: Element, fn: () => void) {
// export const resize_observer_border_box = new SoleResizeObserver({ box: "border-box" });
// export const resize_observer_device_pixel_content_box = new SoleResizeObserver({ box: "device-pixel-content-box" });
export function add_content_box_observer(node: HTMLElement, fn: () => void) {
resize_observer_content_box.addListener(node, fn); resize_observer_content_box.addListener(node, fn);
return ()=> resize_observer_content_box.removeListener(node, fn); return ()=> resize_observer_content_box.removeListener(node, fn);
} }
// export function add_border_box_observer(node: HTMLElement, fn: () => void) { export function add_border_box_observer(node: Element, fn: () => void) {
// resize_observer_border_box.addListener(node, fn); resize_observer_border_box.addListener(node, fn);
// return ()=> resize_observer_border_box.removeListener(node, fn); return ()=> resize_observer_border_box.removeListener(node, fn);
// } }
//
// export function add_device_pixel_content_box_observer(node: HTMLElement, fn: () => void) { export function add_device_pixel_content_box_observer(node: Element, fn: () => void) {
// resize_observer_device_pixel_content_box.addListener(node, fn); resize_observer_device_pixel_content_box.addListener(node, fn);
// return ()=> resize_observer_device_pixel_content_box.removeListener(node, fn); return ()=> resize_observer_device_pixel_content_box.removeListener(node, fn);
// } }
export function get_content_rect(node: Element) {
return resize_observer_content_box.getLastEntry(node)!.contentRect;
}
export function get_content_box_size(node: Element) {
return resize_observer_content_box.getLastEntry(node)!.contentBoxSize;
}
export function get_border_box_size(node: Element) {
return resize_observer_border_box.getLastEntry(node)!.borderBoxSize;
}
export function get_device_pixel_content_box_size(node: Element) {
return resize_observer_device_pixel_content_box.getLastEntry(node)!.devicePixelContentBoxSize;
}
export const add_resize_listener = "ResizeObserver" in window ?
add_content_box_observer :
add_iframe_resize_listener;
export function toggle_class(element, name, toggle) { export function toggle_class(element, name, toggle) {
element.classList[toggle ? 'add' : 'remove'](name); element.classList[toggle ? 'add' : 'remove'](name);

Loading…
Cancel
Save