@codemovie/code-movie-runtime
Advanced tools
Comparing version 2.1.0 to 2.2.0
# Changelog | ||
## 2.2.0 | ||
- **Feature:** `<code-movie-runtime>` now uses its [custom state set](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet) to track the current frame. If the current frame is 7 for example, the CSS selector `code-movie-runtime:state(frame7)` will match. | ||
- **Feature**: support auxiliary content via the `aux` slot | ||
## 2.1.0 | ||
@@ -4,0 +9,0 @@ |
@@ -1,3 +0,6 @@ | ||
var k=Object.defineProperty;var b=n=>{throw TypeError(n)};var v=(n,r,t)=>r in n?k(n,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[r]=t;var p=(n,r,t)=>v(n,typeof r!="symbol"?r+"":r,t),g=(n,r,t)=>r.has(n)||b("Cannot "+t);var s=(n,r,t)=>(g(n,r,"read from private field"),t?t.call(n):r.get(n)),l=(n,r,t)=>r.has(n)?b("Cannot add the same private member more than once"):r instanceof WeakSet?r.add(n):r.set(n,t),a=(n,r,t,e)=>(g(n,r,"write to private field"),e?e.call(n,t):r.set(n,t),t);function y(n){let r=Math.round(Number(n));return Number.isFinite(r)&&!Number.isNaN(r)?r:0}function x(n){return Math.abs(y(n))}function E(n){return n?String(n).split(/\s+/).map(x).sort((r,t)=>r-t):[]}var c,h,i,u,m,d=class d extends HTMLElement{constructor(){super();l(this,c,this.attachShadow({mode:"open"}));l(this,h);l(this,i,[]);l(this,u,0);l(this,m,null);p(this,"_handleClick",t=>{if(t.type==="click"){for(let e of t.composedPath())if(e instanceof HTMLElement){if(e.getAttribute("data-command")==="next"){this.next();return}if(e.getAttribute("data-command")==="prev"){this.prev();return}}}});let[t,e,o]=d._template();a(this,h,t),s(this,c).append(t,e,o),s(this,c).addEventListener("click",this._handleClick),t.addEventListener("slotchange",()=>this._goToCurrent())}static _template(){let t=document.createElement("slot"),e=document.createElement("slot");e.name="controls",e.innerHTML=` | ||
<div part="controls" class="defaultControls"> | ||
var S=Object.defineProperty;var w=s=>{throw TypeError(s)};var A=(s,r,t)=>r in s?S(s,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[r]=t;var b=(s,r,t)=>A(s,typeof r!="symbol"?r+"":r,t),y=(s,r,t)=>r.has(s)||w("Cannot "+t);var n=(s,r,t)=>(y(s,r,"read from private field"),t?t.call(s):r.get(s)),l=(s,r,t)=>r.has(s)?w("Cannot add the same private member more than once"):r instanceof WeakSet?r.add(s):r.set(s,t),o=(s,r,t,e)=>(y(s,r,"write to private field"),e?e.call(s,t):r.set(s,t),t),g=(s,r,t)=>(y(s,r,"access private method"),t);function _(s){let r=Math.round(Number(s));return Number.isFinite(r)&&!Number.isNaN(r)?r:0}function k(s){return Math.abs(_(s))}function C(s){return s?String(s).split(/\s+/).map(k).sort((r,t)=>r-t):[]}var d,h,a,i,c,f,x,v=class v extends HTMLElement{constructor(){super();l(this,f);b(this,"_shadow",this.attachShadow({mode:"open"}));l(this,d,new CSSStyleSheet);l(this,h,this.attachInternals());l(this,a,[]);l(this,i,0);l(this,c,null);b(this,"_handleClick",t=>{if(t.type==="click"){for(let e of t.composedPath())if(e instanceof HTMLElement){if(e.getAttribute("data-command")==="next"){this.next();return}if(e.getAttribute("data-command")==="prev"){this.prev();return}}}});let t=v._template();this._shadow.append(...t),this._shadow.addEventListener("click",this._handleClick);let e=this._shadow.querySelector("slot:not([name])");if(!e)throw new Error("Template does not contain a default slot");e.addEventListener("slotchange",()=>this._goToCurrent()),this._shadow.adoptedStyleSheets.push(n(this,d))}static _template(){let t=document.createElement("div");return t.innerHTML=`<div part="wrapper"> | ||
<slot></slot> | ||
<div part="controls-wrapper"> | ||
<slot name="controls"> | ||
<div part="controls"> | ||
<button part="controls-prevBtn" data-command="prev"> | ||
@@ -10,6 +13,12 @@ <span><</span> | ||
</div> | ||
`;let o=document.createElement("style");return o.innerHTML=` | ||
:host { display: grid; } | ||
:host(:not([controls])) slot[name=controls] { display: none } | ||
.defaultControls { position: relative; z-index: 1337; } | ||
`,[t,e,o]}static get observedAttributes(){return["keyframes","current"]}attributeChangedCallback(t,e,o){e!==o&&(t==="keyframes"?(a(this,i,E(o)),this._goToCurrent()):t==="current"&&(a(this,u,this._toKeyframeIdx(o)),this._goToCurrent()))}get controls(){return this.hasAttribute("controls")}set controls(t){t?this.setAttribute("controls","controls"):this.removeAttribute("controls")}get keyframes(){return s(this,i)}set keyframes(t){Array.isArray(t)?(t=Array.from(new Set(t.map(x).sort((e,o)=>e-o))),this.setAttribute("keyframes",t.join(" ")),a(this,i,t)):(this.removeAttribute("keyframes"),a(this,i,[])),this._goToCurrent()}_toKeyframeIdx(t){let e=y(t);return e<0&&(e=Math.abs(e)-1),e>this.maxFrame&&(e=this.maxFrame),s(this,i).indexOf(e)}get current(){return s(this,i)[s(this,u)]||0}set current(t){let e=this._toKeyframeIdx(t);e!==-1?(a(this,u,e),this.setAttribute("current",String(s(this,i)[e]))):(a(this,u,0),this.setAttribute("current","0"))}get nextCurrent(){return s(this,m)&&s(this,i)[s(this,u)]||null}get maxFrame(){return Math.max(...this.keyframes)}_goToCurrent(){let t=s(this,u);t in s(this,i)||(s(this,i).length>=1&&t<0?t=s(this,i).length-1:t=0),a(this,m,t);let e=this.dispatchEvent(new Event("cm-beforeframechange",{bubbles:!0,cancelable:!0}));a(this,m,null),e&&(this._setClass(s(this,i)[t]),t!==s(this,u)&&a(this,u,t),this.dispatchEvent(new Event("cm-afterframechange",{bubbles:!0})))}_setClass(t){let e=s(this,h).assignedElements()[0];if(e){for(let o of e.classList)/^frame[0-9]+$/.test(o)&&e.classList.remove(o);e.classList.add(`frame${t}`)}}next(){return a(this,u,s(this,u)+1),this._goToCurrent(),this.current}prev(){return a(this,u,s(this,u)-1),this._goToCurrent(),this.current}go(t){return this.current=t,this.current}};c=new WeakMap,h=new WeakMap,i=new WeakMap,u=new WeakMap,m=new WeakMap;var f=d;window.customElements.define("code-movie-runtime",f);export{f as CodeMovieRuntime}; | ||
</slot> | ||
</div> | ||
<div part="aux-wrapper"> | ||
<slot name="aux"></slot> | ||
</div> | ||
</div> | ||
<style> | ||
[part="wrapper"] { display: grid; } | ||
:host(:not([controls])) slot[name="controls"] { display: none } | ||
[part="controls"] { position: relative; z-index: 1337; } | ||
</style>`,Array.from(t.children)}static get observedAttributes(){return["keyframes","current"]}attributeChangedCallback(t,e,u){e!==u&&(t==="keyframes"?(o(this,a,C(u)),g(this,f,x).call(this),this._goToCurrent()):t==="current"&&(o(this,i,this._toKeyframeIdx(u)),this._goToCurrent()))}get controls(){return this.hasAttribute("controls")}set controls(t){t?this.setAttribute("controls","controls"):this.removeAttribute("controls")}get keyframes(){return n(this,a)}set keyframes(t){Array.isArray(t)?(t=Array.from(new Set(t.map(k).sort((e,u)=>e-u))),this.setAttribute("keyframes",t.join(" ")),o(this,a,t)):(this.removeAttribute("keyframes"),o(this,a,[])),g(this,f,x).call(this),this._goToCurrent()}_toKeyframeIdx(t){let e=_(t);return e<0&&(e=Math.abs(e)-1),e>this.maxFrame&&(e=this.maxFrame),n(this,a).indexOf(e)}get current(){return n(this,a)[n(this,i)]||0}set current(t){let e=this._toKeyframeIdx(t);e!==-1?(o(this,i,e),this.setAttribute("current",String(n(this,a)[e]))):(o(this,i,0),this.setAttribute("current","0"))}get nextCurrent(){return n(this,c)&&n(this,a)[n(this,i)]||null}get maxFrame(){return Math.max(...this.keyframes)}_goToCurrent(){let t=n(this,i);t in n(this,a)||(n(this,a).length>=1&&t<0?t=n(this,a).length-1:t=0),o(this,c,t);let e=this.dispatchEvent(new Event("cm-beforeframechange",{bubbles:!0,cancelable:!0}));return o(this,c,null),e?(this._setClassesAndStates(n(this,a)[t]),t!==n(this,i)&&o(this,i,t),this.dispatchEvent(new Event("cm-afterframechange",{bubbles:!0})),!0):!1}_setClassesAndStates(t){for(let m of n(this,h).states)/^frame[0-9]+$/.test(m)&&n(this,h).states.delete(m);n(this,h).states.add(`frame${t}`);let u=this._shadow.querySelector("slot:not([name])")?.assignedElements()[0];if(u){for(let m of u.classList)/^frame[0-9]+$/.test(m)&&u.classList.remove(m);u.classList.add(`frame${t}`)}}next(){let t=n(this,i);return o(this,i,n(this,i)+1),this._goToCurrent()?this.current:(o(this,i,t),t)}prev(){let t=n(this,i);return o(this,i,n(this,i)-1),this._goToCurrent()?this.current:(o(this,i,t),t)}go(t){let e=n(this,i);return o(this,i,this._toKeyframeIdx(t)),this._goToCurrent()?this.current:(o(this,i,e),e)}};d=new WeakMap,h=new WeakMap,a=new WeakMap,i=new WeakMap,c=new WeakMap,f=new WeakSet,x=function(){let t='slot[name="aux"]::slotted(*){ display: none }';for(let e of n(this,a))t+=`:host(:state(frame${e})) slot[name="aux"]::slotted(.frame${e}) { display: block; }`;n(this,d).replaceSync(t)};var p=v;window.customElements.define("code-movie-runtime",p);export{p as CodeMovieRuntime}; |
export declare class CodeMovieRuntime extends HTMLElement { | ||
#private; | ||
static _template(): [HTMLSlotElement, HTMLSlotElement, HTMLStyleElement]; | ||
static _template(): Element[]; | ||
_shadow: ShadowRoot; | ||
constructor(); | ||
@@ -16,4 +17,4 @@ static get observedAttributes(): string[]; | ||
get maxFrame(): number; | ||
_goToCurrent(): void; | ||
_setClass(targetKeyframe: number): void; | ||
_goToCurrent(): boolean; | ||
_setClassesAndStates(targetIdx: number): void; | ||
next(): number; | ||
@@ -20,0 +21,0 @@ prev(): number; |
{ | ||
"name": "@codemovie/code-movie-runtime", | ||
"description": "Web runtime element for Code.Movie animations", | ||
"version": "2.1.0", | ||
"version": "2.2.0", | ||
"type": "module", | ||
@@ -9,8 +9,8 @@ "main": "dist/index.js", | ||
"scripts": { | ||
"lint": "eslint && prettier . --check", | ||
"lint": "prettier . --check && eslint src test", | ||
"types": "tsc -p tsconfig.types.json", | ||
"build": "rm -rf dist && npm run types && esbuild src/index.ts --bundle --minify --format=esm --target=es2020 --outfile=dist/index.js ", | ||
"build-dev": "esbuild src/index.ts --bundle --sourcemap --format=esm --target=es2020 --outfile=dist/index.js --watch", | ||
"test": "jest", | ||
"test-dev": "jest --watch", | ||
"test": "NODE_ENV=test wtr test/**/*.test.ts --playwright --browsers firefox chromium webkit", | ||
"test-dev": "NODE_ENV=test wtr test/**/*.test.ts --playwright --browsers chromium", | ||
"prepublishOnly": "npm run types && npm run lint && npm run test && npm run build", | ||
@@ -23,12 +23,13 @@ "release": "release-it" | ||
"@eslint/js": "^9.5.0", | ||
"@types/eslint__js": "^8.42.3", | ||
"@types/jest": "^29.5.12", | ||
"@esm-bundle/chai": "^4.3.4-fix.0", | ||
"@types/sinon": "^17.0.0", | ||
"@web/dev-server-esbuild": "^1.0.3", | ||
"@web/test-runner": "^0.19.0", | ||
"@web/test-runner-playwright": "^0.11.0", | ||
"esbuild": "^0.24.0", | ||
"eslint": "^8.57.0", | ||
"eslint-config-prettier": "^9.1.0", | ||
"jest": "^29.7.0", | ||
"jest-environment-jsdom": "^29.7.0", | ||
"eslint": "^9.0.0", | ||
"eslint-config-prettier": "^10.0.0", | ||
"prettier": "^3.3.2", | ||
"release-it": "^17.4.0", | ||
"ts-jest": "^29.1.5", | ||
"release-it": "^18.0.0", | ||
"sinon": "^19.0.0", | ||
"typescript": "^5.5.2", | ||
@@ -41,3 +42,3 @@ "typescript-eslint": "^8.8.0" | ||
}, | ||
"homepage": "https://github.com/CodeMovie/code-movie-runtime", | ||
"homepage": "https://code.movie/", | ||
"bugs": { | ||
@@ -48,3 +49,6 @@ "url": "https://github.com/CodeMovie/code-movie-runtime/issues" | ||
"access": "public" | ||
}, | ||
"dependencies": { | ||
"@types/mocha": "^10.0.10" | ||
} | ||
} |
@@ -19,2 +19,3 @@ # `<code-movie-runtime>` - Web runtime for [Code.Movie](https://code.movie/) | ||
<script type="module" src="dist/index.js"></script> | ||
<code-movie-runtime controls keyframes="0 1 2 3"> | ||
@@ -25,3 +26,3 @@ <div>Switch classes on me!</div> | ||
This will cycle classes on the `div` element wrapped by the custom elements from `frame0` to `frame1` to `frame2` to `frame3`. The keyframes are defined as a whitespace-separated list of numbers in the `keyframes` attribute while the existence of the `controls` attribute provides basic forwards/backwards buttons. | ||
This will render twe buttons below the `<div>`. Clicking on them cycles classes on the `<div>` from `frame0` to `frame1` to `frame2` to `frame3`. The keyframes are defined as a whitespace-separated list of numbers in the `keyframes` attribute while the existence of the `controls` attribute provides basic forwards/backwards buttons. | ||
@@ -32,3 +33,3 @@ Attribute summary: | ||
- `keyframes`: Defines the list of keyframes with a value of whitespace-separated positive integers. Values that are anything but a list of whitespace-separated integers are equal to the attribute missing (eg. there are no keyframes at all in this case). The list of keyframes is internally sorted in ascending order and cleared of any duplicates or non-numbers. Negative numbers are interpreted as positive numbers. | ||
- `current`: Indicates the current frame. Can be changed to change the current frame. Reflected by the DOM property `current`. Values that are anything but a positive integer are treated as `0`. | ||
- `current`: Indicates the current frame index. Can be set to another number to change the current frame. Reflected by the DOM property `current`. Values that are anything but a positive integer are treated as `0`. | ||
@@ -48,4 +49,4 @@ ### Custom controls | ||
- `code-movie-runtime::part(controls)`: The container element for the buttons | ||
- `code-movie-runtime::part(controls-prevBtn)`: The "previous" buttons | ||
- `code-movie-runtime::part(controls-nextBtn)`: The "next" buttons | ||
- `code-movie-runtime::part(controls-prevBtn)`: The "previous" button | ||
- `code-movie-runtime::part(controls-nextBtn)`: The "next" button | ||
@@ -74,2 +75,15 @@ The buttons are `<button>` elements with `<span>` elements inside. | ||
### Auxiliary content | ||
Elements assigned to the slot named `aux` are styled to be only visible when their class name matches the currently active frame: | ||
```html | ||
<code-movie-runtime controls keyframes="0 1 2"> | ||
<div>Switch classes on me!</div> | ||
<p slot="aux" class="frame0">Only visible for step 0</p> | ||
<p slot="aux" class="frame1">Only visible for step 1</p> | ||
<p slot="aux" class="frame2">Only visible for step 2</p> | ||
</code-movie-runtime> | ||
``` | ||
## JavaScript API | ||
@@ -115,2 +129,15 @@ | ||
## CSS API | ||
The element uses its [custom state set](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet) to track the current frame. If the current frame is 7 for example, the CSS selector `code-movie-runtime:state(frame7)` will match. | ||
The default shadow DOM template by overriding the static method `_template`. The default template provides several [shadow parts](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_shadow_parts) for your CSS to hook into: | ||
- `wrapper` | ||
- `controls-wrapper` | ||
- `controls` (default controls, can be replaced entirely as explained above) | ||
- `controls-prevBtn` | ||
- `controls-nextBtn` | ||
- `aux-wrapper` | ||
## Notes | ||
@@ -117,0 +144,0 @@ |
113
src/lib.ts
@@ -25,8 +25,9 @@ function toFiniteInt(value: unknown): number { | ||
// The template function must be public to allow users to replace it | ||
static _template(): [HTMLSlotElement, HTMLSlotElement, HTMLStyleElement] { | ||
const mainSlot = document.createElement("slot"); | ||
const controlsSlot = document.createElement("slot"); | ||
controlsSlot.name = "controls"; | ||
controlsSlot.innerHTML = ` | ||
<div part="controls" class="defaultControls"> | ||
static _template(): Element[] { | ||
const tmp = document.createElement("div"); | ||
tmp.innerHTML = `<div part="wrapper"> | ||
<slot></slot> | ||
<div part="controls-wrapper"> | ||
<slot name="controls"> | ||
<div part="controls"> | ||
<button part="controls-prevBtn" data-command="prev"> | ||
@@ -39,21 +40,26 @@ <span><</span> | ||
</div> | ||
`; | ||
const styles = document.createElement("style"); | ||
styles.innerHTML = ` | ||
:host { display: grid; } | ||
:host(:not([controls])) slot[name=controls] { display: none } | ||
.defaultControls { position: relative; z-index: 1337; } | ||
`; | ||
return [mainSlot, controlsSlot, styles]; | ||
</slot> | ||
</div> | ||
<div part="aux-wrapper"> | ||
<slot name="aux"></slot> | ||
</div> | ||
</div> | ||
<style> | ||
[part="wrapper"] { display: grid; } | ||
:host(:not([controls])) slot[name="controls"] { display: none } | ||
[part="controls"] { position: relative; z-index: 1337; } | ||
</style>`; | ||
return Array.from(tmp.children); | ||
} | ||
// Shadow DOM must be open to allow users to mess with its contents | ||
#shadow = this.attachShadow({ mode: "open" }); | ||
_shadow = this.attachShadow({ mode: "open" }); | ||
// Hosts the runtime's content. The first hosted element gets assigned the | ||
// classes that change in lockstep with "current". The class field serves as | ||
// a shortcut to the element in all the methods that need to deal with the | ||
// content | ||
#mainSlot: HTMLSlotElement; | ||
// Controls aux content visibility. This should _not_ be messed with manually. | ||
#auxStyles = new CSSStyleSheet(); | ||
// ElementInternals must NOT be accessible, the element relies on having | ||
// control over its custom states | ||
#internals = this.attachInternals(); | ||
// List of the keyframe indices | ||
@@ -72,7 +78,11 @@ #keyframes: number[] = []; | ||
super(); | ||
const [mainSlot, controlsSlot, styles] = CodeMovieRuntime._template(); | ||
this.#mainSlot = mainSlot; | ||
this.#shadow.append(mainSlot, controlsSlot, styles); | ||
this.#shadow.addEventListener("click", this._handleClick); | ||
mainSlot.addEventListener("slotchange", () => this._goToCurrent()); | ||
const content = CodeMovieRuntime._template(); | ||
this._shadow.append(...content); | ||
this._shadow.addEventListener("click", this._handleClick); | ||
const defaultSlot = this._shadow.querySelector("slot:not([name])"); | ||
if (!defaultSlot) { | ||
throw new Error("Template does not contain a default slot"); | ||
} | ||
defaultSlot.addEventListener("slotchange", () => this._goToCurrent()); | ||
this._shadow.adoptedStyleSheets.push(this.#auxStyles); | ||
} | ||
@@ -92,2 +102,3 @@ | ||
this.#keyframes = parseKeyframesAttributeValue(newValue); | ||
this.#updateAuxStyles(); | ||
this._goToCurrent(); | ||
@@ -127,2 +138,3 @@ } else if (name === "current") { | ||
} | ||
this.#updateAuxStyles(); | ||
this._goToCurrent(); | ||
@@ -168,3 +180,3 @@ } | ||
_goToCurrent() { | ||
_goToCurrent(): boolean { | ||
let targetKeyframeIdx = this.#keyframeIdx; | ||
@@ -189,5 +201,5 @@ if (!(targetKeyframeIdx in this.#keyframes)) { | ||
if (!proceed) { | ||
return; | ||
return false; | ||
} | ||
this._setClass(this.#keyframes[targetKeyframeIdx]); | ||
this._setClassesAndStates(this.#keyframes[targetKeyframeIdx]); | ||
if (targetKeyframeIdx !== this.#keyframeIdx) { | ||
@@ -197,6 +209,15 @@ this.#keyframeIdx = targetKeyframeIdx; | ||
this.dispatchEvent(new Event("cm-afterframechange", { bubbles: true })); | ||
return true; | ||
} | ||
_setClass(targetKeyframe: number): void { | ||
const targetNode = this.#mainSlot.assignedElements()[0]; | ||
_setClassesAndStates(targetIdx: number): void { | ||
for (const state of this.#internals.states) { | ||
if (/^frame[0-9]+$/.test(state)) { | ||
this.#internals.states.delete(state); | ||
} | ||
} | ||
this.#internals.states.add(`frame${targetIdx}`); | ||
const defaultSlot = | ||
this._shadow.querySelector<HTMLSlotElement>("slot:not([name])"); | ||
const targetNode = defaultSlot?.assignedElements()[0]; | ||
if (!targetNode) { | ||
@@ -210,8 +231,21 @@ return; | ||
} | ||
targetNode.classList.add(`frame${targetKeyframe}`); | ||
targetNode.classList.add(`frame${targetIdx}`); | ||
} | ||
#updateAuxStyles(): void { | ||
let css = `slot[name="aux"]::slotted(*){ display: none }`; | ||
for (const frameIdx of this.#keyframes) { | ||
css += `:host(:state(frame${frameIdx})) slot[name="aux"]::slotted(.frame${frameIdx}) { display: block; }`; | ||
} | ||
this.#auxStyles.replaceSync(css); | ||
} | ||
next(): number { | ||
const before = this.#keyframeIdx; | ||
this.#keyframeIdx += 1; | ||
this._goToCurrent(); | ||
const success = this._goToCurrent(); | ||
if (!success) { | ||
this.#keyframeIdx = before; | ||
return before; | ||
} | ||
return this.current; | ||
@@ -221,4 +255,9 @@ } | ||
prev(): number { | ||
const before = this.#keyframeIdx; | ||
this.#keyframeIdx -= 1; | ||
this._goToCurrent(); | ||
const success = this._goToCurrent(); | ||
if (!success) { | ||
this.#keyframeIdx = before; | ||
return before; | ||
} | ||
return this.current; | ||
@@ -228,3 +267,9 @@ } | ||
go(inputValue: number): number { | ||
this.current = inputValue; | ||
const before = this.#keyframeIdx; | ||
this.#keyframeIdx = this._toKeyframeIdx(inputValue); | ||
const success = this._goToCurrent(); | ||
if (!success) { | ||
this.#keyframeIdx = before; | ||
return before; | ||
} | ||
return this.current; | ||
@@ -231,0 +276,0 @@ } |
27274
386
146
1
14
+ Added@types/mocha@^10.0.10
+ Added@types/mocha@10.0.10(transitive)