2025-05-07 09:26:06 -04:00
|
|
|
import { Controller } from "@hotwired/stimulus";
|
|
|
|
import Pickr from "@simonwep/pickr";
|
2025-02-24 21:08:05 +05:00
|
|
|
|
|
|
|
export default class extends Controller {
|
2025-05-07 09:26:06 -04:00
|
|
|
static targets = [
|
|
|
|
"pickerBtn",
|
|
|
|
"colorInput",
|
|
|
|
"colorsSection",
|
|
|
|
"paletteSection",
|
|
|
|
"pickerSection",
|
|
|
|
"colorPreview",
|
|
|
|
"avatar",
|
|
|
|
"details",
|
|
|
|
"icon",
|
|
|
|
"validationMessage",
|
|
|
|
"selection",
|
|
|
|
"colorPickerRadioBtn",
|
2025-05-22 12:50:12 -03:00
|
|
|
"popup",
|
2025-05-07 09:26:06 -04:00
|
|
|
];
|
|
|
|
|
2025-02-24 21:08:05 +05:00
|
|
|
static values = {
|
|
|
|
presetColors: Array,
|
|
|
|
};
|
|
|
|
|
|
|
|
initialize() {
|
2025-05-07 09:26:06 -04:00
|
|
|
this.pickerBtnTarget.addEventListener("click", () => {
|
2025-02-24 21:08:05 +05:00
|
|
|
this.showPaletteSection();
|
|
|
|
});
|
|
|
|
|
2025-05-07 09:26:06 -04:00
|
|
|
this.colorInputTarget.addEventListener("input", (e) => {
|
2025-02-24 21:08:05 +05:00
|
|
|
this.picker.setColor(e.target.value);
|
|
|
|
});
|
|
|
|
|
2025-05-07 09:26:06 -04:00
|
|
|
this.detailsTarget.addEventListener("toggle", (e) => {
|
2025-02-24 21:08:05 +05:00
|
|
|
if (!this.colorInputTarget.checkValidity()) {
|
|
|
|
e.preventDefault();
|
|
|
|
this.colorInputTarget.reportValidity();
|
|
|
|
e.target.open = true;
|
|
|
|
}
|
2025-05-22 12:50:12 -03:00
|
|
|
this.updatePopupPosition()
|
2025-02-24 21:08:05 +05:00
|
|
|
});
|
|
|
|
|
|
|
|
this.selectedIcon = null;
|
|
|
|
|
|
|
|
if (!this.presetColorsValue.includes(this.colorInputTarget.value)) {
|
|
|
|
this.colorPickerRadioBtnTarget.checked = true;
|
|
|
|
}
|
2025-05-22 12:50:12 -03:00
|
|
|
|
|
|
|
document.addEventListener("mousedown", this.handleOutsideClick);
|
2025-02-24 21:08:05 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
initPicker() {
|
|
|
|
const pickerContainer = document.createElement("div");
|
|
|
|
pickerContainer.classList.add("pickerContainer");
|
|
|
|
this.pickerSectionTarget.append(pickerContainer);
|
|
|
|
|
|
|
|
this.picker = Pickr.create({
|
|
|
|
el: this.pickerBtnTarget,
|
2025-05-07 09:26:06 -04:00
|
|
|
theme: "monolith",
|
2025-02-24 21:08:05 +05:00
|
|
|
container: ".pickerContainer",
|
|
|
|
useAsButton: true,
|
|
|
|
showAlways: true,
|
|
|
|
default: this.colorInputTarget.value,
|
|
|
|
components: {
|
|
|
|
hue: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2025-05-07 09:26:06 -04:00
|
|
|
this.picker.on("change", (color) => {
|
2025-02-24 21:08:05 +05:00
|
|
|
const hexColor = color.toHEXA().toString();
|
|
|
|
const rgbacolor = color.toRGBA();
|
|
|
|
|
|
|
|
this.updateAvatarColors(hexColor);
|
|
|
|
this.updateSelectedIconColor(hexColor);
|
|
|
|
|
|
|
|
const backgroundColor = this.backgroundColor(rgbacolor, 10);
|
|
|
|
const contrastRatio = this.contrast(rgbacolor, backgroundColor);
|
|
|
|
|
|
|
|
this.colorInputTarget.value = hexColor;
|
|
|
|
this.colorInputTarget.dataset.colorPickerColorValue = hexColor;
|
|
|
|
this.colorPreviewTarget.style.backgroundColor = hexColor;
|
|
|
|
|
|
|
|
this.handleContrastValidation(contrastRatio);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
updateAvatarColors(color) {
|
|
|
|
this.avatarTarget.style.backgroundColor = `${this.#backgroundColor(color)}`;
|
|
|
|
this.avatarTarget.style.color = color;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleIconColorChange(e) {
|
|
|
|
const selectedIcon = e.target;
|
|
|
|
this.selectedIcon = selectedIcon;
|
2025-05-07 09:26:06 -04:00
|
|
|
|
2025-02-24 21:08:05 +05:00
|
|
|
const currentColor = this.colorInputTarget.value;
|
2025-05-07 09:26:06 -04:00
|
|
|
|
|
|
|
this.iconTargets.forEach((icon) => {
|
2025-02-24 21:08:05 +05:00
|
|
|
const iconWrapper = icon.nextElementSibling;
|
2025-05-07 09:26:06 -04:00
|
|
|
iconWrapper.style.removeProperty("background-color");
|
|
|
|
iconWrapper.style.removeProperty("color");
|
2025-02-24 21:08:05 +05:00
|
|
|
});
|
|
|
|
|
|
|
|
this.updateSelectedIconColor(currentColor);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleIconChange(e) {
|
2025-05-07 09:26:06 -04:00
|
|
|
const iconSVG = e.currentTarget
|
|
|
|
.closest("label")
|
|
|
|
.querySelector("svg")
|
|
|
|
.cloneNode(true);
|
|
|
|
this.avatarTarget.innerHTML = "";
|
|
|
|
iconSVG.style.padding = "0px";
|
|
|
|
iconSVG.classList.add("w-8", "h-8");
|
2025-02-24 21:08:05 +05:00
|
|
|
this.avatarTarget.appendChild(iconSVG);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateSelectedIconColor(color) {
|
|
|
|
if (this.selectedIcon) {
|
|
|
|
const iconWrapper = this.selectedIcon.nextElementSibling;
|
|
|
|
iconWrapper.style.backgroundColor = `${this.#backgroundColor(color)}`;
|
|
|
|
iconWrapper.style.color = color;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleColorChange(e) {
|
|
|
|
const color = e.currentTarget.value;
|
|
|
|
this.colorInputTarget.value = color;
|
|
|
|
this.colorPreviewTarget.style.backgroundColor = color;
|
|
|
|
this.updateAvatarColors(color);
|
|
|
|
this.updateSelectedIconColor(color);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleContrastValidation(contrastRatio) {
|
|
|
|
if (contrastRatio < 4.5) {
|
2025-05-07 09:26:06 -04:00
|
|
|
this.colorInputTarget.setCustomValidity(
|
|
|
|
"Poor contrast, choose darker color or auto-adjust.",
|
|
|
|
);
|
2025-02-24 21:08:05 +05:00
|
|
|
|
|
|
|
this.validationMessageTarget.classList.remove("hidden");
|
|
|
|
} else {
|
|
|
|
this.colorInputTarget.setCustomValidity("");
|
|
|
|
this.validationMessageTarget.classList.add("hidden");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-07 09:26:06 -04:00
|
|
|
autoAdjust(e) {
|
2025-02-24 21:08:05 +05:00
|
|
|
const currentRGBA = this.picker.getColor();
|
|
|
|
const adjustedRGBA = this.darkenColor(currentRGBA).toString();
|
|
|
|
this.picker.setColor(adjustedRGBA);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleParentChange(e) {
|
|
|
|
const parent = e.currentTarget.value;
|
2025-05-07 09:26:06 -04:00
|
|
|
const display =
|
|
|
|
typeof parent === "string" && parent !== "" ? "none" : "flex";
|
2025-02-24 21:08:05 +05:00
|
|
|
this.selectionTarget.style.display = display;
|
|
|
|
}
|
|
|
|
|
2025-05-07 09:26:06 -04:00
|
|
|
backgroundColor([r, g, b, a], percentage) {
|
|
|
|
const mixedR = Math.round(
|
|
|
|
r * (percentage / 100) + 255 * (1 - percentage / 100),
|
|
|
|
);
|
|
|
|
const mixedG = Math.round(
|
|
|
|
g * (percentage / 100) + 255 * (1 - percentage / 100),
|
|
|
|
);
|
|
|
|
const mixedB = Math.round(
|
|
|
|
b * (percentage / 100) + 255 * (1 - percentage / 100),
|
|
|
|
);
|
2025-02-24 21:08:05 +05:00
|
|
|
return [mixedR, mixedG, mixedB];
|
|
|
|
}
|
|
|
|
|
2025-05-07 09:26:06 -04:00
|
|
|
luminance([r, g, b]) {
|
|
|
|
const toLinear = (c) => {
|
2025-02-24 21:08:05 +05:00
|
|
|
const scaled = c / 255;
|
2025-05-07 09:26:06 -04:00
|
|
|
return scaled <= 0.04045
|
|
|
|
? scaled / 12.92
|
2025-02-24 21:08:05 +05:00
|
|
|
: ((scaled + 0.055) / 1.055) ** 2.4;
|
|
|
|
};
|
|
|
|
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
|
|
}
|
|
|
|
|
|
|
|
contrast(foregroundColor, backgroundColor) {
|
|
|
|
const fgLum = this.luminance(foregroundColor);
|
|
|
|
const bgLum = this.luminance(backgroundColor);
|
|
|
|
const [l1, l2] = [Math.max(fgLum, bgLum), Math.min(fgLum, bgLum)];
|
|
|
|
return (l1 + 0.05) / (l2 + 0.05);
|
|
|
|
}
|
|
|
|
|
|
|
|
darkenColor(color) {
|
|
|
|
let darkened = color.toRGBA();
|
|
|
|
const backgroundColor = this.backgroundColor(darkened, 10);
|
|
|
|
let contrastRatio = this.contrast(darkened, backgroundColor);
|
|
|
|
|
2025-05-07 09:26:06 -04:00
|
|
|
while (
|
|
|
|
contrastRatio < 4.5 &&
|
|
|
|
(darkened[0] > 0 || darkened[1] > 0 || darkened[2] > 0)
|
|
|
|
) {
|
2025-02-24 21:08:05 +05:00
|
|
|
darkened = [
|
|
|
|
Math.max(0, darkened[0] - 10),
|
|
|
|
Math.max(0, darkened[1] - 10),
|
|
|
|
Math.max(0, darkened[2] - 10),
|
2025-05-07 09:26:06 -04:00
|
|
|
darkened[3],
|
2025-02-24 21:08:05 +05:00
|
|
|
];
|
|
|
|
contrastRatio = this.contrast(darkened, backgroundColor);
|
|
|
|
}
|
|
|
|
|
|
|
|
return `rgba(${darkened.join(", ")})`;
|
|
|
|
}
|
|
|
|
|
|
|
|
showPaletteSection() {
|
|
|
|
this.initPicker();
|
2025-05-07 09:26:06 -04:00
|
|
|
this.colorsSectionTarget.classList.add("hidden");
|
|
|
|
this.paletteSectionTarget.classList.remove("hidden");
|
|
|
|
this.pickerSectionTarget.classList.remove("hidden");
|
2025-05-22 12:50:12 -03:00
|
|
|
this.updatePopupPosition();
|
2025-02-24 21:08:05 +05:00
|
|
|
this.picker.show();
|
|
|
|
}
|
|
|
|
|
|
|
|
showColorsSection() {
|
2025-05-07 09:26:06 -04:00
|
|
|
this.colorsSectionTarget.classList.remove("hidden");
|
|
|
|
this.paletteSectionTarget.classList.add("hidden");
|
|
|
|
this.pickerSectionTarget.classList.add("hidden");
|
2025-05-22 12:50:12 -03:00
|
|
|
this.updatePopupPosition()
|
2025-02-24 21:08:05 +05:00
|
|
|
if (this.picker) {
|
|
|
|
this.picker.destroyAndRemove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleSections() {
|
2025-05-07 09:26:06 -04:00
|
|
|
if (this.colorsSectionTarget.classList.contains("hidden")) {
|
2025-02-24 21:08:05 +05:00
|
|
|
this.showColorsSection();
|
|
|
|
} else {
|
|
|
|
this.showPaletteSection();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-22 12:50:12 -03:00
|
|
|
handleOutsideClick = (event) => {
|
|
|
|
if (this.detailsTarget.open && !this.detailsTarget.contains(event.target)) {
|
|
|
|
this.detailsTarget.open = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
updatePopupPosition() {
|
|
|
|
const popup = this.popupTarget;
|
|
|
|
popup.style.top = "";
|
|
|
|
popup.style.bottom = "";
|
|
|
|
|
|
|
|
const rect = popup.getBoundingClientRect();
|
|
|
|
const overflow = rect.bottom > window.innerHeight;
|
|
|
|
|
|
|
|
if (overflow) {
|
|
|
|
popup.style.bottom = "0px";
|
|
|
|
} else {
|
|
|
|
popup.style.bottom = "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-24 21:08:05 +05:00
|
|
|
#backgroundColor(color) {
|
|
|
|
return `color-mix(in oklab, ${color} 10%, transparent)`;
|
|
|
|
}
|
|
|
|
}
|