Skip to content

Commit

Permalink
Add utility function to split string into component values (#209)
Browse files Browse the repository at this point in the history
  • Loading branch information
johannesodland authored Feb 1, 2024
1 parent b2f44df commit 770cb37
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 31 deletions.
35 changes: 19 additions & 16 deletions src/proxy-animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
Expand All @@ -951,32 +950,36 @@ function parseAnimationRange(timeline, value) {
// <name> <end-offset>
// <start-offset> <end-offset> --> cover <start-offset> cover <end-offset>
// 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;
}

Expand Down Expand Up @@ -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])) {
Expand Down
27 changes: 12 additions & 15 deletions src/scroll-timeline-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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(/(?<!\([^\)]*)\s(?![^\(]*\))/);
parts = stringParts.map(str => {
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}"`);
}
});
}
Expand Down Expand Up @@ -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');

Expand All @@ -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;
}

Expand Down
56 changes: 56 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit 770cb37

Please sign in to comment.