Skip to content

Commit

Permalink
snapshot
Browse files Browse the repository at this point in the history
  • Loading branch information
martrapp committed Sep 1, 2024
1 parent 5ec1341 commit b365aad
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 31 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
lib
.astro
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ElementCrossing
Preserve what matters across document transitions!
# 🚸 ElementCrossing
Transfer selected element states across cross-document view transitions.

![Build Status](https://github.com/vtbag/element-crossing/actions/workflows/run-tests.yml/badge.svg)
[![npm version](https://img.shields.io/npm/v/@vtbag/element-crossing/latest)](https://www.npmjs.com/package/@vtbag/element-crossing)
Expand All @@ -9,8 +9,8 @@ The @vtbag website can be found at https://vtbag.pages.dev/

## !!! News !!!

## What happened so far:
First official release of this code!

## What is it?

This library provides a robust solution for maintaining HTML elements and their associated states across cross-document view transitions. It is designed to enhance the user experience by ensuring that important elements and their current states are preserved as users navigate through different pages or documents.
This library provides a robust solution for maintaining HTML elements and their associated state across cross-document view transitions.
2 changes: 1 addition & 1 deletion bin/bundle
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ else
OPTS="--minify"
(cd lib && rm *.js.map) >> /dev/null 2>&1
fi
npx esbuild src/index.ts --bundle $OPTS --target=ESnext --outfile=lib/index.js
npx esbuild src/vanilla.ts src/over-the-top.ts --bundle $OPTS --target=ESnext --outdir=lib
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
{
"name": "@vtbag/element-crossing",
"version": "0.0.0",
"main": "lib/index.js",
"main": "lib/vanilla.js",
"description": "Sites using cross-document view transitions look like SPAs but they still do full page loads on navigation. The element crossing provides a way to preserve DOM elements and data objects across page loads.",
"files": [
"lib/index.js"
"lib/vanilla.js",
"lib/over-the-top.js"
],
"exports": {
".": "./lib/vanilla.js",
"./vanilla": "./lib/vanilla.js",
"./over-the-top": "./lib/over-the-top.js"
},
"scripts": {
"dev": "bin/bundle dev",
"build": "npm run format; bin/bundle",
Expand Down
24 changes: 0 additions & 24 deletions src/index.ts

This file was deleted.

103 changes: 103 additions & 0 deletions src/over-the-top.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Spec } from './types';

top!.__vtbag ??= {};
top!.__vtbag.elementCrossing ??= {};
const elementCrossing = top!.__vtbag.elementCrossing!;

console.log('[elc]', 'init');
if (top === self) {
initBorderLands();
} else if (self.parent === top) {
initHeartLand();
} else {
console.log('[elc]', 'neither BorderLands nor HeartLand');
}

function initBorderLands() {
console.log('[elc]', 'init BorderLands');
addEventListener('pagereveal', () => {
console.log('[elc]', 'DOMContentLoaded');
const topDoc = top!.document;
const root = topDoc.documentElement;
root.innerHTML = `<body style="margin:0; overflow=clip"><iframe width=${innerWidth} height=${innerHeight} src="${location.href}"/>`;
root.style.overflow = 'clip';
});
}

function initHeartLand() {
console.log('[elc]', 'init HeartLand');
self.addEventListener('pageswap', pageSwap, { once: true });
self.addEventListener('pagereveal', pageReveal, { once: true });
}

function pageSwap() {
console.log('[elc]', 'pageSwap');

self.document.querySelectorAll<HTMLElement>('[data-vtbag-x]').forEach((el) => {
let id;
const specs: Spec[] = [];
el
.getAttribute('data-vtbag-x')
?.split(' ')
.forEach((value) => {
const [kind, key] = kindAndKey(value);
switch (kind) {
case 'id':
id = key;
break;
case 'class':
specs.push({ kind, key, value: el.classList.contains(key) ? 'true' : 'false' });
break;
case 'style':
specs.push({
kind,
key,
value: '' + (el.style[key as keyof CSSStyleDeclaration] ?? ''),
});
break;
case 'attr':
specs.push({ kind, key, value: el.getAttribute(key) ?? '' });
break;
default:
console.error('[crossing]', 'unknown kind', kind);
break;
}
});
});
}

function pageReveal() {
console.log('[elc]', 'pageReveal');
const topDoc = top!.document;
topDoc.title = document.title;
top!.history.replaceState({}, '', location.href);
}

function kindAndKey(value: string) {
let [kind, key] = value.split(':');
if (key === undefined) {
key = kind.slice(1);
switch (kind[0]) {
case '#':
kind = 'id';
break;
case '.':
kind = 'class';
break;
case '@':
kind = 'attr';
break;
case '-':
kind = 'style';
break;
default:
console.error(
'[crossing]',
'syntax error:',
value,
'is not recognized as a valid specification'
);
}
}
return [kind, key];
}
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ declare global {
};
}
}

export type Spec = {
kind: string;
key: string;
value?: string;
};
export type ElementSpec = {
id: string;
specs: Spec[];
};
169 changes: 169 additions & 0 deletions src/vanilla.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { ElementSpec, Spec } from './types';

top!.__vtbag ??= {};
top!.__vtbag.elementCrossing ??= {};

init();

function init() {
self.addEventListener("onpageswap" in self ? 'pageswap' : 'pagehide', pageSwap, { once: true });
self.addEventListener("onpagereveal" in self ? 'pagereveal' : 'DOMContentLoaded', pageReveal, { once: true });
}

function pageSwap() {
console.log('pageSwap');
sessionStorage.setItem('@vtbag/element-crossing', JSON.stringify(retrieve()));
}

function pageReveal() {
console.log('pageReveal');
const values = sessionStorage.getItem('@vtbag/element-crossing');
console.log(values);
restore(JSON.parse(values ?? "[]"));
}

function retrieve() {
const known = new Set<string>();
const values: ElementSpec[] = [];
self.document
.querySelectorAll<HTMLElement>('[data-vtbag-x]')
.forEach((element) => {
const spec = elementSpec(element);
if (spec) {
if (known.has(spec.id)) console.error('[crossing]', 'non unique id', spec.id);
values.push(spec);
known.add(spec.id);
}
});
return values;
}

function elementSpec(element: HTMLElement) {
function kindAndKey(value: string) {
let [kind, key] = value.split(':', 2);
if (key === undefined) {
key = kind.slice(1);
switch (kind[0]) {
case '#':
kind = 'id';
break;
case '.':
kind = 'class';
break;
case '@':
kind = 'prop';
break;
case '-':
kind = 'style';
break;
case '~':
kind = 'anim';
break;
default:
console.error(
'[crossing]',
'syntax error:',
value,
'is not recognized as a valid specification'
);
}
}
return [kind, key];
}

let id = element.id;
const specs: Spec[] = [];
element
.getAttribute('data-vtbag-x')
?.split(' ')
.forEach((value) => {
const [kind, key] = kindAndKey(value);
switch (kind) {
case 'id':
id = key;
break;
case 'class':
specs.push({ kind, key, value: element.classList.contains(key) ? 'true' : 'false' });
break;
case 'style':
specs.push({
kind,
key,
value: '' + (element.style[key as keyof CSSStyleDeclaration] ?? ''),
});
break;
case 'bool':
case 'num':
case 'prop':
const val = element[key as keyof HTMLElement];
const type = typeof val;
specs.push({ kind: type === "boolean" ? "bool" : (type === "number" ? "num" : "prop"), key, value: "" + val });
break;
case 'anim':
const animations = element.getAnimations().filter(a => a instanceof CSSAnimation && a.animationName === key);
if (animations.length > 1) {
console.error("[crossing]", `retrieval: animation name ${key} is not unique for`, element);
}
if (animations.length > 0) {
specs.push({ kind, key, value: "" + (animations[0].currentTime ?? 0) });
} break;
default:
console.error('[crossing]', 'unknown kind', kind);
break;
}
});
if (id === undefined) console.error('[crossing]', 'missing id in', element);
else return { id, specs };
}

function restore(values: ElementSpec[]) {
values.forEach((elementSpec: ElementSpec) => {
const element = document.querySelector<HTMLElement>(
"#" + elementSpec.id +
",[data-vtbag-x*='#" + elementSpec.id +
"'],[data-vtbag-x*='id:" + elementSpec.id +
"']"
);
if (!element) return;
elementSpec.specs.forEach((s) => {
switch (s.kind) {
case 'class':
element.classList[s.value === 'true' ? 'add' : 'remove'](s.key);
break;
case 'style':
if (s.key === 'length' || s.key === 'parentRule') {
console.error(
'[crossing]',
'Cannot assign to read-only property',
s.key,
'in',
elementSpec.id
);
} else {
element.style.setProperty(s.key, s.value ?? '');
}
break;
case 'prop':
(element as any)[s.key] = s.value;
break;
case 'bool':
(element as any)[s.key] = s.value === "true";
break;
case 'num':
(element as any)[s.key] = parseFloat(s.value ?? "0");
break;
case 'anim':
const animations = element.getAnimations().filter(a => a instanceof CSSAnimation && a.animationName === s.key);
if (animations.length > 1) {
console.warn("[crossing]", `restore: animation name ${s.key} is not unique for`, element);
}
animations.forEach(a => a.currentTime = ~~(s.value ?? "0"));
element.setAttribute(s.key, s.value ?? '');
break;
default:
console.error('[crossing]', 'unknown kind', s.kind);
break;
}
});
});
}

0 comments on commit b365aad

Please sign in to comment.