Skip to content

Commit

Permalink
Handle CSSUnitValue inset and update range measurements on source and…
Browse files Browse the repository at this point in the history
… subject resize
  • Loading branch information
johannesodland committed Dec 21, 2023
1 parent 665687c commit 6cf1d53
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 89 deletions.
120 changes: 120 additions & 0 deletions src/proxy-cssom.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { createAType, invertType, multiplyTypes, to, toSum } from "./numeric-values";
import { simplifyCalculation } from "./simplify-calculation";

export function installCSSOM() {
// Object for storing details associated with an object which are to be kept
Expand Down Expand Up @@ -68,7 +69,126 @@ export function installCSSOM() {
}
}

/**
* Parse a CSSUnitValue from the passed string
* @param {string} str
* @return {CSSUnitValue}
*/
function parseCSSUnitValue(str) {
const UNIT_VALUE_REGEXP = /^(-?\d*[.]?\d+)(r?em|r?ex|r?cap|r?ch|r?ic|r?lh|[sld]?v(w|h|i|b|min|max)|cm|mm|Q|in|pt|pc|px|%)?$/;
const match = str.match(UNIT_VALUE_REGEXP);
if (match) {
let [_, v, unit] = match;
if (typeof unit === 'undefined') {
unit = 'number';
} else if (unit === '%') {
unit = 'percent';
}
return new CSSUnitValue(parseFloat(v), unit);
} else {
throw new SyntaxError(`Unsupported syntax ${str}`);
}
}

/**
* Parse the string as a CSSMathProduct
* @param {string} str
* @return {CSSMathProduct}
*/
function parseCSSMultiplication(str) {
let values = [];
const tokens = str.split(/(?<!\([^\)]*)([*])(?![^\(]*\))/);
values.push(parseCSSDivision(tokens.shift()));
while (tokens.length) {
tokens.shift(); // Consume operator '*'
values.push(parseCSSDivision(tokens.shift()));
}
return new CSSMathProduct(...values);
}

/**
* Parse the string as a CSSMathProduct
* @param {string} str
* @return {CSSMathProduct}
*/
function parseCSSDivision(str) {
let values = [];
const tokens = str.split(/(?<!\([^\)]*)([/])(?![^\(]*\))/);
values.push(parseCSSNumericValue(tokens.shift()));
while (tokens.length) {
tokens.shift(); // Consume operator '/'
values.push(new CSSMathInvert(parseCSSNumericValue(tokens.shift())));
}
return new CSSMathProduct(...values);
}

/**
* Parse the string as a CSSMathSum
* @param {string} str
* @return {CSSMathSum}
*/
function parseCSSMathSum(str) {
let values = [];
const tokens = str.split(/(?<!\([^\)]*)(\s[+-]\s)(?![^\(]*\))/);
values.push(parseCSSMultiplication(tokens.shift()));
while (tokens.length) {
let op = tokens.shift();
let val = tokens.shift();
if (op.trim() === '+') {
values.push(parseCSSMultiplication(val));
} else if (op.trim() === '-') {
values.push(new CSSMathNegate(parseCSSMultiplication(val)));
}
}
return new CSSMathSum(...values);
}

/**
* Parse math function form the passed string and return a matching CSSMathValue
* @param {string} str
* @return {CSSMathValue}
*/
function parseMathFunction(str) {
const MATH_VALUE_REGEXP = /^(calc|min|max)?\((.*)\)$/;
const match = str.match(MATH_VALUE_REGEXP);
if (match) {
let [_, operation = 'parens', value] = match;
switch (operation) {
case 'calc':
case 'parens':
return parseCSSMathSum(value);
case 'min':
return new CSSMathMin(...value.split(',').map(parseCSSNumericValue));
case 'max':
return new CSSMathMax(...value.split(',').map(parseCSSNumericValue));
}
} else {
throw new SyntaxError(`Unsupported syntax ${str}`);
}
}

/**
* A naive parsing function parsing the input string and returning a CSSNumericValue.
* It supports simple expressions as 'calc(10em + 10px)'
*
* @param {string} value
* @return {CSSNumericValue}
*/
function parseCSSNumericValue(value) {
value = value.trim();
if (value.match(/^[a-z(]/i)) {
return parseMathFunction(value);
} else {
return parseCSSUnitValue(value);
}
}

const cssOMTypes = {
'CSSNumericValue': class {
static parse(value) {
return simplifyCalculation(parseCSSNumericValue(value), {});
}
},
'CSSUnitValue': class {
constructor(value, unit) {
privateDetails.set(this, {
Expand Down
145 changes: 99 additions & 46 deletions src/scroll-timeline-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,11 @@ export function measureSource (source) {
clientWidth: source.clientWidth,
clientHeight: source.clientHeight,
writingMode: style.writingMode,
direction: style.direction
direction: style.direction,
scrollPaddingTop: style.scrollPaddingTop,
scrollPaddingBottom: style.scrollPaddingBottom,
scrollPaddingLeft: style.scrollPaddingLeft,
scrollPaddingRight: style.scrollPaddingRight
};
}

Expand All @@ -199,11 +203,13 @@ export function measureSubject(source, subject) {
}
left -= source.offsetLeft + source.clientLeft;
top -= source.offsetTop + source.clientTop;
const style = getComputedStyle(subject);
return {
top,
left,
offsetWidth: subject.offsetWidth,
offsetHeight: subject.offsetHeight
offsetHeight: subject.offsetHeight,
fontSize: style.fontSize,
};
}

Expand Down Expand Up @@ -287,7 +293,7 @@ function updateSource(timeline, source) {
updateMeasurements(record.target);
}
});
mutationObserver.observe(source, {attributes: true, attributeFilter: ['style']});
mutationObserver.observe(source, {attributes: true, attributeFilter: ['style', 'class']});

const scrollListener = () => {
// Sample and store scroll pos
Expand All @@ -304,6 +310,7 @@ function updateSource(timeline, source) {
scrollEventSource(source).addEventListener("scroll", scrollListener);
details.disconnect = () => {
resizeObserver.disconnect();
mutationObserver.disconnect();
scrollEventSource(source).removeEventListener("scroll", scrollListener);
};
}
Expand Down Expand Up @@ -368,7 +375,7 @@ export class ScrollTimeline {

// View timeline
subject: null,
inset: (options ? options.inset : null),
inset: null,

// Internal members
animations: [],
Expand Down Expand Up @@ -599,23 +606,29 @@ export function calculateRange(phase, sourceMeasurements, subjectMeasurements, a
const rtl = sourceMeasurements.direction == 'rtl' || sourceMeasurements.writingMode == 'vertical-rl';
let viewSize = undefined;
let viewPos = undefined;
let containerSize = undefined;
let sizes = {
fontSize: subjectMeasurements.fontSize
};
if (axis == 'x' ||
(axis == 'inline' && horizontalWritingMode) ||
(axis == 'block' && !horizontalWritingMode)) {
viewSize = subjectMeasurements.offsetWidth;
viewPos = subjectMeasurements.left;
if (rtl)
sizes.scrollPadding = [sourceMeasurements.scrollPaddingLeft, sourceMeasurements.scrollPaddingRight];
if (rtl) {
viewPos += sourceMeasurements.scrollWidth - sourceMeasurements.clientWidth;
containerSize = sourceMeasurements.clientWidth;
sizes.scrollPadding = [sourceMeasurements.scrollPaddingRight, sourceMeasurements.scrollPaddingLeft];
}
sizes.containerSize = sourceMeasurements.clientWidth;
} else {
// TODO: support sideways-lr
viewSize = subjectMeasurements.offsetHeight;
viewPos = subjectMeasurements.top;
containerSize = sourceMeasurements.clientHeight;
sizes.scrollPadding = [sourceMeasurements.scrollPaddingTop, sourceMeasurements.scrollPaddingBottom];
sizes.containerSize = sourceMeasurements.clientHeight;
}

const inset = parseInset(optionsInset, containerSize);
const inset = calculateInset(optionsInset, sizes);

// Cover:
// 0% progress represents the position at which the start border edge of the
Expand All @@ -624,7 +637,7 @@ export function calculateRange(phase, sourceMeasurements, subjectMeasurements, a
// 100% progress represents the position at which the end border edge of the
// element’s principal box coincides with the start edge of its view progress
// visibility range.
const coverStartOffset = viewPos - containerSize + inset.end;
const coverStartOffset = viewPos - sizes.containerSize + inset.end;
const coverEndOffset = viewPos + viewSize - inset.start;

// Contain:
Expand All @@ -647,7 +660,7 @@ export function calculateRange(phase, sourceMeasurements, subjectMeasurements, a

let startOffset = undefined;
let endOffset = undefined;
const targetIsTallerThanContainer = viewSize > containerSize ? true : false;
const targetIsTallerThanContainer = viewSize > sizes.containerSize ? true : false;

switch(phase) {
case 'cover':
Expand Down Expand Up @@ -683,47 +696,74 @@ export function calculateRange(phase, sourceMeasurements, subjectMeasurements, a
return { start: startOffset, end: endOffset };
}

function validateInset(value) {
// Validating insets when constructing ViewTimeline by running the parse function.
// TODO: parse insets to CSSNumericValue when constructing ViewTimeline
parseInset(value, 0)
function parseInset(value) {
const inset = { start: 0, end: 0 };

if (!value) return inset;

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') {
return 'auto';
} else {
try {
return CSSNumericValue.parse(str);
} catch (e) {
throw TypeError('Invalid inset');
}
}
});
}
if (parts.length === 0 || parts.length > 2) {
throw TypeError('Invalid inset');
}

// Validate that the parts are 'auto' or <length-percentage>
for (const part of parts) {
if (part === 'auto') {
continue;
}
const type = part.type();
if (!(type.length === 1 || type.percent === 1)) {
throw TypeError('Invalid inset');
}
}

return {
start: parts[0],
end: parts[1] ?? parts[0]
};
}

function parseInset(value, containerSize) {
function calculateInset(value, sizes) {
const inset = { start: 0, end: 0 };

if(!value)
return inset;

const parts = value.split(' ');
const insetParts = [];
parts.forEach(part => {
// TODO: Add support for relative lengths (e.g. em)
if(part.endsWith("%"))
insetParts.push(containerSize / 100 * parseFloat(part));
else if(part.endsWith("px"))
insetParts.push(parseFloat(part));
else if(part === "auto")
insetParts.push(0);
else
throw TypeError("Unsupported inset. Only % and px values are supported (for now).");
});
if (!value) return inset;

if (insetParts.length > 2) {
throw TypeError("Invalid inset");
}
const [start, end] = [value.start, value.end].map((part, i) => {
if (part === 'auto') {
return sizes.scrollPadding[i] === 'auto' ? 0 : parseFloat(sizes.scrollPadding[i]);
}

if(insetParts.length == 1) {
inset.start = insetParts[0];
inset.end = insetParts[0];
} else if(insetParts.length == 2) {
inset.start = insetParts[0];
inset.end = insetParts[1];
}
const simplifiedUnit = simplifyCalculation(part, {
percentageReference: CSS.px(sizes.containerSize),
fontSize: CSS.px(parseFloat(sizes.fontSize))
});
if (simplifiedUnit instanceof CSSUnitValue && simplifiedUnit.unit === 'px') {
return simplifiedUnit.value;
} else {
throw TypeError('Unsupported inset.');
}
});

return inset;
return { start, end };
}


// Calculate the fractional offset of a (phase, percent) pair relative to the
// full cover range.
export function relativePosition(timeline, phase, offset) {
Expand Down Expand Up @@ -763,11 +803,16 @@ export class ViewTimeline extends ScrollTimeline {
details.subject = options && options.subject ? options.subject : undefined;
// TODO: Handle insets.
if (options && options.inset) {
validateInset(options.inset)
details.inset = parseInset(options.inset);
}
if (details.subject) {
const mutationObserver = new MutationObserver(() => {
updateMeasurements(details.source);
});
mutationObserver.observe(details.subject, {attributes: true, attributeFilter: ['class', 'style']});
}

validateSource(this);
details.subjectMeasurements = measureSubject(details.source, details.subject)
details.subjectMeasurements = measureSubject(details.source, details.subject);
updateInternal(this);
}

Expand Down Expand Up @@ -805,4 +850,12 @@ export class ViewTimeline extends ScrollTimeline {
return CSS.percent(100 * progress);
}

get startOffset() {
return CSS.px(range(this,'cover').start);
}

get endOffset() {
return CSS.px(range(this,'cover').end);
}

}
Loading

0 comments on commit 6cf1d53

Please sign in to comment.