@musodojo/note-collection-selector 5.3.0 → 6.0.0
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/examples/example1.html +40 -6
- package/examples/scale.png +0 -0
- package/package.json +1 -1
- package/src/mod.ts +301 -135
package/examples/example1.html
CHANGED
|
@@ -14,19 +14,48 @@
|
|
|
14
14
|
font-size: 1.5em;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
#wrapper {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 1em;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
button {
|
|
23
|
+
font: inherit;
|
|
24
|
+
background: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
button,
|
|
17
28
|
note-collection-selector::part(main-button) {
|
|
18
29
|
border: 0.2em solid currentColor;
|
|
19
30
|
border-radius: 0.6em;
|
|
20
31
|
padding: 0.3em 1em;
|
|
32
|
+
background: linear-gradient(
|
|
33
|
+
45deg,
|
|
34
|
+
transparent,
|
|
35
|
+
light-dark(rgb(0 0 0 / 35%), rgb(255 255 255 / 35%))
|
|
36
|
+
);
|
|
21
37
|
}
|
|
22
38
|
</style>
|
|
23
39
|
</head>
|
|
24
40
|
<body>
|
|
25
|
-
<h2>Select a Note Collection</h2>
|
|
26
|
-
<
|
|
41
|
+
<h2>Select from a Note Collection Selector</h2>
|
|
42
|
+
<div id="wrapper">
|
|
43
|
+
<note-collection-selector></note-collection-selector>
|
|
44
|
+
<note-collection-selector>
|
|
45
|
+
<svg viewBox="0 0 100 100">
|
|
46
|
+
<circle cx="50" cy="50" r="50" fill="currentColor" />
|
|
47
|
+
</svg>
|
|
48
|
+
</note-collection-selector>
|
|
49
|
+
<note-collection-selector>
|
|
50
|
+
<img
|
|
51
|
+
src="./scale.png"
|
|
52
|
+
alt="scale"
|
|
53
|
+
style="object-fit: contain; border-radius: 0.2rem"
|
|
54
|
+
/>
|
|
55
|
+
</note-collection-selector>
|
|
56
|
+
</div>
|
|
27
57
|
<br />
|
|
28
|
-
<
|
|
29
|
-
<button id="random-button">Random Collection</button>
|
|
58
|
+
<button id="random-button">Randomize</button>
|
|
30
59
|
|
|
31
60
|
<script type="module" src="../dist/bundle.js"></script>
|
|
32
61
|
<script type="module">
|
|
@@ -36,12 +65,17 @@
|
|
|
36
65
|
console.log("note collection selected:", event.detail);
|
|
37
66
|
},
|
|
38
67
|
);
|
|
39
|
-
|
|
68
|
+
|
|
69
|
+
const noteCollectionSelector = document.querySelectorAll(
|
|
40
70
|
"note-collection-selector",
|
|
41
71
|
);
|
|
72
|
+
|
|
42
73
|
const randomButton = document.getElementById("random-button");
|
|
74
|
+
|
|
43
75
|
randomButton.addEventListener("click", () => {
|
|
44
|
-
noteCollectionSelector.
|
|
76
|
+
noteCollectionSelector.forEach((selector) =>
|
|
77
|
+
selector.setRandomNoteCollection()
|
|
78
|
+
);
|
|
45
79
|
});
|
|
46
80
|
</script>
|
|
47
81
|
</body>
|
|
Binary file
|
package/package.json
CHANGED
package/src/mod.ts
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* A custom element for selecting musical note collections (scales, chords, etc.).
|
|
4
|
+
*
|
|
5
|
+
* @fires {CustomEvent<NoteCollectionSelectedEventDetail>} note-collection-selected - Dispatched when a note collection is selected.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```html
|
|
9
|
+
* <note-collection-selector
|
|
10
|
+
* id="my-selector"
|
|
11
|
+
* selected-note-collection-key="ionian"
|
|
12
|
+
* ></note-collection-selector>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ```javascript
|
|
16
|
+
* const selector = document.getElementById('my-selector');
|
|
17
|
+
* selector.addEventListener('note-collection-selected', (event) => {
|
|
18
|
+
* console.log('Selected key:', event.detail.noteCollectionKey);
|
|
19
|
+
* console.log('Selected collection:', event.detail.noteCollection);
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Programmatically change the selection
|
|
23
|
+
* selector.selectedNoteCollectionKey = 'dorian';
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
1
26
|
import {
|
|
2
27
|
groupedNoteCollections,
|
|
3
|
-
type NoteCollection,
|
|
4
|
-
type NoteCollectionGroupKey,
|
|
5
28
|
noteCollectionGroupsMetadata,
|
|
6
|
-
type NoteCollectionKey,
|
|
7
29
|
noteCollections,
|
|
8
30
|
} from "@musodojo/music-theory-data";
|
|
31
|
+
import type {
|
|
32
|
+
NoteCollection,
|
|
33
|
+
NoteCollectionGroupKey,
|
|
34
|
+
NoteCollectionKey,
|
|
35
|
+
} from "@musodojo/music-theory-data";
|
|
9
36
|
|
|
10
37
|
const noteCollectionSelectorTemplate = document.createElement("template");
|
|
11
38
|
noteCollectionSelectorTemplate.innerHTML = /* HTML */ `
|
|
@@ -44,27 +71,29 @@ noteCollectionSelectorTemplate.innerHTML = /* HTML */ `
|
|
|
44
71
|
|
|
45
72
|
min-width: var(--_main-icon-size);
|
|
46
73
|
|
|
47
|
-
> #main-button-text-span {
|
|
74
|
+
& > #main-button-text-span {
|
|
48
75
|
grid-area: 1 / 1;
|
|
49
76
|
}
|
|
50
77
|
|
|
51
|
-
> slot {
|
|
78
|
+
& > slot {
|
|
52
79
|
height: var(--_main-icon-size);
|
|
53
80
|
}
|
|
54
81
|
|
|
55
|
-
|
|
56
|
-
::slotted(
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
/* Size icons, but let text content flow naturally */
|
|
83
|
+
& > ::slotted(svg),
|
|
84
|
+
& > ::slotted(img),
|
|
85
|
+
& > slot > svg {
|
|
59
86
|
width: var(--_main-icon-size);
|
|
60
87
|
height: var(--_main-icon-size);
|
|
88
|
+
/* Ensure icons are on the same grid cell if multiple are slotted */
|
|
89
|
+
grid-area: 1 / 1;
|
|
61
90
|
}
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
[part="dialog"] {
|
|
65
94
|
padding: var(--_default-spacing);
|
|
66
95
|
|
|
67
|
-
> [part="close-dialog-button"] {
|
|
96
|
+
& > [part="close-dialog-button"] {
|
|
68
97
|
display: grid;
|
|
69
98
|
place-items: center;
|
|
70
99
|
padding: var(--_default-spacing);
|
|
@@ -73,9 +102,9 @@ noteCollectionSelectorTemplate.innerHTML = /* HTML */ `
|
|
|
73
102
|
margin-block-end: var(--_default-spacing);
|
|
74
103
|
|
|
75
104
|
/* Size icons, but let text content flow naturally */
|
|
76
|
-
::slotted(svg),
|
|
77
|
-
::slotted(img),
|
|
78
|
-
> slot[name="close-dialog-icon"] > svg {
|
|
105
|
+
& > ::slotted(svg),
|
|
106
|
+
& > ::slotted(img),
|
|
107
|
+
& > slot[name="close-dialog-icon"] > svg {
|
|
79
108
|
width: var(--_close-dialog-icon-size);
|
|
80
109
|
height: var(--_close-dialog-icon-size);
|
|
81
110
|
/* Ensure icons are on the same grid cell if multiple are slotted */
|
|
@@ -83,39 +112,42 @@ noteCollectionSelectorTemplate.innerHTML = /* HTML */ `
|
|
|
83
112
|
}
|
|
84
113
|
}
|
|
85
114
|
|
|
86
|
-
>
|
|
115
|
+
& > [part="clear-selection-button"],
|
|
116
|
+
& > #toggle-more-info-label {
|
|
87
117
|
padding: 0.5em;
|
|
88
118
|
border: 0.1em solid currentColor;
|
|
89
119
|
border-radius: 0.5em;
|
|
90
120
|
cursor: pointer;
|
|
91
121
|
}
|
|
92
|
-
|
|
93
|
-
> #note-collections-div {
|
|
122
|
+
|
|
123
|
+
& > #note-collections-div {
|
|
94
124
|
display: flex;
|
|
95
125
|
flex-direction: column;
|
|
96
|
-
gap:
|
|
97
|
-
margin-block-start:
|
|
126
|
+
gap: 2em;
|
|
127
|
+
margin-block-start: 1.5em;
|
|
98
128
|
|
|
99
|
-
> #note-collection-group-wrapper {
|
|
100
|
-
> #note-collection-group-div {
|
|
129
|
+
& > #note-collection-group-wrapper {
|
|
130
|
+
& > #note-collection-group-div {
|
|
101
131
|
margin-block: 0.5em;
|
|
102
132
|
display: flex;
|
|
103
133
|
flex-wrap: wrap;
|
|
104
134
|
gap: 1em;
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
> h3 {
|
|
108
|
-
margin:
|
|
137
|
+
& > h3 {
|
|
138
|
+
margin: 0;
|
|
139
|
+
font-size: 1em;
|
|
140
|
+
font-variant: small-caps;
|
|
109
141
|
}
|
|
110
142
|
}
|
|
111
|
-
}
|
|
143
|
+
}
|
|
112
144
|
}
|
|
113
145
|
|
|
114
146
|
[part="dialog"]::backdrop {
|
|
115
147
|
background: var(--_dialog-backdrop-background);
|
|
116
148
|
}
|
|
117
149
|
|
|
118
|
-
|
|
150
|
+
[part="note-collection-button"] {
|
|
119
151
|
padding: 0.5em;
|
|
120
152
|
min-width: 4ch;
|
|
121
153
|
max-width: 80ch;
|
|
@@ -123,10 +155,16 @@ noteCollectionSelectorTemplate.innerHTML = /* HTML */ `
|
|
|
123
155
|
border-radius: 0.5em;
|
|
124
156
|
cursor: pointer;
|
|
125
157
|
text-align: left;
|
|
126
|
-
text-wrap: pretty;
|
|
158
|
+
text-wrap: pretty;
|
|
159
|
+
|
|
160
|
+
& > .note-collection-name {
|
|
161
|
+
/* font-weight: bold; */
|
|
162
|
+
font-size: 0.9em;
|
|
163
|
+
}
|
|
127
164
|
|
|
128
|
-
|
|
129
|
-
|
|
165
|
+
/* Style for the currently selected option */
|
|
166
|
+
&[data-selected="true"] {
|
|
167
|
+
outline: 0.3em double light-dark(black, white);
|
|
130
168
|
}
|
|
131
169
|
|
|
132
170
|
/* When the "more info" div is hidden, center the text */
|
|
@@ -134,13 +172,14 @@ noteCollectionSelectorTemplate.innerHTML = /* HTML */ `
|
|
|
134
172
|
text-align: center;
|
|
135
173
|
}
|
|
136
174
|
|
|
137
|
-
> .more-info-div {
|
|
175
|
+
& > .more-info-div {
|
|
138
176
|
display: flex;
|
|
139
177
|
flex-direction: column;
|
|
140
178
|
gap: 0.5em;
|
|
141
179
|
}
|
|
142
180
|
|
|
143
|
-
|
|
181
|
+
/* needed a .hidden with a higher specificity here */
|
|
182
|
+
& > .more-info-div.hidden {
|
|
144
183
|
display: none;
|
|
145
184
|
}
|
|
146
185
|
}
|
|
@@ -196,6 +235,8 @@ noteCollectionSelectorTemplate.innerHTML = /* HTML */ `
|
|
|
196
235
|
more info
|
|
197
236
|
</label>
|
|
198
237
|
|
|
238
|
+
<button part="clear-selection-button">Clear Selection</button>
|
|
239
|
+
|
|
199
240
|
<div id="note-collections-div">
|
|
200
241
|
<!-- the buttons in here are dynamically generated
|
|
201
242
|
each with an attribute of part="note-collection-button" -->
|
|
@@ -203,11 +244,20 @@ noteCollectionSelectorTemplate.innerHTML = /* HTML */ `
|
|
|
203
244
|
</dialog>
|
|
204
245
|
`;
|
|
205
246
|
|
|
247
|
+
/**
|
|
248
|
+
* The detail object for the `note-collection-selected` custom event.
|
|
249
|
+
*/
|
|
206
250
|
export interface NoteCollectionSelectedEventDetail {
|
|
251
|
+
/** The unique key of the selected note collection (e.g., "ionian"), or `null` if cleared. */
|
|
207
252
|
noteCollectionKey: NoteCollectionKey | null;
|
|
253
|
+
/** The full data object for the selected note collection, or `null` if cleared. */
|
|
208
254
|
noteCollection: NoteCollection | null;
|
|
209
255
|
}
|
|
210
256
|
|
|
257
|
+
/**
|
|
258
|
+
* A web component that allows users to select a musical note collection.
|
|
259
|
+
* It displays a button which, when clicked, opens a dialog with a list of options.
|
|
260
|
+
*/
|
|
211
261
|
export class NoteCollectionSelector extends HTMLElement {
|
|
212
262
|
#shadowRoot: ShadowRoot;
|
|
213
263
|
|
|
@@ -219,16 +269,29 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
219
269
|
#dialog!: HTMLDialogElement;
|
|
220
270
|
#closeDialogButton!: HTMLButtonElement;
|
|
221
271
|
#toggleMoreInfoCheckbox!: HTMLInputElement;
|
|
272
|
+
#clearSelectionButton!: HTMLButtonElement;
|
|
222
273
|
#noteCollectionsDiv!: HTMLDivElement;
|
|
223
274
|
|
|
275
|
+
/** Caches the currently selected button element in the dialog to avoid re-querying the DOM. */
|
|
276
|
+
#selectedButtonElement: HTMLButtonElement | null = null;
|
|
277
|
+
|
|
278
|
+
/** Manages event listeners to allow for cleanup on disconnection. */
|
|
224
279
|
#abortController: AbortController | null = null;
|
|
280
|
+
/** The internal state for the selected note collection key (e.g., "ionian"). */
|
|
225
281
|
#selectedNoteCollectionKey: NoteCollectionKey | null = null;
|
|
282
|
+
/** The internal state for the selected note collection key's matching object. */
|
|
226
283
|
#selectedNoteCollection: NoteCollection | null = null;
|
|
227
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Specifies which attributes should trigger `attributeChangedCallback` when they change.
|
|
287
|
+
*/
|
|
228
288
|
static get observedAttributes(): string[] {
|
|
229
289
|
return ["selected-note-collection-key"];
|
|
230
290
|
}
|
|
231
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Initializes the component by creating the shadow DOM and caching essential elements.
|
|
294
|
+
*/
|
|
232
295
|
constructor() {
|
|
233
296
|
super();
|
|
234
297
|
this.#shadowRoot = this.attachShadow({ mode: "open" });
|
|
@@ -238,17 +301,32 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
238
301
|
this.#cacheDomElements();
|
|
239
302
|
}
|
|
240
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Called when the element is inserted into the DOM.
|
|
306
|
+
* Finalizes setup by populating dynamic content and attaching event listeners.
|
|
307
|
+
*/
|
|
241
308
|
connectedCallback() {
|
|
242
309
|
this.#populateNoteCollectionsDiv();
|
|
243
310
|
this.#addEventListeners();
|
|
244
|
-
this.#updateMainButton();
|
|
245
311
|
this.#syncSelectedNoteCollectionKeyAttribute();
|
|
312
|
+
this.#updateSelectedButtonElementState();
|
|
313
|
+
this.#updateMainButton();
|
|
246
314
|
}
|
|
247
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Called when the element is removed from the DOM.
|
|
318
|
+
* Cleans up by aborting any pending operations and removing event listeners.
|
|
319
|
+
*/
|
|
248
320
|
disconnectedCallback() {
|
|
249
321
|
this.#abortController?.abort();
|
|
250
322
|
}
|
|
251
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Called when an observed attribute of the custom element is added, removed, or changed.
|
|
326
|
+
* @param name - The name of the attribute that changed.
|
|
327
|
+
* @param oldValue - The old value of the attribute.
|
|
328
|
+
* @param newValue - The new value of the attribute.
|
|
329
|
+
*/
|
|
252
330
|
attributeChangedCallback(
|
|
253
331
|
name: string,
|
|
254
332
|
oldValue: string | null,
|
|
@@ -256,14 +334,20 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
256
334
|
) {
|
|
257
335
|
// Only proceed if the attribute's value has actually changed
|
|
258
336
|
if (oldValue === newValue) return;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
337
|
+
|
|
338
|
+
switch (name) {
|
|
339
|
+
case "selected-note-collection-key":
|
|
340
|
+
this.selectedNoteCollectionKey = newValue as NoteCollectionKey | null;
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
console.log("Unexpected attribute changed:", name);
|
|
264
344
|
}
|
|
265
345
|
}
|
|
266
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Queries the shadow DOM for critical elements and caches them as private properties.
|
|
349
|
+
* Throws an error if any required element is not found, failing early.
|
|
350
|
+
*/
|
|
267
351
|
#cacheDomElements() {
|
|
268
352
|
const mainButton = this.#shadowRoot.querySelector<HTMLButtonElement>(
|
|
269
353
|
'[part="main-button"]',
|
|
@@ -289,6 +373,12 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
289
373
|
"#toggle-more-info-checkbox",
|
|
290
374
|
);
|
|
291
375
|
|
|
376
|
+
const clearSelectionButton = this.#shadowRoot.querySelector<
|
|
377
|
+
HTMLButtonElement
|
|
378
|
+
>(
|
|
379
|
+
'[part="clear-selection-button"]',
|
|
380
|
+
);
|
|
381
|
+
|
|
292
382
|
const noteCollectionsDiv = this.#shadowRoot.querySelector<HTMLDivElement>(
|
|
293
383
|
"#note-collections-div",
|
|
294
384
|
);
|
|
@@ -299,8 +389,9 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
299
389
|
!mainButtonSlot ||
|
|
300
390
|
!dialog ||
|
|
301
391
|
!closeDialogButton ||
|
|
302
|
-
!
|
|
303
|
-
!
|
|
392
|
+
!toggleMoreInfoCheckbox ||
|
|
393
|
+
!clearSelectionButton ||
|
|
394
|
+
!noteCollectionsDiv
|
|
304
395
|
) {
|
|
305
396
|
throw new Error(
|
|
306
397
|
"NoteCollectionSelector: Critical elements not found in shadow DOM.",
|
|
@@ -313,40 +404,13 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
313
404
|
this.#dialog = dialog;
|
|
314
405
|
this.#closeDialogButton = closeDialogButton;
|
|
315
406
|
this.#toggleMoreInfoCheckbox = toggleMoreInfoCheckbox;
|
|
407
|
+
this.#clearSelectionButton = clearSelectionButton;
|
|
316
408
|
this.#noteCollectionsDiv = noteCollectionsDiv;
|
|
317
409
|
}
|
|
318
410
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.#abortController = new AbortController();
|
|
323
|
-
const { signal } = this.#abortController;
|
|
324
|
-
|
|
325
|
-
this.#mainButton.addEventListener(
|
|
326
|
-
"click",
|
|
327
|
-
() => {
|
|
328
|
-
this.#dialog.showModal();
|
|
329
|
-
},
|
|
330
|
-
{ signal },
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
this.#closeDialogButton.addEventListener(
|
|
334
|
-
"click",
|
|
335
|
-
() => {
|
|
336
|
-
this.#dialog.close();
|
|
337
|
-
},
|
|
338
|
-
{ signal },
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
this.#toggleMoreInfoCheckbox.addEventListener(
|
|
342
|
-
"change",
|
|
343
|
-
() => {
|
|
344
|
-
this.#updateMoreInfoVisibility();
|
|
345
|
-
},
|
|
346
|
-
{ signal },
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
411
|
+
/**
|
|
412
|
+
* Dynamically generates and populates the note collection buttons in the dialog.
|
|
413
|
+
*/
|
|
350
414
|
#populateNoteCollectionsDiv() {
|
|
351
415
|
const frag = document.createDocumentFragment();
|
|
352
416
|
|
|
@@ -357,9 +421,7 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
357
421
|
// wrapper for heading and note-collection
|
|
358
422
|
const groupWrapper = document.createElement("div");
|
|
359
423
|
groupWrapper.id = "note-collection-group-wrapper";
|
|
360
|
-
groupWrapper.innerHTML =
|
|
361
|
-
${groupMetadata.displayName}
|
|
362
|
-
</h3>`;
|
|
424
|
+
groupWrapper.innerHTML = `<h3>${groupMetadata.displayName}</h3>`;
|
|
363
425
|
|
|
364
426
|
// more info on the group (can hide)
|
|
365
427
|
const groupMoreInfoDiv = document.createElement("div");
|
|
@@ -377,26 +439,23 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
377
439
|
|
|
378
440
|
Object.entries(currentNoteCollectionGroup).forEach(
|
|
379
441
|
([key, collection]) => {
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
442
|
+
const noteCollectionBtn = document.createElement("button");
|
|
443
|
+
noteCollectionBtn.setAttribute("part", "note-collection-button");
|
|
444
|
+
// Set a data attribute to use later
|
|
445
|
+
noteCollectionBtn.dataset.noteCollectionKey = key;
|
|
446
|
+
noteCollectionBtn.innerHTML = /* HTML */ `<span
|
|
447
|
+
class="note-collection-name"
|
|
448
|
+
>
|
|
383
449
|
${collection.primaryName}
|
|
384
|
-
</
|
|
450
|
+
</span>`;
|
|
385
451
|
|
|
386
452
|
const collectionMoreInfoDiv = document.createElement("div");
|
|
387
453
|
collectionMoreInfoDiv.classList.add("more-info-div", "hidden");
|
|
388
|
-
collectionMoreInfoDiv.innerHTML = this.#
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.#selectedNoteCollection = collection;
|
|
394
|
-
this.#updateMainButton();
|
|
395
|
-
this.#syncSelectedNoteCollectionKeyAttribute();
|
|
396
|
-
this.#dialog.close();
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
noteCollectionGroupDiv.appendChild(noteCollectionDiv);
|
|
454
|
+
collectionMoreInfoDiv.innerHTML = this.#getMoreInfoHTMLString(
|
|
455
|
+
collection,
|
|
456
|
+
);
|
|
457
|
+
noteCollectionBtn.appendChild(collectionMoreInfoDiv);
|
|
458
|
+
noteCollectionGroupDiv.appendChild(noteCollectionBtn);
|
|
400
459
|
},
|
|
401
460
|
);
|
|
402
461
|
|
|
@@ -408,36 +467,69 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
408
467
|
}
|
|
409
468
|
|
|
410
469
|
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* @param {NoteCollection} noteCollection - The note collection data object.
|
|
414
|
-
* @returns {string} An HTML string containing the detailed information.
|
|
470
|
+
* Attaches all necessary event listeners for the component's interactive elements.
|
|
471
|
+
* Uses an AbortController to facilitate easy cleanup on disconnection.
|
|
415
472
|
*/
|
|
416
|
-
#
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
<div>${noteCollection.intervals.join(", ")}</div>
|
|
422
|
-
<div>${noteCollection.type.join(", ")}</div>
|
|
423
|
-
<div>${noteCollection.characteristics.join(", ")}</div>
|
|
424
|
-
<div>${noteCollection.patternShort.join("-")}</div>
|
|
425
|
-
<div>${noteCollection.pattern.join(", ")}</div>
|
|
426
|
-
`;
|
|
427
|
-
}
|
|
473
|
+
#addEventListeners() {
|
|
474
|
+
// abort any previous controllers before creating a new one
|
|
475
|
+
this.#abortController?.abort();
|
|
476
|
+
this.#abortController = new AbortController();
|
|
477
|
+
const { signal } = this.#abortController;
|
|
428
478
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
479
|
+
this.#mainButton.addEventListener(
|
|
480
|
+
"click",
|
|
481
|
+
() => {
|
|
482
|
+
this.#dialog.showModal();
|
|
483
|
+
this.#selectedButtonElement?.scrollIntoView({
|
|
484
|
+
block: "center",
|
|
485
|
+
behavior: "smooth",
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
{ signal },
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
this.#closeDialogButton.addEventListener(
|
|
492
|
+
"click",
|
|
493
|
+
() => {
|
|
494
|
+
this.#dialog.close();
|
|
495
|
+
},
|
|
496
|
+
{ signal },
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
this.#toggleMoreInfoCheckbox.addEventListener(
|
|
500
|
+
"change",
|
|
501
|
+
() => {
|
|
502
|
+
this.#updateMoreInfoVisibility();
|
|
503
|
+
},
|
|
504
|
+
{ signal },
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
this.#clearSelectionButton.addEventListener(
|
|
508
|
+
"click",
|
|
509
|
+
() => {
|
|
510
|
+
this.selectedNoteCollectionKey = null;
|
|
511
|
+
this.#dialog.close();
|
|
512
|
+
},
|
|
513
|
+
{ signal },
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
this.#noteCollectionsDiv.addEventListener(
|
|
517
|
+
"click",
|
|
518
|
+
(event) => {
|
|
519
|
+
const button = (event.target as HTMLElement).closest<HTMLButtonElement>(
|
|
520
|
+
'[part="note-collection-button"]',
|
|
521
|
+
);
|
|
522
|
+
if (button) {
|
|
523
|
+
this.selectedNoteCollectionKey =
|
|
524
|
+
button.dataset.noteCollectionKey as NoteCollectionKey ??
|
|
525
|
+
null;
|
|
526
|
+
this.#dialog.close();
|
|
527
|
+
} else {
|
|
528
|
+
console.warn("no note collection button found");
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
{ signal },
|
|
532
|
+
);
|
|
441
533
|
}
|
|
442
534
|
|
|
443
535
|
/**
|
|
@@ -465,13 +557,65 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
465
557
|
}
|
|
466
558
|
}
|
|
467
559
|
|
|
560
|
+
#updateSelectedButtonElementState() {
|
|
561
|
+
// Clear the highlight from the previously selected button
|
|
562
|
+
if (this.#selectedButtonElement) {
|
|
563
|
+
this.#selectedButtonElement.removeAttribute("data-selected");
|
|
564
|
+
this.#selectedButtonElement = null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Add the highlight to the newly selected button
|
|
568
|
+
if (this.#selectedNoteCollectionKey) {
|
|
569
|
+
const newSelectedButton = this.#noteCollectionsDiv.querySelector<
|
|
570
|
+
HTMLButtonElement
|
|
571
|
+
>(
|
|
572
|
+
`[data-note-collection-key="${this.#selectedNoteCollectionKey}"]`,
|
|
573
|
+
);
|
|
574
|
+
newSelectedButton?.setAttribute("data-selected", "true");
|
|
575
|
+
this.#selectedButtonElement = newSelectedButton;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Renders the detailed "more info" content for a given note collection.
|
|
581
|
+
* @private
|
|
582
|
+
* @param {NoteCollection} noteCollection - The note collection data object.
|
|
583
|
+
* @returns {string} A HTML string containing the detailed information.
|
|
584
|
+
*/
|
|
585
|
+
#getMoreInfoHTMLString(noteCollection: NoteCollection): string {
|
|
586
|
+
// TODO: replace this with updated code
|
|
587
|
+
// <div>${noteCollection.exampleNotes.join(", ")}</div>
|
|
588
|
+
return /* HTML */ `
|
|
589
|
+
<div>${noteCollection.names.join(", ")}</div>
|
|
590
|
+
<div>${noteCollection.intervals.join(", ")}</div>
|
|
591
|
+
<div>${noteCollection.type.join(", ")}</div>
|
|
592
|
+
<div>${noteCollection.characteristics.join(", ")}</div>
|
|
593
|
+
<div>${noteCollection.patternShort.join("-")}</div>
|
|
594
|
+
<div>${noteCollection.pattern.join(", ")}</div>
|
|
595
|
+
`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Toggles the visibility of all "more info" sections within the element
|
|
600
|
+
* based on the state of the `toggleMoreInfoCheckbox`.
|
|
601
|
+
* @private
|
|
602
|
+
*/
|
|
603
|
+
#updateMoreInfoVisibility() {
|
|
604
|
+
const moreInfoElements = this.#shadowRoot?.querySelectorAll(
|
|
605
|
+
".more-info-div",
|
|
606
|
+
) as NodeListOf<HTMLDivElement>;
|
|
607
|
+
moreInfoElements.forEach((el) => {
|
|
608
|
+
el.classList.toggle("hidden", !this.#toggleMoreInfoCheckbox.checked);
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
468
612
|
/**
|
|
469
613
|
* Synchronizes the `selected-note-collection-key` attribute on the host element
|
|
470
614
|
* with the component's internal state.
|
|
471
615
|
* @private
|
|
472
616
|
*/
|
|
473
617
|
#syncSelectedNoteCollectionKeyAttribute() {
|
|
474
|
-
if (this.#selectedNoteCollectionKey) {
|
|
618
|
+
if (this.#selectedNoteCollectionKey !== null) {
|
|
475
619
|
this.setAttribute(
|
|
476
620
|
"selected-note-collection-key",
|
|
477
621
|
this.#selectedNoteCollectionKey,
|
|
@@ -515,6 +659,8 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
515
659
|
noteCollections,
|
|
516
660
|
) as NoteCollectionKey[];
|
|
517
661
|
|
|
662
|
+
if (noteCollectionKeys.length <= 1) return;
|
|
663
|
+
|
|
518
664
|
let randomNoteCollectionKey: NoteCollectionKey;
|
|
519
665
|
|
|
520
666
|
// Keep selecting at random until it's different from the current one.
|
|
@@ -528,7 +674,9 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
528
674
|
|
|
529
675
|
/**
|
|
530
676
|
* Gets the unique key of the currently selected note collection.
|
|
531
|
-
*
|
|
677
|
+
* Setting this property updates the component's selection and dispatches a `note-collection-selected` event.
|
|
678
|
+
*
|
|
679
|
+
* @attr {NoteCollectionKey | null} selected-note-collection-key - Reflects the selected note collection key.
|
|
532
680
|
* @returns {NoteCollectionKey | null} The note collection key (e.g., "ionian") or `null` if no collection is selected.
|
|
533
681
|
*/
|
|
534
682
|
get selectedNoteCollectionKey(): NoteCollectionKey | null {
|
|
@@ -539,34 +687,52 @@ export class NoteCollectionSelector extends HTMLElement {
|
|
|
539
687
|
* Sets the currently selected note collection by its unique key.
|
|
540
688
|
* This will update the component's display and internal state. If a valid key
|
|
541
689
|
* is provided, the corresponding `NoteCollection` object will be looked up
|
|
542
|
-
* and stored internally
|
|
690
|
+
* and stored internally along witht the key.
|
|
691
|
+
* An invalid key is equivalent to setting the key to null.
|
|
692
|
+
* Setting to `null` clears the selection.
|
|
543
693
|
* @param {NoteCollectionKey | null} newNoteCollectionKey - The unique key of the note collection to select.
|
|
544
|
-
* @prop {NoteCollectionKey | null} selectedNoteCollectionKey
|
|
545
694
|
*/
|
|
546
695
|
set selectedNoteCollectionKey(
|
|
547
696
|
newNoteCollectionKey: NoteCollectionKey | null,
|
|
548
697
|
) {
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
698
|
+
// Resolve the input to a valid key or null
|
|
699
|
+
const resolvedNoteCollection =
|
|
700
|
+
noteCollections[newNoteCollectionKey as NoteCollectionKey] as
|
|
701
|
+
| NoteCollection
|
|
702
|
+
| undefined;
|
|
703
|
+
const resolvedKey = newNoteCollectionKey !== null &&
|
|
704
|
+
resolvedNoteCollection !== undefined
|
|
705
|
+
? newNoteCollectionKey
|
|
706
|
+
: null;
|
|
707
|
+
|
|
708
|
+
const hasChanged = this.#selectedNoteCollectionKey !== resolvedKey;
|
|
709
|
+
|
|
710
|
+
if (hasChanged) {
|
|
711
|
+
this.#selectedNoteCollectionKey = resolvedKey;
|
|
712
|
+
this.#selectedNoteCollection = resolvedKey
|
|
713
|
+
? resolvedNoteCollection as NoteCollection
|
|
714
|
+
: null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Only perform DOM updates and dispatch events if the component is connected
|
|
718
|
+
if (this.isConnected) {
|
|
719
|
+
// invalid values must sync attribute to null no matter what
|
|
720
|
+
// e.g. A user or script sets the attribute to an invalid value
|
|
721
|
+
// <note-collection-selector selected-note-collection-key="InvalidValue">
|
|
722
|
+
// should be synced to null
|
|
723
|
+
this.#syncSelectedNoteCollectionKeyAttribute();
|
|
724
|
+
if (hasChanged) {
|
|
725
|
+
this.#updateMainButton();
|
|
726
|
+
this.#updateSelectedButtonElementState();
|
|
727
|
+
this.#dispatchNoteCollectionSelectedEvent();
|
|
728
|
+
}
|
|
560
729
|
}
|
|
561
|
-
this.#updateMainButton();
|
|
562
|
-
this.#syncSelectedNoteCollectionKeyAttribute();
|
|
563
|
-
// No need to dispatch event here, as attributeChangedCallback will do it
|
|
564
730
|
}
|
|
565
731
|
|
|
566
732
|
/**
|
|
567
733
|
* Gets the full data object for the currently selected note collection.
|
|
568
|
-
* This property is read-only and is derived from `selectedNoteCollectionKey`.
|
|
569
|
-
*
|
|
734
|
+
* This property is read-only and is derived from the `selectedNoteCollectionKey`.
|
|
735
|
+
*
|
|
570
736
|
* @returns {NoteCollection | null} The full note collection object or `null` if no collection is selected.
|
|
571
737
|
* @readonly
|
|
572
738
|
*/
|