@glissade/element 0.10.1 → 0.11.0-pre.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/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ declare class GsPlayerElement extends HTMLElement {
14
14
  get player(): _glissade_player0.Player | null;
15
15
  connectedCallback(): void;
16
16
  disconnectedCallback(): void;
17
- attributeChangedCallback(): void;
17
+ attributeChangedCallback(name: string): void;
18
18
  }
19
19
  /** Define <gs-player> (idempotent). Importing this module defines it. */
20
20
  declare function defineGsPlayer(tagName?: string): void;
package/dist/index.js CHANGED
@@ -5,53 +5,33 @@ const TEMPLATE = `
5
5
  <style>
6
6
  :host { display: inline-block; }
7
7
  canvas { display: block; width: 100%; height: auto; }
8
- .controls { display: none; gap: 8px; align-items: center; padding: 6px 4px; font: 12px system-ui, sans-serif; }
9
- :host([controls]) .controls { display: flex; }
8
+ .controls { display: flex; gap: 8px; align-items: center; padding: 6px 4px; font: 12px system-ui, sans-serif; }
10
9
  button { font: inherit; cursor: pointer; }
11
10
  input[type='range'] { flex: 1; }
12
11
  .time { font-variant-numeric: tabular-nums; opacity: 0.75; min-width: 9ch; text-align: right; }
13
12
  </style>
14
13
  <canvas part="canvas"></canvas>
15
- <div class="controls" part="controls">
16
- <button part="button" aria-label="Play or pause">Play</button>
17
- <input part="scrubber" type="range" min="0" max="1" step="0.0001" value="0" aria-label="Seek" />
18
- <span class="time" part="time"></span>
19
- </div>
20
14
  `;
21
15
  var GsPlayerElement = class extends HTMLElement {
22
- static observedAttributes = ["loop", "autoplay"];
16
+ static observedAttributes = [
17
+ "loop",
18
+ "autoplay",
19
+ "controls"
20
+ ];
23
21
  #mounted = null;
24
22
  #scene = null;
25
23
  #unsubscribe = null;
26
24
  #scrubbing = false;
25
+ #playhead = null;
26
+ #shadow;
27
27
  #canvas;
28
- #button;
29
- #scrubber;
30
- #time;
28
+ #controls = null;
31
29
  constructor() {
32
30
  super();
33
31
  const root = this.attachShadow({ mode: "open" });
34
32
  root.innerHTML = TEMPLATE;
33
+ this.#shadow = root;
35
34
  this.#canvas = root.querySelector("canvas");
36
- this.#button = root.querySelector("button");
37
- this.#scrubber = root.querySelector("input");
38
- this.#time = root.querySelector(".time");
39
- this.#button.addEventListener("click", () => {
40
- const player = this.#mounted?.player;
41
- if (!player) return;
42
- if (player.playing) player.pause();
43
- else player.play();
44
- this.#syncButton();
45
- });
46
- this.#scrubber.addEventListener("input", () => {
47
- const player = this.#mounted?.player;
48
- if (!player) return;
49
- this.#scrubbing = true;
50
- player.pause();
51
- player.seek(parseFloat(this.#scrubber.value) * player.duration);
52
- this.#scrubbing = false;
53
- this.#syncButton();
54
- });
55
35
  }
56
36
  /** The scene module to play; assigning (re)mounts. */
57
37
  get scene() {
@@ -66,19 +46,109 @@ var GsPlayerElement = class extends HTMLElement {
66
46
  return this.#mounted?.player ?? null;
67
47
  }
68
48
  connectedCallback() {
49
+ this.#syncControls();
69
50
  this.#remount();
70
51
  }
71
52
  disconnectedCallback() {
72
53
  this.#teardown();
73
54
  }
74
- attributeChangedCallback() {
55
+ attributeChangedCallback(name) {
56
+ if (name === "controls") {
57
+ this.#syncControls();
58
+ this.#syncControlsSubscription();
59
+ return;
60
+ }
75
61
  if (this.isConnected && this.#scene) this.#remount();
76
62
  }
63
+ /** Wire the scrubber/time playhead subscription when controls exist, unwire
64
+ * when they don't — against the existing playhead, so toggling controls never
65
+ * disturbs playback position. */
66
+ #syncControlsSubscription() {
67
+ if (this.#controls && !this.#unsubscribe && this.#playhead) {
68
+ this.#unsubscribe = this.#playhead.subscribe(() => {
69
+ requestAnimationFrame(() => {
70
+ if (!this.#controls) return;
71
+ this.#syncReadout();
72
+ this.#syncButton();
73
+ });
74
+ });
75
+ this.#syncReadout();
76
+ this.#syncButton();
77
+ } else if (!this.#controls && this.#unsubscribe) {
78
+ this.#unsubscribe();
79
+ this.#unsubscribe = null;
80
+ }
81
+ }
82
+ /** Build the controls subtree + listeners on demand; tear them down when gone. */
83
+ #syncControls() {
84
+ const want = this.hasAttribute("controls");
85
+ if (want && !this.#controls) this.#buildControls();
86
+ else if (!want && this.#controls) this.#destroyControls();
87
+ }
88
+ #buildControls() {
89
+ const div = document.createElement("div");
90
+ div.className = "controls";
91
+ div.setAttribute("part", "controls");
92
+ const button = document.createElement("button");
93
+ button.setAttribute("part", "button");
94
+ button.setAttribute("aria-label", "Play or pause");
95
+ button.textContent = "Play";
96
+ const scrubber = document.createElement("input");
97
+ scrubber.setAttribute("part", "scrubber");
98
+ scrubber.type = "range";
99
+ scrubber.min = "0";
100
+ scrubber.max = "1";
101
+ scrubber.step = "0.0001";
102
+ scrubber.value = "0";
103
+ scrubber.setAttribute("aria-label", "Seek");
104
+ const time = document.createElement("span");
105
+ time.className = "time";
106
+ time.setAttribute("part", "time");
107
+ const onClick = () => {
108
+ const player = this.#mounted?.player;
109
+ if (!player) return;
110
+ if (player.playing) player.pause();
111
+ else player.play();
112
+ this.#syncButton();
113
+ };
114
+ const onInput = () => {
115
+ const player = this.#mounted?.player;
116
+ if (!player) return;
117
+ this.#scrubbing = true;
118
+ player.pause();
119
+ player.seek(parseFloat(scrubber.value) * player.duration);
120
+ this.#scrubbing = false;
121
+ this.#syncButton();
122
+ };
123
+ button.addEventListener("click", onClick);
124
+ scrubber.addEventListener("input", onInput);
125
+ div.append(button, scrubber, time);
126
+ this.#shadow.append(div);
127
+ this.#controls = {
128
+ root: div,
129
+ button,
130
+ scrubber,
131
+ time,
132
+ onClick,
133
+ onInput
134
+ };
135
+ this.#syncReadout();
136
+ this.#syncButton();
137
+ }
138
+ #destroyControls() {
139
+ const c = this.#controls;
140
+ if (!c) return;
141
+ c.button.removeEventListener("click", c.onClick);
142
+ c.scrubber.removeEventListener("input", c.onInput);
143
+ c.root.remove();
144
+ this.#controls = null;
145
+ }
77
146
  #teardown() {
78
147
  this.#unsubscribe?.();
79
148
  this.#unsubscribe = null;
80
149
  this.#mounted?.dispose();
81
150
  this.#mounted = null;
151
+ this.#playhead = null;
82
152
  }
83
153
  #remount() {
84
154
  this.#teardown();
@@ -90,21 +160,22 @@ var GsPlayerElement = class extends HTMLElement {
90
160
  loop: this.hasAttribute("loop"),
91
161
  autoplay: this.hasAttribute("autoplay")
92
162
  });
93
- const player = this.#mounted.player;
94
- this.#unsubscribe = scene.playhead.subscribe(() => {
95
- requestAnimationFrame(() => {
96
- const t = scene.playhead.peek();
97
- this.#time.textContent = `${t.toFixed(2)} / ${player.duration.toFixed(2)}s`;
98
- if (!this.#scrubbing && player.duration > 0) this.#scrubber.value = String(t / player.duration);
99
- this.#syncButton();
100
- });
101
- });
102
- this.#time.textContent = `0.00 / ${player.duration.toFixed(2)}s`;
103
- this.#syncButton();
163
+ this.#playhead = scene.playhead;
164
+ this.#syncControlsSubscription();
165
+ }
166
+ #syncReadout() {
167
+ const c = this.#controls;
168
+ const player = this.#mounted?.player;
169
+ if (!c || !player) return;
170
+ const t = this.#playhead?.peek() ?? 0;
171
+ c.time.textContent = `${t.toFixed(2)} / ${player.duration.toFixed(2)}s`;
172
+ if (!this.#scrubbing && player.duration > 0) c.scrubber.value = String(t / player.duration);
104
173
  }
105
174
  #syncButton() {
175
+ const c = this.#controls;
176
+ if (!c) return;
106
177
  const playing = this.#mounted?.player.playing ?? false;
107
- this.#button.textContent = playing ? "Pause" : "Play";
178
+ c.button.textContent = playing ? "Pause" : "Play";
108
179
  }
109
180
  };
110
181
  /** Define <gs-player> (idempotent). Importing this module defines it. */
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@glissade/element",
3
- "version": "0.10.1",
3
+ "version": "0.11.0-pre.1",
4
4
  "description": "glissade <gs-player> custom element: zero-framework embedding with minimal, CSS-part themable controls.",
5
5
  "license": "Apache-2.0",
6
+ "engines": {
7
+ "node": ">=20.19"
8
+ },
6
9
  "type": "module",
7
10
  "sideEffects": true,
8
11
  "exports": {
@@ -15,10 +18,10 @@
15
18
  "dist"
16
19
  ],
17
20
  "dependencies": {
18
- "@glissade/backend-canvas2d": "0.10.1",
19
- "@glissade/core": "0.10.1",
20
- "@glissade/player": "0.10.1",
21
- "@glissade/scene": "0.10.1"
21
+ "@glissade/backend-canvas2d": "0.11.0-pre.1",
22
+ "@glissade/core": "0.11.0-pre.1",
23
+ "@glissade/player": "0.11.0-pre.1",
24
+ "@glissade/scene": "0.11.0-pre.1"
22
25
  },
23
26
  "repository": {
24
27
  "type": "git",