diff --git a/CHANGES.md b/CHANGES.md index 216122511..7e3dfb321 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,8 @@ ### :sparkles: New features - Add integration with gitpod.io (an online IDE) [#807](https://github.com/penpot/penpot/pull/807) -- Allow basic math operations in inputs [1383](https://tree.taiga.io/project/penpot/us/1383) +- Allow basic math operations in inputs [Taiga 1383](https://tree.taiga.io/project/penpot/us/1383) +- Autocomplete color names in hex inputs [Taiga 1596](https://tree.taiga.io/project/penpot/us/1596) - Allow to group assets (components and graphics) [Taiga #1289](https://tree.taiga.io/project/penpot/us/1289) - Internal: refactor of http client, replace internal xhr usage with more modern Fetch API. - New features for paths: snap points on edition, add/remove nodes, merge/join/split nodes. [Taiga #907](https://tree.taiga.io/project/penpot/us/907) diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index 115ee933e..5f04d8990 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -498,7 +498,7 @@ height: 20px; margin: 5px 0 0 0; padding: 0 $x-small; - width: 64px; + width: 84px; font-size: $fs13; &:focus { diff --git a/frontend/src/app/main/ui/components/color_input.cljs b/frontend/src/app/main/ui/components/color_input.cljs new file mode 100644 index 000000000..112aecf33 --- /dev/null +++ b/frontend/src/app/main/ui/components/color_input.cljs @@ -0,0 +1,105 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.components.color-input + (:require + [app.common.data :as d] + [app.common.math :as math] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.util.color :as uc] + [app.util.dom :as dom] + [app.util.keyboard :as kbd] + [app.util.object :as obj] + [app.util.simple-math :as sm] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(mf/defc color-input + {::mf/wrap-props false + ::mf/forward-ref true} + [props external-ref] + (let [value (obj/get props "value") + on-change (obj/get props "onChange") + + ;; We need a ref pointing to the input dom element, but the user + ;; of this component may provide one (that is forwarded here). + ;; So we use the external ref if provided, and the local one if not. + local-ref (mf/use-ref) + ref (or external-ref local-ref) + + parse-value + (mf/use-callback + (mf/deps ref) + (fn [] + (let [input-node (mf/ref-val ref)] + (try + (let [new-value (-> (dom/get-value input-node) + (uc/expand-hex) + (uc/parse-color) + (uc/prepend-hash))] + (dom/set-validity! input-node "") + new-value) + (catch :default _ + (dom/set-validity! input-node (tr "errors.invalid-color")) + nil))))) + + update-input + (mf/use-callback + (mf/deps ref) + (fn [new-value] + (let [input-node (mf/ref-val ref)] + (dom/set-value! input-node (uc/remove-hash new-value))))) + + apply-value + (mf/use-callback + (mf/deps on-change update-input) + (fn [new-value] + (when new-value + (when on-change + (on-change new-value)) + (update-input new-value)))) + + handle-key-down + (mf/use-callback + (mf/deps apply-value update-input) + (fn [event] + (let [enter? (kbd/enter? event) + esc? (kbd/esc? event)] + (when enter? + (dom/prevent-default event) + (let [new-value (parse-value)] + (apply-value new-value))) + (when esc? + (dom/prevent-default event) + (update-input value))))) + + handle-blur + (mf/use-callback + (mf/deps parse-value apply-value update-input) + (fn [event] + (let [new-value (parse-value)] + (if new-value + (apply-value new-value) + (update-input value))))) + + list-id (str "colors-" (uuid/next)) + + props (-> props + (obj/without ["value" "onChange"]) + (obj/set! "type" "text") + (obj/set! "ref" ref) + (obj/set! "list" list-id) + (obj/set! "defaultValue" value) + (obj/set! "onKeyDown" handle-key-down) + (obj/set! "onBlur" handle-blur))] + + [:* + [:> :input props] + [:datalist {:id list-id} + (for [color-name uc/color-names] + [:option color-name])]])) + diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index 729f07a69..b460d7b6c 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -18,15 +18,18 @@ (mf/defc numeric-input {::mf/wrap-props false ::mf/forward-ref true} - [props ref] + [props external-ref] (let [value-str (obj/get props "value") min-val-str (obj/get props "min") max-val-str (obj/get props "max") wrap-value? (obj/get props "data-wrap") on-change (obj/get props "onChange") + ;; We need a ref pointing to the input dom element, but the user + ;; of this component may provide one (that is forwarded here). + ;; So we use the external ref if provided, and the local one if not. local-ref (mf/use-ref) - ref (or ref local-ref) + ref (or external-ref local-ref) value (d/parse-integer value-str) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 396a9a719..96e5f4021 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -21,6 +21,7 @@ [app.main.ui.icons :as i] [app.main.ui.context :as ctx] [app.main.ui.components.color-bullet :as cb] + [app.main.ui.components.color-input :refer [color-input]] [app.main.ui.components.numeric-input :refer [numeric-input]])) (defn color-picker-callback @@ -38,13 +39,6 @@ (handle-open) (modal/show! :colorpicker props)))) - -(defn remove-hash [value] - (if (or (nil? value) (= value :multiple)) "" (subs value 1))) - -(defn append-hash [value] - (str "#" value)) - (defn opacity->string [opacity] (if (= opacity :multiple) "" @@ -92,13 +86,9 @@ handle-close (fn [value opacity id file-id] (when on-close (on-close value opacity id file-id))) - handle-value-change (fn [event] - (let [target (dom/get-target event)] - (when (dom/valid? target) - (-> target - dom/get-value - append-hash - change-value)))) + handle-value-change (fn [new-value] + (-> new-value + change-value)) handle-opacity-change (fn [value] (change-opacity (/ value 100))) @@ -155,13 +145,12 @@ :else [:* [:div.color-info - [:input {:value (if (uc/multiple? color) - "" - (-> color :color remove-hash)) - :pattern "^[0-9a-fA-F]{0,6}$" - :placeholder (tr "settings.multiple") - :on-click select-all - :on-change handle-value-change}]] + [:> color-input {:value (if (uc/multiple? color) + "" + (-> color :color uc/remove-hash)) + :placeholder (tr "settings.multiple") + :on-click select-all + :on-change handle-value-change}]] (when (and (not disable-opacity) (not (:gradient color))) diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index 6da55669a..05183e2eb 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -9,6 +9,7 @@ (:require [cuerdas.core :as str] [app.common.math :as math] + [app.util.object :as obj] [goog.color :as gcolor])) (defn rgb->str @@ -80,6 +81,34 @@ [hsv] (hex->hsl (hsv->hex hsv))) +(defn expand-hex + [v] + (cond + (re-matches #"^[0-9A-Fa-f]$" v) + (str v v v v v v) + + (re-matches #"^[0-9A-Fa-f]{2}$" v) + (str v v v) + + (re-matches #"^[0-9A-Fa-f]{3}$" v) + (let [a (nth v 0) + b (nth v 1) + c (nth v 2)] + (str a a b b c c)) + + :default + v)) + +(defn prepend-hash + [color] + (gcolor/prependHashIfNecessaryHelper color)) + +(defn remove-hash + [color] + (if (str/starts-with? color "#") + (subs color 1) + color)) + (defn gradient->css [{:keys [type stops]}] (let [parse-stop (fn [{:keys [offset color opacity]}] @@ -122,5 +151,8 @@ (let [result (gcolor/parse color-str)] (str (.-hex ^js result)))) +(def color-names + (obj/get-keys ^js gcolor/names)) + (def empty-color (into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity])) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 7ee03f6cf..95679b7c8 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -108,6 +108,15 @@ [node] (.-valid (.-validity node))) +(defn set-validity! + "Manually set the validity status of a node that + is a form input. If the state is an empty string, + the input will be valid. If not, the string will + be set as the error message." + [node status] + (.setCustomValidity node status) + (.reportValidity node)) + (defn clean-value! [node] (set! (.-value node) "")) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index ab14004d4..ad6697a42 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -22,6 +22,10 @@ (let [result (get obj k)] (if (undefined? result) default result)))) +(defn get-keys + [obj] + (js/Object.keys ^js obj)) + (defn get-in [obj keys] (loop [key (first keys) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index eb794a40a..2d971b4c9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -443,6 +443,10 @@ msgstr "Something wrong has happened." msgid "errors.google-auth-not-enabled" msgstr "Authentication with google disabled on backend" +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "Invalid color" + #: src/app/main/ui/auth/login.cljs msgid "errors.ldap-disabled" msgstr "LDAP authentication is disabled." @@ -2509,4 +2513,4 @@ msgid "workspace.updates.update" msgstr "Update" msgid "workspace.viewport.click-to-close-path" -msgstr "Click to close the path" \ No newline at end of file +msgstr "Click to close the path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 5685e0ce4..b2621ae1e 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -428,6 +428,10 @@ msgstr "Ha ocurrido algún error." msgid "errors.google-auth-not-enabled" msgstr "Autenticación con google esta dehabilitada en el servidor" +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "Color inválido" + #: src/app/main/ui/auth/login.cljs msgid "errors.ldap-disabled" msgstr "La autheticacion via LDAP esta deshabilitada." @@ -2488,14 +2492,3 @@ msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" - -#: src/app/main/ui/auth/register.cljs -#, fuzzy -msgid "auth.check-your-email" -msgstr "" -"Comprueba tu email y haz click en el link de verificación para comenzar a " -"usar Penpot." - -#: src/app/main/ui/auth/register.cljs -msgid "auth.verification-email-sent" -msgstr "Hemos enviado un email de verificación a"