diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index d457c3d7a..1b862b22d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -20,12 +20,12 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.components.editable-select :refer [editable-select]] [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.main.ui.workspace.tokens.core :as wtc] + [app.main.ui.workspace.tokens.editable-select :refer [editable-select]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [clojure.set :refer [rename-keys union]] @@ -100,7 +100,11 @@ tokens (mf/deref refs/workspace-tokens) border-radius-tokens (mf/use-memo (mf/deps tokens) #(wtc/tokens-name-map-for-type :border-radius tokens)) - border-radius-options (mf/use-memo (mf/deps border-radius-tokens) #(map (comp :name val) border-radius-tokens)) + border-radius-options (mf/use-memo (mf/deps shape border-radius-tokens) + #(map (fn [[_k {:keys [name] :as item}]] + (cond-> (assoc item :label name) + (wtc/token-applied? item shape (wtc/token-attributes :border-radius)) (assoc :selected? true))) + border-radius-tokens)) flex-child? (->> selection-parents (some ctl/flex-layout?)) absolute? (ctl/item-absolute? shape) @@ -291,8 +295,7 @@ (mf/use-fn (mf/deps ids change-radius border-radius-tokens) (fn [value] - (let [token (when (symbol? value) - (get border-radius-tokens (str value))) + (let [token (when (map? value) value) token-value (some-> token wtc/resolve-token-value)] (st/emit! (change-radius (fn [shape] diff --git a/frontend/src/app/main/ui/workspace/tokens/editable_select.cljs b/frontend/src/app/main/ui/workspace/tokens/editable_select.cljs new file mode 100644 index 000000000..65264f703 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/editable_select.cljs @@ -0,0 +1,216 @@ +;; 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) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.editable-select + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.math :as mth] + [app.common.uuid :as uuid] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.keyboard :as kbd] + [app.util.timers :as timers] + [rumext.v2 :as mf])) + +(defn on-number-input-key-down [{:keys [event min-val max-val set-value!]}] + (let [up? (kbd/up-arrow? event) + down? (kbd/down-arrow? event)] + (when (or up? down?) + (dom/prevent-default event) + (let [value (-> event dom/get-target dom/get-value) + value (or (d/parse-double value) value) + increment (cond + (kbd/shift? event) (if up? 10 -10) + (kbd/alt? event) (if up? 0.1 -0.1) + :else (if up? 1 -1)) + new-value (+ value increment) + new-value (cond + (and (d/num? min-val) (< new-value min-val)) min-val + (and (d/num? max-val) (> new-value max-val)) max-val + :else new-value)] + (set-value! new-value))))) + +(mf/defc dropdown-select [{:keys [on-close element-id element-ref options on-select]}] + [:& dropdown {:show true + :on-close on-close} + [:div {:class (stl/css :custom-select-dropdown) + :ref element-ref} + [:ul {:class (stl/css :custom-select-dropdown-list)} + (for [[index item] (d/enumerate options)] + (cond + (= :separator item) [:li {:class (stl/css :separator) + :key (dm/str element-id "-" index)}] + :else (let [{:keys [value label selected?]} item] + [:li + {:key (str element-id "-" index) + :class (stl/css-case :dropdown-element true + :is-selected selected?) + :data-label label + :on-click on-select} + [:span {:class (stl/css :label)} label] + [:span {:class (stl/css :value)} value] + [:span {:class (stl/css :check-icon)} i/tick]])))]]]) + +(mf/defc editable-select + [{:keys [value type options class on-change placeholder on-blur input-class] :as params}] + (let [state* (mf/use-state {:id (uuid/next) + :is-open? false + :current-value value + :current-item nil + :top nil + :left nil + :bottom nil}) + state (deref state*) + is-open? (:is-open? state) + current-value (:current-value state) + element-id (:id state) + + min-val (get params :min) + max-val (get params :max) + + emit-blur? (mf/use-ref nil) + select-wrapper-ref (mf/use-ref) + + toggle-dropdown + (mf/use-fn + (mf/deps state) + #(swap! state* update :is-open? not)) + + close-dropdown + (fn [event] + (dom/stop-propagation event) + (swap! state* assoc :is-open? false)) + + labels-map (->> (map (fn [{:keys [label] :as item}] + [label item]) + options) + (into {})) + + set-value + (fn [value] + (swap! state* assoc :current-value value) + (when on-change (on-change value))) + + select-item + (mf/use-fn + (mf/deps on-change on-blur labels-map) + (fn [event] + (let [label (-> (dom/get-current-target event) + (dom/get-data "label") + (d/read-string) + (str)) + {:keys [value] :as item} (get labels-map label)] + (swap! state* assoc + :current-value value + :current-item item) + (when on-change (on-change item)) + (when on-blur (on-blur))))) + + handle-change-input + (fn [event] + (let [value (-> event dom/get-target dom/get-value) + value (or (d/parse-double value) value)] + (set-value value))) + + on-node-load + (fn [node] + ;; There is a problem when changing the state in this callback that + ;; produces the dropdown to close in the same event + (when node + (timers/schedule + #(when-let [bounds (when node (dom/get-bounding-rect node))] + (let [{window-height :height} (dom/get-window-size) + {:keys [left top height]} bounds + bottom (when (< (- window-height top) 300) (- window-height top)) + top (when (>= (- window-height top) 300) (+ top height))] + (swap! state* + assoc + :left left + :top top + :bottom bottom)))))) + + handle-key-down + (mf/use-fn + (mf/deps set-value is-open?) + (fn [event] + (cond + is-open? (let [up? (kbd/up-arrow? event) + down? (kbd/down-arrow? event)] + (dom/prevent-default event) + (js/console.log "up? down?" up? down?)) + (= type "number") (on-number-input-key-down {:event event + :min-val min-val + :max-val max-val + :set-value! set-value})))) + + handle-focus + (mf/use-fn + (fn [] + (mf/set-ref-val! emit-blur? false))) + + handle-blur + (mf/use-fn + (fn [] + (mf/set-ref-val! emit-blur? true) + (timers/schedule + 200 + (fn [] + (when (and on-blur (mf/ref-val emit-blur?)) (on-blur))))))] + + (mf/use-effect + (mf/deps value current-value) + #(when (not= (str value) current-value) + (reset! state* {:current-value value}))) + + (mf/with-effect [is-open?] + (let [wrapper-node (mf/ref-val select-wrapper-ref) + node (dom/get-element-by-class "checked-element is-selected" wrapper-node) + nodes (dom/get-elements-by-class "checked-element-value" wrapper-node) + closest (fn [a b] (first (sort-by #(mth/abs (- % b)) a))) + closest-value (str (closest options value))] + (when is-open? + (if (some? node) + (dom/scroll-into-view-if-needed! node) + (some->> nodes + (d/seek #(= closest-value (dom/get-inner-text %))) + (dom/scroll-into-view-if-needed!))))) + + (mf/set-ref-val! emit-blur? (not is-open?))) + + + [:div {:class (dm/str class " " (stl/css :editable-select)) + :ref on-node-load} + (if (= type "number") + [:> numeric-input* {:value (or current-value "") + :className input-class + :on-change set-value + :on-focus handle-focus + :on-blur handle-blur + :placeholder placeholder}] + [:input {:value (or current-value "") + :class input-class + :on-change handle-change-input + :on-key-down handle-key-down + :on-focus handle-focus + :on-blur handle-blur + :placeholder placeholder + :type type}]) + + (when (seq options) + [:span {:class (stl/css :dropdown-button) + :on-click toggle-dropdown} + i/arrow]) + + (when (and is-open? (seq options)) + [:& dropdown-select {:on-close close-dropdown + :element-id element-id + :element-ref select-wrapper-ref + :options options + :on-select select-item}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/editable_select.scss b/frontend/src/app/main/ui/workspace/tokens/editable_select.scss new file mode 100644 index 000000000..bfc2a0c9e --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/editable_select.scss @@ -0,0 +1,101 @@ +// 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) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.editable-select { + @extend .asset-element; + margin: 0; + position: relative; + display: flex; + height: calc($s-32 - 2px); // Fixes border being clipped by the input field + width: 100%; + padding: $s-8; + border-radius: $br-8; + cursor: pointer; + + .dropdown-button { + @include flexCenter; + margin-right: -$s-8; + padding-left: 0; + aspect-ratio: 0.8 / 1; + width: auto; + + svg { + @extend .button-icon-small; + transform: rotate(90deg); + stroke: var(--icon-foreground); + } + } + + .custom-select-dropdown-list { + min-width: 150px; + width: 100%; + max-width: 200px; + margin-bottom: 0; + } + + .custom-select-dropdown { + @extend .dropdown-wrapper; + max-height: $s-320; + width: auto; + margin-top: $s-4; + right: 0; + left: unset; + + .separator { + margin: 0; + height: $s-12; + } + + .dropdown-element { + @extend .dropdown-element-base; + color: var(--menu-foreground-color-rest); + padding: 0; + display: flex; + + .label, + .value { + width: fit-content; + } + + .label { + text-transform: unset; + flex: 1; + } + + .value { + text-align: right; + flex: 0.6; + } + + .check-icon { + @include flexCenter; + translate: -$s-4 0; + svg { + @extend .button-icon-small; + visibility: hidden; + stroke: var(--icon-foreground); + } + } + + &.is-selected { + color: var(--menu-foreground-color); + .check-icon svg { + stroke: var(--menu-foreground-color); + visibility: visible; + } + } + &:hover { + background-color: var(--menu-background-color-hover); + color: var(--menu-foreground-color-hover); + .check-icon svg { + stroke: var(--menu-foreground-color-hover); + } + } + } + } +}