Skip to Content
OWL inputs

Custom Color Picker

Odoo 19 OWL component — Custom Color Picker (core)

Live preview Interactive
Source excerpt web/static/src/core/color_picker/custom_color_picker/custom_color_picker.js
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
import { _t } from "@web/core/l10n/translation";
import {
    convertCSSColorToRgba,
    convertHslToRgb,
    convertRgbaToCSSColor,
    convertRgbToHsl,
    normalizeCSSColor,
} from "@web/core/utils/colors";
import { uniqueId } from "@web/core/utils/functions";
import { clamp } from "@web/core/utils/numbers";
import { debounce, useThrottleForAnimation } from "@web/core/utils/timing";

import { Component, onMounted, onWillUpdateProps, useExternalListener, useRef } from "@odoo/owl";

const ARROW_KEYS = ["arrowup", "arrowdown", "arrowleft", "arrowright"];
const SLIDER_KEYS = [...ARROW_KEYS, "pageup", "pagedown", "home", "end"];

const DEFAULT_COLOR = "#FF0000";

export class CustomColorPicker extends Component {
    static template = "web.CustomColorPicker";
    static props = {
        document: { type: true, optional: true },
        defaultColor: { type: String, optional: true },
        selectedColor: { type: String, optional: true },
        noTransparency: { type: Boolean, optional: true },
        stopClickPropagation: { type: Boolean, optional: true },
        onColorSelect: { type: Function, optional: true },
        onColorPreview: { type: Function, optional: true },
        onInputEnter: { type: Function, optional: true },
        defaultOpacity: { type: Number, optional: true },
        setOnCloseCallback: { type: Function, optional: true },
        setOperationCallbacks: { type: Function, optional: true },
    };
    static defaultProps = {
        document: window.document,
        defaultColor: DEFAULT_COLOR,
        defaultOpacity: 100,
        noTransparency: false,
        stopClickPropagation: false,
        onColorSelect: () => {},
        onColorPreview: () => {},
        onInputEnter: () => {},
    };

    setup() {
        this.pickerFlag = false;
        this.sliderFlag = false;
        this.opacitySliderFlag = false;
        if (this.props.defaultOpacity > 0 && this.props.defaultOpacity <= 1) {
            this.props.defaultOpacity *= 100;
        }
        if (this.props.defaultColor.length <= 7) {
            const opacityHex = Math.round((this.props.defaultOpacity / 100) * 255)
                .toString(16)
                .padStart(2, "0");
            this.props.defaultColor += opacityHex;
        }
        this.colorComponents = {};
        this.uniqueId = uniqueId("colorpicker");
        this.selectedHexValue = "";
        this.shouldSetSelectedColor = false;
        this.lastFocusedSliderEl = undefined;
        if (!this.props.selectedColor) {
            this.props.selectedColor = this.props.defaultColor;
        }
        this.debouncedOnChangeInputs = debounce(this.onChangeInputs.bind(this), 10, true);

        this.elRef = useRef("el");
        this.colorPickerAreaRef = useRef("colorPickerArea");
        this.colorPickerPointerRef = useRef("colorPickerPointer");
        this.colorSliderRef = useRef("colorSlider");
        this.colorSliderPointerRef = useRef("colorSliderPointer");
        this.opacitySliderRef = useRef("opacitySlider");
        this.opacitySliderPointerRef = useRef("opacitySliderPointer");

        // Need to be bound on all documents to work in all possible cases (we
        // have to be able to start dragging/moving from the colorpicker to
        // anywhere on the screen, crossing iframes).
        const documents = [
            window.top,
            ...Array.from(window.top.frames).filter((frame) => {
                try {
                    const document = frame.document;
                    return !!document;
                } catch {
                    // We cannot access the document (cross origin).
                    return false;
                }
            }),
        ].map((w) => w.document);
        this.throttleOnPointerMove = useThrottleForAnimation((ev) => {
            this.onPointerMovePicker(ev);
            this.onPointerMoveSlider(ev);
            this.onPointerMoveOpacitySlider(ev);
        });

        for (const doc of documents) {
            useExternalListener(doc, "pointermove", this.throttleOnPointerMove);
            useExternalListener(doc, "pointerup", this.onPointerUp.bind(this));
            useExternalListener(doc, "keydown", this.onEscapeKeydown.bind(this), { capture: true });
        }
        // Apply the previewed custom color when the popover is closed.
        this.props.setOnCloseCallback?.(() => {
            if (this.shouldSetSelectedColor) {
                this._colorSelected();
            }
        });
        this.props.setOperationCallbacks?.({
            getPreviewColor: () => {
                if (this.shouldSetSelectedColor) {
                    return this.colorComponents.hex;
                }
            },
            onApplyCallback: () => {
                this.shouldSetSelectedColor = false;
            },
            // Reapply the current custom color preview after reverting a preview.
            // Typical usecase: 1) modify the custom color, 2) hover one of the
Registry / API
Registry name
CustomColorPicker
Category
Module
web
Slug
custom-color-picker
Nav group
inputs