diff --git a/src/components/Number.ts b/src/components/Number.ts new file mode 100644 index 0000000..b4938e4 --- /dev/null +++ b/src/components/Number.ts @@ -0,0 +1,131 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { ref } from "lit/directives/ref.js"; + +import { LocalizedMessage, localizedMessagePropertyOptions } from "../core/"; + +import { Focusable, Input, Persisted } from "../mixins"; + +@customElement("sdpi-number") +export class NumberElement extends Persisted(Focusable(Input(LitElement))) { + /** @inheritdoc */ + public static get styles() { + return [ + ...super.styles, + css` + input { + background-color: var(--input-bg-color); + padding: calc(var(--spacer) + 3px) var(--spacer); + flex: 1; + min-width: unset; + max-width: unset; + } + + ::-webkit-inner-spin-button { + -webkit-appearance: none; + } + + input:disabled { + opacity: var(--opacity-disabled); + } + + .number-container { + display: flex; + width: 100%; + } + ` + ]; + } + + /** + * The maximum value. + */ + @property({ type: Number }) + public max?: number; + + /** + * The minimum value. + */ + @property({ type: Number }) + public min?: number; + + /** + * Specifies the granularity that the value must adhere to. + */ + @property({ type: Number }) + public step?: number; + + /** + * Specifies the placeholder + */ + @property(localizedMessagePropertyOptions) + public placeholder?: LocalizedMessage; + + /** + * When specified, the user input will be clamped to the maximum and maximum values provided + */ + @property({ + attribute: "clamp", + type: Boolean, + }) + public clamp = false; + + /** @inheritdoc */ + protected delaySave = true; + + /** @inheritdoc */ + protected render() { + const value = this.value?.toString() || this.defaultValue?.toString() || ""; + return html` + `; + } + + private setValue(ev: HTMLInputEvent): void { + let value = ev.target.valueAsNumber; + if (Number.isNaN(value)) { + // No value provided + this.value = undefined; + return + } + + // Constrain value to min and max if provided + const min = this.clamp ? this.min : undefined; + const max = this.clamp ? this.max : undefined; + if (max != undefined) { + value = Math.min(value, max); + } + if (min != undefined) { + value = Math.max(value, min); + } + + // Force step size + if (this.step != undefined) { + // This matches the native step size on the number type input + const stepStart = this.min ?? 0.0; + value = Math.round((value - stepStart) / this.step) * this.step + stepStart; + } + + this.value = value; + ev.target.value = String(this.value); + } +} + +declare global { + interface HTMLElementTagNameMap { + "sdpi-number": NumberElement; + } +} diff --git a/src/index.ts b/src/index.ts index 3ddf000..7e85ff7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import "./components/color"; import "./components/delegate"; import "./components/file"; import "./components/i18n"; +import "./components/number"; import "./components/password"; import "./components/radio"; import "./components/range";