neumorphic-peripheral
Version:
A lightweight, framework-agnostic JavaScript/TypeScript library for beautiful neumorphic styling
439 lines (438 loc) • 17.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ToggleComponent = void 0;
exports.toggle = toggle;
const base_1 = require("./base");
const utils_1 = require("../utils");
class ToggleComponent extends base_1.BaseComponent {
constructor(element, config = {}) {
if (!(element instanceof HTMLInputElement) ||
!['checkbox', 'radio'].includes(element.type)) {
throw new Error('Toggle component requires an HTMLInputElement with type checkbox or radio');
}
super(element, config);
this._isChecked = false;
this._toggleConfig = {
type: 'switch',
size: 'md',
animated: true,
checked: false,
...config
};
this._originalInput = element;
this._isChecked = this._originalInput.checked || this._toggleConfig.checked || false;
}
get inputElement() {
return this._originalInput;
}
init() {
this.applyBaseStyles();
this.createToggleStructure();
this.applyToggleStyles();
this.syncState();
}
bindEvents() {
super.bindEvents();
// Handle clicks on the wrapper or slider
if (this._wrapper) {
this.addEventListener(this._wrapper, 'click', this.handleClick.bind(this));
}
// Handle keyboard events
this.addEventListener(this._element, 'keydown', this.handleKeydown.bind(this));
// Listen to original input changes (external updates)
this.addEventListener(this._originalInput, 'change', () => {
this._isChecked = this._originalInput.checked;
this.updateVisualState();
this.emitChange();
});
// Focus events
this.addEventListener(this._element, 'focus', this.handleFocus.bind(this));
this.addEventListener(this._element, 'blur', this.handleBlur.bind(this));
}
createToggleStructure() {
const type = this._toggleConfig.type;
if (type === 'switch') {
this.createSwitchStructure();
}
else if (type === 'checkbox') {
this.createCheckboxStructure();
}
else if (type === 'radio') {
this.createRadioStructure();
}
}
createSwitchStructure() {
// Hide original input
this._originalInput.style.position = 'absolute';
this._originalInput.style.opacity = '0';
this._originalInput.style.pointerEvents = 'none';
// Create wrapper
this._wrapper = (0, utils_1.createElement)('div', {
class: 'np-toggle-wrapper np-toggle-switch'
});
// Create slider
this._slider = (0, utils_1.createElement)('div', {
class: 'np-toggle-slider'
});
// Insert wrapper after original input
const parent = this._originalInput.parentElement;
parent.insertBefore(this._wrapper, this._originalInput.nextSibling);
// Move original input into wrapper for accessibility
this._wrapper.appendChild(this._originalInput);
this._wrapper.appendChild(this._slider);
// Set wrapper as the main element for styling
this._element = this._wrapper;
}
createCheckboxStructure() {
// Hide original input
this._originalInput.style.position = 'absolute';
this._originalInput.style.opacity = '0';
this._originalInput.style.pointerEvents = 'none';
// Create wrapper
this._wrapper = (0, utils_1.createElement)('div', {
class: 'np-toggle-wrapper np-toggle-checkbox'
});
// Create checkmark
this._slider = (0, utils_1.createElement)('div', {
class: 'np-toggle-checkmark'
});
// Add checkmark icon
this._slider.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<polyline points="20,6 9,17 4,12"/>
</svg>
`;
// Insert wrapper after original input
const parent = this._originalInput.parentElement;
parent.insertBefore(this._wrapper, this._originalInput.nextSibling);
this._wrapper.appendChild(this._originalInput);
this._wrapper.appendChild(this._slider);
this._element = this._wrapper;
}
createRadioStructure() {
// Hide original input
this._originalInput.style.position = 'absolute';
this._originalInput.style.opacity = '0';
this._originalInput.style.pointerEvents = 'none';
// Create wrapper
this._wrapper = (0, utils_1.createElement)('div', {
class: 'np-toggle-wrapper np-toggle-radio'
});
// Create radio dot
this._slider = (0, utils_1.createElement)('div', {
class: 'np-toggle-radio-dot'
});
// Insert wrapper after original input
const parent = this._originalInput.parentElement;
parent.insertBefore(this._wrapper, this._originalInput.nextSibling);
this._wrapper.appendChild(this._originalInput);
this._wrapper.appendChild(this._slider);
this._element = this._wrapper;
}
applyToggleStyles() {
if (!this._wrapper || !this._slider)
return;
const type = this._toggleConfig.type;
const size = this._toggleConfig.size;
(0, utils_1.addClassName)(this._wrapper, 'toggle');
(0, utils_1.addClassName)(this._wrapper, `toggle-${type}`);
(0, utils_1.addClassName)(this._wrapper, `toggle-${size}`);
// Common wrapper styles
this._wrapper.style.position = 'relative';
this._wrapper.style.display = 'inline-flex';
this._wrapper.style.alignItems = 'center';
this._wrapper.style.cursor = 'pointer';
this._wrapper.style.userSelect = 'none';
if (type === 'switch') {
this.applySwitchStyles();
}
else if (type === 'checkbox') {
this.applyCheckboxStyles();
}
else if (type === 'radio') {
this.applyRadioStyles();
}
this.setupInteractionEffects();
}
applySwitchStyles() {
const size = this._toggleConfig.size;
const sizeMap = {
sm: { width: 32, height: 18, sliderSize: 14 },
md: { width: 44, height: 24, sliderSize: 20 },
lg: { width: 56, height: 32, sliderSize: 28 }
};
const dimensions = sizeMap[size];
// Wrapper (track) styles
this._wrapper.style.width = `${dimensions.width}px`;
this._wrapper.style.height = `${dimensions.height}px`;
this._wrapper.style.borderRadius = `${dimensions.height / 2}px`;
this._wrapper.style.backgroundColor = this._theme.colors.surface;
this._wrapper.style.boxShadow = this.createShadowStyle('inset');
this._wrapper.style.transition = `all ${this._theme.animation.duration} ${this._theme.animation.easing}`;
// Slider (thumb) styles
this._slider.style.position = 'absolute';
this._slider.style.top = '50%';
this._slider.style.left = '2px';
this._slider.style.width = `${dimensions.sliderSize}px`;
this._slider.style.height = `${dimensions.sliderSize}px`;
this._slider.style.borderRadius = '50%';
this._slider.style.backgroundColor = this._theme.colors.surface;
this._slider.style.boxShadow = this.createShadowStyle('raised');
this._slider.style.transform = 'translateY(-50%)';
this._slider.style.transition = `all ${this._theme.animation.duration} ${this._theme.animation.easing}`;
}
applyCheckboxStyles() {
const size = this._toggleConfig.size;
const sizeMap = {
sm: 18,
md: 24,
lg: 32
};
const dimensions = sizeMap[size];
// Wrapper styles
this._wrapper.style.width = `${dimensions}px`;
this._wrapper.style.height = `${dimensions}px`;
this._wrapper.style.borderRadius = '4px';
this._wrapper.style.backgroundColor = this._theme.colors.surface;
this._wrapper.style.boxShadow = this.createShadowStyle('inset');
this._wrapper.style.transition = `all ${this._theme.animation.duration} ${this._theme.animation.easing}`;
// Checkmark styles
this._slider.style.position = 'absolute';
this._slider.style.top = '50%';
this._slider.style.left = '50%';
this._slider.style.transform = 'translate(-50%, -50%) scale(0)';
this._slider.style.color = this._theme.colors.accent;
this._slider.style.transition = `transform ${this._theme.animation.duration} ${this._theme.animation.easing}`;
}
applyRadioStyles() {
const size = this._toggleConfig.size;
const sizeMap = {
sm: { outer: 18, inner: 8 },
md: { outer: 24, inner: 12 },
lg: { outer: 32, inner: 16 }
};
const dimensions = sizeMap[size];
// Wrapper styles
this._wrapper.style.width = `${dimensions.outer}px`;
this._wrapper.style.height = `${dimensions.outer}px`;
this._wrapper.style.borderRadius = '50%';
this._wrapper.style.backgroundColor = this._theme.colors.surface;
this._wrapper.style.boxShadow = this.createShadowStyle('inset');
this._wrapper.style.transition = `all ${this._theme.animation.duration} ${this._theme.animation.easing}`;
// Radio dot styles
this._slider.style.position = 'absolute';
this._slider.style.top = '50%';
this._slider.style.left = '50%';
this._slider.style.width = `${dimensions.inner}px`;
this._slider.style.height = `${dimensions.inner}px`;
this._slider.style.borderRadius = '50%';
this._slider.style.backgroundColor = this._theme.colors.accent;
this._slider.style.transform = 'translate(-50%, -50%) scale(0)';
this._slider.style.transition = `transform ${this._theme.animation.duration} ${this._theme.animation.easing}`;
}
setupInteractionEffects() {
if (!this._wrapper)
return;
// Hover effects
this.addEventListener(this._wrapper, 'mouseenter', () => {
if (!this._config.disabled) {
this._wrapper.style.boxShadow = this.createHoverShadowStyle('inset');
}
});
this.addEventListener(this._wrapper, 'mouseleave', () => {
if (!this._config.disabled) {
this._wrapper.style.boxShadow = this.createShadowStyle('inset');
}
});
// Active state
this.addEventListener(this._wrapper, 'mousedown', () => {
if (!this._config.disabled) {
this._wrapper.style.boxShadow = this.createActiveShadowStyle('inset');
}
});
this.addEventListener(this._wrapper, 'mouseup', () => {
if (!this._config.disabled) {
this._wrapper.style.boxShadow = this.createHoverShadowStyle('inset');
}
});
}
handleClick(e) {
e.preventDefault();
if (this._config.disabled)
return;
this.toggle();
}
handleKeydown(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this.toggle();
}
}
handleFocus() {
if (this._wrapper) {
this._wrapper.style.outline = `2px solid ${this._theme.colors.accent}40`;
this._wrapper.style.outlineOffset = '2px';
}
this.emit('focus');
}
handleBlur() {
if (this._wrapper) {
this._wrapper.style.outline = 'none';
}
this.emit('blur');
}
syncState() {
this._isChecked = this._originalInput.checked;
this.updateVisualState();
}
updateVisualState() {
const type = this._toggleConfig.type;
if (type === 'switch') {
this.updateSwitchState();
}
else if (type === 'checkbox') {
this.updateCheckboxState();
}
else if (type === 'radio') {
this.updateRadioState();
}
// Update ARIA attributes
this._originalInput.setAttribute('aria-checked', this._isChecked.toString());
}
updateSwitchState() {
if (!this._wrapper || !this._slider)
return;
const size = this._toggleConfig.size;
const sizeMap = {
sm: { width: 32, sliderSize: 14 },
md: { width: 44, sliderSize: 20 },
lg: { width: 56, sliderSize: 28 }
};
const dimensions = sizeMap[size];
const translateX = this._isChecked ? dimensions.width - dimensions.sliderSize - 4 : 2;
if (this._isChecked) {
this._wrapper.style.backgroundColor = this._theme.colors.accent;
this._slider.style.transform = `translateY(-50%) translateX(${translateX}px)`;
(0, utils_1.addClassName)(this._wrapper, 'checked');
}
else {
this._wrapper.style.backgroundColor = this._theme.colors.surface;
this._slider.style.transform = 'translateY(-50%) translateX(0)';
this._wrapper.classList.remove('np-checked');
}
}
updateCheckboxState() {
if (!this._wrapper || !this._slider)
return;
if (this._isChecked) {
this._wrapper.style.backgroundColor = this._theme.colors.accent;
this._wrapper.style.boxShadow = this.createShadowStyle('flat');
this._slider.style.transform = 'translate(-50%, -50%) scale(1)';
this._slider.style.color = 'white';
(0, utils_1.addClassName)(this._wrapper, 'checked');
}
else {
this._wrapper.style.backgroundColor = this._theme.colors.surface;
this._wrapper.style.boxShadow = this.createShadowStyle('inset');
this._slider.style.transform = 'translate(-50%, -50%) scale(0)';
this._wrapper.classList.remove('np-checked');
}
}
updateRadioState() {
if (!this._wrapper || !this._slider)
return;
if (this._isChecked) {
this._slider.style.transform = 'translate(-50%, -50%) scale(1)';
(0, utils_1.addClassName)(this._wrapper, 'checked');
}
else {
this._slider.style.transform = 'translate(-50%, -50%) scale(0)';
this._wrapper.classList.remove('np-checked');
}
}
emitChange() {
this.emit('change', {
checked: this._isChecked,
value: this._originalInput.value
});
if (this._toggleConfig.onChange) {
this._toggleConfig.onChange(this._isChecked);
}
}
// Public API methods
toggle() {
if (this._config.disabled)
return;
this._isChecked = !this._isChecked;
this._originalInput.checked = this._isChecked;
this.updateVisualState();
this.emitChange();
// Trigger native change event
this._originalInput.dispatchEvent(new Event('change', { bubbles: true }));
}
check() {
if (!this._isChecked) {
this.toggle();
}
}
uncheck() {
if (this._isChecked) {
this.toggle();
}
}
setChecked(checked) {
if (this._isChecked !== checked) {
this.toggle();
}
}
isChecked() {
return this._isChecked;
}
getValue() {
return this._originalInput.value;
}
setValue(value) {
this._originalInput.value = value;
}
onUpdate(newConfig) {
const oldConfig = { ...this._toggleConfig };
this._toggleConfig = { ...this._toggleConfig, ...newConfig };
// Update checked state
if (newConfig.checked !== undefined && newConfig.checked !== oldConfig.checked) {
this.setChecked(newConfig.checked);
}
// Update size if changed
if (newConfig.size && newConfig.size !== oldConfig.size) {
this._wrapper?.classList.remove(`np-toggle-${oldConfig.size}`);
(0, utils_1.addClassName)(this._wrapper, `toggle-${newConfig.size}`);
this.applyToggleStyles();
}
// Update animation setting
if (newConfig.animated !== oldConfig.animated) {
const duration = newConfig.animated ? this._theme.animation.duration : '0ms';
if (this._wrapper)
this._wrapper.style.transition = `all ${duration} ${this._theme.animation.easing}`;
if (this._slider)
this._slider.style.transition = `all ${duration} ${this._theme.animation.easing}`;
}
}
onDestroy() {
// Restore original input
if (this._originalInput && this._wrapper) {
const parent = this._wrapper.parentElement;
if (parent) {
parent.insertBefore(this._originalInput, this._wrapper);
parent.removeChild(this._wrapper);
}
// Restore original input styles
this._originalInput.style.position = '';
this._originalInput.style.opacity = '';
this._originalInput.style.pointerEvents = '';
}
}
}
exports.ToggleComponent = ToggleComponent;
// Factory function for easy usage
function toggle(element, config = {}) {
return new ToggleComponent(element, config);
}