UNPKG

@radial-color-picker/vue-color-picker

Version:
376 lines (375 loc) 10.9 kB
"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;