diff --git a/packages/ui-default/components/datepicker/datepicker.page.tsx b/packages/ui-default/components/datepicker/datepicker.page.tsx index 783ff13f7..b9f509f16 100644 --- a/packages/ui-default/components/datepicker/datepicker.page.tsx +++ b/packages/ui-default/components/datepicker/datepicker.page.tsx @@ -2,26 +2,92 @@ import 'flatpickr/dist/flatpickr.min.css'; import flatpickr from 'flatpickr'; import $ from 'jquery'; -import Picker from 'pickadate/lib/picker'; import { AutoloadPage } from 'vj/misc/Page'; -import TimePicker from './timepicker'; + +function padTwo(n: number) { + return n < 10 ? `0${n}` : `${n}`; +} + +function initTimePicker(input: HTMLInputElement) { + const $input = $(input); + const interval = 15; + const $wrapper = $('
').css({ position: 'relative', display: 'inline-block', width: '100%' }); + $input.wrap($wrapper); + + const $dropdown = $('
').css({ + position: 'absolute', + top: $input.outerHeight(), + left: 0, + right: 0, + maxHeight: 0, + overflowY: 'auto', + background: 'var(--bg-color, #fff)', + border: '1px solid #ccc', + borderTop: 'none', + borderRadius: '0 0 4px 4px', + zIndex: 1000, + opacity: 0, + boxShadow: '0 2px 8px rgba(0,0,0,0.15)', + transition: 'max-height .2s ease, opacity .2s ease', + pointerEvents: 'none', + }).addClass('time-picker-dropdown'); + + for (let h = 0; h < 24; h++) { + for (let m = 0; m < 60; m += interval) { + const time = `${padTwo(h)}:${padTwo(m)}`; + const $item = $('
').text(time).css({ + padding: '6px 12px', + cursor: 'pointer', + }).on('mousedown', (e) => { + e.preventDefault(); + $input.val(time).trigger('change'); + closeDropdown(); + }).on('mouseenter', function () { + $(this).css('background', 'var(--highlight-color, #e8f0fe)'); + }).on('mouseleave', function () { + $(this).css('background', ''); + }); + $dropdown.append($item); + } + } + + let isOpen = false; + + function openDropdown() { + if (isOpen) return; + isOpen = true; + $dropdown.css({ maxHeight: '200px', opacity: 1, pointerEvents: 'auto' }); + const val = $input.val() as string; + if (val) { + $dropdown.children().each(function cb() { + if ($(this).text() !== val) return; + const el = this as HTMLElement; + el.parentElement!.scrollTop = el.offsetTop - el.parentElement!.offsetTop - 80; + }); + } + } + + function closeDropdown() { + if (!isOpen) return; + isOpen = false; + $dropdown.css({ maxHeight: 0, opacity: 0, pointerEvents: 'none' }); + } + + $input.after($dropdown); + $input.on('focus', openDropdown); + // Only close when the element actually loses focus within the page, + // not when the whole window loses focus (alt-tab) + $input.on('blur', () => { + if (document.hasFocus()) closeDropdown(); + }); +} const datepickerPage = new AutoloadPage('datepickerPage', async () => { $('[data-pick-date]').each(function () { flatpickr(this, { allowInput: true }); }); $('[data-pick-time]').each(function () { - const $this = $(this); - if (!$this.data('pickatime')) { - // eslint-disable-next-line no-new - new Picker(this, 'pickatime', TimePicker, { - format: 'H:i', - editable: true, - interval: 15, - clear: false, - onSet() { $this.trigger('change'); }, - }); - } + initTimePicker(this as HTMLInputElement); }); }); diff --git a/packages/ui-default/components/datepicker/datepicker.styl b/packages/ui-default/components/datepicker/datepicker.styl deleted file mode 100644 index 68e155e70..000000000 --- a/packages/ui-default/components/datepicker/datepicker.styl +++ /dev/null @@ -1,53 +0,0 @@ -.picker__holder - margin-top: -1rem - border-radius: 0 - transform: scale(0.9) translateZ(0) - transform-origin: 0 0 - outline: 0 - font-size: rem($font-size-secondary) - transition: transform .1s ease-in-out, - opacity .1s ease-in-out, - max-height 0s .1s, - border-width 0s .1s - -.picker--opened .picker__holder - transform: scale(1) translateZ(0) - transition: transform .1s ease-in-out, - opacity .1s ease-in-out, - max-height 0s, - border-width 0s - -.picker__list - padding: 0 - -.picker__list-item - border: 0 - -.picker__list-item:hover - background: $datapicker-list-item-bg-color-hover - -.picker__list-item--selected, .picker__list-item--selected:hover, .picker--focused .picker__list-item--selected - background: $primary-color - -.picker__day--infocus:hover, .picker__day--outfocus:hover - background: $datapicker-list-item-bg-color-hover - -.picker__day--highlighted - border-color: transparent - -.picker__day--highlighted:hover, .picker--focused .picker__day--highlighted - background: transparent - -.picker__day--selected, .picker__day--selected:hover, .picker--focused .picker__day--selected - background: $primary-color - -.picker__button--today:hover, .picker__button--clear:hover, .picker__button--close:hover - background: $datapicker-list-item-bg-color-hover - border-color: $datapicker-list-item-bg-color-hover - -.picker__button--today:focus, .picker__button--clear:focus, .picker__button--close:focus - background: $datapicker-list-item-bg-color-hover - border-color: $datapicker-list-item-bg-color-hover - -.picker__nav--prev:hover, .picker__nav--next:hover - background: $datapicker-list-item-bg-color-hover diff --git a/packages/ui-default/components/datepicker/timepicker.ts b/packages/ui-default/components/datepicker/timepicker.ts deleted file mode 100644 index c891dffc2..000000000 --- a/packages/ui-default/components/datepicker/timepicker.ts +++ /dev/null @@ -1,307 +0,0 @@ -/* eslint-disable */ -import Picker from 'pickadate/lib/picker'; - -const HOURS_IN_DAY = 24; -const MINUTES_IN_HOUR = 60; -const MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR; -const { _ } = Picker; -export default class TimePicker { - queue = { - interval: 'i', - min: 'measure create', - max: 'measure create', - now: 'now create', - select: 'parse create', - highlight: 'parse create', - view: 'parse create', - }; - - item = { - clear: null, - interval: 15, - disable: [], - enable: 1, - select: undefined, - highlight: undefined, - min: undefined, - max: undefined, - view: undefined, - }; - - key = { - 40: 1, // Down - 38: -1, // Up - 39: 1, // Right - 37: -1, // Left - go(timeChange) { - this.set( - 'highlight', - this.item.highlight.pick + timeChange * this.item.interval, - { interval: timeChange * this.item.interval }, - ); - this.render(); - }, - }; - - $node: any; - - constructor(picker, public settings) { - const clock = this; - const elementValue = picker.$node[0].value; - const elementDataValue = picker.$node.data('value'); - const valueString = elementDataValue || elementValue; - const formatString = elementDataValue ? settings.formatSubmit : settings.format; - clock.$node = picker.$node; - clock - .set('min', settings.min) - .set('max', settings.max) - .set('select', valueString || '00:00', { - format: formatString, - }); - - picker - .on('render', () => { - const $pickerHolder = picker.$root.children(); - const $viewset = $pickerHolder.find(`.${settings.klass.viewset}`); - const vendors = function (prop) { - return ['webkit', 'moz', 'ms', 'o', ''].map((vendor) => (vendor ? `-${vendor}-` : '') + prop); - }; - const animations = function ($el, state) { - vendors('transform').map((prop) => { - $el.css(prop, state); - }); - vendors('transition').map((prop) => { - $el.css(prop, state); - }); - }; - if ($viewset.length) { - animations($pickerHolder, 'none'); - $pickerHolder[0].scrollTop = ~~$viewset.position().top - ($viewset[0].clientHeight * 2); - animations($pickerHolder, ''); - } - }, 1) - .on('open', () => { - picker.$root.find('button').attr('disabled', false); - }, 1) - .on('close', () => { - picker.$root.find('button').attr('disabled', true); - }, 1); - } - - set(type, value?, options?) { - const clock = this; - const clockItem = clock.item; - - if (value === null) { - if (type == 'clear') type = 'select'; - clockItem[type] = value; - return clock; - } - - // Otherwise go through the queue of methods, and invoke the functions. - // Update this as the time unit, and set the final value as this item. - // * In the case of `enable`, keep the queue but set `disable` instead. - clockItem[type] = clock.queue[type].split(' ').map((method) => { - value = clock[method](type, value, options); - return value; - }).pop(); - - if (type == 'select') { - clock.set('highlight', clockItem.select, options); - } else if (type == 'highlight') { - clock.set('view', clockItem.highlight, options); - } else if (type == 'min') { - clock.set('max', clockItem.max, options); - } - return clock; - } - - get(type) { - return this.item[type]; - } - - formats = { - H(string, timeObject) { - return string ? _.digits(string) : `${timeObject.hour % 24}`; - }, - i(string, timeObject) { - return string ? 2 : _.lead(timeObject.mins); - }, - - // Create an array by splitting the formatting string passed. - toArray(formatString) { return formatString.split(/(h{1,2}|H{1,2}|i|a|A|!.)/g); }, - // Format an object into a string using the formatting options. - toString(formatString, itemObject) { - const clock = this; - return clock.formats.toArray(formatString).map((label) => _.trigger(clock.formats[label], clock, [0, itemObject]) || label.replace(/^!/, '')).join(''); - }, - }; - - create(type, value?, options?) { - const clock = this; - value = value === undefined ? type : value; - if (_.isDate(value)) { - value = [value.getHours(), value.getMinutes()]; - } - if (value && typeof value === 'object' && _.isInteger(value.pick)) { - value = value.pick; - } else if (Array.isArray(value)) { - value = +value[0] * MINUTES_IN_HOUR + (+value[1]); - } - if (type == 'max' && value < clock.item.min.pick) { - value += MINUTES_IN_DAY; - } - if (type != 'min' && type != 'max' && (value - clock.item.min.pick) % clock.item.interval !== 0) { - value += clock.item.interval; - } - value = clock.normalize(type, value, options); - return { - hour: ~~(HOURS_IN_DAY + value / MINUTES_IN_HOUR) % HOURS_IN_DAY, - mins: (MINUTES_IN_HOUR + value % MINUTES_IN_HOUR) % MINUTES_IN_HOUR, - time: (MINUTES_IN_DAY + value) % MINUTES_IN_DAY, - pick: value % MINUTES_IN_DAY, - }; - } - - normalize(type, value, _options) { - const { interval } = this.item; - const minTime = this.item.min && this.item.min.pick || 0; - value -= type == 'min' ? 0 : (value - minTime) % interval; - return value; - } - - measure(type, value, options) { - value ||= type == 'min' ? [0, 0] : [HOURS_IN_DAY - 1, MINUTES_IN_HOUR - 1]; - if (typeof value === 'string') { - value = this.parse(type, value); - } else if (value && typeof value === 'object' && _.isInteger(value.pick)) { - value = this.normalize(type, value.pick, options); - } - return value; - } - - scope = function (timeObject) { - const minLimit = this.item.min.pick; - const maxLimit = this.item.max.pick; - return this.create(timeObject.pick > maxLimit ? maxLimit : timeObject.pick < minLimit ? minLimit : timeObject); - }; // TimePicker.prototype.scope - - parse(type, value, options?) { - let hour; let minutes; let item; let parseValue; - const clock = this; - const parsingObject = {}; - if (!value || typeof value !== 'string') return value; - - if (!(options && options.format)) { - options = options || {}; - options.format = clock.settings.format; - } - - clock.formats.toArray(options.format).map((label) => { - let substring; - const formattingLabel = clock.formats[label]; - const formatLength = formattingLabel - ? _.trigger(formattingLabel, clock, [value, parsingObject]) - : label.replace(/^!/, '').length; - if (formattingLabel) { - substring = value.substr(0, formatLength); - parsingObject[label] = substring.match(/^\d+$/) ? +substring : substring; - } - value = value.substr(formatLength); - }); - - // Grab the hour and minutes from the parsing object. - for (item in parsingObject) { - parseValue = parsingObject[item]; - if (_.isInteger(parseValue)) { - if (item.match(/^(h|hh)$/i)) hour = parseValue; - else if (item == 'i') minutes = parseValue; - } - } - - // Calculate it in minutes and return. - return hour * MINUTES_IN_HOUR + minutes; - } - - nodes(isOpen) { - const clock = this; - const { settings } = clock; - const selectedObject = clock.item.select; - const highlightedObject = clock.item.highlight; - const viewsetObject = clock.item.view; - - return _.node( - 'ul', - _.group({ - min: clock.item.min.pick, - max: clock.item.max.pick, - i: clock.item.interval, - node: 'li', - item(loopedTime) { - loopedTime = clock.create(loopedTime); - const timeMinutes = loopedTime.pick; - const isSelected = selectedObject && selectedObject.pick == timeMinutes; - const isHighlighted = highlightedObject && highlightedObject.pick == timeMinutes; - const formattedTime = _.trigger(clock.formats.toString, clock, [settings.format, loopedTime]); - return [ - _.trigger(clock.formats.toString, clock, [_.trigger(settings.formatLabel, clock, [loopedTime]) || settings.format, loopedTime]), - (function (klasses) { - if (isSelected) klasses.push(settings.klass.selected); - - if (isHighlighted) klasses.push(settings.klass.highlighted); - - if (viewsetObject && viewsetObject.pick == timeMinutes) { - klasses.push(settings.klass.viewset); - } - - return klasses.join(' '); - }([settings.klass.listItem])), - `data-pick=${loopedTime.pick} ${_.ariaAttr({ - role: 'option', - label: formattedTime, - selected: isSelected && clock.$node.val() === formattedTime ? true : null, - activedescendant: isHighlighted ? true : null, - disabled: null, - })}`, - ]; - }, - }) - + _.node( - 'li', - _.node( - 'button', - settings.clear, - settings.klass.buttonClear, - `type=button data-clear=1${isOpen ? '' : ' disabled'} ${_.ariaAttr({ controls: clock.$node[0].id })}`, - ), - '', - _.ariaAttr({ role: 'presentation' }), - ), - settings.klass.list, - _.ariaAttr({ role: 'listbox', controls: clock.$node[0].id }), - ); - } - - static defaults = (function (prefix) { - return { - clear: 'Clear', - format: 'h:i A', - interval: 30, - closeOnSelect: true, - closeOnClear: true, - updateInput: true, - klass: { - picker: `${prefix} ${prefix}--time`, - holder: `${prefix}__holder`, - list: `${prefix}__list`, - listItem: `${prefix}__list-item`, - disabled: `${prefix}__list-item--disabled`, - selected: `${prefix}__list-item--selected`, - highlighted: `${prefix}__list-item--highlighted`, - viewset: `${prefix}__list-item--viewset`, - now: `${prefix}__list-item--now`, - buttonClear: `${prefix}__button--clear`, - }, - }; - }(Picker.klasses().picker)); -} diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 954d87213..ae3bc477d 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -34,7 +34,6 @@ "@types/katex": "^0.16.8", "@types/markdown-it": "^14.1.2", "@types/nunjucks": "^3.2.6", - "@types/pickadate": "^3.5.35", "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -81,7 +80,6 @@ "normalize.css": "^8.0.1", "nprogress": "^0.2.0", "p-queue": "^9.1.2", - "pickadate": "patch:pickadate@npm%3A3.6.4#~/.yarn/patches/pickadate-npm-3.6.4-5795acb19d.patch", "postcss-loader": "7.3.4", "postcss-preset-mantine": "^1.18.0", "prism-themes": "^1.9.0", diff --git a/packages/ui-default/theme/default.js b/packages/ui-default/theme/default.js index 6e5c17597..417db58c9 100644 --- a/packages/ui-default/theme/default.js +++ b/packages/ui-default/theme/default.js @@ -1,8 +1,5 @@ import 'normalize.css/normalize.css'; import 'allotment/dist/style.css'; -import 'pickadate/lib/themes/classic.css'; -import 'pickadate/lib/themes/classic.date.css'; -import 'pickadate/lib/themes/classic.time.css'; import 'katex/dist/katex.min.css'; import '@mantine/core/styles.css'; import '@mantine/notifications/styles.css'; @@ -26,7 +23,6 @@ import 'vj/misc/structure.styl'; import 'vj/misc/section.styl'; import 'vj/misc/nothing.styl'; import 'vj/components/editor/cmeditor.styl'; -import 'vj/components/datepicker/datepicker.styl'; import './dark.styl'; import './bp6-compat.css';