1414 @submit.prevent =" onSubmit()" >
1515
1616 <div v-if =" channel.editMode === `add-existing`" class =" existing-channel-input-container" >
17- <LabeledInput :formstate =" formstate" name =" existingChannelUuid" label =" Select an existing channel" >
18- <select
19- v-model =" channel.uuid"
17+ <LabeledInput
18+ :formstate =" formstate"
19+ name =" existingChannelUuid"
20+ multiple-inputs >
21+ <input
22+ v-model =" selectedChannelUuidsString"
2023 name =" existingChannelUuid"
21- size =" 10"
22- style =" width : 100% ;"
24+ type =" hidden"
2325 required >
24- <option
25- v-for =" channelUuid of currentModeUnchosenChannels"
26- :key =" channelUuid"
27- :value =" channelUuid" >
28- {{ getChannelName(channelUuid) }}
29- </option >
30- </select >
26+ <fieldset class =" channel-list" >
27+ <legend >Select existing channel(s)</legend >
28+ <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -- double click is just a shortcut, all functionality is still accessible via keyboard -->
29+ <label
30+ v-for =" item of currentModeUnchosenChannels"
31+ :key =" item.uuid"
32+ :for =" item.inputId"
33+ class =" channel-list-item"
34+ @dblclick =" onChannelDoubleClick(item.uuid)" >
35+ <input
36+ :id =" item.inputId"
37+ :checked =" item.isSelected"
38+ type =" checkbox"
39+ class =" channel-checkbox"
40+ @change =" toggleChannelSelection(item.uuid)" >
41+ <span class =" channel-name" >{{ item.name }}</span >
42+ <code v-if =" item.showUuid" class =" channel-uuid" >{{ item.uuid }}</code >
43+ </label >
44+ </fieldset >
3145 </LabeledInput >
3246
3347 <p >or <a href =" #create-channel" @click.prevent =" setEditModeCreate()" >create a new channel</a ></p >
230244 display : block ;
231245}
232246
247+ .channel-list {
248+ max-height : 400px ;
249+ padding : 0 ;
250+ margin : 0 ;
251+ overflow-y : auto ;
252+ list-style : none ;
253+ background-color : theme-color (card-background );
254+ border : 1px solid theme-color (text-secondary );
255+
256+ legend {
257+ display : block ;
258+ width : calc (100% + 2px );
259+ padding : 0 ;
260+ margin : 0 -1px ;
261+ color : theme-color (text-secondary );
262+ border-bottom : 1px solid theme-color (text-secondary );
263+ }
264+ }
265+
266+ .channel-list-item {
267+ display : flex ;
268+ gap : 1ex ;
269+ align-items : center ;
270+ padding : 0.5ex 2ex ;
271+ cursor : pointer ;
272+ user-select : none ;
273+ border-bottom : 1px solid theme-color (divider );
274+ transition : background-color 0.15s ;
275+
276+ & :where (:has(.channel-checkbox:checked )) {
277+ background-color : theme-color (active-background );
278+ }
279+
280+ & :last-child {
281+ border-bottom : none ;
282+ }
283+
284+ & :hover ,
285+ & :has (.channel-checkbox :focus-visible ) {
286+ background-color : theme-color (hover-background );
287+ }
288+ }
289+
290+ .channel-checkbox {
291+ flex-shrink : 0 ;
292+ margin : 0 ;
293+ cursor : pointer ;
294+ }
295+
296+ .channel-uuid {
297+ font-size : 0.85em ;
298+ color : theme-color (text-secondary );
299+ }
300+
233301@media (min-width : $phone ) {
234302 #channel-dialog ::v- deep .dialog {
235303 width : 80% ;
@@ -291,6 +359,7 @@ export default {
291359 channelProperties,
292360 singleColors: capabilityTypes .ColorIntensity .properties .color .enum ,
293361 constants,
362+ selectedChannelUuids: [],
294363 };
295364 },
296365 computed: {
@@ -302,43 +371,22 @@ export default {
302371 const modeIndex = this .fixture .modes .findIndex (mode => mode .uuid === uuid);
303372 return this .fixture .modes [modeIndex];
304373 },
374+ currentModeUnchosenChannelUuids () {
375+ return Object .keys (this .fixture .availableChannels ).filter (
376+ channelUuid => ! this .currentMode .channels .includes (channelUuid),
377+ );
378+ },
305379 currentModeUnchosenChannels () {
306- return Object .keys (this .fixture .availableChannels ).filter (channelUuid => {
307- if (this .currentMode .channels .includes (channelUuid)) {
308- // already used
309- return false ;
310- }
311-
312- const channel = this .fixture .availableChannels [channelUuid];
313- if (` coarseChannelId` in channel) {
314- // should we include this fine channel?
315-
316- if (! this .currentMode .channels .includes (channel .coarseChannelId )) {
317- // its coarse channel is not yet in the mode
318- return false ;
319- }
320-
321- const modeChannels = this .currentMode .channels .map (
322- uuid => this .fixture .availableChannels [uuid],
323- );
324-
325- const otherFineChannels = modeChannels .filter (
326- otherChannel => ` coarseChannelId` in otherChannel && otherChannel .coarseChannelId === channel .coarseChannelId ,
327- );
328-
329- const maxFoundResolution = Math .max (
330- constants .RESOLUTION_8BIT ,
331- ... otherFineChannels .map (otherFineChannel => otherFineChannel .resolution ),
332- );
333-
334- if (maxFoundResolution !== channel .resolution - 1 ) {
335- // the finest channel currently used is not its next coarser channel
336- return false ;
337- }
338- }
339-
340- return true ;
341- });
380+ return this .currentModeUnchosenChannelUuids .map (channelUuid => ({
381+ inputId: ` unchosen-channel-${ channelUuid} ` ,
382+ uuid: channelUuid,
383+ name: this .getChannelName (channelUuid),
384+ showUuid: ! this .isChannelNameUnique (channelUuid),
385+ isSelected: this .isChannelSelected (channelUuid),
386+ }));
387+ },
388+ selectedChannelUuidsString () {
389+ return this .selectedChannelUuids .join (` ,` );
342390 },
343391 currentModeDisplayName () {
344392 let modeName = ` #${ this .fixture .modes .indexOf (this .currentMode ) + 1 } ` ;
@@ -372,7 +420,8 @@ export default {
372420 },
373421 submitButtonTitle () {
374422 if (this .channel .editMode === ` add-existing` ) {
375- return ` Add channel` ;
423+ const count = this .selectedChannelUuids .length ;
424+ return count <= 1 ? ` Add channel` : ` Add ${ count} channels` ;
376425 }
377426
378427 if (this .channel .editMode === ` create` ) {
@@ -404,6 +453,110 @@ export default {
404453 return fixtureEditor .getChannelName (channelUuid);
405454 },
406455
456+ isChannelNameUnique (channelUuid ) {
457+ const fixtureEditor = this .$parent ;
458+ return fixtureEditor .isChannelNameUnique (channelUuid);
459+ },
460+
461+ isChannelSelected (channelUuid ) {
462+ return this .selectedChannelUuids .includes (channelUuid);
463+ },
464+
465+ modeHasChannel (channelUuid ) {
466+ return this .currentMode .channels .includes (channelUuid);
467+ },
468+
469+ toggleChannelSelection (channelUuid ) {
470+ if (this .isChannelSelected (channelUuid)) {
471+ this .deselectChannel (channelUuid);
472+ }
473+ else {
474+ this .selectChannel (channelUuid);
475+ }
476+
477+ this .channelChanged = true ;
478+ },
479+
480+ deselectChannel (channelUuid ) {
481+ const deselectedChannel = this .fixture .availableChannels [channelUuid];
482+ const isFineChannel = ` coarseChannelId` in deselectedChannel;
483+
484+ // Deselect the channel
485+ this .selectedChannelUuids = this .selectedChannelUuids .filter (uuid => uuid !== channelUuid);
486+
487+ if (isFineChannel) {
488+ // Deselect all finer channels
489+ this .selectedChannelUuids = this .selectedChannelUuids .filter (uuid => {
490+ const channel = this .fixture .availableChannels [uuid];
491+ return (
492+ ! (` coarseChannelId` in channel) ||
493+ channel .coarseChannelId !== deselectedChannel .coarseChannelId ||
494+ channel .resolution < deselectedChannel .resolution
495+ );
496+ });
497+ return ;
498+ }
499+
500+ // Deselect all fine channels belonging to this coarse channel
501+ this .selectedChannelUuids = this .selectedChannelUuids .filter (uuid => {
502+ const channel = this .fixture .availableChannels [uuid];
503+ return ! (` coarseChannelId` in channel) || channel .coarseChannelId !== channelUuid;
504+ });
505+ },
506+
507+ selectChannel (channelUuid ) {
508+ if (this .isChannelSelected (channelUuid)) {
509+ return ;
510+ }
511+
512+ const selectedChannel = this .fixture .availableChannels [channelUuid];
513+ const isFineChannel = ` coarseChannelId` in selectedChannel;
514+
515+ if (! isFineChannel) {
516+ this .selectedChannelUuids .push (channelUuid);
517+ return ;
518+ }
519+
520+ // Add the coarse channel if not already selected
521+ const coarseChannelId = selectedChannel .coarseChannelId ;
522+ if (! this .isChannelSelected (coarseChannelId) && ! this .modeHasChannel (coarseChannelId)) {
523+ this .selectedChannelUuids .push (coarseChannelId);
524+ }
525+
526+ // Add all finer channels between coarse and selected fine channel
527+ const currentResolution = selectedChannel .resolution ;
528+ for (const uuid of this .currentModeUnchosenChannelUuids ) {
529+ const channel = this .fixture .availableChannels [uuid];
530+ if (
531+ ` coarseChannelId` in channel &&
532+ channel .coarseChannelId === coarseChannelId &&
533+ channel .resolution < currentResolution &&
534+ ! this .isChannelSelected (uuid) &&
535+ ! this .modeHasChannel (uuid)
536+ ) {
537+ this .selectedChannelUuids .push (uuid);
538+ }
539+ }
540+
541+ this .selectedChannelUuids .push (channelUuid);
542+ },
543+
544+ async onChannelDoubleClick (channelUuid ) {
545+ // Select the channel if not already selected
546+ if (! this .isChannelSelected (channelUuid)) {
547+ this .toggleChannelSelection (channelUuid);
548+ }
549+
550+ if (this .selectedChannelUuids .some (uuid => uuid !== channelUuid)) {
551+ // another channel is already selected, do not submit on double-click
552+ return ;
553+ }
554+
555+ // wait until validation state is updated
556+ await this .$nextTick ();
557+ this .onSubmit ();
558+ },
559+
407560 async onChannelDialogOpen () {
408561 if (this .restored ) {
409562 this .restored = false ;
@@ -415,6 +568,7 @@ export default {
415568 }
416569 else if (this .channel .editMode === ` add-existing` ) {
417570 this .channel .uuid = ` ` ;
571+ this .selectedChannelUuids = [];
418572 }
419573 else if (this .channel .editMode === ` edit-all` || this .channel .editMode === ` edit-duplicate` ) {
420574 this .copyPropertiesFromChannel (this .fixture .availableChannels [this .channel .uuid ]);
@@ -624,7 +778,14 @@ export default {
624778 },
625779
626780 addExistingChannel () {
627- this .currentMode .channels .push (this .channel .uuid );
781+ for (const channelUuid of this .selectedChannelUuids ) {
782+ if (! this .modeHasChannel (channelUuid)) {
783+ this .currentMode .channels .push (channelUuid);
784+ }
785+ }
786+
787+ // Reset selection
788+ this .selectedChannelUuids = [];
628789 },
629790
630791 /**
0 commit comments