Skip to content

Commit 59646c1

Browse files
authored
Nice category chooser in Fixture Editor (#365)
1 parent 45d6a4b commit 59646c1

File tree

5 files changed

+100
-21
lines changed

5 files changed

+100
-21
lines changed

views/pages/fixture_editor.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,19 @@ module.exports = function(options) {
127127
str += '</label>';
128128
str += '</section>';
129129

130-
str += '<section class="categories">';
131-
str += '<label class="validate-group">';
130+
const fixtureCategories = JSON.stringify(properties.category.enum.map(
131+
cat => ({
132+
name: cat,
133+
icon: svg.getCategoryIcon(cat)
134+
})
135+
)).replace(/"/g, '\'');
136+
137+
str += '<section class="categories validate-group">';
132138
str += '<span class="label">Categories</span>';
133139
str += '<span class="value">';
134-
str += `<select multiple required size="${Object.keys(options.register.categories).length}" v-model="fixture.categories">`;
135-
for (const cat of properties.category.enum) {
136-
str += `<option value="${cat}">${cat}</option>`;
137-
}
138-
str += '</select>';
140+
str += `<category-chooser :all-categories="${fixtureCategories}" v-model="fixture.categories"></category-chooser>`;
139141
str += '<span class="error-message" hidden></span>';
140142
str += '</span>';
141-
str += '</label>';
142143
str += '</section>';
143144

144145
str += '<section class="comment">';
@@ -253,6 +254,7 @@ module.exports = function(options) {
253254

254255
str += '</div>';
255256

257+
str += getCategoryChooserTemplate();
256258
str += getPhysicalTemplate();
257259
str += getModeTemplate();
258260
str += getCapabilityTemplate();
@@ -265,6 +267,26 @@ module.exports = function(options) {
265267
return str;
266268
};
267269

270+
/**
271+
* @returns {!string} The Vue template for a <div> containing the fixture's category chooser.
272+
*/
273+
function getCategoryChooserTemplate() {
274+
let str = '<script type="text/x-template" id="template-category-chooser">';
275+
str += '<div>';
276+
277+
str += '<draggable v-model="selectedCategories" element="span">';
278+
str += ' <a href="#" v-for="cat in selectedCategories" @click.prevent="deselect(cat)" class="category-badge selected"><span v-html="cat.icon"></span> {{cat.name}}</a>';
279+
str += '</draggable>';
280+
281+
str += '<a href="#" v-for="cat in unselectedCategories" @click.prevent="select(cat)" class="category-badge"><span v-html="cat.icon"></span> {{cat.name}}</a>';
282+
283+
str += '<span class="hint">Select and reorder all applicable categories, the most suitable first.</span>';
284+
str += '</div>';
285+
str += '</script>'; // #template-category-chooser
286+
287+
return str;
288+
}
289+
268290
/**
269291
* @returns {!string} The Vue template for a <div> containing a fixture's or mode's physical information.
270292
*/
@@ -471,6 +493,7 @@ function getModeTemplate() {
471493

472494
str += '<h3>Channels</h3>';
473495

496+
str += '<div class="validate-group mode-channels">';
474497
str += '<draggable v-model="mode.channels" :options="dragOptions">';
475498
str += ' <transition-group class="mode-channels" name="mode-channels" tag="ol">';
476499
str += ' <li v-for="(channelUuid, index) in mode.channels" :key="channelUuid" :data-channel-uuid="channelUuid">';
@@ -482,6 +505,8 @@ function getModeTemplate() {
482505
str += ' </li>';
483506
str += ' </transition-group>';
484507
str += '</draggable>';
508+
str += '<span class="error-message" hidden></span>';
509+
str += '</div>';
485510

486511
str += '<a href="#add-channel" class="button primary" @click.prevent="addChannel">add channel</a>';
487512

views/pages/single_fixture.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,9 @@ function handleFixtureInfo() {
226226
let str = '<section class="categories">';
227227
str += ' <span class="label">Categories</span>';
228228
str += ' <span class="value">';
229-
str += fixture.categories.map(
230-
cat => `<a href="/categories/${encodeURIComponent(cat)}" class="category-badge">${svg.getCategoryIcon(cat)} ${cat}</a>`
231-
).join(' ');
229+
for (const cat of fixture.categories) {
230+
str += `<a href="/categories/${encodeURIComponent(cat)}" class="category-badge">${svg.getCategoryIcon(cat)} ${cat}</a>`;
231+
}
232232
str += ' </span>';
233233
str += '</section>';
234234

views/scripts/fixture-editor.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,43 @@ Vue.component('a11y-dialog', {
9999

100100
Vue.component('draggable', draggable);
101101

102+
Vue.component('category-chooser', {
103+
template: '#template-category-chooser',
104+
props: ['value', 'allCategories'],
105+
computed: {
106+
selectedCategories: {
107+
get: function() {
108+
var self = this;
109+
return this.value.map(function(catName) {
110+
return self.allCategories.find(function(cat) {
111+
return cat.name === catName;
112+
});
113+
});
114+
},
115+
set: function(newSelectedCategories) {
116+
this.$emit('input', newSelectedCategories.map(function(cat) {
117+
return cat.name;
118+
}));
119+
}
120+
},
121+
unselectedCategories: function() {
122+
var self = this;
123+
return this.allCategories.filter(function(cat) {
124+
return self.value.indexOf(cat.name) === -1;
125+
});
126+
}
127+
},
128+
methods: {
129+
select: function(cat) {
130+
this.value.push(cat.name);
131+
},
132+
deselect: function(cat) {
133+
var index = this.value.indexOf(cat.name);
134+
this.value.splice(index, 1);
135+
}
136+
}
137+
});
138+
102139
Vue.component('physical-data', {
103140
template: '#template-physical',
104141
props: ['value'],

views/scripts/validate.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,25 @@ module.exports = (function() {
6565
}
6666
}
6767

68+
if (field.matches('.categories')) {
69+
if (field.querySelectorAll('.selected').length === 0) {
70+
return 'Please select at least one category.';
71+
}
72+
}
73+
6874
if (field.matches('.physical-lens-degrees') || field.matches('.capability')) {
6975
var range = field.querySelectorAll('input');
7076
if (range[0].value !== '' && range[1].value !== '' && Number(range[0].value) > Number(range[1].value)) {
7177
return 'The start value of a range must not be greater than its end.';
7278
}
7379
}
7480

81+
if (field.matches('.mode-channels')) {
82+
if (field.querySelectorAll('li').length === 0) {
83+
return 'A mode must contain at least one channel.';
84+
}
85+
}
86+
7587
if (field.matches('.channelName')) {
7688
if (/\bfine\b|\d+(?:\s|-|_)*bit|\bMSB\b|\bLSB\b/i.test(field.value)) {
7789
return 'Please don\'t create fine channels here, set its resolution below instead.';
@@ -224,6 +236,10 @@ module.exports = (function() {
224236
}
225237
});
226238

239+
if (errors.length === 0 && customError) {
240+
errors = [customError];
241+
}
242+
227243
validate.showError(group, errors.join(' '));
228244

229245
return errors.length > 0 ? errors : null;
@@ -312,19 +328,22 @@ module.exports = (function() {
312328

313329
event.preventDefault();
314330

331+
var firstErrorGroup;
315332
var firstErrorField;
316333

317334
[].forEach.call(form.querySelectorAll(settings.groupSelector), function(group) {
318335
var error = validate.validateGroup(group);
319-
if (error && !firstErrorField) {
336+
if (error && !firstErrorGroup) {
337+
firstErrorGroup = group;
320338
firstErrorField = group.querySelector(settings.fieldSelector);
321339
}
322340
});
323341

324342
// If there are errors, focus on first element with error
325-
if (firstErrorField) {
326-
var scrollContainer = firstErrorField.closest('.dialog') || window;
327-
scrollIntoView(firstErrorField, {
343+
if (firstErrorGroup) {
344+
var scrollToElem = firstErrorField ? firstErrorField : firstErrorGroup;
345+
var scrollContainer = scrollToElem.closest('.dialog') || window;
346+
scrollIntoView(scrollToElem, {
328347
time: 300,
329348
align: {
330349
top: 0,
@@ -335,7 +354,7 @@ module.exports = (function() {
335354
return target === scrollContainer;
336355
}
337356
}, function() {
338-
firstErrorField.focus();
357+
scrollToElem.focus();
339358
});
340359

341360
return;
@@ -400,9 +419,6 @@ module.exports = (function() {
400419
* @param {validateOnSubmitCallback} onSubmit function to call after successful validation
401420
*/
402421
validate.init = function(onSubmit) {
403-
// Destroy any existing initializations
404-
validate.destroy();
405-
406422
settings.onSubmit = onSubmit;
407423

408424
// Add the `novalidate` attribute to all forms

views/stylesheets/fixture.scss

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
.category-badge {
9797
display: inline-block;
9898
padding: 4px 1.5ex 4px 1ex;
99-
margin: 0 0 4px;
99+
margin: 0 4px 4px 0;
100100
border-radius: 5000px; /* see http://stackoverflow.com/a/18795153/451391 */
101101
background: $grey-100;
102102

@@ -112,6 +112,7 @@
112112

113113
&.selected {
114114
background-color: $blue-700;
115+
cursor: move;
115116

116117
&:link,
117118
&:visited {
@@ -333,7 +334,7 @@ a.fixture-mode {
333334
content: " #" counter(modes);
334335
}
335336

336-
.mode-channels {
337+
ol.mode-channels {
337338
padding-left: 1.9em;
338339
min-height: 1em;
339340

0 commit comments

Comments
 (0)