diff --git a/src/proxy-animation.js b/src/proxy-animation.js index 79b7c95..72fb8eb 100644 --- a/src/proxy-animation.js +++ b/src/proxy-animation.js @@ -5,14 +5,13 @@ import { removeAnimation, fractionalOffset, } from "./scroll-timeline-base"; +import {splitIntoComponentValues} from './utils'; const nativeDocumentGetAnimations = document.getAnimations; const nativeElementGetAnimations = window.Element.prototype.getAnimations; const nativeElementAnimate = window.Element.prototype.animate; const nativeAnimation = window.Animation; -const rangeNameRegExp = new RegExp(`(${ANIMATION_RANGE_NAMES.join('|')})(?!-)`); - class PromiseWrapper { constructor() { this.state = 'pending'; @@ -926,7 +925,7 @@ function getNormalEndRange(timeline) { if (timeline instanceof ViewTimeline) { return { rangeName: 'cover', offset: CSS.percent(100) }; } - + if (timeline instanceof ScrollTimeline) { return CSS.percent(100); } @@ -951,32 +950,36 @@ function parseAnimationRange(timeline, value) { // // --> cover cover // TODO: Support all formatting options once ratified in the spec. - const parts = value.split(/\s+/); + const parts = splitIntoComponentValues(value); const rangeNames = []; const offsets = []; - + parts.forEach(part => { - if (part.endsWith('%')) - offsets.push(parseFloat(part)); - else + if (ANIMATION_RANGE_NAMES.includes(part)) { rangeNames.push(part); + } else { + try { + offsets.push(CSSNumericValue.parse(part)); + } catch (e) { + throw TypeError(`Could not parse range "${value}"`); + } + } }); - + if (rangeNames.length > 2 || offsets.length > 2 || offsets.length == 1) { throw TypeError("Invalid time range or unsupported time range format."); } - + if (rangeNames.length) { animationRange.start.rangeName = rangeNames[0]; animationRange.end.rangeName = rangeNames.length > 1 ? rangeNames[1] : rangeNames[0]; } - - // TODO: allow calc() in the offsets + if (offsets.length > 1) { - animationRange.start.offset = CSS.percent(offsets[0]); - animationRange.end.offset = CSS.percent(offsets[1]); + animationRange.start.offset = offsets[0]; + animationRange.end.offset = offsets[1]; } - + return animationRange; } @@ -1017,7 +1020,7 @@ function parseTimelineRangePart(timeline, value, position) { } // Author passed in something like `"cover 100%"` else { - const parts = value.split(rangeNameRegExp).map(part => part.trim()).filter(Boolean); + const parts = splitIntoComponentValues(value); if (parts.length === 1) { if (ANIMATION_RANGE_NAMES.includes(parts[0])) { diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js index ce71cc4..195d1b8 100644 --- a/src/scroll-timeline-base.js +++ b/src/scroll-timeline-base.js @@ -14,7 +14,7 @@ import {installCSSOM} from "./proxy-cssom.js"; import {simplifyCalculation} from "./simplify-calculation"; -import {normalizeAxis} from './utils.js'; +import {normalizeAxis, splitIntoComponentValues} from './utils.js'; installCSSOM(); @@ -721,17 +721,14 @@ function parseInset(value) { let parts = value; // Parse string parts to if (typeof value === 'string') { - // Split value into separate parts - const stringParts = value.split(/(? { - if (str.trim() === 'auto') { + parts = splitIntoComponentValues(value).map(str => { + if (str === 'auto') { return 'auto'; - } else { - try { - return CSSNumericValue.parse(str); - } catch (e) { - throw TypeError('Invalid inset'); - } + } + try { + return CSSNumericValue.parse(str); + } catch (e) { + throw TypeError(`Could not parse inset "${value}"`); } }); } @@ -784,7 +781,7 @@ function calculateInset(value, sizes) { export function fractionalOffset(timeline, value) { if (timeline instanceof ViewTimeline) { const { rangeName, offset } = value; - + const phaseRange = range(timeline, rangeName); const coverRange = range(timeline, 'cover'); @@ -794,17 +791,17 @@ export function fractionalOffset(timeline, value) { if (timeline instanceof ScrollTimeline) { const { axis, source } = timeline; const { sourceMeasurements } = sourceDetails.get(source); - + let sourceScrollDistance = undefined; if (normalizeAxis(axis, sourceMeasurements) === 'x') { sourceScrollDistance = source.scrollWidth; } else { sourceScrollDistance = source.scrollHeight; } - + const position = resolvePx(value, sourceScrollDistance); const fractionalOffset = position / sourceScrollDistance; - + return fractionalOffset; } diff --git a/src/utils.js b/src/utils.js index 73a6538..133a922 100644 --- a/src/utils.js +++ b/src/utils.js @@ -21,4 +21,60 @@ export function normalizeAxis(axis, computedStyle) { } return axis; +} + +/** + * Split an input string into a list of individual component value strings, + * so that each can be handled as a keyword or parsed with `CSSNumericValue.parse()`; + * + * Examples: + * splitIntoComponentValues('cover'); // ['cover'] + * splitIntoComponentValues('auto 0%'); // ['auto', '100%'] + * splitIntoComponentValues('calc(0% + 50px) calc(100% - 50px)'); // ['calc(0% + 50px)', 'calc(100% - 50px)'] + * splitIntoComponentValues('1px 2px').map(val => CSSNumericValue.parse(val)) // [new CSSUnitValue(1, 'px'), new CSSUnitValue(2, 'px')] + * + * @param {string} input + * @return {string[]} + */ +export function splitIntoComponentValues(input) { + const res = []; + let i = 0; + + function consumeComponentValue() { + let level = 0; + const startIndex = i; + while (i < input.length) { + const nextChar = input.slice(i, i + 1); + if (/\s/.test(nextChar) && level === 0) { + break; + } else if (nextChar === '(') { + level += 1; + } else if (nextChar === ')') { + level -= 1; + if (level === 0) { + // Consume the next character and break + i++; + break; + } + } + i++; + } + return input.slice(startIndex, i); + } + + function consumeWhitespace() { + while (/\s/.test(input.slice(i, i + 1))) { + i++; + } + } + + while(i < input.length) { + const nextChar = input.slice(i, i + 1); + if (/\s/.test(nextChar)) { + consumeWhitespace(); + } else { + res.push(consumeComponentValue()); + } + } + return res; } \ No newline at end of file