163 lines
5.3 KiB
TypeScript
163 lines
5.3 KiB
TypeScript
import { FolkElement } from '@lib';
|
|
import { css } from '@lit/reactive-element';
|
|
import { property } from '@lit/reactive-element/decorators.js';
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'intl-number': IntlNumber;
|
|
}
|
|
}
|
|
|
|
type NumberFormatOptions = Intl.NumberFormatOptions;
|
|
|
|
// Ported from https://github.com/elematic/heximal/blob/main/packages/components/src/lib/num.ts
|
|
export class IntlNumber extends FolkElement {
|
|
static tagName = 'intl-number';
|
|
|
|
static override styles = css`
|
|
slot {
|
|
display: none;
|
|
}
|
|
`;
|
|
|
|
// `lang` is a global attribute, should be navigate up the DOM tree to find it?
|
|
@property({ reflect: true }) locale: string | undefined;
|
|
|
|
// Locale options
|
|
@property({ reflect: true }) localeMatcher: NumberFormatOptions['localeMatcher'];
|
|
|
|
@property({ reflect: true }) numberingSystem: NumberFormatOptions['numberingSystem'];
|
|
|
|
// Digit options
|
|
@property({ reflect: true, type: Number }) minimumIntegerDigits: NumberFormatOptions['minimumIntegerDigits'];
|
|
|
|
@property({ reflect: true, type: Number }) minimumFractionDigits: NumberFormatOptions['minimumFractionDigits'];
|
|
|
|
@property({ reflect: true, type: Number }) maximumFractionDigits: NumberFormatOptions['maximumFractionDigits'];
|
|
|
|
@property({ reflect: true, type: Number }) minimumSignificantDigits: NumberFormatOptions['minimumSignificantDigits'];
|
|
|
|
@property({ reflect: true, type: Number }) maximumSignificantDigits: NumberFormatOptions['maximumSignificantDigits'];
|
|
|
|
@property({ reflect: true }) roundingPriority: NumberFormatOptions['roundingPriority'];
|
|
|
|
@property({ reflect: true, type: Number }) roundingIncrement: NumberFormatOptions['roundingIncrement'];
|
|
|
|
@property({ reflect: true }) roundingMode: NumberFormatOptions['roundingMode'];
|
|
|
|
@property({ reflect: true }) trailingZeroDisplay: NumberFormatOptions['trailingZeroDisplay'];
|
|
|
|
// Style options
|
|
// There is a name collision with the style property, so call it display instead.
|
|
@property({ reflect: true }) display: NumberFormatOptions['style'];
|
|
|
|
@property({ reflect: true }) currency: NumberFormatOptions['currency'];
|
|
|
|
@property({ reflect: true }) currencyDisplay: NumberFormatOptions['currencyDisplay'];
|
|
|
|
@property({ reflect: true }) currencySign: NumberFormatOptions['currencySign'];
|
|
|
|
@property({ reflect: true }) unit: NumberFormatOptions['unit'];
|
|
|
|
@property({ reflect: true }) unitDisplay: NumberFormatOptions['unitDisplay'];
|
|
|
|
// Other options
|
|
@property({ reflect: true }) notation: NumberFormatOptions['notation'];
|
|
|
|
@property({ reflect: true }) compactDisplay: NumberFormatOptions['compactDisplay'];
|
|
|
|
@property({ reflect: true }) useGrouping: NumberFormatOptions['useGrouping'];
|
|
|
|
@property({ reflect: true }) signDisplay: NumberFormatOptions['signDisplay'];
|
|
|
|
#format: Intl.NumberFormat | undefined;
|
|
#value: number = NaN;
|
|
#slot = document.createElement('slot');
|
|
#span = document.createElement('span');
|
|
|
|
get value() {
|
|
return this.#value;
|
|
}
|
|
|
|
set value(value) {
|
|
this.#value = value;
|
|
this.#updateValue();
|
|
}
|
|
|
|
get formattedValue() {
|
|
return this.#span.textContent;
|
|
}
|
|
|
|
override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
const root = super.createRenderRoot();
|
|
|
|
this.#span.part.add('number');
|
|
|
|
this.#slot.addEventListener('slotchange', () => {
|
|
this.value = parseFloat(this.textContent?.trim() || '');
|
|
});
|
|
|
|
root.append(this.#slot, this.#span);
|
|
|
|
return root;
|
|
}
|
|
|
|
override connectedCallback(): void {
|
|
super.connectedCallback();
|
|
|
|
window.addEventListener('languagechange', this.#onLanguageChange);
|
|
}
|
|
|
|
override disconnectedCallback(): void {
|
|
super.disconnectedCallback();
|
|
|
|
window.removeEventListener('languagechange', this.#onLanguageChange);
|
|
}
|
|
|
|
#onLanguageChange = () => this.requestUpdate();
|
|
|
|
override willUpdate(): void {
|
|
// Any change to properties requires re-creating the formatter.
|
|
|
|
// Default locale to navigator.language since it's the browsers language setting
|
|
// Passing undefined seems to reflect the OS's language setting.
|
|
this.#format = new Intl.NumberFormat(this.locale || navigator.language, {
|
|
localeMatcher: this.localeMatcher,
|
|
numberingSystem: this.numberingSystem,
|
|
|
|
minimumIntegerDigits: this.minimumIntegerDigits,
|
|
minimumFractionDigits: this.minimumFractionDigits,
|
|
maximumFractionDigits: this.maximumFractionDigits,
|
|
minimumSignificantDigits: this.minimumSignificantDigits,
|
|
maximumSignificantDigits: this.maximumSignificantDigits,
|
|
roundingPriority: this.roundingPriority,
|
|
roundingIncrement: this.roundingIncrement,
|
|
roundingMode: this.roundingMode,
|
|
trailingZeroDisplay: this.trailingZeroDisplay,
|
|
|
|
// If there are is no style attribute, try to infer it.
|
|
style: this.display ?? (this.currency ? 'currency' : this.unit ? 'unit' : 'decimal'),
|
|
currency: this.currency,
|
|
currencyDisplay: this.currencyDisplay,
|
|
currencySign: this.currencySign,
|
|
unit: this.unit,
|
|
unitDisplay: this.unitDisplay,
|
|
|
|
notation: this.notation,
|
|
compactDisplay: this.compactDisplay,
|
|
useGrouping: this.useGrouping,
|
|
signDisplay: this.signDisplay,
|
|
});
|
|
|
|
this.#updateValue();
|
|
}
|
|
|
|
#updateValue = () => {
|
|
if (Number.isNaN(this.#value)) {
|
|
this.#span.textContent = '';
|
|
} else if (this.#format) {
|
|
this.#span.textContent = this.#format.format(this.#value);
|
|
}
|
|
};
|
|
}
|