@phin-tech/dottie 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PARITY.md CHANGED
@@ -59,6 +59,6 @@ fixture, so the gap is visible rather than silent.
59
59
  | Clicks that set and toggle variables | Supported | [edge/state](../fixtures/edge/state.dottie) | Change what shows without touching the playhead |
60
60
  | Looping scenes (`loop` on a marker) | Supported | [edge/state](../fixtures/edge/state.dottie) | No stop on entering, round from the scene's end to its start |
61
61
  | Links out of the drawing (`url`) | Supported | [edge/links](../fixtures/edge/links.dottie) | A web or mail `url` opens when clicked if there is no `click`, and SVG export wraps it in `<a>`; a link to another drawing does nothing on the web |
62
- | Links between shapes and pages | Partial | [edge/clicks](../fixtures/edge/clicks.dottie) | A link to a shape on another page shows that page; one on the same page does nothing. The HTML export holds one page, so it has neither |
62
+ | Links between shapes and pages | Partial | [edge/clicks](../fixtures/edge/clicks.dottie) | A link to a shape on another page shows that page; one on the same page does nothing. The HTML export of one page has neither; with every page (`--all-pages`, `history`) it shows the page at its own address, and split a page per file (`--split`, `links`) it goes to that page's file |
63
63
  | Editing | Missing | | |
64
64
  | Ticking task boxes in notes | Missing | | Drawn, not clickable |
package/README.md CHANGED
@@ -28,6 +28,8 @@ prefer reduced motion. A still page is just the drawing.
28
28
  | `clean` | Draw precisely instead of sketchy |
29
29
  | `no-controls`, `no-autoplay`, `no-loop` | |
30
30
  | `label` | The accessible name; the page name by default |
31
+ | `history` | Each page a link shows gets its own address (`#` and the page's name), so Back and Forward go between pages and a link can open any of them. One such player to a page |
32
+ | `links` | JSON from the id of an element a link points at to the web page that shows it (`{"…": "details.html"}`), for a drawing exported one page per file: a link to another page goes there. Only relative `.html` paths are followed |
31
33
 
32
34
  Elements the drawing makes clickable work with the mouse and the keyboard (Tab, then Enter or Space). A click plays
33
35
  a named scene on to the next one, opens a link in a new tab (only `http`, `https` and `mailto`), or shows the page a
@@ -43,7 +45,8 @@ region a click took down. A looping scene plays round until a click or next leav
43
45
 
44
46
  The element has `play()`, `pause()`, `nextScene()`, `prevScene()`, `activate(elementId)`, `currentTime`
45
47
  (settable), `duration`, `paused`, `variables` and `setState({name: value})`, and fires `load`, `frame`, `action`,
46
- `state` (with `{variables}`, after a change) and `error` events. Its page color comes from the drawing's theme; set
48
+ `state` (with `{variables}`, after a change), `page` (with `{index, name, first}`, when the page shown changes) and
49
+ `error` events. Its page color comes from the drawing's theme; set
47
50
  `--dottie-radius` to round it, and style its parts with `::part(stage)` and `::part(controls)`.
48
51
 
49
52
  Without modules, `dist/dottie-player.min.js` is a classic script that registers the element and sets
@@ -93,7 +96,9 @@ handwriting fonts the drawing uses (15 to 30 KB each), so one file shows it anyw
93
96
  ```
94
97
 
95
98
  `dottie-compile` takes `--page`, `--at`, `--clean`, `--no-controls`, `--no-autoplay`, `--no-loop` and `--label`;
96
- only the chosen page (and its pictures) goes in. The Dottie Mac app's command-line tool writes the same file:
99
+ only the chosen page (and its pictures) goes in. With `--all-pages` every page goes in, for a drawing people drill
100
+ into: a link to a shape on another page shows that page, each page gets its own address (`tour.html#Details`), and
101
+ Back goes back (`--page` is then the page to start on). The Dottie Mac app's command-line tool writes the same file:
97
102
  `dottie render tour.dottie -f js`. From code: `import { compile } from "@phin-tech/dottie/compile"`.
98
103
 
99
104
  ## Use the library
package/dist/compile.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.11.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.11.1 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -763,6 +763,10 @@ function playerAttributes(options) {
763
763
  if (options.noLoop) out["no-loop"] = "";
764
764
  if (options.label !== void 0) out.label = options.label;
765
765
  if (options.logicEngine) out["allow-logic"] = "";
766
+ if (options.allPages) {
767
+ out.history = "";
768
+ if (options.page !== void 0) out.page = String(options.page);
769
+ }
766
770
  return out;
767
771
  }
768
772
  function compile(source, runtime, options = {}) {
@@ -777,28 +781,31 @@ function compile(source, runtime, options = {}) {
777
781
  const file = parseDottie(raw);
778
782
  const json = raw;
779
783
  const i = pageIndex(file, options.page === void 0 ? null : String(options.page));
780
- const page = json.pages[i];
784
+ const chosen = options.allPages ? json.pages.map((_, k) => k) : [i];
785
+ const pages = chosen.map((k) => json.pages[k]);
781
786
  const refs = /* @__PURE__ */ new Set();
782
- JSON.stringify(page, (k, v) => {
787
+ JSON.stringify(pages, (k, v) => {
783
788
  if (k === "ref" && typeof v === "string") refs.add(v);
784
789
  return v;
785
790
  });
786
791
  const images = Object.fromEntries(Object.entries(json.images ?? {}).filter(([k]) => refs.has(k)));
787
- const data = { version: json.version, pages: [page] };
792
+ const data = { version: json.version, pages };
788
793
  if (Object.keys(images).length) data.images = images;
789
794
  const fonts = {};
790
- for (const family of options.fontData ? usedFonts(file.pages[i]) : []) {
795
+ const families2 = new Set(chosen.flatMap((k) => options.fontData ? usedFonts(file.pages[k]) : []));
796
+ for (const family of families2) {
791
797
  for (const face of fontFaces(family)) {
792
798
  const b64 = options.fontData(family, face);
793
799
  if (b64) (fonts[family] ?? (fonts[family] = {}))[face] = b64;
794
800
  }
795
801
  }
802
+ const logic = chosen.some((k) => file.pages[k].logic);
796
803
  const fill = {
797
804
  DOTTIE_DATA: () => scriptJSON(data),
798
- DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(file.pages[i].logic ? options : { ...options, logicEngine: void 0 })),
805
+ DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(logic ? options : { ...options, logicEngine: void 0 })),
799
806
  DOTTIE_FONTS: () => scriptJSON(fonts)
800
807
  };
801
- const engine = options.logicEngine && file.pages[i].logic ? options.logicEngine.trimEnd() + "\n" : "";
808
+ const engine = options.logicEngine && logic ? options.logicEngine.trimEnd() + "\n" : "";
802
809
  return engine + runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
803
810
  }
804
811
  export {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /*! dottie.js 0.11.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
+ /*! dottie.js 0.11.1 | Apache-2.0 | https://github.com/phin-tech/dottie */
3
3
  var __defProp = Object.defineProperty;
4
4
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
5
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -771,6 +771,10 @@ function playerAttributes(options2) {
771
771
  if (options2.noLoop) out["no-loop"] = "";
772
772
  if (options2.label !== void 0) out.label = options2.label;
773
773
  if (options2.logicEngine) out["allow-logic"] = "";
774
+ if (options2.allPages) {
775
+ out.history = "";
776
+ if (options2.page !== void 0) out.page = String(options2.page);
777
+ }
774
778
  return out;
775
779
  }
776
780
  function compile(source2, runtime, options2 = {}) {
@@ -785,33 +789,36 @@ function compile(source2, runtime, options2 = {}) {
785
789
  const file = parseDottie(raw);
786
790
  const json = raw;
787
791
  const i = pageIndex(file, options2.page === void 0 ? null : String(options2.page));
788
- const page = json.pages[i];
792
+ const chosen = options2.allPages ? json.pages.map((_, k) => k) : [i];
793
+ const pages = chosen.map((k) => json.pages[k]);
789
794
  const refs = /* @__PURE__ */ new Set();
790
- JSON.stringify(page, (k, v) => {
795
+ JSON.stringify(pages, (k, v) => {
791
796
  if (k === "ref" && typeof v === "string") refs.add(v);
792
797
  return v;
793
798
  });
794
799
  const images = Object.fromEntries(Object.entries(json.images ?? {}).filter(([k]) => refs.has(k)));
795
- const data = { version: json.version, pages: [page] };
800
+ const data = { version: json.version, pages };
796
801
  if (Object.keys(images).length) data.images = images;
797
802
  const fonts = {};
798
- for (const family of options2.fontData ? usedFonts(file.pages[i]) : []) {
803
+ const families2 = new Set(chosen.flatMap((k) => options2.fontData ? usedFonts(file.pages[k]) : []));
804
+ for (const family of families2) {
799
805
  for (const face of fontFaces(family)) {
800
806
  const b64 = options2.fontData(family, face);
801
807
  if (b64) (fonts[family] ?? (fonts[family] = {}))[face] = b64;
802
808
  }
803
809
  }
810
+ const logic = chosen.some((k) => file.pages[k].logic);
804
811
  const fill = {
805
812
  DOTTIE_DATA: () => scriptJSON(data),
806
- DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(file.pages[i].logic ? options2 : { ...options2, logicEngine: void 0 })),
813
+ DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(logic ? options2 : { ...options2, logicEngine: void 0 })),
807
814
  DOTTIE_FONTS: () => scriptJSON(fonts)
808
815
  };
809
- const engine = options2.logicEngine && file.pages[i].logic ? options2.logicEngine.trimEnd() + "\n" : "";
816
+ const engine = options2.logicEngine && logic ? options2.logicEngine.trimEnd() + "\n" : "";
810
817
  return engine + runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
811
818
  }
812
819
 
813
820
  // src/index.ts
814
- var VERSION = "0.11.0";
821
+ var VERSION = "0.11.1";
815
822
 
816
823
  // src/cli.ts
817
824
  var usage = `dottie-compile ${VERSION}: a Dottie drawing as one script that plays it on any page.
@@ -825,10 +832,12 @@ USAGE
825
832
  OPTIONS
826
833
  -o, --output <path> Where to write it ("-" for stdout); default drawing.js next to the input
827
834
  -p, --page <page> Page name or 1-based number; only that page is included (default the first)
835
+ --all-pages Include every page, with links between them working and Back going back (with
836
+ --page, the page to start on)
828
837
  --at <seconds> Show that moment as a still
829
838
  --clean Draw precisely instead of sketchy
830
839
  --no-controls, --no-autoplay, --no-loop
831
- --allow-logic let the page's click logic run (ADR 0007); it carries the engine (about 850 KB)
840
+ --allow-logic let the page's click logic run (ADR 0007); it carries the engine (about 850 KB)
832
841
  --label <text> The player's accessible name
833
842
  `;
834
843
  function fail(message, code = 2) {
@@ -868,6 +877,9 @@ for (let i = 0; i < args.length; i++) {
868
877
  options.at = t;
869
878
  break;
870
879
  }
880
+ case "--all-pages":
881
+ options.allPages = true;
882
+ break;
871
883
  case "--clean":
872
884
  options.clean = true;
873
885
  break;
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.11.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.11.1 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  "use strict";(()=>{var Sx=Object.defineProperty;var w=(x,F)=>()=>(x&&(F=x(x=0)),F);var E0=(x,F)=>{for(var A in F)Sx(x,A,{get:F[A],enumerable:!0})};var T,f0,Z,H,L,K=w(()=>{T={JS_EVAL_TYPE_GLOBAL:0,JS_EVAL_TYPE_MODULE:1,JS_EVAL_TYPE_DIRECT:2,JS_EVAL_TYPE_INDIRECT:3,JS_EVAL_TYPE_MASK:3,JS_EVAL_FLAG_STRICT:8,JS_EVAL_FLAG_STRIP:16,JS_EVAL_FLAG_COMPILE_ONLY:32,JS_EVAL_FLAG_BACKTRACE_BARRIER:64},f0={BaseObjects:1,Date:2,Eval:4,StringNormalize:8,RegExp:16,RegExpCompiler:32,JSON:64,Proxy:128,MapSet:256,TypedArrays:512,Promise:1024,BigInt:2048,BigFloat:4096,BigDecimal:8192,OperatorOverloading:16384,BignumExt:32768},Z={Pending:0,Fulfilled:1,Rejected:2},H={JS_GPN_STRING_MASK:1,JS_GPN_SYMBOL_MASK:2,JS_GPN_PRIVATE_MASK:4,JS_GPN_ENUM_ONLY:16,JS_GPN_SET_ENUM:32,QTS_GPN_NUMBER_MASK:64,QTS_STANDARD_COMPLIANT_NUMBER:128},L={IsStrictlyEqual:0,IsSameValue:1,IsSameValueZero:2}});function J(...x){D0&&console.log("quickjs-emscripten:",...x)}function*xx(x){return yield x}function Ix(x){return xx(i0(x))}function R0(x,F){return(...A)=>{let v=F.call(x,n0,...A);return i0(v)}}function wx(x,F){let A=F.call(x,n0);return i0(A)}function i0(x){function F(A){return A.done?A.value:A.value instanceof Promise?A.value.then(v=>F(x.next(v)),v=>F(x.throw(v))):F(x.next(A.value))}return F(x.next())}function j0(x,F){let A;try{x.dispose()}catch(v){A=v}if(F&&A)throw Object.assign(F,{message:`${F.message}
3
3
  Then, failed to dispose scope: ${A.message}`,disposeError:A}),F;if(F||A)throw F||A}function Rx(x){let F=x?Array.from(x):[];function A(){return F.forEach(E=>E.alive?E.dispose():void 0)}function v(){return F.some(E=>E.alive)}return Object.defineProperty(F,C0,{configurable:!0,enumerable:!1,value:A}),Object.defineProperty(F,"dispose",{configurable:!0,enumerable:!1,value:A}),Object.defineProperty(F,"alive",{configurable:!0,enumerable:!1,get:v}),F}function W(x){return!!(x&&(typeof x=="object"||typeof x=="function")&&"alive"in x&&typeof x.alive=="boolean"&&"dispose"in x&&typeof x.dispose=="function")}function Jx(x){if(!x)return 0;let F=0;for(let[A,v]of Object.entries(x)){if(!(A in f0))throw new X0(A);v&&(F|=f0[A])}return F}function Ox(x){if(typeof x=="number")return x;if(x===void 0)return 0;let{type:F,strict:A,strip:v,compileOnly:E,backtraceBarrier:j}=x,e=0;return F==="global"&&(e|=T.JS_EVAL_TYPE_GLOBAL),F==="module"&&(e|=T.JS_EVAL_TYPE_MODULE),A&&(e|=T.JS_EVAL_FLAG_STRICT),v&&(e|=T.JS_EVAL_FLAG_STRIP),E&&(e|=T.JS_EVAL_FLAG_COMPILE_ONLY),j&&(e|=T.JS_EVAL_FLAG_BACKTRACE_BARRIER),e}function Vx(x){if(typeof x=="number")return x;if(x===void 0)return 0;let{strings:F,symbols:A,quickjsPrivate:v,onlyEnumerable:E,numbers:j,numbersAsStrings:e}=x,t=0;return F&&(t|=H.JS_GPN_STRING_MASK),A&&(t|=H.JS_GPN_SYMBOL_MASK),v&&(t|=H.JS_GPN_PRIVATE_MASK),E&&(t|=H.JS_GPN_ENUM_ONLY),j&&(t|=H.QTS_GPN_NUMBER_MASK),e&&(t|=H.QTS_STANDARD_COMPLIANT_NUMBER),t}function Zx(...x){let F=[];for(let A of x)A!==void 0&&(F=F.concat(A));return F}function e0(x){return x>>8}function s0(x,F){F.interruptHandler&&x.setInterruptHandler(F.interruptHandler),F.maxStackSizeBytes!==void 0&&x.setMaxStackSize(F.maxStackSizeBytes),F.memoryLimitBytes!==void 0&&x.setMemoryLimit(F.memoryLimitBytes)}function l0(x,F){F.moduleLoader&&x.setModuleLoader(F.moduleLoader),F.shouldInterrupt&&x.setInterruptHandler(F.shouldInterrupt),F.memoryLimitBytes!==void 0&&x.setMemoryLimit(F.memoryLimitBytes),F.maxStackSizeBytes!==void 0&&x.setMaxStackSize(F.maxStackSizeBytes)}var gx,Tx,D0,Hx,t0,V0,b0,Z0,K0,U0,yx,Nx,X0,W0,Y0,z0,$,n0,m,C0,M0,k,y,P0,q,o0,Mx,Px,R,Lx,vx,i8,o8,Kx,L0,J0,X,Ux,O0,Xx,Wx,Yx,zx,a0,Bx,rx=w(()=>{K();K();gx=Object.defineProperty,Tx=(x,F)=>{for(var A in F)gx(x,A,{get:F[A],enumerable:!0})},D0=!1;Hx={};Tx(Hx,{QuickJSAsyncifyError:()=>K0,QuickJSAsyncifySuspended:()=>U0,QuickJSEmptyGetOwnPropertyNames:()=>Y0,QuickJSEmscriptenModuleError:()=>Nx,QuickJSHostRefInvalid:()=>$,QuickJSHostRefRangeExceeded:()=>z0,QuickJSMemoryLeakDetected:()=>yx,QuickJSNotImplemented:()=>Z0,QuickJSPromisePending:()=>W0,QuickJSUnknownIntrinsic:()=>X0,QuickJSUnwrapError:()=>t0,QuickJSUseAfterFree:()=>b0,QuickJSWrongOwner:()=>V0});t0=class extends Error{constructor(x,F){let A=typeof x=="object"&&x&&"message"in x?String(x.message):String(x);super(A),this.cause=x,this.context=F,this.name="QuickJSUnwrapError"}},V0=class extends Error{constructor(){super(...arguments),this.name="QuickJSWrongOwner"}},b0=class extends Error{constructor(){super(...arguments),this.name="QuickJSUseAfterFree"}},Z0=class extends Error{constructor(){super(...arguments),this.name="QuickJSNotImplemented"}},K0=class extends Error{constructor(){super(...arguments),this.name="QuickJSAsyncifyError"}},U0=class extends Error{constructor(){super(...arguments),this.name="QuickJSAsyncifySuspended"}},yx=class extends Error{constructor(){super(...arguments),this.name="QuickJSMemoryLeakDetected"}},Nx=class extends Error{constructor(){super(...arguments),this.name="QuickJSEmscriptenModuleError"}},X0=class extends TypeError{constructor(){super(...arguments),this.name="QuickJSUnknownIntrinsic"}},W0=class extends Error{constructor(){super(...arguments),this.name="QuickJSPromisePending"}},Y0=class extends Error{constructor(){super(...arguments),this.name="QuickJSEmptyGetOwnPropertyNames"}},z0=class extends Error{constructor(){super(...arguments),this.name="QuickJSHostRefRangeExceeded"}},$=class extends Error{constructor(){super(...arguments),this.name="QuickJSHostRefInvalid"}};n0=xx;n0.of=Ix;m=class{[Symbol.dispose](){return this.dispose()}},C0=Symbol.dispose??Symbol.for("Symbol.dispose"),M0=m.prototype;M0[C0]||(M0[C0]=function(){return this.dispose()});k=class Ax extends m{constructor(F,A,v,E){super(),this._value=F,this.copier=A,this.disposer=v,this._owner=E,this._alive=!0,this._constructorStack=D0?new Error("Lifetime constructed").stack:void 0}get alive(){return this._alive}get value(){return this.assertAlive(),this._value}get owner(){return this._owner}get dupable(){return!!this.copier}dup(){if(this.assertAlive(),!this.copier)throw new Error("Non-dupable lifetime");return new Ax(this.copier(this._value),this.copier,this.disposer,this._owner)}consume(F){this.assertAlive();let A=F(this);return this.dispose(),A}map(F){return this.assertAlive(),F(this)}tap(F){return F(this),this}dispose(){this.assertAlive(),this.disposer&&this.disposer(this._value),this._alive=!1}assertAlive(){if(!this.alive)throw this._constructorStack?new b0(`Lifetime not alive
4
4
  ${this._constructorStack}
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.11.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.11.1 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
4
4
  var __export = (target, all) => {
@@ -4030,7 +4030,7 @@ function sceneNamed(name, markers) {
4030
4030
  const exact = markers.findIndex((m) => m.name === name);
4031
4031
  return exact >= 0 ? exact : markers.findIndex((m) => m.name.toLowerCase() === name.toLowerCase());
4032
4032
  }
4033
- function clickTargets(page, file) {
4033
+ function clickTargets(page, file, hrefs) {
4034
4034
  const markers = page.timeline?.markers ?? [];
4035
4035
  const out = /* @__PURE__ */ new Map();
4036
4036
  let pageOf = null;
@@ -4066,11 +4066,19 @@ function clickTargets(page, file) {
4066
4066
  });
4067
4067
  }
4068
4068
  const target = pageOf.get(e.link);
4069
- if (target !== void 0 && file.pages[target] !== page) out.set(e.id, { page: target });
4069
+ if (target !== void 0 && file.pages[target] !== page) {
4070
+ out.set(e.id, { page: target });
4071
+ continue;
4072
+ }
4070
4073
  }
4074
+ const href = e.link && hrefs && Object.prototype.hasOwnProperty.call(hrefs, e.link) ? pageHref(hrefs[e.link]) : null;
4075
+ if (href) out.set(e.id, { href });
4071
4076
  }
4072
4077
  return out;
4073
4078
  }
4079
+ function pageHref(href) {
4080
+ return typeof href === "string" && /^[A-Za-z0-9._~%/-]+\.html(#[^\s:]*)?$/.test(href) && !href.startsWith("//") ? href : null;
4081
+ }
4074
4082
  function hoveredElement(e) {
4075
4083
  const h = e.hover;
4076
4084
  if (!h) return e;
@@ -5143,7 +5151,8 @@ output { font-variant-numeric: tabular-nums; min-width: 4.5em; text-align: right
5143
5151
  .error { padding: 16px; font-family: ui-monospace, Menlo, monospace; white-space: pre-wrap; }
5144
5152
  ${CLICK_STYLE}`;
5145
5153
  var Playback = class {
5146
- /** `file` is the drawing `page` belongs to, so links to its other pages are clickable. */
5154
+ /** `file` is the drawing `page` belongs to, so links to its other pages are clickable; `hrefs`, the web pages that
5155
+ * show the elements links point at, for pages that aren't in it (clickTargets). */
5147
5156
  constructor(page, into, options = {}) {
5148
5157
  __publicField(this, "page");
5149
5158
  __publicField(this, "plan");
@@ -5185,7 +5194,7 @@ var Playback = class {
5185
5194
  this.page = options.clean ? cleanCopy(page) : page;
5186
5195
  addFonts(usedFonts(this.page));
5187
5196
  this.plan = plan(this.page);
5188
- this.clicks = clickTargets(page, options.file);
5197
+ this.clicks = clickTargets(page, options.file, options.hrefs);
5189
5198
  const table = symbolTable(this.page);
5190
5199
  for (const e of this.page.elements) {
5191
5200
  const s = e.kind === "symbol" && e.symbol !== null ? table.get(e.symbol) : void 0;
@@ -5454,6 +5463,11 @@ var DottiePlayer = class extends ElementBase {
5454
5463
  __publicField(this, "stillTime", null);
5455
5464
  __publicField(this, "visible", true);
5456
5465
  __publicField(this, "observer", null);
5466
+ /** The page last shown, so `page` fires only when it changes. */
5467
+ __publicField(this, "shownPage", null);
5468
+ __publicField(this, "onPopState", () => {
5469
+ if (this.file) this.mount();
5470
+ });
5457
5471
  /** Runs handler `handler` from the page's logic for a click on `id` (DottieDocument.clicked). Clicks take turns, so
5458
5472
  * each handler sees the state the one before left. */
5459
5473
  __publicField(this, "logicQueue", Promise.resolve());
@@ -5502,15 +5516,21 @@ var DottiePlayer = class extends ElementBase {
5502
5516
  });
5503
5517
  this.observer.observe(this);
5504
5518
  }
5519
+ if (this.hasAttribute("history")) window.addEventListener("popstate", this.onPopState);
5505
5520
  void this.load();
5506
5521
  }
5507
5522
  disconnectedCallback() {
5508
5523
  cancelAnimationFrame(this.raf);
5509
5524
  this.raf = 0;
5510
5525
  this.observer?.disconnect();
5526
+ window.removeEventListener("popstate", this.onPopState);
5511
5527
  }
5512
5528
  attributeChangedCallback(name, old, value2) {
5513
5529
  if (old === value2 || !this.isConnected) return;
5530
+ if (name === "history") {
5531
+ if (value2 === null) window.removeEventListener("popstate", this.onPopState);
5532
+ else window.addEventListener("popstate", this.onPopState);
5533
+ }
5514
5534
  if (name === "src") void this.load();
5515
5535
  else if (name === "label") this.stage.setAttribute("aria-label", value2 ?? "");
5516
5536
  else if (this.file) this.mount();
@@ -5559,6 +5579,7 @@ var DottiePlayer = class extends ElementBase {
5559
5579
  }
5560
5580
  showFile(file) {
5561
5581
  this.file = file;
5582
+ this.shownPage = null;
5562
5583
  for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
5563
5584
  this.mount(() => this.dispatchEvent(new CustomEvent("load", { detail: { file } })));
5564
5585
  }
@@ -5566,7 +5587,7 @@ var DottiePlayer = class extends ElementBase {
5566
5587
  * showing stays until it is ready; a newer mount, file or failed load in the meantime wins. */
5567
5588
  mount(mounted) {
5568
5589
  const file = this.file;
5569
- const page = file.pages[pageIndex(file, this.getAttribute("page"))];
5590
+ const page = file.pages[this.addressedPage(file) ?? pageIndex(file, this.getAttribute("page"))];
5570
5591
  const clean = this.hasAttribute("clean");
5571
5592
  const token = ++this.mountToken;
5572
5593
  const shown = clean ? cleanCopy(page) : page;
@@ -5581,10 +5602,41 @@ var DottiePlayer = class extends ElementBase {
5581
5602
  mounted?.();
5582
5603
  });
5583
5604
  }
5605
+ /** With `history`, the page the address names after its `#` (by name, any case), if it names one. */
5606
+ addressedPage(file) {
5607
+ if (!this.hasAttribute("history") || typeof location === "undefined" || location.hash.length < 2) return null;
5608
+ let name;
5609
+ try {
5610
+ name = decodeURIComponent(location.hash.slice(1));
5611
+ } catch {
5612
+ return null;
5613
+ }
5614
+ const i = file.pages.findIndex((p) => p.name.toLowerCase() === name.toLowerCase());
5615
+ return i >= 0 ? i : null;
5616
+ }
5617
+ /** The `links` attribute read: element id to web page, the pages that can't be named left out. */
5618
+ hrefs() {
5619
+ const raw = this.getAttribute("links");
5620
+ if (!raw) return void 0;
5621
+ let v;
5622
+ try {
5623
+ v = JSON.parse(raw);
5624
+ } catch {
5625
+ console.warn("dottie-player: links isn't JSON");
5626
+ return void 0;
5627
+ }
5628
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return void 0;
5629
+ return v;
5630
+ }
5584
5631
  mountPage(file, page, clean) {
5585
- const p = new Playback(page, this.stage, { clean, file });
5632
+ const p = new Playback(page, this.stage, { clean, file, hrefs: this.hrefs() });
5586
5633
  p.still = this.hasAttribute("at");
5587
5634
  this.playback = p;
5635
+ if (this.shownPage !== page) {
5636
+ const first = this.shownPage === null;
5637
+ this.shownPage = page;
5638
+ queueMicrotask(() => this.dispatchEvent(new CustomEvent("page", { detail: { index: file.pages.indexOf(page), name: page.name, first } })));
5639
+ }
5588
5640
  this.style.setProperty("--dottie-page", color(p.background));
5589
5641
  this.style.setProperty("--dottie-ink", color(p.ink));
5590
5642
  this.style.colorScheme = luminance(p.background) < 0.5 ? "dark" : "light";
@@ -5696,7 +5748,15 @@ var DottiePlayer = class extends ElementBase {
5696
5748
  if ("url" in click) {
5697
5749
  if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
5698
5750
  } else if ("page" in click) {
5699
- this.setAttribute("page", String(click.page + 1));
5751
+ const target = this.file?.pages[click.page];
5752
+ if (this.hasAttribute("history") && target) {
5753
+ history.pushState(null, "", "#" + encodeURIComponent(target.name));
5754
+ this.mount();
5755
+ } else {
5756
+ this.setAttribute("page", String(click.page + 1));
5757
+ }
5758
+ } else if ("href" in click) {
5759
+ location.assign(click.href);
5700
5760
  } else if (click.scene >= 0 && click.scene < p.scenes.length) {
5701
5761
  if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
5702
5762
  this.setPlaying(false);
@@ -5842,7 +5902,7 @@ var DottiePlayer = class extends ElementBase {
5842
5902
  if (handled) e.preventDefault();
5843
5903
  }
5844
5904
  };
5845
- __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label", "allow-logic"]);
5905
+ __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label", "allow-logic", "history", "links"]);
5846
5906
  function clickable(target) {
5847
5907
  const el = target;
5848
5908
  return el && typeof el.closest === "function" ? el.closest(".dottie-a") : null;
@@ -5947,7 +6007,7 @@ function render(source, options = {}) {
5947
6007
  function isFile(v) {
5948
6008
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
5949
6009
  }
5950
- var VERSION = "0.11.0";
6010
+ var VERSION = "0.11.1";
5951
6011
 
5952
6012
  // src/player-entry.ts
5953
6013
  servePlans();