Add a dd-range component for repeated checkboxes that represent something like "1/3"
This commit is contained in:
parent
0972a0491f
commit
c3e7aee501
3 changed files with 209 additions and 0 deletions
139
module/components/range.mjs
Normal file
139
module/components/range.mjs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { DotDungeonIcon } from "./icon.mjs";
|
||||
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
||||
|
||||
/**
|
||||
Attributes:
|
||||
@property {string} name - The path to the value to update in the datamodel
|
||||
@property {number} value - The actual value of the input
|
||||
@property {number} max - The maximum value that this range has
|
||||
|
||||
@extends {HTMLElement}
|
||||
*/
|
||||
export class DotDungeonRange
|
||||
extends StyledShadowElement(
|
||||
HTMLElement,
|
||||
{ mode: `open`, delegatesFocus: true }
|
||||
) {
|
||||
static elementName = `dd-range`;
|
||||
static formAssociated = true;
|
||||
|
||||
static observedAttributes = [`max`];
|
||||
|
||||
static _stylePath = `v3/components/range.css`;
|
||||
|
||||
_internals;
|
||||
#input;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Form internals
|
||||
this._internals = this.attachInternals();
|
||||
this._internals.role = `spinbutton`;
|
||||
};
|
||||
|
||||
get form() {
|
||||
return this._internals.form;
|
||||
};
|
||||
|
||||
get name() {
|
||||
return this.getAttribute(`name`);
|
||||
};
|
||||
set name(value) {
|
||||
this.setAttribute(`name`, value);
|
||||
};
|
||||
|
||||
get value() {
|
||||
try {
|
||||
return parseInt(this.getAttribute(`value`));
|
||||
} catch {
|
||||
throw new Error(`Failed to parse attribute: "value" - Make sure it's an integer`);
|
||||
};
|
||||
};
|
||||
set value(value) {
|
||||
this.setAttribute(`value`, value);
|
||||
};
|
||||
|
||||
get max() {
|
||||
try {
|
||||
return parseInt(this.getAttribute(`max`));
|
||||
} catch {
|
||||
throw new Error(`Failed to parse attribute: "max" - Make sure it's an integer`);
|
||||
};
|
||||
};
|
||||
set max(value) {
|
||||
this.setAttribute(`max`, value);
|
||||
};
|
||||
|
||||
get type() {
|
||||
return `number`;
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// Attribute validation
|
||||
if (!this.hasAttribute(`max`)) {
|
||||
throw new Error(`dotdungeon | Cannot have a range without a maximum value`);
|
||||
};
|
||||
|
||||
// Keyboard accessible input for the thing
|
||||
this.#input = document.createElement(`input`);
|
||||
this.#input.type = `number`;
|
||||
this.#input.min = 0;
|
||||
this.#input.max = this.max;
|
||||
this.#input.value = this.value;
|
||||
this.#input.addEventListener(`change`, () => {
|
||||
const inputValue = parseInt(this.#input.value);
|
||||
if (inputValue === this.value) return;
|
||||
this._updateValue.bind(this)(Math.sign(this.value - inputValue));
|
||||
this._updateValue(Math.sign(this.value - inputValue));
|
||||
});
|
||||
this._shadow.appendChild(this.#input);
|
||||
|
||||
// Shadow-DOM construction
|
||||
this._elements = new Array(this.max);
|
||||
const container = document.createElement(`div`);
|
||||
container.classList.add(`container`);
|
||||
|
||||
// Creating the node for filled content
|
||||
const filledContainer = document.createElement(`div`);
|
||||
filledContainer.classList.add(`range-increment`, `filled`);
|
||||
const filledNode = this.querySelector(`[slot="filled"]`);
|
||||
if (filledNode) filledContainer.appendChild(filledNode);
|
||||
|
||||
const emptyContainer = document.createElement(`div`);
|
||||
emptyContainer.classList.add(`range-increment`, `empty`);
|
||||
const emptyNode = this.querySelector(`[slot="empty"]`);
|
||||
if (emptyNode) emptyContainer.appendChild(emptyNode);
|
||||
|
||||
this._elements.fill(filledContainer, 0, this.value);
|
||||
this._elements.fill(emptyContainer, this.value);
|
||||
container.append(...this._elements.map((slot, i) => {
|
||||
const node = slot.cloneNode(true);
|
||||
node.setAttribute(`data-index`, i + 1);
|
||||
node.addEventListener(`click`, () => {
|
||||
const filled = node.classList.contains(`filled`);
|
||||
this._updateValue(filled ? -1 : 1);
|
||||
});
|
||||
return node;
|
||||
}));
|
||||
this._shadow.appendChild(container);
|
||||
|
||||
/*
|
||||
This converts all of the namespace prefixed properties on the element to
|
||||
CSS variables so that they don't all need to be provided by doing style=""
|
||||
*/
|
||||
for (const attrVar of this.attributes) {
|
||||
if (attrVar.name?.startsWith(`var:`)) {
|
||||
const prop = attrVar.name.replace(`var:`, ``);
|
||||
this.style.setProperty(`--` + prop, attrVar.value);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
_updateValue(delta) {
|
||||
this.value += delta;
|
||||
this.dispatchEvent(new Event(`change`, { bubbles: true }));
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue