From d75ab8544e963bbee615376580e9ab931f707e4c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 9 Sep 2017 12:40:33 -0400 Subject: [PATCH] remove requestAnimationFrame stuff, convert time ranges to {start, end} objects --- .../dom/visitors/Element/Binding.ts | 88 +++-- src/shared/dom.js | 8 + .../samples/media-bindings/expected-bundle.js | 320 ++++++++++++++++++ test/js/samples/media-bindings/expected.js | 141 ++++++++ test/js/samples/media-bindings/input.html | 1 + 5 files changed, 511 insertions(+), 47 deletions(-) create mode 100644 test/js/samples/media-bindings/expected-bundle.js create mode 100644 test/js/samples/media-bindings/expected.js create mode 100644 test/js/samples/media-bindings/input.html diff --git a/src/generators/dom/visitors/Element/Binding.ts b/src/generators/dom/visitors/Element/Binding.ts index 3e01312391..549cb05ecb 100644 --- a/src/generators/dom/visitors/Element/Binding.ts +++ b/src/generators/dom/visitors/Element/Binding.ts @@ -8,6 +8,13 @@ import { State } from '../../interfaces'; import getObject from '../../../../utils/getObject'; import getTailSnippet from '../../../../utils/getTailSnippet'; +const readOnlyMediaAttributes = new Set([ + 'duration', + 'buffered', + 'seekable', + 'played' +]); + export default function visitBinding( generator: DomGenerator, block: Block, @@ -25,9 +32,9 @@ export default function visitBinding( state.allUsedContexts.push(context); }); - const eventName = getBindingEventName(node, attribute); + const eventNames = getBindingEventName(node, attribute); const handler = block.getUniqueName( - `${state.parentNode}_${eventName}_handler` + `${state.parentNode}_${eventNames.join('_')}_handler` ); const isMultipleSelect = node.name === 'select' && @@ -38,6 +45,10 @@ export default function visitBinding( const bindingGroup = attribute.name === 'group' ? getBindingGroup(generator, attribute.value) : null; + + const isMediaElement = node.name === 'audio' || node.name === 'video'; + const isReadOnly = isMediaElement && readOnlyMediaAttributes.has(attribute.name) + const value = getBindingValue( generator, block, @@ -45,6 +56,7 @@ export default function visitBinding( node, attribute, isMultipleSelect, + isMediaElement, bindingGroup, type ); @@ -52,10 +64,9 @@ export default function visitBinding( let setter = getSetter(block, name, snippet, state.parentNode, attribute, dependencies, value); let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`; - const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others? + const needsLock = !isReadOnly && node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others? const lock = `#${state.parentNode}_updating`; let updateConditions = needsLock ? [`!${lock}`] : []; - let readOnly = false; if (needsLock) block.addVariable(lock, 'false'); @@ -115,7 +126,7 @@ export default function visitBinding( ); updateElement = `${state.parentNode}.checked = ${condition};`; - } else if (node.name === 'audio' || node.name === 'video') { + } else if (isMediaElement) { generator.hasComplexBindings = true; block.builders.hydrate.addBlock(`#component._root._beforecreate.push(${handler});`); @@ -129,8 +140,6 @@ export default function visitBinding( `; updateConditions.push(`!isNaN(${snippet})`); - } else if (attribute.name === 'duration') { - readOnly = true; } else if (attribute.name === 'paused') { // this is necessary to prevent the audio restarting by itself const last = block.getUniqueName(`${state.parentNode}_paused_value`); @@ -138,28 +147,6 @@ export default function visitBinding( updateConditions = [`${last} !== (${last} = ${snippet})`]; updateElement = `${state.parentNode}[${last} ? "pause" : "play"]();`; - } else if (attribute.name === 'buffered') { - const frame = block.getUniqueName(`${state.parentNode}_animationframe`); - block.addVariable(frame); - setter = deindent` - cancelAnimationFrame(${frame}); - ${frame} = requestAnimationFrame(${handler}); - ${setter} - `; - - updateConditions.push(`${snippet}.start`); - readOnly = true; - } else if (attribute.name === 'seekable' || attribute.name === 'played') { - const frame = block.getUniqueName(`${state.parentNode}_animationframe`); - block.addVariable(frame); - setter = deindent` - cancelAnimationFrame(${frame}); - if (!${state.parentNode}.paused) ${frame} = requestAnimationFrame(${handler}); - ${setter} - `; - - updateConditions.push(`${snippet}.start`); - readOnly = true; } } @@ -183,21 +170,23 @@ export default function visitBinding( @removeListener(${state.parentNode}, "change", ${handler}); `); } else { - block.builders.hydrate.addLine( - `@addListener(${state.parentNode}, "${eventName}", ${handler});` - ); - - block.builders.destroy.addLine( - `@removeListener(${state.parentNode}, "${eventName}", ${handler});` - ); + eventNames.forEach(eventName => { + block.builders.hydrate.addLine( + `@addListener(${state.parentNode}, "${eventName}", ${handler});` + ); + + block.builders.destroy.addLine( + `@removeListener(${state.parentNode}, "${eventName}", ${handler});` + ); + }); } - if (node.name !== 'audio' && node.name !== 'video') { + if (!isMediaElement) { node.initialUpdate = updateElement; node.initialUpdateNeedsStateObject = !block.contexts.has(name); } - if (!readOnly) { // audio/video duration is read-only, it never updates + if (!isReadOnly) { // audio/video duration is read-only, it never updates if (updateConditions.length) { block.builders.update.addBlock(deindent` if (${updateConditions.join(' && ')}) { @@ -228,18 +217,18 @@ function getBindingEventName(node: Node, attribute: Node) { ); const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static - return type === 'checkbox' || type === 'radio' ? 'change' : 'input'; + return [type === 'checkbox' || type === 'radio' ? 'change' : 'input']; } - if (node.name === 'textarea') return 'input'; - if (attribute.name === 'currentTime') return 'timeupdate'; - if (attribute.name === 'duration') return 'durationchange'; - if (attribute.name === 'paused') return 'pause'; - if (attribute.name === 'buffered') return 'progress'; - if (attribute.name === 'seekable') return 'timeupdate'; - if (attribute.name === 'played') return 'timeupdate'; + if (node.name === 'textarea') return ['input']; + if (attribute.name === 'currentTime') return ['timeupdate']; + if (attribute.name === 'duration') return ['durationchange']; + if (attribute.name === 'paused') return ['pause']; + if (attribute.name === 'buffered') return ['progress', 'loadedmetadata']; + if (attribute.name === 'seekable') return ['loadedmetadata']; + if (attribute.name === 'played') return ['timeupdate']; - return 'change'; + return ['change']; } function getBindingValue( @@ -249,6 +238,7 @@ function getBindingValue( node: Node, attribute: Node, isMultipleSelect: boolean, + isMediaElement: boolean, bindingGroup: number, type: string ) { @@ -276,6 +266,10 @@ function getBindingValue( return `@toNumber(${state.parentNode}.${attribute.name})`; } + if (isMediaElement && attribute.name === 'buffered' || attribute.name === 'seekable' || attribute.name === 'played') { + return `@timeRangesToArray(${state.parentNode}.${attribute.name})` + } + // everything else return `${state.parentNode}.${attribute.name}`; } diff --git a/src/shared/dom.js b/src/shared/dom.js index e3b1676e1c..65c38061ff 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -102,6 +102,14 @@ export function toNumber(value) { return value === '' ? undefined : +value; } +export function timeRangesToArray(ranges) { + var array = []; + for (let i = 0; i < ranges.length; i += 1) { + array.push({ start: ranges.start(i), end: ranges.end(i) }); + } + return array; +} + export function children (element) { return Array.from(element.childNodes); } diff --git a/test/js/samples/media-bindings/expected-bundle.js b/test/js/samples/media-bindings/expected-bundle.js new file mode 100644 index 0000000000..631999398c --- /dev/null +++ b/test/js/samples/media-bindings/expected-bundle.js @@ -0,0 +1,320 @@ +function noop() {} + +function assign(target) { + var k, + source, + i = 1, + len = arguments.length; + for (; i < len; i++) { + source = arguments[i]; + for (k in source) target[k] = source[k]; + } + + return target; +} + +function insertNode(node, target, anchor) { + target.insertBefore(node, anchor); +} + +function detachNode(node) { + node.parentNode.removeChild(node); +} + +function createElement(name) { + return document.createElement(name); +} + +function addListener(node, event, handler) { + node.addEventListener(event, handler, false); +} + +function removeListener(node, event, handler) { + node.removeEventListener(event, handler, false); +} + +function timeRangesToArray(ranges) { + var array = []; + for (let i = 0; i < ranges.length; i += 1) { + array.push({ start: ranges.start(i), end: ranges.end(i) }); + } + return array; +} + +function destroy(detach) { + this.destroy = noop; + this.fire('destroy'); + this.set = this.get = noop; + + if (detach !== false) this._fragment.unmount(); + this._fragment.destroy(); + this._fragment = this._state = null; +} + +function differs(a, b) { + return a !== b || ((a && typeof a === 'object') || typeof a === 'function'); +} + +function dispatchObservers(component, group, changed, newState, oldState) { + for (var key in group) { + if (!changed[key]) continue; + + var newValue = newState[key]; + var oldValue = oldState[key]; + + var callbacks = group[key]; + if (!callbacks) continue; + + for (var i = 0; i < callbacks.length; i += 1) { + var callback = callbacks[i]; + if (callback.__calling) continue; + + callback.__calling = true; + callback.call(component, newValue, oldValue); + callback.__calling = false; + } + } +} + +function get(key) { + return key ? this._state[key] : this._state; +} + +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) { + handlers[i].call(this, data); + } +} + +function observe(key, callback, options) { + var group = options && options.defer + ? this._observers.post + : this._observers.pre; + + (group[key] || (group[key] = [])).push(callback); + + if (!options || options.init !== false) { + callback.__calling = true; + callback.call(this, this._state[key]); + callback.__calling = false; + } + + return { + cancel: function() { + var index = group[key].indexOf(callback); + if (~index) group[key].splice(index, 1); + } + }; +} + +function on(eventName, handler) { + if (eventName === 'teardown') return this.on('destroy', 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 (differs(newState[key], oldState[key])) changed[key] = dirty = true; + } + if (!dirty) return; + + this._state = assign({}, oldState, newState); + this._recompute(changed, this._state, oldState, false); + if (this._bind) this._bind(changed, this._state); + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.update(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); +} + +function callAll(fns) { + while (fns && fns.length) fns.pop()(); +} + +function _mount(target, anchor) { + this._fragment.mount(target, anchor); +} + +function _unmount() { + this._fragment.unmount(); +} + +var proto = { + destroy: destroy, + get: get, + fire: fire, + observe: observe, + on: on, + set: set, + teardown: destroy, + _recompute: noop, + _set: _set, + _mount: _mount, + _unmount: _unmount +}; + +function create_main_fragment(state, component) { + var audio, audio_updating = false, audio_animationframe, audio_paused_value = true; + + function audio_progress_loadedmetadata_handler() { + audio_updating = true; + component.set({ buffered: timeRangesToArray(audio.buffered) }); + audio_updating = false; + } + + function audio_loadedmetadata_handler() { + audio_updating = true; + component.set({ seekable: timeRangesToArray(audio.seekable) }); + audio_updating = false; + } + + function audio_timeupdate_handler() { + audio_updating = true; + component.set({ played: timeRangesToArray(audio.played) }); + audio_updating = false; + } + + function audio_timeupdate_handler_1() { + audio_updating = true; + cancelAnimationFrame(audio_animationframe); + if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler_1); + component.set({ currentTime: audio.currentTime }); + audio_updating = false; + } + + function audio_durationchange_handler() { + audio_updating = true; + component.set({ duration: audio.duration }); + audio_updating = false; + } + + function audio_pause_handler() { + audio_updating = true; + component.set({ paused: audio.paused }); + audio_updating = false; + } + + return { + create: function() { + audio = createElement("audio"); + addListener(audio, "play", audio_pause_handler); + this.hydrate(); + }, + + hydrate: function(nodes) { + component._root._beforecreate.push(audio_progress_loadedmetadata_handler); + + addListener(audio, "progress", audio_progress_loadedmetadata_handler); + addListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler); + + component._root._beforecreate.push(audio_loadedmetadata_handler); + + addListener(audio, "loadedmetadata", audio_loadedmetadata_handler); + + component._root._beforecreate.push(audio_timeupdate_handler); + + addListener(audio, "timeupdate", audio_timeupdate_handler); + + component._root._beforecreate.push(audio_timeupdate_handler_1); + + addListener(audio, "timeupdate", audio_timeupdate_handler_1); + + component._root._beforecreate.push(audio_durationchange_handler); + + addListener(audio, "durationchange", audio_durationchange_handler); + + component._root._beforecreate.push(audio_pause_handler); + + addListener(audio, "pause", audio_pause_handler); + }, + + mount: function(target, anchor) { + insertNode(audio, target, anchor); + }, + + update: function(changed, state) { + if (!audio_updating && !isNaN(state.currentTime )) { + audio.currentTime = state.currentTime ; + } + + if (audio_paused_value !== (audio_paused_value = state.paused)) { + audio[audio_paused_value ? "pause" : "play"](); + } + }, + + unmount: function() { + detachNode(audio); + }, + + destroy: function() { + removeListener(audio, "progress", audio_progress_loadedmetadata_handler); + removeListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler); + removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler); + removeListener(audio, "timeupdate", audio_timeupdate_handler); + removeListener(audio, "timeupdate", audio_timeupdate_handler_1); + removeListener(audio, "durationchange", audio_durationchange_handler); + removeListener(audio, "pause", audio_pause_handler); + removeListener(audio, "play", audio_pause_handler); + } + }; +} + +function SvelteComponent(options) { + this.options = options; + this._state = options.data || {}; + + this._observers = { + pre: Object.create(null), + post: Object.create(null) + }; + + this._handlers = Object.create(null); + + this._root = options._root || this; + this._yield = options._yield; + this._bind = options._bind; + + if (!options._root) { + this._oncreate = []; + this._beforecreate = []; + } + + this._fragment = create_main_fragment(this._state, this); + + if (options.target) { + this._fragment.create(); + this._fragment.mount(options.target, options.anchor || null); + + callAll(this._beforecreate); + } +} + +assign(SvelteComponent.prototype, proto ); + +export default SvelteComponent; diff --git a/test/js/samples/media-bindings/expected.js b/test/js/samples/media-bindings/expected.js new file mode 100644 index 0000000000..52a6a00a53 --- /dev/null +++ b/test/js/samples/media-bindings/expected.js @@ -0,0 +1,141 @@ +import { addListener, assign, callAll, createElement, detachNode, insertNode, proto, removeListener, timeRangesToArray } from "svelte/shared.js"; + +function create_main_fragment(state, component) { + var audio, audio_updating = false, audio_animationframe, audio_paused_value = true; + + function audio_progress_loadedmetadata_handler() { + audio_updating = true; + component.set({ buffered: timeRangesToArray(audio.buffered) }); + audio_updating = false; + } + + function audio_loadedmetadata_handler() { + audio_updating = true; + component.set({ seekable: timeRangesToArray(audio.seekable) }); + audio_updating = false; + } + + function audio_timeupdate_handler() { + audio_updating = true; + component.set({ played: timeRangesToArray(audio.played) }); + audio_updating = false; + } + + function audio_timeupdate_handler_1() { + audio_updating = true; + cancelAnimationFrame(audio_animationframe); + if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler_1); + component.set({ currentTime: audio.currentTime }); + audio_updating = false; + } + + function audio_durationchange_handler() { + audio_updating = true; + component.set({ duration: audio.duration }); + audio_updating = false; + } + + function audio_pause_handler() { + audio_updating = true; + component.set({ paused: audio.paused }); + audio_updating = false; + } + + return { + create: function() { + audio = createElement("audio"); + addListener(audio, "play", audio_pause_handler); + this.hydrate(); + }, + + hydrate: function(nodes) { + component._root._beforecreate.push(audio_progress_loadedmetadata_handler); + + addListener(audio, "progress", audio_progress_loadedmetadata_handler); + addListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler); + + component._root._beforecreate.push(audio_loadedmetadata_handler); + + addListener(audio, "loadedmetadata", audio_loadedmetadata_handler); + + component._root._beforecreate.push(audio_timeupdate_handler); + + addListener(audio, "timeupdate", audio_timeupdate_handler); + + component._root._beforecreate.push(audio_timeupdate_handler_1); + + addListener(audio, "timeupdate", audio_timeupdate_handler_1); + + component._root._beforecreate.push(audio_durationchange_handler); + + addListener(audio, "durationchange", audio_durationchange_handler); + + component._root._beforecreate.push(audio_pause_handler); + + addListener(audio, "pause", audio_pause_handler); + }, + + mount: function(target, anchor) { + insertNode(audio, target, anchor); + }, + + update: function(changed, state) { + if (!audio_updating && !isNaN(state.currentTime )) { + audio.currentTime = state.currentTime ; + } + + if (audio_paused_value !== (audio_paused_value = state.paused)) { + audio[audio_paused_value ? "pause" : "play"](); + } + }, + + unmount: function() { + detachNode(audio); + }, + + destroy: function() { + removeListener(audio, "progress", audio_progress_loadedmetadata_handler); + removeListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler); + removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler); + removeListener(audio, "timeupdate", audio_timeupdate_handler); + removeListener(audio, "timeupdate", audio_timeupdate_handler_1); + removeListener(audio, "durationchange", audio_durationchange_handler); + removeListener(audio, "pause", audio_pause_handler); + removeListener(audio, "play", audio_pause_handler); + } + }; +} + +function SvelteComponent(options) { + this.options = options; + this._state = options.data || {}; + + this._observers = { + pre: Object.create(null), + post: Object.create(null) + }; + + this._handlers = Object.create(null); + + this._root = options._root || this; + this._yield = options._yield; + this._bind = options._bind; + + if (!options._root) { + this._oncreate = []; + this._beforecreate = []; + } + + this._fragment = create_main_fragment(this._state, this); + + if (options.target) { + this._fragment.create(); + this._fragment.mount(options.target, options.anchor || null); + + callAll(this._beforecreate); + } +} + +assign(SvelteComponent.prototype, proto ); + +export default SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/media-bindings/input.html b/test/js/samples/media-bindings/input.html new file mode 100644 index 0000000000..07a5cc1bc1 --- /dev/null +++ b/test/js/samples/media-bindings/input.html @@ -0,0 +1 @@ +