257 lines
7.4 KiB
JavaScript
257 lines
7.4 KiB
JavaScript
// saladui/components/slider.js
|
|
import Component from "../core/component";
|
|
import SaladUI from "../index";
|
|
|
|
class SliderComponent extends Component {
|
|
constructor(el, hookContext) {
|
|
super(el, { hookContext });
|
|
|
|
// Get slider elements
|
|
this.track = this.getPart("track");
|
|
this.range = this.getPart("range");
|
|
this.thumb = this.getPart("thumb");
|
|
|
|
// Initialize values
|
|
this.parseValues();
|
|
|
|
// Add drag handling
|
|
this.isDragging = false;
|
|
this.setupDragHandling();
|
|
|
|
// Set initial position
|
|
this.updatePosition();
|
|
}
|
|
|
|
parseValues() {
|
|
// Get values from options with defaults
|
|
this.min = parseFloat(this.options.min || 0);
|
|
this.max = parseFloat(this.options.max || 100);
|
|
this.step = parseFloat(this.options.step || 1);
|
|
this.disabled = !!this.options.disabled;
|
|
|
|
// Get value from data attribute, fallback to defaultValue from options, then to min
|
|
const dataValue = this.el.dataset.value;
|
|
const defaultValue = this.options.defaultValue;
|
|
this.value = parseFloat(
|
|
dataValue !== undefined && dataValue !== null
|
|
? dataValue
|
|
: (defaultValue !== undefined ? defaultValue : this.min),
|
|
);
|
|
|
|
// Clamp value to min/max
|
|
this.value = Math.max(this.min, Math.min(this.max, this.value));
|
|
}
|
|
|
|
getComponentConfig() {
|
|
return {
|
|
stateMachine: {
|
|
idle: {
|
|
enter: "onIdleEnter",
|
|
transitions: {
|
|
drag: "dragging",
|
|
},
|
|
},
|
|
dragging: {
|
|
enter: "onDraggingEnter",
|
|
exit: "onDraggingExit",
|
|
transitions: {
|
|
end: "idle",
|
|
},
|
|
},
|
|
},
|
|
events: {
|
|
idle: {
|
|
keyMap: {
|
|
ArrowLeft: () => this.decrementValue(),
|
|
ArrowRight: () => this.incrementValue(),
|
|
ArrowDown: () => this.decrementValue(),
|
|
ArrowUp: () => this.incrementValue(),
|
|
Home: () => this.setValueAndUpdate(this.min),
|
|
End: () => this.setValueAndUpdate(this.max),
|
|
},
|
|
},
|
|
dragging: {
|
|
// These are handled by event listeners
|
|
},
|
|
},
|
|
ariaConfig: {
|
|
root: {
|
|
all: {
|
|
role: "slider",
|
|
valuemin: () => this.min?.toString(),
|
|
valuemax: () => this.max?.toString(),
|
|
valuenow: () => this.value?.toString(),
|
|
valuetext: () => this.value?.toString(),
|
|
orientation: "horizontal",
|
|
disabled: () => (this.disabled ? "true" : null),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
setupDragHandling() {
|
|
// Set up event handlers with proper binding
|
|
this.onPointerMove = this.onPointerMove.bind(this);
|
|
this.onPointerUp = this.onPointerUp.bind(this);
|
|
this.onPointerDown = this.onPointerDown.bind(this);
|
|
// Mouse events
|
|
this.el.addEventListener("mousedown", this.onPointerDown);
|
|
|
|
// Touch events
|
|
this.el.addEventListener("touchstart", this.onPointerDown, {
|
|
passive: false,
|
|
});
|
|
}
|
|
|
|
onIdleEnter() {
|
|
// Set proper tabindex when component is idle (not being dragged)
|
|
this.el.setAttribute("tabindex", "0");
|
|
}
|
|
|
|
onDraggingEnter() {
|
|
// Add global event listeners
|
|
document.addEventListener("mousemove", this.onPointerMove);
|
|
document.addEventListener("touchmove", this.onPointerMove, {
|
|
passive: false,
|
|
});
|
|
document.addEventListener("mouseup", this.onPointerUp);
|
|
document.addEventListener("touchend", this.onPointerUp);
|
|
}
|
|
|
|
onDraggingExit() {
|
|
// Remove global event listeners
|
|
document.removeEventListener("mousemove", this.onPointerMove);
|
|
document.removeEventListener("touchmove", this.onPointerMove);
|
|
document.removeEventListener("mouseup", this.onPointerUp);
|
|
document.removeEventListener("touchend", this.onPointerUp);
|
|
}
|
|
|
|
onPointerDown(event) {
|
|
// Skip if disabled
|
|
if (this.disabled) return;
|
|
|
|
// Prevent default to avoid text selection during drag
|
|
event.preventDefault();
|
|
|
|
// Handle mousedown or touchstart
|
|
this.transition("drag");
|
|
|
|
// Update value based on pointer position
|
|
this.updateValueFromPointer(event);
|
|
}
|
|
|
|
onPointerMove(event) {
|
|
// Prevent default to avoid scrolling during drag
|
|
event.preventDefault();
|
|
|
|
// Update value based on pointer position
|
|
this.updateValueFromPointer(event);
|
|
}
|
|
|
|
onPointerUp() {
|
|
// End dragging
|
|
this.transition("end");
|
|
|
|
// Notify of value change
|
|
this.pushEvent("value-changed", { value: this.value });
|
|
}
|
|
|
|
updateValueFromPointer(event) {
|
|
// Get coordinates depending on event type
|
|
const clientX = event.touches ? event.touches[0].clientX : event.clientX;
|
|
|
|
// Get track bounds
|
|
const trackRect = this.track.getBoundingClientRect();
|
|
|
|
// Calculate percentage within track
|
|
let percentage = Math.max(
|
|
0,
|
|
Math.min(1, (clientX - trackRect.left) / trackRect.width),
|
|
);
|
|
|
|
// Calculate value based on percentage
|
|
const rawValue = this.min + percentage * (this.max - this.min);
|
|
|
|
// Snap to step
|
|
const steppedValue = Math.round(rawValue / this.step) * this.step;
|
|
|
|
// Set and update
|
|
this.setValueAndUpdate(steppedValue);
|
|
}
|
|
|
|
incrementValue() {
|
|
this.setValueAndUpdate(Math.min(this.max, this.value + this.step));
|
|
this.pushEvent("value-changed", { value: this.value });
|
|
}
|
|
|
|
decrementValue() {
|
|
this.setValueAndUpdate(Math.max(this.min, this.value - this.step));
|
|
this.pushEvent("value-changed", { value: this.value });
|
|
}
|
|
|
|
setValueAndUpdate(newValue) {
|
|
// Ensure value is within bounds
|
|
this.value = Math.max(this.min, Math.min(this.max, newValue));
|
|
|
|
// Update visual position
|
|
this.updatePosition();
|
|
|
|
// Update ARIA attributes
|
|
this.el.setAttribute("aria-valuenow", this.value.toString());
|
|
this.el.setAttribute("aria-valuetext", this.value.toString());
|
|
}
|
|
|
|
updatePosition() {
|
|
// Calculate percentage for positioning
|
|
const percentage = ((this.value - this.min) / (this.max - this.min)) * 100;
|
|
|
|
// Update range width
|
|
this.range.style.width = `${percentage}%`;
|
|
|
|
// Get track and thumb dimensions
|
|
const trackRect = this.track.getBoundingClientRect();
|
|
const thumbRect = this.thumb.getBoundingClientRect();
|
|
|
|
// Calculate the percentage offset needed to keep thumb fully within track
|
|
// This accounts for the thumb's width relative to the track
|
|
const thumbHalfWidthPercentage =
|
|
(thumbRect.width / 2 / trackRect.width) * 100;
|
|
|
|
// Constrain the thumb position to keep it fully inside the track
|
|
const thumbPercentage = Math.max(
|
|
thumbHalfWidthPercentage,
|
|
Math.min(100 - thumbHalfWidthPercentage, percentage),
|
|
);
|
|
|
|
// Update thumb position
|
|
this.thumb.style.left = `${thumbPercentage}%`;
|
|
this.thumb.style.transform = "translateX(-50%)";
|
|
}
|
|
|
|
// Handle direct server-side commands
|
|
handleCommand(command, params) {
|
|
if (command === "setValue") {
|
|
this.setValueAndUpdate(parseFloat(params.value));
|
|
return true;
|
|
}
|
|
return super.handleCommand(command, params);
|
|
}
|
|
|
|
// Clean up
|
|
beforeDestroy() {
|
|
document.removeEventListener("mousemove", this.onPointerMove);
|
|
document.removeEventListener("touchmove", this.onPointerMove);
|
|
document.removeEventListener("mouseup", this.onPointerUp);
|
|
document.removeEventListener("touchend", this.onPointerUp);
|
|
// Remove local event listeners
|
|
this.el.removeEventListener("mousedown", this.onPointerDown);
|
|
this.el.removeEventListener("touchstart", this.onPointerDown);
|
|
}
|
|
}
|
|
|
|
// Register component
|
|
SaladUI.register("slider", SliderComponent);
|
|
|
|
export default SliderComponent;
|