@radial-color-picker/vue-color-picker
Version:
Radial Color Picker - Vue
376 lines (375 loc) • 10.9 kB
JavaScript
"use strict";
const vue = require("vue");
const TO_DEGREES = 180 / Math.PI;
const normalizeAngle = (angle) => {
const mod = angle % 360;
return mod < 0 ? 360 + mod : mod;
};
const getRotationFromCoords = ({
x,
y
}, rect) => {
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
return Math.atan2(y - cy, x - cx) * TO_DEGREES;
};
const noop = () => {
};
class Rotator {
constructor(element, options) {
this.active = false;
this.element = element;
this.element.style.willChange = "transform";
this.initOptions(options);
this.updateCSS();
this.bindHandlers();
this.addListeners();
}
get angle() {
return this._angle;
}
set angle(value) {
if (this._angle !== value) {
this._angle = normalizeAngle(value);
this.updateCSS();
}
}
initOptions(options) {
options = options || {};
this.onRotate = options.onRotate || noop;
this.onDragStart = options.onDragStart || noop;
this.onDragStop = options.onDragStop || noop;
this._angle = options.angle || 0;
}
bindHandlers() {
this.onRotationStart = this.onRotationStart.bind(this);
this.onRotated = this.onRotated.bind(this);
this.onRotationStop = this.onRotationStop.bind(this);
}
addListeners() {
this.element.addEventListener("touchstart", this.onRotationStart, {
passive: true
});
document.addEventListener("touchmove", this.onRotated, {
passive: false
});
document.addEventListener("touchend", this.onRotationStop, {
passive: true
});
document.addEventListener("touchcancel", this.onRotationStop, {
passive: true
});
this.element.addEventListener("mousedown", this.onRotationStart, {
passive: true
});
document.addEventListener("mousemove", this.onRotated, {
passive: false
});
document.addEventListener("mouseup", this.onRotationStop, {
passive: true
});
document.addEventListener("mouseleave", this.onRotationStop, {
passive: false
});
}
removeListeners() {
this.element.removeEventListener("touchstart", this.onRotationStart);
document.removeEventListener("touchmove", this.onRotated);
document.removeEventListener("touchend", this.onRotationStop);
document.removeEventListener("touchcancel", this.onRotationStop);
this.element.removeEventListener("mousedown", this.onRotationStart);
document.removeEventListener("mousemove", this.onRotated);
document.removeEventListener("mouseup", this.onRotationStop);
document.removeEventListener("mouseleave", this.onRotationStop);
}
destroy() {
this.onRotationStop();
this.removeListeners();
}
onRotationStart(event) {
if (event.type === "touchstart" || event.button === 0) {
this.active = true;
this.onDragStart(event);
this.setAngleFromEvent(event);
}
}
onRotationStop() {
if (this.active) {
this.active = false;
this.onDragStop();
}
this.active = false;
}
onRotated(event) {
if (this.active) {
event.preventDefault();
this.setAngleFromEvent(event);
}
}
setAngleFromEvent(event) {
const ev = event.targetTouches ? event.targetTouches[0] : event;
const newAngle = getRotationFromCoords({
x: ev.clientX,
y: ev.clientY
}, this.element.getBoundingClientRect());
this._angle = normalizeAngle(newAngle + 90);
this.updateCSS();
this.onRotate(this._angle);
}
updateCSS() {
this.element.style.transform = "rotate(" + this._angle + "deg)";
}
}
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
const colors = ["red", "yellow", "green", "cyan", "blue", "magenta", "red"];
const keys = {
ArrowUp: (oldAngle, step) => oldAngle + step,
ArrowRight: (oldAngle, step) => oldAngle + step,
ArrowDown: (oldAngle, step) => oldAngle - step,
ArrowLeft: (oldAngle, step) => oldAngle - step,
PageUp: (oldAngle, step) => oldAngle + step * 10,
PageDown: (oldAngle, step) => oldAngle - step * 10,
Home: () => 0,
End: () => 359
};
const _sfc_main = {
name: "ColorPicker",
emits: ["select", "input", "change"],
props: {
hue: {
default: 0
},
saturation: {
default: 100
},
luminosity: {
default: 50
},
alpha: {
default: 1
},
step: {
default: 1
},
mouseScroll: {
default: false
},
variant: {
default: "collapsible"
// collapsible | persistent
},
disabled: {
default: false
},
initiallyCollapsed: {
default: false
},
ariaLabel: {
default: "color picker"
},
ariaRoledescription: {
default: "radial slider"
},
ariaValuetext: {
default: ""
},
ariaLabelColorWell: {
default: "color well"
}
},
setup(props, { emit }) {
vue.ref(null);
const rotator = vue.ref(null);
let rcp = null;
const initialAngle = props.hue + "deg";
const angle = vue.ref(props.hue);
const isPaletteIn = vue.ref(!props.initiallyCollapsed);
const isKnobIn = vue.ref(!props.initiallyCollapsed);
const isPressed = vue.ref(false);
const isRippling = vue.ref(false);
const color = vue.computed(
() => `hsla(${angle.value}, ${props.saturation}%, ${props.luminosity}%, ${props.alpha})`
);
const valuetext = vue.computed(() => {
return colors[Math.round(angle.value / 60)];
});
vue.watch(
() => props.hue,
(value) => {
angle.value = value;
rcp.angle = value;
}
);
if (process.env.NODE_ENV === "development" && props.initiallyCollapsed && props.variant === "persistent") {
console.warn(
`Incorrect config: using variant="persistent" and :initiallyCollapsed="true" at the same time is not supported.`
);
}
vue.onMounted(() => {
rcp = new Rotator(rotator.value, {
angle: angle.value,
onRotate(hue) {
angle.value = hue;
emit("input", angle.value);
},
onDragStop() {
emit("change", angle.value);
}
});
});
vue.onBeforeUnmount(() => {
rcp.destroy();
rcp = null;
});
const onKeyDown = (ev) => {
if (props.disabled || isPressed.value || !isKnobIn.value || !(ev.key in keys))
return;
ev.preventDefault();
rcp.angle = keys[ev.key](rcp.angle, props.step);
angle.value = rcp.angle;
emit("input", angle.value);
emit("change", angle.value);
};
const onScroll = (ev) => {
if (isPressed.value || !isKnobIn.value)
return;
ev.preventDefault();
if (ev.deltaY > 0) {
rcp.angle += props.step;
} else {
rcp.angle -= props.step;
}
angle.value = rcp.angle;
emit("input", angle.value);
emit("change", angle.value);
};
const selectColor = () => {
isPressed.value = true;
if (isPaletteIn.value && isKnobIn.value) {
emit("select", angle.value);
isRippling.value = true;
} else {
isPaletteIn.value = true;
}
};
const togglePicker = () => {
if (props.variant !== "persistent") {
if (isKnobIn.value) {
isKnobIn.value = false;
} else {
isKnobIn.value = true;
isPaletteIn.value = true;
}
}
isRippling.value = false;
isPressed.value = false;
};
const hidePalette = () => {
if (!isKnobIn.value) {
isPaletteIn.value = false;
}
};
return {
// private API intented for testing memory leaks
rcp,
// refs
rotator,
// state
initialAngle,
angle,
isPaletteIn,
isKnobIn,
isRippling,
isPressed,
// computed
color,
valuetext,
// methods
onKeyDown,
onScroll,
selectColor,
togglePicker,
hidePalette
};
}
};
const _hoisted_1 = ["aria-roledescription", "aria-label", "aria-expanded", "aria-valuenow", "aria-valuetext", "aria-disabled", "tabindex"];
const _hoisted_2 = /* @__PURE__ */ vue.createElementVNode(
"div",
{ class: "rcp__palette" },
null,
-1
/* HOISTED */
);
const _hoisted_3 = ["aria-label", "disabled", "tabindex"];
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return vue.openBlock(), vue.createElementBlock("div", {
role: "slider",
"aria-roledescription": $props.ariaRoledescription,
"aria-label": $props.ariaLabel,
"aria-expanded": $setup.isPaletteIn,
"aria-valuemin": "0",
"aria-valuemax": "359",
"aria-valuenow": $setup.angle,
"aria-valuetext": $props.ariaValuetext || $setup.valuetext,
"aria-disabled": $props.disabled,
class: "rcp",
tabindex: $props.disabled ? -1 : 0,
style: vue.normalizeStyle({ "--rcp-initial-angle": $setup.initialAngle }),
onKeyup: _cache[3] || (_cache[3] = vue.withKeys((...args) => $setup.selectColor && $setup.selectColor(...args), ["enter"])),
onKeydown: _cache[4] || (_cache[4] = (...args) => $setup.onKeyDown && $setup.onKeyDown(...args))
}, [
_hoisted_2,
vue.createElementVNode(
"div",
vue.mergeProps({
class: "rcp__rotator",
style: {
"pointer-events": $props.disabled || $setup.isPressed || !$setup.isKnobIn ? "none" : null
}
}, vue.toHandlers($props.mouseScroll ? { wheel: $setup.onScroll } : {}, true), { ref: "rotator" }),
[
vue.createElementVNode(
"div",
{
class: vue.normalizeClass(["rcp__knob", $setup.isKnobIn ? "in" : "out"]),
onTransitionend: _cache[0] || (_cache[0] = (...args) => $setup.hidePalette && $setup.hidePalette(...args))
},
null,
34
/* CLASS, NEED_HYDRATION */
)
],
16
/* FULL_PROPS */
),
vue.createElementVNode(
"div",
{
class: vue.normalizeClass(["rcp__ripple", { rippling: $setup.isRippling }]),
style: vue.normalizeStyle({ borderColor: $setup.color })
},
null,
6
/* CLASS, STYLE */
),
vue.createElementVNode("button", {
type: "button",
class: vue.normalizeClass(["rcp__well", { pressed: $setup.isPressed }]),
"aria-label": $props.ariaLabelColorWell,
disabled: $props.disabled,
tabindex: $props.disabled ? -1 : 0,
style: vue.normalizeStyle({ backgroundColor: $setup.color }),
onAnimationend: _cache[1] || (_cache[1] = (...args) => $setup.togglePicker && $setup.togglePicker(...args)),
onClick: _cache[2] || (_cache[2] = (...args) => $setup.selectColor && $setup.selectColor(...args))
}, null, 46, _hoisted_3)
], 44, _hoisted_1);
}
const ColorPicker = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
module.exports = ColorPicker;