catalog/ts_web/elements/consentsoftware-toggle.ts

256 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { LitElement, html, css, type TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { property } from 'lit/decorators/property.js';
import { delayFor } from '@push.rocks/smartdelay';
@customElement('consentsoftware-toggle')
export class ConsentsoftwareToggle extends LitElement {
@property({ type: Boolean })
public required = false;
@property({ type: Boolean, reflect: true })
public selected = false;
/**
* We always track the knobs left offset in `currentX`.
* - 0 => fully left
* - 30 => fully right
*/
private currentX = 0;
/**
* Drag state
*/
private isDragging = false;
private hasDragged = false;
private startX = 0; // pointerdown offset
private readonly knobWidth = 30;
private readonly trackWidth = 60;
public static styles = css`
:host {
display: block;
position: relative;
}
.label {
margin-bottom: 16px;
}
.toggle {
user-select: none; /* helps avoid text selection on drag */
}
.toggleKnobArea {
position: relative;
margin: auto;
height: 30px;
width: 60px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0);
overflow: hidden;
transition: background 0.2s ease;
cursor: pointer;
}
:host([selected]) .toggleKnobArea {
background: green;
}
.toggleKnobMover {
position: relative;
height: 100%;
width: 100%;
}
.toggleKnobInner {
position: absolute;
top: 0;
left: 0;
width: 30px;
height: 30px;
border-radius: 15px;
background: rgba(255, 255, 255, 0.5);
transition: left 0.2s ease, background 0.2s ease;
transform: scale(0.7);
/* Prevent scroll gestures on mobile */
touch-action: none;
}
.toggleKnobInner.dragging {
transition: background 0.2s ease;
}
:host([selected]) .toggleKnobInner {
background: white;
}
:host([required]) .toggleKnobArea {
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
}
:host([required]) .toggleKnobInner {
background: rgba(255, 255, 255, 0.1);
}
`;
constructor() {
super();
}
public render(): TemplateResult {
return html`
<div class="label">${this.getText()}</div>
<!-- A user can click anywhere in this toggle area. -->
<div class="toggle" @click=${this.handleClick}>
<div class="toggleKnobArea">
<div class="toggleKnobMover">
<!-- The knob itself, with pointer events for dragging. -->
<div
class="toggleKnobInner"
style="left: ${this.currentX}px;"
@pointerdown=${this.onPointerDown}
@pointermove=${this.onPointerMove}
@pointerup=${this.onPointerUp}
@pointercancel=${this.onPointerUp}
></div>
</div>
</div>
</div>
`;
}
/**
* If required = true on first render, auto-select and set the knob to the right.
*/
public async firstUpdated() {
if (this.required) {
// If "required" => always selected
this.selected = true;
this.currentX = this.knobWidth; // 30
this.requestUpdate();
} else {
// If not required, set knob to 0 or 30 depending on `selected`
this.currentX = this.selected ? this.knobWidth : 0;
this.requestUpdate();
}
}
/**
* CLICK HANDLER
*/
public async handleClick(event: MouseEvent) {
// If the user truly dragged the knob, skip the normal click toggle.
if (this.isDragging || this.hasDragged) {
event.stopPropagation();
event.preventDefault();
return;
}
if (this.required) {
// small bounce from 30 -> 20 -> 30
this.currentX = this.knobWidth; // ensure at 30
this.requestUpdate();
await new Promise((r) => setTimeout(r, 10));
this.currentX = 20; // small bounce left
this.requestUpdate();
await delayFor(200);
this.currentX = this.knobWidth; // back to 30
this.requestUpdate();
return;
}
// Normal toggle if no drag & not required
event.stopPropagation();
event.preventDefault();
this.selected = !this.selected;
this.currentX = this.selected ? this.knobWidth : 0; // snap knob left(0) or right(30)
this.requestUpdate();
this.dispatchEvent(new CustomEvent('toggle', { detail: { selected: this.selected } }));
delayFor(0).then(() => {
this.hasDragged = false;
});
}
/**
* DRAG HANDLERS (pointer events)
*/
private onPointerDown(event: PointerEvent) {
if (this.required) {
// If "required", disallow manual dragging
return;
}
// Start dragging
this.isDragging = true;
// The difference between the pointers X and the knobs current position
this.startX = event.clientX - this.currentX;
// capture pointer so we keep receiving pointermove/pointerup
(event.target as HTMLElement).setPointerCapture(event.pointerId);
}
private onPointerMove(event: PointerEvent) {
if (!this.isDragging) return;
const newX = event.clientX - this.startX;
this.hasDragged = true;
const toggleKnobInner: HTMLDivElement = this.shadowRoot.querySelector('.toggleKnobInner');
toggleKnobInner.classList.add('dragging');
// Clamp
this.currentX = Math.max(0, Math.min(newX, this.trackWidth - this.knobWidth));
this.requestUpdate();
}
private onPointerUp(event: PointerEvent) {
if (!this.isDragging) return;
(event.target as HTMLElement).releasePointerCapture(event.pointerId);
this.isDragging = false;
// If we didnt truly drag, pointerup does nothing; click handler handles toggling.
if (!this.hasDragged) {
return;
}
const toggleKnobInner: HTMLDivElement = this.shadowRoot.querySelector('.toggleKnobInner');
toggleKnobInner.classList.remove('dragging');
// Real drag => decide final side
const midpoint = (this.trackWidth - this.knobWidth) / 2; // 15
this.selected = this.currentX > midpoint;
this.currentX = this.selected ? this.knobWidth : 0; // snap to edge
this.requestUpdate();
// Dispatch toggle event
this.dispatchEvent(new CustomEvent('toggle', { detail: { selected: this.selected } }));
delayFor(0).then(() => {
this.hasDragged = false;
});
}
/**
* If external code sets `selected = true/false`, we also sync the knob position
*/
protected updated(changedProperties: Map<PropertyKey, unknown>): void {
if (
changedProperties.has('selected') &&
!this.isDragging &&
!this.hasDragged &&
!this.required
) {
this.currentX = this.selected ? this.knobWidth : 0;
this.requestUpdate();
}
super.updated(changedProperties);
}
public getText(): string | null {
return this.textContent;
}
}