@web-atoms/web-controls 2.1.214 → 2.1.217

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.
@@ -0,0 +1,104 @@
1
+ import Bind from "@web-atoms/core/dist/core/Bind";
2
+ import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty";
3
+ import Colors from "@web-atoms/core/dist/core/Colors";
4
+ import { IDisposable } from "@web-atoms/core/dist/core/types";
5
+ import XNode from "@web-atoms/core/dist/core/XNode";
6
+ import StyleRule from "@web-atoms/core/dist/style/StyleRule";
7
+ import CSS from "@web-atoms/core/dist/web/styles/CSS";
8
+ import AtomRepeater, { askSuggestion } from "./AtomRepeater";
9
+
10
+ CSS(StyleRule()
11
+ .margin(5)
12
+ .flexLayout({})
13
+ .overflow("hidden")
14
+ .child(StyleRule(".header")
15
+ .color(Colors.darkOrange)
16
+ .whiteSpace("nowrap")
17
+ )
18
+ .child(StyleRule(".items")
19
+ .flexStretch()
20
+ .flexLayout({ justifyContent: "flex-start", gap: 0 })
21
+ .overflow("hidden")
22
+ .child(StyleRule("*")
23
+ .whiteSpace("nowrap")
24
+ .padding(3)
25
+ )
26
+ )
27
+ .child(StyleRule(".more")
28
+ .fontSize("smaller")
29
+ .color(Colors.blue)
30
+ .textTransform("lowercase")
31
+ .textDecoration("underline")
32
+ )
33
+ , "*[data-suggestions=suggestions]");
34
+
35
+ export default class AtomSuggestions extends AtomRepeater {
36
+
37
+ public eventItemClick: any;
38
+
39
+ @BindableProperty
40
+ public valuePath: any;
41
+
42
+ // Title to be displayed on the popup window for e.g. When we click on more in project tags
43
+ @BindableProperty
44
+ public title: string;
45
+
46
+ @BindableProperty
47
+ public match: (text) => (item) => boolean;
48
+
49
+ @BindableProperty
50
+ public version: number;
51
+
52
+ @BindableProperty
53
+ public suggestionRenderer: (item) => XNode;
54
+
55
+ private selectedItemsWatcher: IDisposable;
56
+
57
+ public onPropertyChanged(name: keyof AtomSuggestions): void {
58
+ super.onPropertyChanged(name);
59
+ switch (name) {
60
+ case "selectedItems":
61
+ this.selectedItemsWatcher?.dispose();
62
+ const si = this.selectedItems;
63
+ if (!si) {
64
+ this.selectedItemsWatcher = null;
65
+ return;
66
+ }
67
+ const d = si.watch(() => this.version++);
68
+ this.selectedItemsWatcher = this.registerDisposable(d);
69
+ this.version++;
70
+ break;
71
+ case "version":
72
+ // this.updateVisibility(this.itemsPresenter);
73
+ const vp = this.valuePath ?? ((item) => item);
74
+ const selectedValues = (this.selectedItems ?? []).map(vp);
75
+ this.visibilityFilter = (item) => {
76
+ const v = vp(item);
77
+ return selectedValues.length === 0 || selectedValues.indexOf(v) === -1;
78
+ };
79
+ break;
80
+ }
81
+ }
82
+
83
+ protected create(): void {
84
+ this.version = 1;
85
+ this.render(<div data-suggestions="suggestions" eventItemClick={(e) => this.selectedItems?.add(e.detail)}>
86
+ <span class="header" text={Bind.oneWay(() => this.title)}/>
87
+ <div class="items"></div>
88
+ <div class="more" eventClick={Bind.event(() => this.more())}>More</div>
89
+ </div>);
90
+ this.itemsPresenter = this.element.children[1] as HTMLElement;
91
+ this.updateItems();
92
+ }
93
+
94
+ protected async more() {
95
+ const vf = this.visibilityFilter ?? ((item) => true);
96
+ const selected = await askSuggestion(
97
+ this.items.filter(vf),
98
+ this.suggestionRenderer ?? this.itemRenderer,
99
+ (text: string) => this.match(text),
100
+ { title: this.title });
101
+ this.selectedItems?.add(selected);
102
+ }
103
+
104
+ }
@@ -0,0 +1,360 @@
1
+ import Bind from "@web-atoms/core/dist/core/Bind";
2
+ import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty";
3
+ import Colors from "@web-atoms/core/dist/core/Colors";
4
+ import XNode from "@web-atoms/core/dist/core/XNode";
5
+ import StyleRule from "@web-atoms/core/dist/style/StyleRule";
6
+ import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
7
+ import CSS from "@web-atoms/core/dist/web/styles/CSS";
8
+ import { ChildEnumerator } from "@web-atoms/core/dist/web/core/AtomUI";
9
+
10
+
11
+ CSS(StyleRule()
12
+ .display("grid")
13
+ .gridTemplateRows("auto 1fr auto")
14
+ .gridTemplateColumns("auto 1fr auto auto")
15
+ .backgroundColor(Colors.black)
16
+ .child(StyleRule("[data-element=video]")
17
+ .gridRowStart("1")
18
+ .gridRowEnd("span 3")
19
+ .gridColumnStart("1")
20
+ .gridColumnEnd("span 3")
21
+ .alignSelf("stretch")
22
+ .justifySelf("stretch")
23
+ )
24
+ .child(StyleRule("[data-element=play-element]")
25
+ .gridRowStart("1")
26
+ .gridRowEnd("span 3")
27
+ .gridColumnStart("1")
28
+ .gridColumnEnd("span 3")
29
+ .alignSelf("stretch")
30
+ .justifySelf("stretch")
31
+ .flexLayout({ justifyContent: "center"})
32
+ .child(StyleRule("button.play")
33
+ .display("inline-flex")
34
+ .alignItems("center")
35
+ .justifyContent("center")
36
+ .color(Colors.white)
37
+ .backgroundColor(Colors.blue)
38
+ .borderRadius(9999)
39
+ .fontSize(25)
40
+ .padding(10)
41
+ .width(50)
42
+ .height(50)
43
+ .textAlign("center")
44
+ .verticalAlign("middle")
45
+ .child(StyleRule("i")
46
+ .marginLeft(4)
47
+ )
48
+ )
49
+ )
50
+ .child(StyleRule("[data-element=progress]")
51
+ .zIndex("11")
52
+ .gridRowStart("2")
53
+ .gridColumnStart("1")
54
+ .gridColumnEnd("span 3")
55
+ .alignSelf("flex-end")
56
+ .height(4)
57
+ .justifySelf("stretch" as any)
58
+ .backgroundColor(Colors.black)
59
+ .width("100%")
60
+ .cursor("pointer")
61
+ )
62
+ .child(StyleRule("[data-element=toolbar]")
63
+ .zIndex("10")
64
+ .gridRowStart("3")
65
+ .gridColumnStart("1")
66
+ .gridColumnEnd("span 3")
67
+ .backgroundColor(Colors.black.withAlphaPercent(0.3))
68
+ .color(Colors.white)
69
+ .flexLayout({
70
+ justifyContent: "flex-start"
71
+ })
72
+ .child(StyleRule("*")
73
+ .minWidth(20)
74
+ .marginLeft(5)
75
+ .padding(5)
76
+ )
77
+ .child(StyleRule("[data-style=button]")
78
+ .width(20)
79
+ )
80
+ .child(StyleRule("[data-font-size=small]")
81
+ .fontSize("x-small")
82
+ )
83
+ .child(StyleRule("[data-element=volume-range]")
84
+ .height(2)
85
+ .color(Colors.green)
86
+ )
87
+ .child(StyleRule("[data-element=full-screen]")
88
+ .marginLeft("auto")
89
+ .marginRight(5)
90
+ )
91
+ )
92
+ .and(StyleRule("[data-controls=true]")
93
+ .child(StyleRule("[data-element=toolbar]")
94
+ .display("flex")
95
+ )
96
+ .child(StyleRule("[data-element=progress]")
97
+ .display("flex")
98
+ )
99
+ )
100
+ .and(StyleRule("[data-state=pause]")
101
+ .child(StyleRule("[data-element=toolbar]")
102
+ .display("flex")
103
+ )
104
+ .child(StyleRule("[data-element=toolbar]")
105
+ .child(StyleRule("[data-element=pause]")
106
+ .display("none")
107
+ )
108
+ )
109
+ )
110
+ .and(StyleRule("[data-state=play]")
111
+ .child(StyleRule("[data-element=play-element]")
112
+ .display("none")
113
+ )
114
+ .child(StyleRule("[data-element=toolbar]")
115
+ .child(StyleRule("[data-element=play]")
116
+ .display("none")
117
+ )
118
+ )
119
+ .and(StyleRule("[data-controls=false]")
120
+ .child(StyleRule("[data-element=toolbar]")
121
+ .display("none")
122
+ )
123
+ .child(StyleRule("[data-element=progress]")
124
+ .display("none")
125
+ )
126
+ )
127
+ )
128
+ , "*[data-video-player=video-player]");
129
+
130
+ const gatherElements = (e: HTMLElement, data = {}) => {
131
+ const ce = ChildEnumerator.enumerate(e);
132
+ for (const iterator of ce) {
133
+ const elementName = iterator.dataset.element?.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
134
+ if (elementName) {
135
+ data[elementName] = iterator;
136
+ }
137
+ gatherElements(iterator, data);
138
+ }
139
+ return data;
140
+ }
141
+
142
+ const numberToText = (n: number) => {
143
+ if (n < 10) {
144
+ return "0" + n;
145
+ }
146
+ return n.toString();
147
+ };
148
+
149
+ const durationText = (n: number, total: number) => {
150
+ if (n === null || n === undefined) {
151
+ return "";
152
+ }
153
+ const minutes = Math.floor(n / 60);
154
+ const seconds = numberToText(Math.ceil(n % 60));
155
+ const totalMinutes = Math.floor(total / 60);
156
+ const totalSeconds = numberToText(Math.ceil(total % 60));
157
+ return `${minutes}:${seconds} / ${totalMinutes}:${totalSeconds}`;
158
+ };
159
+
160
+ const noSoundIcon = "fa-duotone fa-volume-slash",
161
+ mute = "fa-duotone fa-volume-xmark",
162
+ low = "fa-duotone fa-volume-low",
163
+ mid = "fa-duotone fa-volume",
164
+ high = "fa-duotone fa-volume-high";
165
+
166
+ export default class AtomVideoPlayer extends AtomControl {
167
+
168
+ @BindableProperty
169
+ public source: any;
170
+
171
+ public duration: number;
172
+
173
+ public time: number;
174
+
175
+ private video: HTMLVideoElement;
176
+
177
+ private progress: HTMLCanvasElement;
178
+
179
+ private poster: HTMLImageElement;
180
+
181
+ private currentTimeSpan: HTMLSpanElement;
182
+ private soundIcon: HTMLElement;
183
+ private volumeRange: HTMLInputElement;
184
+
185
+ public onPropertyChanged(name: keyof AtomVideoPlayer): void {
186
+ switch (name) {
187
+ case "source":
188
+ this.updateSource();
189
+ break;
190
+ }
191
+ }
192
+
193
+ protected create(): void {
194
+ this.element.dataset.videoPlayer = "video-player";
195
+ this.bindEvent(this.element, "togglePlay", (e: CustomEvent) => {
196
+ if (this.video.paused) {
197
+ this.video.play();
198
+ } else {
199
+ this.video.pause();
200
+ }
201
+ });
202
+ this.bindEvent(this.element, "volume", (e: CustomEvent) => {
203
+ this.video.muted = !this.video.muted;
204
+ this.updateVolume();
205
+ });
206
+ this.bindEvent(this.element, "fullScreen", (e: CustomEvent) => {
207
+ const f = e.target as HTMLElement;
208
+ if (this.element === document.fullscreenElement) {
209
+ document.exitFullscreen();
210
+ return;
211
+ }
212
+ document.onfullscreenchange = () => {
213
+ if (document.fullscreenElement !== this.element) {
214
+ f.className = "fa-solid fa-expand";
215
+ document.onfullscreenchange = undefined;
216
+ }
217
+ };
218
+ this.element.requestFullscreen({ navigationUI: "show" });
219
+ f.className = "fa-solid fa-compress";
220
+ });
221
+ this.render(<div data-click-event="toggle-play" data-state="pause">
222
+ <video
223
+ event-abort={() => this.element.dataset.state = "abort"}
224
+ event-durationchange={(e: Event) => this.duration = this.video.duration}
225
+ event-ended={() => this.element.dataset.state = "ended"}
226
+ event-loadedmetadata={() => {
227
+ this.duration = this.video.duration;
228
+ this.updateVolume();
229
+ this.currentTimeSpan.textContent = durationText(0, this.duration);
230
+ this.updateProgress();
231
+ }}
232
+ event-pause={() => this.element.dataset.state = "pause"}
233
+ event-play={() => this.element.dataset.state = "play"}
234
+ event-progress={(e) => this.updateProgress()}
235
+ event-timeupdate={() => {
236
+ this.time = this.video.currentTime;
237
+ this.currentTimeSpan.textContent = durationText(this.time, this.duration);
238
+ this.element.dataset.state = "play";
239
+ this.updateProgress();
240
+ }}
241
+ event-waiting={() => this.element.dataset.state = "waiting"}
242
+ event-volumechange={() => this.updateVolume()}
243
+ autoplay={false}
244
+ data-element="video"/>
245
+ <canvas
246
+ data-element="progress"
247
+ />
248
+ <img data-element="poster"/>
249
+ <div data-element="toolbar">
250
+ <i
251
+ data-element="play"
252
+ data-style="button"
253
+ class="fa-solid fa-play"/>
254
+ <i
255
+ data-element="pause"
256
+ data-style="button"
257
+ class="fa-solid fa-pause"/>
258
+ <i
259
+ data-click-event="volume"
260
+ data-style="button"
261
+ data-element="sound"
262
+ class="fa-duotone fa-volume-slash"></i>
263
+ <input
264
+ data-click-event="none"
265
+ data-element="volume-range"
266
+ type="range"
267
+ min={0}
268
+ max={1}
269
+ step={0.1}
270
+ />
271
+ <span
272
+ data-font-size="small"
273
+ data-element="current" text="0:00"/>
274
+ <i
275
+ data-click-event="full-screen"
276
+ data-style="button"
277
+ data-element="full-screen"
278
+ class="fa-solid fa-expand"></i>
279
+ </div>
280
+ <div
281
+ data-element="play-element">
282
+ <button class="play">
283
+ <i class="fa-solid fa-play"/>
284
+ </button>
285
+ </div>
286
+ </div>);
287
+
288
+ const all = gatherElements(this.element) as any;
289
+ this.video = all.video;
290
+ this.progress = all.progress;
291
+ this.currentTimeSpan = all.current;
292
+ this.soundIcon = all.sound;
293
+ this.volumeRange = all.volumeRange;
294
+ this.bindEvent(this.volumeRange, "input", () => {
295
+ setTimeout(() => {
296
+ this.video.volume = parseFloat(this.volumeRange.value);
297
+ }, 1);
298
+ });
299
+ this.bindEvent(this.element, "pointerenter", () => {
300
+ this.element.dataset.controls = "true";
301
+ });
302
+ this.bindEvent(this.element, "pointerleave", () => {
303
+ this.element.dataset.controls = "false";
304
+ });
305
+ this.bindEvent(this.progress, "click", (e: MouseEvent) => {
306
+ e.preventDefault();
307
+ const scale = e.clientX / this.progress.clientWidth;
308
+ this.video.currentTime = this.video.duration * scale;
309
+ });
310
+ }
311
+
312
+ private updateProgress() {
313
+ const context = this.progress.getContext("2d");
314
+ // context.fillStyle = "rgba(0,0,0,0)";
315
+ context.strokeStyle = "rgba(0,0,0,0)";
316
+ const width = this.progress.clientWidth;
317
+ const height = this.progress.clientHeight;
318
+ this.progress.width = width;
319
+ this.progress.height = height;
320
+ context.clearRect(0,0, width, height);
321
+ const max = this.video.duration;
322
+ const seekable = this.video.buffered;
323
+ const scale = width / max;
324
+ context.fillStyle = "rgba(255,255,255,0.5)";
325
+ for (let index = 0; index < seekable.length; index++) {
326
+ const start = seekable.start(index) * scale;
327
+ const end = seekable.end(index) * scale;
328
+ context.fillRect(start, 0, end, height);
329
+ }
330
+ context.fillStyle = "#ffffff";
331
+ context.fillRect(0, 0, this.video.currentTime * scale, height);
332
+ }
333
+
334
+ private updateVolume() {
335
+ if (this.video.muted) {
336
+ this.soundIcon.className = mute;
337
+ this.volumeRange.style.display = "none";
338
+ this.soundIcon.title = "Unmute";
339
+ return;
340
+ }
341
+ const audio = this.video.volume;
342
+ this.volumeRange.style.display = "";
343
+ this.volumeRange.value = audio?.toString();
344
+ this.soundIcon.title = "Mute";
345
+ if (audio > 0.8) {
346
+ this.soundIcon.className = high;
347
+ return;
348
+ }
349
+ if (audio < 0.2) {
350
+ this.soundIcon.className = low;
351
+ return;
352
+ }
353
+ this.soundIcon.className = mid;
354
+ }
355
+
356
+ protected updateSource() {
357
+ this.video.src = this.source;
358
+ }
359
+
360
+ }
@@ -0,0 +1,21 @@
1
+ import XNode from "@web-atoms/core/dist/core/XNode";
2
+ import StyleRule from "@web-atoms/core/dist/style/StyleRule";
3
+ import CSS from "@web-atoms/core/dist/web/styles/CSS";
4
+
5
+ CSS(StyleRule()
6
+ .height(5)
7
+ .child(StyleRule("*")
8
+ .position("absolute")
9
+ .left(0)
10
+ .top(0)
11
+ )
12
+ , "*[data-track-progress=track-progress]");
13
+
14
+ export default function TrackProgress(a) {
15
+ return <div
16
+ data-track-progress="track-progress">
17
+ <div class="available"/>
18
+ <div class="done"/>
19
+ <div class="thumb"/>
20
+ </div>;
21
+ }