mirror of
https://github.com/penpot/penpot.git
synced 2025-06-10 04:02:12 +02:00
🎉 Allow change fill color of multiple objects
This commit is contained in:
parent
ef0330502b
commit
be9780be55
17 changed files with 214 additions and 130 deletions
|
@ -119,6 +119,14 @@
|
|||
(def selected-shapes
|
||||
(l/derived :selected workspace-local))
|
||||
|
||||
(def selected-objects
|
||||
(letfn [(selector [state]
|
||||
(let [selected (get-in state [:workspace-local :selected])
|
||||
page-id (get-in state [:workspace-page :id])
|
||||
objects (get-in state [:workspace-data page-id :objects])]
|
||||
(->> selected (map #(get objects %)))))]
|
||||
(l/derived selector st/state)))
|
||||
|
||||
(def selected-shapes-with-children
|
||||
(letfn [(selector [state]
|
||||
(let [selected (get-in state [:workspace-local :selected])
|
||||
|
|
|
@ -26,11 +26,12 @@
|
|||
[uxbox.main.ui.workspace.sidebar.options.group :as group]
|
||||
[uxbox.main.ui.workspace.sidebar.options.icon :as icon]
|
||||
[uxbox.main.ui.workspace.sidebar.options.image :as image]
|
||||
[uxbox.main.ui.workspace.sidebar.options.interactions :refer [interactions-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.text :as text]
|
||||
[uxbox.main.ui.workspace.sidebar.options.page :as page]
|
||||
[uxbox.main.ui.workspace.sidebar.options.multiple :as multiple]
|
||||
[uxbox.main.ui.workspace.sidebar.options.interactions :refer [interactions-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.path :as path]
|
||||
[uxbox.main.ui.workspace.sidebar.options.rect :as rect]
|
||||
[uxbox.main.ui.workspace.sidebar.options.text :as text]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.http :as http]
|
||||
[uxbox.util.i18n :as i18n :refer [tr t]]
|
||||
|
@ -106,7 +107,7 @@
|
|||
|
||||
(mf/defc options-content
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [section selected shape page] :as props}]
|
||||
[{:keys [section shapes page] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)]
|
||||
[:div.tool-window
|
||||
[:div.tool-window-content
|
||||
|
@ -116,26 +117,22 @@
|
|||
:title (t locale "workspace.options.design")}
|
||||
[:div.element-options
|
||||
[:& align-options]
|
||||
(if (= (count selected) 1)
|
||||
[:& shape-options {:shape shape :page page}]
|
||||
[:& page/options {:page page}])]]
|
||||
(case (count shapes)
|
||||
0 [:& page/options {:page page}]
|
||||
1 [:& shape-options {:shape (first shapes)}]
|
||||
[:& multiple/options {:shapes shapes}])]]
|
||||
|
||||
[:& tab-element {:id :prototype
|
||||
:title (t locale "workspace.options.prototype")}
|
||||
[:div.element-options
|
||||
[:& interactions-menu {:shape shape}]]]]]]))
|
||||
[:& interactions-menu {:shape (first shapes)}]]]]]]))
|
||||
|
||||
|
||||
(mf/defc options-toolbox
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [page local] :as props}]
|
||||
(let [selected (:selected local)
|
||||
section (:options-mode local)
|
||||
shape-id (first selected)
|
||||
page-id (:id page)
|
||||
shape-iref (-> (mf/deps shape-id page-id)
|
||||
(mf/use-memo #(refs/object-by-id shape-id)))
|
||||
shape (mf/deref shape-iref)]
|
||||
(let [section (:options-mode local)
|
||||
shapes (mf/deref refs/selected-objects)]
|
||||
[:& options-content {:selected selected
|
||||
:shape shape
|
||||
:page page
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
(ns uxbox.main.ui.workspace.sidebar.options.circle
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.measures :refer [measures-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]))
|
||||
|
||||
|
@ -19,5 +19,5 @@
|
|||
[:div
|
||||
[:& measures-menu {:shape shape
|
||||
:options #{:size :position :rotation}}]
|
||||
[:& fill-menu {:shape shape}]
|
||||
[:& fill-menu {:ids [(:id shape)] :values (select-keys shape fill-attrs)}]
|
||||
[:& stroke-menu {:shape shape}]])
|
||||
|
|
|
@ -10,35 +10,39 @@
|
|||
(ns uxbox.main.ui.workspace.sidebar.options.fill
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.data.workspace :as udw]
|
||||
[uxbox.main.data.workspace.common :as dwc]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
|
||||
[uxbox.util.object :as obj]
|
||||
[uxbox.util.i18n :as i18n :refer [tr t]]))
|
||||
|
||||
(def fill-attrs [:fill-color :fill-opacity])
|
||||
|
||||
(defn- fill-menu-memo-equals?
|
||||
[np op]
|
||||
(let [new-shape (obj/get np "shape")
|
||||
old-shape (obj/get op "shape")]
|
||||
(and (= (:id new-shape)
|
||||
(:id old-shape))
|
||||
(identical? (:fill-color new-shape)
|
||||
(:fill-color old-shape))
|
||||
(identical? (:fill-opacity new-shape)
|
||||
(:fill-opacity old-shape)))))
|
||||
(let [new-ids (obj/get np "ids")
|
||||
old-ids (obj/get op "ids")
|
||||
new-values (obj/get np "values")
|
||||
old-values (obj/get op "values")]
|
||||
(and (= new-ids old-ids)
|
||||
(identical? (:fill-color new-values)
|
||||
(:fill-color old-values))
|
||||
(identical? (:fill-opacity new-values)
|
||||
(:fill-opacity old-values)))))
|
||||
|
||||
(mf/defc fill-menu
|
||||
{::mf/wrap [#(mf/memo' % fill-menu-memo-equals?)]}
|
||||
[{:keys [shape] :as props}]
|
||||
[{:keys [ids values] :as props}]
|
||||
(let [locale (i18n/use-locale)
|
||||
color {:value (:fill-color shape)
|
||||
:opacity (:fill-opacity shape)}
|
||||
color {:value (:fill-color values)
|
||||
:opacity (:fill-opacity values)}
|
||||
handle-change-color (fn [value opacity]
|
||||
(let [change {:fill-color value
|
||||
:fill-opacity opacity}]
|
||||
(st/emit! (udw/update-shape (:id shape) change))))]
|
||||
(let [change #(cond-> %
|
||||
value (assoc :fill-color value)
|
||||
opacity (assoc :fill-opacity opacity))]
|
||||
(st/emit! (dwc/update-shapes ids change))))]
|
||||
[:div.element-set
|
||||
[:div.element-set-title (t locale "workspace.options.fill")]
|
||||
[:div.element-set-content
|
||||
[:& color-row {:value color
|
||||
[:& color-row {:color color
|
||||
:on-change handle-change-color}]]]))
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[uxbox.main.data.workspace :as udw]
|
||||
[uxbox.main.ui.icons :as i]
|
||||
[uxbox.main.ui.components.dropdown :refer [dropdown]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.frame-grid :refer [frame-grid]]))
|
||||
|
||||
|
@ -202,7 +202,7 @@
|
|||
[{:keys [shape] :as props}]
|
||||
[:div
|
||||
[:& measures-menu {:shape shape}]
|
||||
[:& fill-menu {:shape shape}]
|
||||
[:& fill-menu {:ids [(:id shape)] :values (select-keys shape fill-attrs)}]
|
||||
[:& stroke-menu {:shape shape}]
|
||||
[:& frame-grid {:shape shape}]])
|
||||
|
||||
|
|
|
@ -218,7 +218,7 @@
|
|||
:value (:margin params)
|
||||
:on-change (handle-change :params :margin)}]])
|
||||
|
||||
[:& color-row {:value (:color params)
|
||||
[:& color-row {:color (:color params)
|
||||
:on-change handle-change-color}]
|
||||
[:div.row-flex
|
||||
[:button.btn-options {:disabled is-default
|
||||
|
|
|
@ -11,12 +11,12 @@
|
|||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.ui.workspace.sidebar.options.measures :refer [measures-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]))
|
||||
|
||||
(mf/defc options
|
||||
[{:keys [shape] :as props}]
|
||||
[:div
|
||||
[:& measures-menu {:shape shape}]
|
||||
[:& fill-menu {:shape shape}]
|
||||
[:& fill-menu {:ids [(:id shape)] :values (select-keys shape fill-attrs)}]
|
||||
[:& stroke-menu {:shape shape}]])
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns uxbox.main.ui.workspace.sidebar.options.multiple
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.measures :refer [measures-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]))
|
||||
|
||||
(defn- get-multi
|
||||
[shapes attrs]
|
||||
(let [combine-value #(if (= %1 %2) %1 :multiple)
|
||||
|
||||
combine-values (fn [attrs shape values]
|
||||
(map #(combine-value (get shape %) (get values %)) attrs))
|
||||
|
||||
reducer (fn [result shape]
|
||||
(zipmap attrs (combine-values attrs shape result)))]
|
||||
|
||||
(reduce reducer (select-keys (first shapes) attrs) (rest shapes))))
|
||||
|
||||
|
||||
(mf/defc options
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [shapes] :as props}]
|
||||
(let [ids (map :id shapes)
|
||||
fill-values (get-multi shapes fill-attrs)]
|
||||
[:div
|
||||
[:& fill-menu {:ids ids :values fill-values}]]))
|
||||
|
|
@ -37,8 +37,7 @@
|
|||
[:div.element-set-title (t locale "workspace.options.canvas-background")]
|
||||
[:div.element-set-content
|
||||
[:& color-row {:disable-opacity true
|
||||
:value {:value (get options :background "#E8E9EA")
|
||||
:color {:value (get options :background "#E8E9EA")
|
||||
:opacity 1}
|
||||
:on-change handle-change-color}]]])
|
||||
)
|
||||
:on-change handle-change-color}]]]))
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.common.data :as d]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]))
|
||||
|
||||
(mf/defc options
|
||||
[{:keys [shape] :as props}]
|
||||
[:div
|
||||
[:& fill-menu {:shape shape}]
|
||||
[:& fill-menu {:ids [(:id shape)] :values (select-keys shape fill-attrs)}]
|
||||
[:& stroke-menu {:shape shape}]])
|
||||
|
|
|
@ -10,14 +10,15 @@
|
|||
(ns uxbox.main.ui.workspace.sidebar.options.rect
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.measures :refer [measures-menu]]
|
||||
[uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]))
|
||||
|
||||
(mf/defc options
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [shape] :as props}]
|
||||
[:div
|
||||
[:& measures-menu {:shape shape}]
|
||||
[:& fill-menu {:shape shape}]
|
||||
[:& stroke-menu {:shape shape}]])
|
||||
(let [fill-values (select-keys shape fill-attrs)]
|
||||
[:div
|
||||
[:& measures-menu {:shape shape}]
|
||||
[:& fill-menu {:ids [(:id shape)] :values fill-values}]
|
||||
[:& stroke-menu {:shape shape}]]))
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
[rumext.alpha :as mf]
|
||||
[uxbox.common.math :as math]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.data :refer [classnames]]
|
||||
[uxbox.util.i18n :as i18n :refer [tr]]
|
||||
[uxbox.main.ui.modal :as modal]
|
||||
[uxbox.main.ui.workspace.colorpicker :refer [colorpicker-modal]]
|
||||
[uxbox.common.data :as d]))
|
||||
|
@ -29,76 +31,99 @@
|
|||
:disable-opacity disable-opacity}]
|
||||
(modal/show! colorpicker-modal props))))
|
||||
|
||||
(defn value-to-background [value]
|
||||
(if (= value :multiple) "transparent" value))
|
||||
|
||||
(defn remove-hash [value]
|
||||
(if (= value :multiple) "" (subs value 1)))
|
||||
|
||||
(defn append-hash [value]
|
||||
(str "#" value))
|
||||
|
||||
(defn opacity->string [opacity]
|
||||
(if (and opacity (not= opacity ""))
|
||||
(if (= opacity :multiple)
|
||||
""
|
||||
(str (-> opacity
|
||||
(d/coalesce 1)
|
||||
(* 100)
|
||||
(math/round)))
|
||||
""))
|
||||
(math/round)))))
|
||||
|
||||
(defn string->opacity [opacity-str]
|
||||
(when (and opacity-str (not= "" opacity-str))
|
||||
(-> opacity-str
|
||||
(d/parse-integer 1)
|
||||
(/ 100))))
|
||||
(-> opacity-str
|
||||
(d/parse-integer 1)
|
||||
(/ 100)))
|
||||
|
||||
(mf/defc color-row [{:keys [value on-change disable-opacity]}]
|
||||
(let [default-value {:value "#000000" :opacity 1}
|
||||
(defn remove-multiple [v]
|
||||
(if (= v :multiple) nil v))
|
||||
|
||||
parse-value (fn [value]
|
||||
(-> (merge default-value value)
|
||||
(mf/defc color-row [{:keys [color on-change disable-opacity]}]
|
||||
(let [default-color {:value "#000000" :opacity 1}
|
||||
|
||||
parse-color (fn [color]
|
||||
(-> (merge default-color color)
|
||||
(update :value #(or % "#000000"))
|
||||
(update :opacity #(or % 1))))
|
||||
|
||||
state (mf/use-state (parse-value value))
|
||||
state (mf/use-state (parse-color color))
|
||||
|
||||
change-color (fn [new-value]
|
||||
(let [{:keys [value opacity]} @state]
|
||||
(swap! state assoc :value new-value)
|
||||
(when on-change (on-change new-value opacity))))
|
||||
value (:value @state)
|
||||
opacity (:opacity @state)
|
||||
|
||||
change-value (fn [new-value]
|
||||
(swap! state assoc :value new-value)
|
||||
(when on-change (on-change new-value (remove-multiple opacity))))
|
||||
|
||||
change-opacity (fn [new-opacity]
|
||||
(let [{:keys [value opacity]} @state]
|
||||
(swap! state assoc :opacity new-opacity)
|
||||
(when (and new-opacity on-change) (on-change value new-opacity))))
|
||||
(swap! state assoc :opacity new-opacity)
|
||||
(when on-change (on-change (remove-multiple value) new-opacity)))
|
||||
|
||||
handle-pick-color (fn [color opacity]
|
||||
(reset! state {:value color :opacity opacity})
|
||||
(when on-change (on-change color opacity)))
|
||||
handle-pick-color (fn [new-value new-opacity]
|
||||
(reset! state {:value new-value :opacity new-opacity})
|
||||
(when on-change (on-change new-value new-opacity)))
|
||||
|
||||
handle-value-change (fn [event]
|
||||
(let [target (dom/get-target event)]
|
||||
(when (dom/valid? target)
|
||||
(-> target
|
||||
dom/get-value
|
||||
append-hash
|
||||
change-value))))
|
||||
|
||||
handle-input-color-change (fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)]
|
||||
(when (dom/valid? target)
|
||||
(change-color (str "#" value)))))
|
||||
handle-opacity-change (fn [event]
|
||||
(-> event
|
||||
dom/get-target
|
||||
dom/get-value
|
||||
string->opacity
|
||||
change-opacity))
|
||||
select-all #(-> % (dom/get-target) (.select))]
|
||||
(let [target (dom/get-target event)]
|
||||
(when (dom/valid? target)
|
||||
(-> target
|
||||
dom/get-value
|
||||
string->opacity
|
||||
change-opacity))))
|
||||
|
||||
select-all (fn [event]
|
||||
(dom/select-text! (dom/get-target event)))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps value)
|
||||
#(reset! state (parse-value value)))
|
||||
(mf/deps color)
|
||||
#(reset! state (parse-color color)))
|
||||
;; is this necessary?
|
||||
|
||||
[:div.row-flex.color-data
|
||||
[:span.color-th
|
||||
{:style {:background-color (-> @state :value)}
|
||||
:on-click (color-picker-callback @state handle-pick-color disable-opacity)}]
|
||||
{:style {:background-color (-> value value-to-background)}
|
||||
:on-click (color-picker-callback @state handle-pick-color disable-opacity)}
|
||||
(when (= value :multiple) "?")]
|
||||
|
||||
[:div.color-info
|
||||
[:input {:value (-> @state :value (subs 1))
|
||||
[:input {:value (-> value remove-hash)
|
||||
:pattern "^[0-9a-fA-F]{0,6}$"
|
||||
:placeholder (tr "settings.multiple")
|
||||
:on-click select-all
|
||||
:on-change handle-input-color-change}]]
|
||||
:on-change handle-value-change}]]
|
||||
|
||||
(when (not disable-opacity)
|
||||
[:div.input-element.percentail
|
||||
[:div.input-element
|
||||
{:class (classnames :percentail (not= opacity :multiple))}
|
||||
[:input.input-text {:type "number"
|
||||
:value (-> @state :opacity opacity->string)
|
||||
:value (-> opacity opacity->string)
|
||||
:placeholder (tr "settings.multiple")
|
||||
:on-click select-all
|
||||
:on-change handle-opacity-change
|
||||
:min "0"
|
||||
|
@ -107,7 +132,7 @@
|
|||
#_[:input.slidebar {:type "range"
|
||||
:min "0"
|
||||
:max "100"
|
||||
:value (-> @state :opacity opacity->string)
|
||||
:value (-> opacity opacity->string)
|
||||
:step "1"
|
||||
:on-change handle-opacity-change}]]))
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
|
||||
[:div.element-set-content
|
||||
;; Stroke Color
|
||||
[:& color-row {:value current-stroke-color
|
||||
[:& color-row {:color current-stroke-color
|
||||
:on-change handle-change-stroke-color}]
|
||||
|
||||
;; Stroke Width, Alignment & Style
|
||||
|
|
|
@ -199,9 +199,8 @@
|
|||
:attrs {:fill value
|
||||
:opacity opacity}})))]
|
||||
|
||||
[:& color-row {:value current-color
|
||||
:on-change handle-change-color}]
|
||||
))
|
||||
[:& color-row {:color current-color
|
||||
:on-change handle-change-color}]))
|
||||
|
||||
(mf/defc spacing-options
|
||||
[{:keys [editor shape locale] :as props}]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue