motion-dom
Version:
147 lines (144 loc) • 6.46 kB
JavaScript
import { resolveElements } from '../../utils/resolve-elements.mjs';
let nameCount = 0;
/**
* Generated names live in their own namespace so we can tell a name we own
* (and must clean up) from an author-defined one - and so a stale generated
* name left behind by an interrupted transition is re-owned, not mistaken for
* the author's and leaked.
*/
const generatedName = () => `motion-view-${nameCount++}`;
const isGeneratedName = (name) => name.startsWith("motion-view-");
/**
* Tag a captured element with a `view-transition-class` so authors can target
* its generated layer from CSS (e.g. `::view-transition-group(.hero)`) without
* the opaque generated name. Tracked in `classed` - separate from the generated
* names in `assigned` - so cleanup removes the class without ever stripping an
* author's own inline `view-transition-name`.
*/
function tagClass(element, className, classed) {
if (!className)
return;
element.style?.setProperty("view-transition-class", className);
classed.push(element);
}
/**
* Set the element's `view-transition-group` so its layer reconstructs the DOM
* hierarchy in the pseudo-tree (`contain`) - or stays flat (`none`). Tracked in
* `grouped` for cleanup. When the element clips (any non-`visible` overflow) its
* name is recorded in `clipChildren` so the caller can clip the nested children
* (`::view-transition-group-children(name)`), mirroring the live clip through
* the whole transition rather than only at the live-DOM handoff.
*
* Ignored by browsers without nested view-transition groups, where it harmlessly
* degrades to the flat default.
*/
function applyGroup(element, name, group, grouped, clipChildren) {
if (!group)
return;
element.style?.setProperty("view-transition-group", group);
grouped.push(element);
if (group !== "none" && clipChildren) {
const style = getComputedStyle(element);
if (style.overflowX !== "visible" || style.overflowY !== "visible") {
clipChildren.add(name);
}
}
}
/**
* Resolve a selector/Element to elements and ensure each one carries a
* `view-transition-name` we can target from script.
*
* Author-defined names are reused as-is. Elements that are unnamed (or use
* the browser's `auto`/`match-element`, whose generated name is not exposed
* to script) are given a unique generated name, set inline so it's captured,
* and tracked in `assigned` for later cleanup.
*
* `registry` maps each Element to its name so the same element keeps its name
* across both captures (before and after the update), which is what allows a
* persistent element to animate as a single `group` layer.
*/
function assignViewTransitionNames(definition, registry, assigned, forcedNames, className, classed = [], group, grouped = [], clipChildren) {
const elements = resolveElements(definition);
/**
* The new end of a paired morph: give each element the matching name from
* the old end (by index) so the two share one layer and morph. If the new
* end resolves to *more* elements than the old end named, the extras have no
* counterpart - give them a fresh name so they animate as newcomers rather
* than being silently left unnamed. We return the names actually assigned
* (sized to the resolved elements), not the raw `forcedNames`, so stagger
* totals and the layer set stay in step with what's on the page.
*/
if (forcedNames) {
return elements.map((element, i) => {
const existing = registry.get(element);
if (existing)
return existing;
const name = forcedNames[i] ?? generatedName();
element.style?.setProperty("view-transition-name", name);
assigned.push(element);
registry.set(element, name);
tagClass(element, className, classed);
applyGroup(element, name, group, grouped, clipChildren);
return name;
});
}
/**
* Read every current name up front, before assigning any. Interleaving the
* reads with the inline `setProperty` writes below would dirty styles
* between reads and force a style recalc per element; batching the reads
* keeps it to one. Elements already in the registry keep their name and
* need no read.
*/
const currentNames = elements.map((element) => registry.has(element)
? undefined
: getComputedStyle(element).getPropertyValue("view-transition-name"));
return elements.map((element, i) => {
const existing = registry.get(element);
if (existing)
return existing;
const current = currentNames[i];
let name;
if (current &&
current !== "none" &&
current !== "auto" &&
current !== "match-element" &&
!isGeneratedName(current)) {
/**
* The author already named this layer - target it as-is and leave
* it to them to clean up. `auto`/`match-element` are overridden
* because their generated name is not exposed to script, and a
* stale `motion-view-*` (e.g. left by an interrupted transition) is
* re-owned rather than adopted as an author name and leaked.
*/
name = current;
}
else {
name = generatedName();
element.style?.setProperty("view-transition-name", name);
assigned.push(element);
}
registry.set(element, name);
tagClass(element, className, classed);
applyGroup(element, name, group, grouped, clipChildren);
return name;
});
}
/**
* Remove the `view-transition-name`s we generated and the
* `view-transition-class`es we applied. Author-defined names are never touched
* (they're not in `assigned`). Safe to call more than once (e.g. on both a
* finished and an interrupted transition).
*/
function releaseViewTransitionNames(assigned, classed = [], grouped = []) {
for (const element of assigned) {
element.style?.removeProperty("view-transition-name");
}
for (const element of classed) {
element.style?.removeProperty("view-transition-class");
}
for (const element of grouped) {
element.style?.removeProperty("view-transition-group");
}
}
export { assignViewTransitionNames, releaseViewTransitionNames };
//# sourceMappingURL=assign-names.mjs.map