From 0fd471772f54ef09c878845eb324029e08173327 Mon Sep 17 00:00:00 2001 From: Phoenix Wong Date: Sun, 17 Jan 2021 15:36:55 +0800 Subject: [PATCH 1/4] Manual Input mode: added support to colon and space key Support moving to the next token slot when user press the colon or space key in manual-input mode --- src/vue-timepicker.vue | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/vue-timepicker.vue b/src/vue-timepicker.vue index 22b6e5d..388b03d 100644 --- a/src/vue-timepicker.vue +++ b/src/vue-timepicker.vue @@ -1440,6 +1440,11 @@ export default { } else if (evt.keyCode === 9) { this.clearKbInputLog() this.tabHandler(evt) + // Colon|Space + } else if (evt.keyCode === 186 || evt.keyCode === 32) { + evt.preventDefault() + this.clearKbInputLog() + this.toNextSlot() // Prevent any Non-ESC and non-pasting inputs } else if (evt.keyCode !== 27 && !(evt.metaKey || evt.ctrlKey)) { evt.preventDefault() @@ -1734,6 +1739,17 @@ export default { this.debounceSetInputSelection(firstChunkPos) }, + toNextSlot () { + if (!this.inputIsEmpty && this.tokenChunksPos && this.tokenChunksPos.length) { + const currentChunk = this.getCurrentTokenChunk() + if (!currentChunk) { return } + const lastChunk = this.tokenChunksPos[this.tokenChunksPos.length - 1] + if (currentChunk.token !== lastChunk.token) { + this.toLateralToken(false) + } + } + }, + toLateralToken (toLeft) { const currentChunk = this.getCurrentTokenChunk() if (!currentChunk) { From 017e03b26c5b56a3e507db2309c06786077bed76 Mon Sep 17 00:00:00 2001 From: Phoenix Wong Date: Sun, 17 Jan 2021 21:04:20 +0800 Subject: [PATCH 2/4] Added new `append-to-body` feature --- README.md | 37 +++++++- demo/src/components/Playground.vue | 18 ++++ src/vue-timepicker.vue | 145 ++++++++++++++++++++++++++--- 3 files changed, 184 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d3cb69a..3875ffd 100644 --- a/README.md +++ b/README.md @@ -593,7 +593,6 @@ Works with `drop-direction="auto"` either. Defaults to `160` (unit: _px_) if the ``` - ### Lazy Event Mode ```html @@ -604,9 +603,44 @@ When `lazy` event mode is toggled on, only an actual user behavior can trigger t - The user opened the dropdown and picked a new value - The user clicked the ("×") clear button +- The user inputted a new value or clear the existing value in the Manual Input mode In other words, on `lazy` mode, Timepicker won't emit `input` and `change` events on mounted, nor after the value got modified programmatically. + +### Append To Body + +Append the dropdown menu to the end of the document ``. Try this if you have `z-index` or `overflow` layout issue with the dropdown. + +```html + +``` + +The body-appended dropdown's CSS class is `vue__time-picker-dropdown`. Its default `z-index` is `100`. You can change the value by adding the following style in your app -- + +```css +/* E.g. set the z-index to 5000 */ +.vue__time-picker-dropdown { + z-index: 5000; +} +``` + +**NOTE**: If you have to override some of the CSS styles within the dropdown, you will need to update their selectors' class names as well. Simply change any `.vue__time-picker .dropdown` selector to `.vue__time-picker-dropdown`. + +For example, when you have a customized background color set for selected values: + +```css +/* Default override (not using "append-to-body") */ +.vue__time-picker .dropdown ul li:not([disabled]).active { + background: steelblue; +} + +/* When using "append-to-body" */ +.vue__time-picker-dropdown ul li:not([disabled]).active { + background: steelblue; +} +``` + ### Enable Debug Mode ```html @@ -671,6 +705,7 @@ Prop | Type | Required | Default Value **container-id** | _String_ | no | _undefined_ **drop-offset-height** | _Number_ | no | 160 **lazy** | _Boolean_ | no | false +**append-to-body** | _Boolean_ | no | false **debug-mode** | _Boolean_ | no | false diff --git a/demo/src/components/Playground.vue b/demo/src/components/Playground.vue index 1c0c3fd..353a862 100644 --- a/demo/src/components/Playground.vue +++ b/demo/src/components/Playground.vue @@ -74,6 +74,7 @@ export default { lazyMode: false, autoScroll: false, skipErrorStyle: false, + appendToBody: false, debugMode: false, customBlurDelay: false, @@ -220,6 +221,10 @@ export default { start += ('\n auto-scroll') } + if (this.appendToBody) { + start += ('\n append-to-body') + } + if (this.disablePicker) { start += ('\n disabled') } @@ -865,6 +870,18 @@ section#playground input(v-model="skipErrorStyle" type="radio" id="skip_error_false" name="skip_error", :value="false") |  Disable + #appendToBody.config-block + h3.subtitle + a.anchor # + | Append To Body + config-row(is-group) + label.options(for="append_to_body_true") + input(v-model="appendToBody" type="radio" id="append_to_body_true" name="append_to_body", :value="true") + |  Enable + label.options(for="append_to_body_false") + input(v-model="appendToBody" type="radio" id="append_to_body_false" name="append_to_body", :value="false") + |  Disable + #debugMode.config-block h3.subtitle a.anchor # @@ -905,6 +922,7 @@ section#playground :disabled="disablePicker" :lazy="lazyMode" :auto-scroll="autoScroll" + :append-to-body="appendToBody" :debug-mode="debugMode" :input-class="skipErrorStyle ? 'skip-error-style' : null" @change="changeHandler" diff --git a/src/vue-timepicker.vue b/src/vue-timepicker.vue index 388b03d..997a7a2 100644 --- a/src/vue-timepicker.vue +++ b/src/vue-timepicker.vue @@ -18,7 +18,6 @@ const DEFAULT_OPTIONS = { hideDisabledMinutes: false, hideDisabledSeconds: false, hideDisabledItems: false, - advancedKeyboard: false, hideDropdown: false, blurDelay: 300, manualInputTimeout: 1000, @@ -70,6 +69,7 @@ export default { dropDirection: { type: String, default: 'down' }, dropOffsetHeight: { type: [ Number, String ] }, containerId: { type: String }, + appendToBody: { type: Boolean, default: false }, manualInput: { type: Boolean, default: false }, manualInputTimeout: { type: [ Number, String ] }, @@ -1157,6 +1157,9 @@ export default { setDropdownState (toShow, fromUserClick = false) { if (toShow) { + if (this.appendToBody) { + this.appendDropdownToBody() + } this.keepFocusing() if (this.autoDirectionEnabled) { this.checkDropDirection() @@ -1173,7 +1176,54 @@ export default { } else { this.showDropdown = false this.$emit('close') + if (this.appendToBody) { + this.removeDropdownFromBody() + } + } + }, + + appendDropdownToBody () { + const dropdown = this.$refs && this.$refs.dropdown + const body = document.getElementsByTagName('body')[0] + if (body && dropdown) { + window.addEventListener('scroll', this.updateDropdownPos) + dropdown.classList.add('vue__time-picker-dropdown') + this.updateDropdownPos() + body.appendChild(dropdown) + } + }, + + updateDropdownPos () { + if (!this.appendToBody) { return } + const dropdown = this.$refs && this.$refs.dropdown + const body = document.getElementsByTagName('body')[0] + if (body && dropdown) { + const box = this.$el.getBoundingClientRect() + if (this.dropdownDirClass === 'drop-up') { + dropdown.style.bottom = `${window.innerHeight - box.y}px` + dropdown.style.top = 'auto' + } else { + dropdown.style.top = `${box.y + box.height}px` + dropdown.style.bottom = 'auto' + } + dropdown.style.left = `${box.x}px` + } + }, + + removeDropdownFromBody () { + const dropdown = this.$refs && this.$refs.dropdown + const body = document.getElementsByTagName('body')[0] + if (body && dropdown && body.contains(dropdown)) { + body.removeChild(dropdown) + } + if (dropdown) { + dropdown.classList.remove('vue__time-picker-dropdown') + dropdown.style.top = '' + dropdown.style.bottom = '' + dropdown.style.left = '' + this.$el.appendChild(dropdown) } + window.removeEventListener('scroll', this.updateDropdownPos) }, blurEvent () { @@ -1229,7 +1279,12 @@ export default { scrollToSelected (column, allowFallback = false) { if (!this.timeValue || this.inputIsEmpty) { return } - const targetList = this.$el.querySelectorAll(`ul.${column}s`)[0] + let targetList + if (this.appendToBody && this.$refs && this.$refs.dropdown) { + targetList = this.$refs.dropdown.querySelectorAll(`ul.${column}s`)[0] + } else { + targetList = this.$el.querySelectorAll(`ul.${column}s`)[0] + } let targetValue = this.activeItemInCol(column)[0] if (!targetValue && allowFallback) { // No value selected in the target column, fallback to the first found valid item @@ -1298,13 +1353,35 @@ export default { } }, + onTab (column, value, evt) { + if (this.appendToBody && evt.shiftKey) { + const firstColumn = this.inUse.types[0] + if (column !== firstColumn) { return } + const firstValidValue = this.validItemsInCol(firstColumn)[0] + // Is the first valid item in the first column + if (firstValidValue && firstValidValue.getAttribute('data-key') === String(value)) { + evt.preventDefault() + // Focus back on + if (this.$refs && this.$refs.input) { + this.$refs.input.focus() + } + } + } + }, + validItemsInCol (column) { const columnClass = `${column}s` + if (this.appendToBody && this.$refs && this.$refs.dropdown) { + return this.$refs.dropdown.querySelectorAll(`ul.${columnClass} > li:not(.hint):not([disabled])`) + } return this.$el.querySelectorAll(`ul.${columnClass} > li:not(.hint):not([disabled])`) }, activeItemInCol (column) { const columnClass = `${column}s` + if (this.appendToBody && this.$refs && this.$refs.dropdown) { + return this.$refs.dropdown.querySelectorAll(`ul.${columnClass} > li.active:not(.hint)`) + } return this.$el.querySelectorAll(`ul.${columnClass} > li.active:not(.hint)`) }, @@ -1531,7 +1608,7 @@ export default { } }, - tabHandler (evt) { + tabHandler (evt) { if (!this.inputIsEmpty && this.tokenChunksPos && this.tokenChunksPos.length) { const currentChunk = this.getCurrentTokenChunk() if (!currentChunk) { return } @@ -1541,6 +1618,16 @@ export default { evt.preventDefault() this.toLateralToken(evt.shiftKey) } + } else if (this.appendToBody && this.advancedKeyboard) { + if (evt.shiftKey) { return } + evt.preventDefault() + if (this.inputIsEmpty) { + const firstColumn = this.inUse.types[0] + const targetValue = this.validItemsInCol(firstColumn)[0] + if (targetValue) { + targetValue.focus() + } + } } }, @@ -1987,7 +2074,7 @@ export default {
-