Et2Date: Add up/down buttons on hover to adjust value without opening popup

Buttons adjust by day or minuteIncrement.  Minute values are now always rounded to multiples of minuteIncrement, unless freeMinuteEntries=true (or minuteIncrement=1)
This commit is contained in:
nathan 2022-10-20 15:27:24 -06:00
parent 34b2bc135c
commit 19bbea7aca
3 changed files with 224 additions and 36 deletions

View File

@ -10,11 +10,8 @@
import {css, html} from "@lion/core"; import {css, html} from "@lion/core";
import {FormControlMixin, ValidateMixin} from "@lion/form-core";
import 'lit-flatpickr'; import 'lit-flatpickr';
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {dateStyles} from "./DateStyles"; import {dateStyles} from "./DateStyles";
import {LitFlatpickr} from "lit-flatpickr";
import {Instance} from 'flatpickr/dist/types/instance'; import {Instance} from 'flatpickr/dist/types/instance';
import "flatpickr/dist/plugins/scrollPlugin.js"; import "flatpickr/dist/plugins/scrollPlugin.js";
import "shortcut-buttons-flatpickr/dist/shortcut-buttons-flatpickr"; import "shortcut-buttons-flatpickr/dist/shortcut-buttons-flatpickr";
@ -23,7 +20,13 @@ import flatpickr from "flatpickr";
import {egw} from "../../jsapi/egw_global"; import {egw} from "../../jsapi/egw_global";
import {HTMLElementWithValue} from "@lion/form-core/types/FormControlMixinTypes"; import {HTMLElementWithValue} from "@lion/form-core/types/FormControlMixinTypes";
import {Et2Textbox} from "../Et2Textbox/Et2Textbox"; import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
import {Et2ButtonIcon} from "../Et2Button/Et2ButtonIcon";
import {FormControlMixin} from "@lion/form-core";
import {LitFlatpickr} from "lit-flatpickr";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import shoelace from "../Styles/shoelace";
const button = new Et2ButtonIcon();
// Request this year's holidays now // Request this year's holidays now
holidays(new Date().getFullYear()); holidays(new Date().getFullYear());
@ -306,12 +309,14 @@ export function formatDateTime(date : Date, options = {dateFormat: "", timeForma
return formatDate(date, options) + " " + formatTime(date, options); return formatDate(date, options) + " " + formatTime(date, options);
} }
export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFlatpickr))) // !!! ValidateMixin !!!
export class Et2Date extends Et2InputWidget(FormControlMixin(LitFlatpickr))
{ {
static get styles() static get styles()
{ {
return [ return [
...super.styles, ...(super.styles ? (Array.isArray(super.styles) ? super.styles : [super.styles]) : []),
shoelace,
dateStyles, dateStyles,
css` css`
:host { :host {
@ -321,6 +326,28 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
{ {
flex: 1 1 auto; flex: 1 1 auto;
} }
/* Scroll buttons */
.input-group__container {
position: relative;
}
.input-group__container:hover .et2-date-time__scrollbuttons {
display: flex;
}
.et2-date-time__scrollbuttons {
display: none;
flex-direction: column;
width: calc(var(--sl-input-height-medium) / 2);
position: absolute;
right: 0px;
}
.et2-date-time__scrollbuttons > * {
font-size: var(--sl-font-size-2x-small);
height: calc(var(--sl-input-height-medium) / 2);
}
.et2-date-time__scrollbuttons > *::part(base) {
padding: 3px;
}
`, `,
]; ];
} }
@ -337,6 +364,16 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
* Placeholder text for input * Placeholder text for input
*/ */
placeholder: {type: String}, placeholder: {type: String},
/**
* Allow value that is not a multiple of minuteIncrement
*
* eg: 11:23 with default 5 minuteIncrement = 11:25
* 16:47 with 30 minuteIncrement = 17:00
* If false (default), it is impossible to have a time that is not a multiple of minuteIncrement.
* Does not affect scroll, which always goes to nearest multiple.
*/
freeMinuteEntry: {type: Boolean}
} }
} }
@ -350,7 +387,7 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
const text = <Et2Textbox>document.createElement('et2-textbox'); const text = <Et2Textbox>document.createElement('et2-textbox');
text.type = "text"; text.type = "text";
text.placeholder = this.placeholder; text.placeholder = this.placeholder;
text.setAttribute("data-input", ""); text.required = this.required;
return text; return text;
} }
} }
@ -360,8 +397,12 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
{ {
super(); super();
// By default, 5 minute resolution (see minuteIncrement to change resolution)
this.freeMinuteEntry = false;
this._onDayCreate = this._onDayCreate.bind(this); this._onDayCreate = this._onDayCreate.bind(this);
this._handleInputChange = this._handleInputChange.bind(this); this._handleInputChange = this._handleInputChange.bind(this);
this.handleScroll = this.handleScroll.bind(this);
} }
@ -376,8 +417,7 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
{ {
super.disconnectedCallback(); super.disconnectedCallback();
this._inputNode?.removeEventListener('change', this._onChange); this._inputNode?.removeEventListener('change', this._onChange);
this._inputNode?.removeEventListener("input", this._handleInputChange); this.findInputField()?.removeEventListener("input", this._handleInputChange);
this.destroy();
} }
/** /**
@ -392,13 +432,29 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
} }
if(typeof this._instance === "undefined") if(typeof this._instance === "undefined")
{ {
if(this.getOptions().allowInput)
{
// Change this so it uses findInputField() to get the input
this._hasSlottedElement = true;
// Wait for everything to be there before we start flatpickr
await this.updateComplete;
this._inputNode.requestUpdate();
await this._inputNode.updateComplete;
// Set flag attribute on _internal_ input - flatpickr needs an <input>
if(this._inputNode.shadowRoot.querySelectorAll("input[type='text']").length == 1)
{
this.findInputField().setAttribute("data-input", "");
}
}
this.initializeComponent(); this.initializeComponent();
// This has to go in init() rather than connectedCallback() because flatpickr creates its nodes in // This has to go in init() rather than connectedCallback() because flatpickr creates its nodes in
// initializeComponent() so this._inputNode is not available before this // initializeComponent() so this._inputNode is not available before this
this._inputNode.setAttribute("slot", "input"); this.findInputField().addEventListener('change', this._updateValueOnChange);
this._inputNode.addEventListener('change', this._updateValueOnChange); this.findInputField().addEventListener("input", this._handleInputChange);
this._inputNode.addEventListener("input", this._handleInputChange);
} }
} }
@ -408,7 +464,7 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
* @see https://flatpickr.js.org/options/ * @see https://flatpickr.js.org/options/
* @returns {any} * @returns {any}
*/ */
protected getOptions() public getOptions()
{ {
let options = super.getOptions(); let options = super.getOptions();
@ -417,7 +473,9 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
options.allowInput = true; options.allowInput = true;
options.dateFormat = "Y-m-dT00:00:00\\Z"; options.dateFormat = "Y-m-dT00:00:00\\Z";
options.weekNumbers = true; options.weekNumbers = true;
options.wrap = true; // Wrap needs to be false because flatpickr can't look inside et2-textbox and find the <input> it wants
// We provide it directly through findInputField()
options.wrap = false;
options.onDayCreate = this._onDayCreate; options.onDayCreate = this._onDayCreate;
@ -506,7 +564,6 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
if(!value || value == 0 || value == "0") if(!value || value == 0 || value == "0")
{ {
value = ""; value = "";
this.modelValue = "";
this.clear(); this.clear();
return; return;
} }
@ -515,7 +572,7 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
if (typeof value === 'string' && (value[0] === '+' || value[0] === '-')) if (typeof value === 'string' && (value[0] === '+' || value[0] === '-'))
{ {
date = new Date(this.getValue()); date = new Date(this.getValue());
date.set_value(date.getSeconds() + parseInt(value)); date.setSeconds(date.getSeconds() + parseInt(value));
} }
else else
{ {
@ -535,12 +592,11 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
get value() get value()
{ {
// Copied from flatpickr, since Et2InputWidget overwrote flatpickr.getValue() if(!this._inputElement)
if(!this._inputNode)
{ {
return ''; return '';
} }
let value = this._valueNode.value; let value = this._inputElement.value;
// Empty field, return '' // Empty field, return ''
if(!value) if(!value)
@ -551,6 +607,11 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
return value; return value;
} }
get parse() : Function
{
return parseDate;
}
/** /**
* Inline calendars need a slot * Inline calendars need a slot
* *
@ -577,7 +638,12 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
_handleInputChange(e : InputEvent) _handleInputChange(e : InputEvent)
{ {
// Update // Update
const value = this._inputNode.value; const value = this.findInputField().value;
if(value === "" && this._instance.selectedDates.length > 0)
{
return this._instance.clear();
}
let parsedDate = null let parsedDate = null
try try
{ {
@ -591,7 +657,9 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
if(parsedDate) if(parsedDate)
{ {
const formattedDate = this._instance.formatDate(parsedDate, this._instance.config.altFormat) const formattedDate = this._instance.formatDate(parsedDate, this._instance.config.altFormat)
if(value === formattedDate) if(value === formattedDate &&
// Avoid infinite loop of setting the same value back triggering another change
this._instance.input.value !== this._instance.formatDate(parsedDate, this._instance.config.dateFormat))
{ {
this._instance.setDate(value, true, this._instance.config.altFormat) this._instance.setDate(value, true, this._instance.config.altFormat)
} }
@ -721,21 +789,23 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
} }
/** /**
* Override from flatpickr * Override from flatpickr - This is the node we tell flatpickr to use
* It must be an <input>, flatpickr doesn't understand anything else
* @returns {any} * @returns {any}
*/ */
findInputField() : HTMLInputElement findInputField() : HTMLInputElement
{ {
return <HTMLInputElement><unknown>this; return <HTMLInputElement>this._inputNode.shadowRoot.querySelector('input:not([type="hidden"])');
} }
/** /**
* The interactive (form) element. * The interactive (form) element.
* This is an et2-textbox, which causes some problems with flatpickr
* @protected * @protected
*/ */
get _inputNode() : HTMLElementWithValue get _inputNode() : HTMLElementWithValue
{ {
return this.querySelector('et2-textbox:not([data-input])'); return this.querySelector('[slot="input"]');
} }
/** /**
@ -743,7 +813,92 @@ export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFl
*/ */
get _valueNode() : HTMLElementWithValue get _valueNode() : HTMLElementWithValue
{ {
return this.querySelector('[data-input]'); return this.querySelector('et2-textbox');
}
/**
* Handle clicks on scroll buttons
*
* @param e
*/
public handleScroll(e)
{
if(e.target && !e.target.dataset.direction)
{
return;
}
e.stopPropagation();
const direction = parseInt(e.target.dataset.direction, 10) || 1;
this.increment(direction, "day", true);
}
/**
* Increment the current value
*
* @param {number} delta Amount of change, positive or negative
* @param {"day" | "hour" | "minute"} field
* @param {boolean} roundToDelta Round the current value to a multiple of delta before incrementing
* Useful for keeping things to a multiple of 5, for example.
*/
public increment(delta : number, field : "day" | "hour" | "minute", roundToDelta = true)
{
let date;
if(this._inputElement.value)
{
date = new Date(this._inputElement.value);
// Handle timezone offset, flatpickr uses local time
date = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
}
else
{
// No current value - start with "now", but don't increment at all
date = new Date();
delta = 0;
}
const fieldMap = {day: "UTCDate", hour: "UTCHours", minute: "UTCMinutes"};
const original = date["get" + fieldMap[field]]();
// Avoid divide by 0 in case we have no current value, or delta of 0 passed in
const roundResolution = delta || {
day: 1,
hour: this.getOptions().hourIncrement,
minute: this.getOptions().minuteIncrement
}[field];
let bound = roundToDelta ? (Math.round(original / roundResolution) * roundResolution) : original;
date["set" + fieldMap[field]](bound + delta);
this.setDate(date, false, null);
}
render()
{
return html`
<div class="form-field__group-one">${this._groupOneTemplate()}</div>
<div class="form-field__group-two">${this._groupTwoTemplate()}</div>
`;
}
protected _inputGroupInputTemplate()
{
return html`
<slot name="input"></slot>
<div class="et2-date-time__scrollbuttons" part="scrollbuttons" @click=${this.handleScroll}>
<et2-button-icon
noSubmit
name="chevron-up"
data-direction="1"
>
</et2-button-icon>
<et2-button-icon
noSubmit
name="chevron-down"
data-direction="-1"
>
</et2-button-icon>
</div>
`;
} }
} }

View File

@ -53,7 +53,7 @@ export class Et2DateTime extends Et2Date
* @see https://flatpickr.js.org/options/ * @see https://flatpickr.js.org/options/
* @returns {any} * @returns {any}
*/ */
protected getOptions() public getOptions()
{ {
let options = super.getOptions(); let options = super.getOptions();
@ -79,7 +79,7 @@ export class Et2DateTime extends Et2Date
_updateValueOnChange(selectedDates : Date[], dateStr : string, instance : Instance) _updateValueOnChange(selectedDates : Date[], dateStr : string, instance : Instance)
{ {
super._updateValueOnChange(selectedDates, dateStr, instance); super._updateValueOnChange(selectedDates, dateStr, instance);
if(this._instance && instance.config.minuteIncrement > 1) if(!this.freeMinuteEntry && dateStr && instance && instance.config.minuteIncrement > 1)
{ {
let i = instance.latestSelectedDateObj; let i = instance.latestSelectedDateObj;
const d = i ? i : new Date(); const d = i ? i : new Date();
@ -109,6 +109,23 @@ export class Et2DateTime extends Et2Date
onClick: this._handleShortcutButtonClick onClick: this._handleShortcutButtonClick
}) })
} }
/**
* Handle clicks on scroll buttons
*
* @param e
*/
public handleScroll(e)
{
if(e.target && !e.target.dataset.direction)
{
return;
}
e.stopPropagation();
const direction = parseInt(e.target.dataset.direction, 10) || 1;
this.increment(direction * this.getOptions().minuteIncrement, "minute", true);
}
} }
// @ts-ignore TypeScript is not recognizing that Et2DateTime is a LitElement // @ts-ignore TypeScript is not recognizing that Et2DateTime is a LitElement

View File

@ -35,6 +35,20 @@ export class SidemenuDate extends Et2Date
]; ];
} }
get slots()
{
return {
...super.slots,
input: () =>
{
// This element gets hidden and used for value - overridden from parent
const text = document.createElement('input');
text.type = "text";
return text;
}
}
}
constructor() constructor()
{ {
super(); super();
@ -91,10 +105,11 @@ export class SidemenuDate extends Et2Date
* @see https://flatpickr.js.org/options/ * @see https://flatpickr.js.org/options/
* @returns {any} * @returns {any}
*/ */
protected getOptions() public getOptions()
{ {
let options = super.getOptions(); let options = super.getOptions();
options.allowInput = false;
options.inline = true; options.inline = true;
options.dateFormat = "Y-m-dT00:00:00\\Z"; options.dateFormat = "Y-m-dT00:00:00\\Z";
@ -111,6 +126,16 @@ export class SidemenuDate extends Et2Date
return null; return null;
} }
/**
* Override from parent - This is the node we tell flatpickr to use
* It must be an <input>, flatpickr doesn't understand anything else
* @returns {any}
*/
findInputField() : HTMLInputElement
{
return <HTMLInputElement>this._inputNode;
}
set_value(value) set_value(value)
{ {
if(typeof value !== "string" && value.length == 8) if(typeof value !== "string" && value.length == 8)
@ -123,15 +148,6 @@ export class SidemenuDate extends Et2Date
} }
} }
/**
* Override from super due to customisation
* @returns {any}
*/
findInputField() : HTMLInputElement
{
return this._valueNode;
}
/** /**
* Handler for change events. Re-bound to be able to cancel month changes, since it's an input and emits them * Handler for change events. Re-bound to be able to cancel month changes, since it's an input and emits them
* *