Time conductor validation (#2273)

* Fixed validation issues for bounds

* Validate offsets correctly

* Reset validation on mode change

* Use toggle mixin for Conductor menus
This commit is contained in:
Andrew Henry 2019-01-25 13:40:46 -08:00 committed by Pegah Sarram
parent 075d4deecb
commit ac2b9acccb
8 changed files with 118 additions and 99 deletions

View File

@ -22,9 +22,8 @@
<template> <template>
<div class="c-conductor" <div class="c-conductor"
:class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode']"> :class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode']">
<form class="u-contents" ref="conductorForm" <form class="u-contents" ref="conductorForm" @submit.prevent="updateTimeFromConductor">
@submit="isFixed ? setBoundsFromView($event) : setOffsetsFromView($event)"> <button class="c-input--submit" type="submit" ref="submitButton"></button>
<ConductorModeIcon class="c-conductor__mode-icon"></ConductorModeIcon> <ConductorModeIcon class="c-conductor__mode-icon"></ConductorModeIcon>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed" <div class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
@ -35,7 +34,7 @@
type="text" autocorrect="off" spellcheck="false" type="text" autocorrect="off" spellcheck="false"
ref="startDate" ref="startDate"
v-model="formattedBounds.start" v-model="formattedBounds.start"
@change="validateBounds('start', $event.target); setBoundsFromView()" /> @change="validateAllBounds(); submitForm()" />
<date-picker <date-picker
:default-date-time="formattedBounds.start" :default-date-time="formattedBounds.start"
:formatter="timeFormatter" :formatter="timeFormatter"
@ -48,9 +47,10 @@
<div class="c-direction-indicator icon-minus"></div> <div class="c-direction-indicator icon-minus"></div>
<input class="c-input--hrs-min-sec" <input class="c-input--hrs-min-sec"
type="text" autocorrect="off" type="text" autocorrect="off"
ref="startOffset"
spellcheck="false" spellcheck="false"
v-model="offsets.start" v-model="offsets.start"
@change="validateOffsets($event); setOffsetsFromView()"> @change="validateAllOffsets(); submitForm()">
</div> </div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed"> <div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
@ -63,7 +63,7 @@
v-model="formattedBounds.end" v-model="formattedBounds.end"
:disabled="!isFixed" :disabled="!isFixed"
ref="endDate" ref="endDate"
@change="validateBounds('end', $event.target); setBoundsFromView()"> @change="validateAllBounds(); submitForm()">
<date-picker <date-picker
class="c-ctrl-wrapper--menus-left" class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.end" :default-date-time="formattedBounds.end"
@ -80,8 +80,9 @@
type="text" type="text"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
ref="endOffset"
v-model="offsets.end" v-model="offsets.end"
@change="validateOffsets($event); setOffsetsFromView()"> @change="validateAllOffsets(); submitForm()">
</div> </div>
<conductor-axis <conductor-axis
@ -101,6 +102,14 @@
<style lang="scss"> <style lang="scss">
@import "~styles/sass-base"; @import "~styles/sass-base";
.c-input--submit {
// Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work
visibility: none;
height: 0;
width: 0;
padding: 0;
}
/*********************************************** CONDUCTOR LAYOUT */ /*********************************************** CONDUCTOR LAYOUT */
.c-conductor { .c-conductor {
display: grid; display: grid;
@ -357,6 +366,7 @@ export default {
}, },
setViewFromClock(clock) { setViewFromClock(clock) {
this.isFixed = clock === undefined; this.isFixed = clock === undefined;
this.clearAllValidation();
}, },
setViewFromBounds(bounds) { setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start); this.formattedBounds.start = this.timeFormatter.format(bounds.start);
@ -368,11 +378,32 @@ export default {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start)); this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end)); this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end));
}, },
validateBounds(startOrEnd, input) { updateTimeFromConductor() {
if (this.isFixed) {
this.setBoundsFromView();
} else {
this.setOffsetsFromView();
}
},
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate, this.$refs.startOffset, this.$refs.endOffset].forEach((input) => {
input.setCustomValidity('');
input.title = '';
});
},
validateAllBounds() {
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let validationResult = true; let validationResult = true;
let formattedDate;
if (!this.timeFormatter.validate(input.value)){ if (input === this.$refs.startDate) {
validationResult = 'Invalid date value'; formattedDate = this.formattedBounds.start;
} else {
formattedDate = this.formattedBounds.end;
}
if (!this.timeFormatter.validate(formattedDate)){
validationResult = 'Invalid date';
} else { } else {
let boundsValues = { let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start), start: this.timeFormatter.parse(this.formattedBounds.start),
@ -383,16 +414,28 @@ export default {
if (validationResult !== true){ if (validationResult !== true){
input.setCustomValidity(validationResult); input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else { } else {
input.setCustomValidity(''); input.setCustomValidity('');
input.title = '';
return true;
} }
});
}, },
validateOffsets(event) { validateAllOffsets(event) {
let input = event.target; return [this.$refs.startOffset, this.$refs.endOffset].every((input) => {
let validationResult = true; let validationResult = true;
let formattedOffset;
if (!this.durationFormatter.validate(input.value)) { if (input === this.$refs.startOffset) {
validationResult = 'Invalid offset value'; formattedOffset = this.offsets.start;
} else {
formattedOffset = this.offsets.end;
}
if (!this.durationFormatter.validate(formattedOffset)) {
validationResult = 'Offsets must be in the format hh:mm:ss and less than 24 hours in duration';
} else { } else {
let offsetValues = { let offsetValues = {
start: 0 - this.durationFormatter.parse(this.offsets.start), start: 0 - this.durationFormatter.parse(this.offsets.start),
@ -403,10 +446,19 @@ export default {
if (validationResult !== true){ if (validationResult !== true){
input.setCustomValidity(validationResult); input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else { } else {
input.setCustomValidity(''); input.setCustomValidity('');
input.title = '';
return true;
} }
});
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.$refs.submitButton.click());
}, },
getFormatter(key) { getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({ return this.openmct.telemetry.getValueFormatter({
@ -415,13 +467,13 @@ export default {
}, },
startDateSelected(date){ startDateSelected(date){
this.formattedBounds.start = this.timeFormatter.format(date); this.formattedBounds.start = this.timeFormatter.format(date);
this.validateBounds('start', this.$refs.startDate); this.validateAllBounds();
this.setBoundsFromView(); this.submitForm();
}, },
endDateSelected(date){ endDateSelected(date){
this.formattedBounds.end = this.timeFormatter.format(date); this.formattedBounds.end = this.timeFormatter.format(date);
this.validateBounds('end', this.$refs.endDate); this.validateAllBounds();
this.setBoundsFromView(); this.submitForm();
}, },
}, },
mounted() { mounted() {

View File

@ -22,11 +22,11 @@
<template> <template>
<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"> <div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<button class="c-button--menu c-mode-button" <button class="c-button--menu c-mode-button"
@click="toggleMenu($event)"> @click.prevent="toggle">
<span class="c-button__label">{{selectedMode.name}}</span> <span class="c-button__label">{{selectedMode.name}}</span>
</button> </button>
<div class="c-menu c-super-menu c-conductor__mode-menu" <div class="c-menu c-super-menu c-conductor__mode-menu"
v-if="showMenu"> v-if="open">
<div class="c-super-menu__menu"> <div class="c-super-menu__menu">
<ul> <ul>
<li v-for="mode in modes" <li v-for="mode in modes"
@ -69,8 +69,11 @@
</style> </style>
<script> <script>
import toggleMixin from '../../ui/mixins/toggle-mixin';
export default { export default {
inject: ['openmct', 'configuration'], inject: ['openmct', 'configuration'],
mixins: [toggleMixin],
data: function () { data: function () {
let activeClock = this.openmct.time.clock(); let activeClock = this.openmct.time.clock();
if (activeClock !== undefined) { if (activeClock !== undefined) {
@ -81,8 +84,7 @@ export default {
selectedMode: this.getModeOptionForClock(activeClock), selectedMode: this.getModeOptionForClock(activeClock),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())), selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
modes: [], modes: [],
hoveredMode: {}, hoveredMode: {}
showMenu: false
}; };
}, },
methods: { methods: {
@ -177,18 +179,8 @@ export default {
setViewFromClock(clock) { setViewFromClock(clock) {
this.selectedMode = this.getModeOptionForClock(clock); this.selectedMode = this.getModeOptionForClock(clock);
},
toggleMenu(event) {
this.showMenu = !this.showMenu;
if (this.showMenu) {
document.addEventListener('click', this.toggleMenu, true);
} else {
document.removeEventListener('click', this.toggleMenu, true);
} }
}, },
},
mounted: function () { mounted: function () {
this.loadClocksFromConfiguration(); this.loadClocksFromConfiguration();

View File

@ -24,10 +24,10 @@
v-if="selectedTimeSystem.name"> v-if="selectedTimeSystem.name">
<button class="c-button--menu c-time-system-button" <button class="c-button--menu c-time-system-button"
:class="selectedTimeSystem.cssClass" :class="selectedTimeSystem.cssClass"
@click="toggleMenu($event)"> @click.prevent="toggle">
<span class="c-button__label">{{selectedTimeSystem.name}}</span> <span class="c-button__label">{{selectedTimeSystem.name}}</span>
</button> </button>
<div class="c-menu" v-if="showMenu"> <div class="c-menu" v-if="open">
<ul> <ul>
<li @click="setTimeSystemFromView(timeSystem)" <li @click="setTimeSystemFromView(timeSystem)"
v-for="timeSystem in timeSystems" v-for="timeSystem in timeSystems"
@ -41,15 +41,17 @@
</template> </template>
<script> <script>
import toggleMixin from '../../ui/mixins/toggle-mixin';
export default { export default {
inject: ['openmct', 'configuration'], inject: ['openmct', 'configuration'],
mixins: [toggleMixin],
data: function () { data: function () {
let activeClock = this.openmct.time.clock(); let activeClock = this.openmct.time.clock();
return { return {
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())), selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
timeSystems: this.getValidTimesystemsForClock(activeClock), timeSystems: this.getValidTimesystemsForClock(activeClock)
showMenu: false
}; };
}, },
methods: { methods: {
@ -94,16 +96,6 @@ export default {
return this.configuration.menuOptions.filter(configMatches)[0]; return this.configuration.menuOptions.filter(configMatches)[0];
}, },
toggleMenu(event) {
this.showMenu = !this.showMenu;
if (this.showMenu) {
document.addEventListener('click', this.toggleMenu, true);
} else {
document.removeEventListener('click', this.toggleMenu, true);
}
},
setViewFromTimeSystem(timeSystem) { setViewFromTimeSystem(timeSystem) {
this.selectedTimeSystem = timeSystem; this.selectedTimeSystem = timeSystem;
}, },

View File

@ -22,12 +22,12 @@
<template> <template>
<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up c-datetime-picker__wrapper" ref="calendarHolder"> <div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up c-datetime-picker__wrapper" ref="calendarHolder">
<a class="c-click-icon icon-calendar" <a class="c-click-icon icon-calendar"
@click="togglePicker()"></a> @click="toggle"></a>
<div class="c-menu c-menu--mobile-modal c-datetime-picker" <div class="c-menu c-menu--mobile-modal c-datetime-picker"
v-if="showPicker"> v-if="open">
<div class="c-datetime-picker__close-button"> <div class="c-datetime-picker__close-button">
<button class="c-click-icon icon-x-in-circle" <button class="c-click-icon icon-x-in-circle"
@click="togglePicker()"></button> @click="toggle"></button>
</div> </div>
<div class="c-datetime-picker__pager c-pager l-month-year-pager"> <div class="c-datetime-picker__pager c-pager l-month-year-pager">
<div class="c-pager__prev c-click-icon icon-arrow-left" <div class="c-pager__prev c-click-icon icon-arrow-left"
@ -160,6 +160,7 @@
<script> <script>
import moment from 'moment'; import moment from 'moment';
import toggleMixin from '../../ui/mixins/toggle-mixin';
const TIME_NAMES = { const TIME_NAMES = {
'hours': "Hour", 'hours': "Hour",
@ -181,13 +182,13 @@ const TIME_OPTIONS = (function makeRanges() {
export default { export default {
inject: ['openmct'], inject: ['openmct'],
mixins: [toggleMixin],
props: { props: {
defaultDateTime: String, defaultDateTime: String,
formatter: Object formatter: Object
}, },
data: function () { data: function () {
return { return {
showPicker: false,
picker: { picker: {
year: undefined, year: undefined,
month: undefined, month: undefined,
@ -285,7 +286,6 @@ export default {
this.date.year = cell.year; this.date.year = cell.year;
this.date.day = cell.day; this.date.day = cell.day;
this.updateFromView(); this.updateFromView();
this.showPicker = false;
}, },
dateEquals(d1, d2) { dateEquals(d1, d2) {
@ -315,23 +315,6 @@ export default {
optionsFor(key) { optionsFor(key) {
return TIME_OPTIONS[key]; return TIME_OPTIONS[key];
}, },
hidePicker(event) {
let path = event.composedPath();
if (path.indexOf(this.$refs.calendarHolder) === -1) {
this.showPicker = false;
}
},
togglePicker() {
this.showPicker = !this.showPicker;
if (this.showPicker) {
document.addEventListener('click', this.hidePicker, {
capture: true
});
}
}
}, },
mounted: function () { mounted: function () {
this.updateFromModel(this.defaultDateTime); this.updateFromModel(this.defaultDateTime);

View File

@ -29,7 +29,7 @@
<script> <script>
import toggleMixin from './toggle-mixin'; import toggleMixin from '../../mixins/toggle-mixin';
export default { export default {
mixins: [toggleMixin], mixins: [toggleMixin],

View File

@ -22,7 +22,7 @@
</template> </template>
<script> <script>
import toggle from './toggle-mixin'; import toggle from '../../mixins/toggle-mixin';
export default { export default {
mixins: [toggle], mixins: [toggle],
props: { props: {

View File

@ -19,7 +19,7 @@
</template> </template>
<script> <script>
import toggleMixin from './toggle-mixin'; import toggleMixin from '../../mixins/toggle-mixin';
export default { export default {
mixins: [toggleMixin], mixins: [toggleMixin],