diff --git a/frontend/resources/styles/main/partials/sidebar-document-history.scss b/frontend/resources/styles/main/partials/sidebar-document-history.scss index 1be215ef76..8172906603 100644 --- a/frontend/resources/styles/main/partials/sidebar-document-history.scss +++ b/frontend/resources/styles/main/partials/sidebar-document-history.scss @@ -27,7 +27,7 @@ overflow: auto; margin: 0.5rem; - &.selected { + &.transaction { border: 2px solid $color-primary; } } diff --git a/frontend/src/app/main/data/colors.cljs b/frontend/src/app/main/data/colors.cljs index fe54eb6519..f946b1848c 100644 --- a/frontend/src/app/main/data/colors.cljs +++ b/frontend/src/app/main/data/colors.cljs @@ -166,14 +166,14 @@ (assoc-in [:workspace-local :picked-shift?] shift?))))) -(defn change-fill-selected [color id file-id] - (ptk/reify ::change-fill-selected +(defn change-fill [ids color id file-id] + (ptk/reify ::change-fill ptk/WatchEvent (watch [_ state s] - (let [selected (get-in state [:workspace-local :selected]) - objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects]) - children (mapcat #(cph/get-children % objects) selected) - ids (into selected children) + (let [pid (:current-page-id state) + objects (get-in state [:workspace-data :pages-index pid :objects]) + children (mapcat #(cph/get-children % objects) ids) + ids (into ids children) is-text? #(= :text (:type (get objects %))) text-ids (filter is-text? ids) @@ -183,19 +183,22 @@ :fill-color-ref-id id :fill-color-ref-file file-id)) editor (get-in state [:workspace-local :editor]) - converted-attrs {:fill color}] + converted-attrs {:fill color} + + reduce-fn (fn [state id] + (update-in state [:workspace-data :pages-index pid :objects id] update-fn))] + (rx/from (conj (map #(dwt/update-text-attrs {:id % :editor editor :attrs converted-attrs}) text-ids) (dwc/update-shapes shape-ids update-fn))))))) -(defn change-stroke-selected [color id file-id] - (ptk/reify ::change-stroke-selected +(defn change-stroke [ids color id file-id] + (ptk/reify ::change-stroke ptk/WatchEvent (watch [_ state s] - (let [selected (get-in state [:workspace-local :selected]) - objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects]) - children (mapcat #(cph/get-children % objects) selected) - ids (into selected children) + (let [objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects]) + children (mapcat #(cph/get-children % objects) ids) + ids (into ids children) update-fn (fn [s] (cond-> s @@ -211,12 +214,14 @@ (rx/of (dwc/update-shapes ids update-fn)))))) (defn picker-for-selected-shape [] + ;; TODO: replace st/emit! by a subject push and set that in the WatchEvent (let [handle-change-color (fn [color _ shift?] - (st/emit! - (if shift? - (change-stroke-selected color nil nil) - (change-fill-selected color nil nil)) - (md/hide-modal)))] + (let [ids (get-in @st/state [:workspace-local :selected])] + (st/emit! + (if shift? + (change-stroke ids color nil nil) + (change-fill ids color nil nil)) + (md/hide-modal))))] (ptk/reify ::start-picker ptk/UpdateEvent (update [_ state] @@ -224,5 +229,5 @@ (assoc-in [:workspace-local :picking-color?] true) (assoc ::md/modal {:id (random-uuid) :type :colorpicker - :props {:on-change handle-change-color} + :props {:on-close handle-change-color} :allow-click-outside true})))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 599be081f2..b70766f0d5 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1456,6 +1456,7 @@ :option :background :value color}] [{:type :set-option + :page-id page-id :option :background :value ccolor}] {:commit-local? true})))))) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 4fd3c4a364..f42eda699f 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -265,14 +265,49 @@ (update :workspace-undo dissoc :undo-index) (update-in [:workspace-undo :items] (fn [queue] (into [] (take (inc index) queue)))))))) +(defn- add-undo-entry [state entry] + (if entry + (let [state (update-in state [:workspace-undo :items] (fnil conj-undo-entry []) entry)] + (assoc-in state [:workspace-undo :index] (dec (count (get-in state [:workspace-undo :items]))))) + state)) + +(defn- accumulate-undo-entry [state {:keys [undo-changes redo-changes]}] + (-> state + (update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %)) + (update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes)))) + (defn- append-undo [entry] (us/verify ::undo-entry entry) (ptk/reify ::append-undo ptk/UpdateEvent (update [_ state] - (let [state (update-in state [:workspace-undo :items] (fnil conj-undo-entry []) entry)] - (assoc-in state [:workspace-undo :index] (dec (count (get-in state [:workspace-undo :items])))))))) + (if (get-in state [:workspace-undo :transaction]) + (accumulate-undo-entry state entry) + (add-undo-entry state entry))))) + +(def start-undo-transaction + (ptk/reify ::start-undo-transaction + ptk/UpdateEvent + (update [_ state] + ;; We commit the old transaction before starting the new one + (-> state + (add-undo-entry (get-in state [:workspace-undo :transaction])) + (assoc-in [:workspace-undo :transaction] {:undo-changes [] + :redo-changes []}))))) +(def discard-undo-transaction + (ptk/reify ::discard-undo-transaction + ptk/UpdateEvent + (update [_ state] + (update state :workspace-undo dissoc :transaction)))) + +(def commit-undo-transaction + (ptk/reify ::commit-undo-transaction + ptk/UpdateEvent + (update [_ state] + (-> state + (add-undo-entry (get-in state [:workspace-undo :transaction])) + (update :workspace-undo dissoc :transaction))))) (def undo (ptk/reify ::undo diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index d2e6f99535..2774c99919 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -427,5 +427,7 @@ uchanges (conj (dwc/generate-changes page-id objects2 objects0) regchg) ] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) - (dwc/rehash-shape-frame-relationship ids)))))) + (rx/of dwc/start-undo-transaction + (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (dwc/rehash-shape-frame-relationship ids) + dwc/commit-undo-transaction))))) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index d66c04373b..a6190bdbc4 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -369,30 +369,41 @@ (t locale "workspace.libraries.colors.save-color")]])]) ) +(defn calculate-position + "Calculates the style properties for the given coordinates and position" + [position x y] + (cond + (or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"} + (= position :left) {:left (str (- x 270) "px") :top (str (- y 50) "px")} + :else {:left (str (+ x 24) "px") :top (str (- y 50) "px")})) + + (mf/defc colorpicker-modal {::mf/register modal/components ::mf/register-as :colorpicker} - [{:keys [x y default value opacity page on-change disable-opacity position on-accept] :as props}] - (let [position (or position :left) - style (cond - (or (nil? x) (nil? y)) - {:left "auto" - :right "16rem" - :top "4rem"} + [{:keys [x y default value opacity page on-change on-close disable-opacity position on-accept] :as props}] + (let [dirty? (mf/use-var false) + last-change (mf/use-var nil) + position (or position :left) + style (calculate-position position x y) - (= position :left) - {:left (str (- x 270) "px") - :top (str (- y 50) "px")} + handle-change (fn [new-value new-opacity op1 op2] + (when (or (not= new-value value) (not= new-opacity opacity)) + (reset! dirty? true)) + (reset! last-change [new-value new-opacity op1 op2]) + (on-change new-value new-opacity op1 op2))] + + (mf/use-effect + (fn [] + #(when (and @dirty? on-close) + (when-let [[value opacity op1 op2] @last-change] + (on-close value opacity op1 op2))))) - :else - {:left (str (+ x 24) "px") - :top (str (- y 50) "px")}) - ] [:div.colorpicker-tooltip {:style (clj->js style)} [:& colorpicker {:value (or value default) :opacity (or opacity 1) - :on-change on-change + :on-change handle-change :on-accept on-accept :disable-opacity disable-opacity}]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index b571173b33..2186fd01a7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -15,6 +15,7 @@ [rumext.alpha :as mf] [app.common.data :as d] [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.store :as st] @@ -273,9 +274,12 @@ (fn [] (let [lkey1 (events/listen js/document EventType.CLICK on-click) lkey2 (events/listen js/document EventType.KEYUP on-key-up)] - (st/emit! (dwt/assign-editor id editor)) + (st/emit! (dwt/assign-editor id editor) + dwc/start-undo-transaction) + #(do - (st/emit! (dwt/assign-editor id nil)) + (st/emit! (dwt/assign-editor id nil) + dwc/commit-undo-transaction) (events/unlistenByKey lkey1) (events/unlistenByKey lkey2)))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 11e83dd66a..7de10fcf53 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -136,12 +136,12 @@ :top nil :left nil :editing rename?}) - click-color (fn [event] - (if (kbd/shift? event) - (st/emit! (dc/change-stroke-selected (:value color) id (if local? nil file-id))) - (st/emit! (dc/change-fill-selected (:value color) id (if local? nil file-id))))) + (let [ids (get-in @st/state [:workspace-local :selected])] + (if (kbd/shift? event) + (st/emit! (dc/change-stroke ids (:value color) id (if local? nil file-id))) + (st/emit! (dc/change-fill ids (:value color) id (if local? nil file-id)))))) rename-color (fn [name] diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.cljs b/frontend/src/app/main/ui/workspace/sidebar/history.cljs index 1cfcca9ac0..c3797afae0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/history.cljs @@ -27,24 +27,40 @@ (def workspace-undo (l/derived :workspace-undo st/state)) +(mf/defc undo-entry [{:keys [index entry objects is-transaction?] :or {is-transaction? false}}] + (let [{:keys [redo-changes]} entry] + [:li.undo-entry {:class (when is-transaction? "transaction")} + (for [[idx-change {:keys [type id operations]}] (map-indexed vector redo-changes)] + [:div.undo-entry-change + [:div.undo-entry-change-data (when type (str type)) " " (when id (str (get-in objects [id :name] (subs (str id) 0 8))))] + (when operations + [:div.undo-entry-change-data (str/join ", " (map (comp name :attr) operations))])])])) + (mf/defc history-toolbox [] (let [locale (mf/deref i18n/locale) - {:keys [items index]} (mf/deref workspace-undo) + {:keys [items index transaction]} (mf/deref workspace-undo) objects (mf/deref refs/workspace-page-objects)] [:div.history-toolbox [:div.history-toolbox-title "History"] - (when (> (count items) 0) - [:ul.undo-history - [:* - (when (or (nil? index) (>= index (count items))) [:hr.separator]) - (for [[idx-entry {:keys [redo-changes]}] (->> items (map-indexed vector) reverse)] - [:* - (when (= index idx-entry) [:hr.separator {:data-index index}]) - [:li.undo-entry {:key (str "entry-" idx-entry)} - (for [[idx-change {:keys [type id operations]}] (map-indexed vector redo-changes)] - [:div.undo-entry-change - [:div.undo-entry-change-data (when type (str type)) " " (when id (str (get-in objects [id :name] (subs (str id) 0 8))))] - (when operations - [:div.undo-entry-change-data (str/join ", " (map (comp name :attr) operations))])])]]) - (when (= index -1) [:hr.separator])]])])) + [:ul.undo-history + [:* + (when (and + (> (count items) 0) + (or (nil? index) + (>= index (count items)))) + [:hr.separator]) + + (when transaction + [:& undo-entry {:key (str "transaction") + :objects objects + :is-transaction? true + :entry transaction}]) + + (for [[idx-entry entry] (->> items (map-indexed vector) reverse)] + [:* + (when (= index idx-entry) [:hr.separator {:data-index index}]) + [:& undo-entry {:key (str "entry-" idx-entry) + :objects objects + :entry entry}]]) + (when (= index -1) [:hr.separator])]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs index 6f1ce8a575..98f6c2bd6b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs @@ -13,6 +13,7 @@ [app.common.pages :as cp] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.texts :as dwt] + [app.main.data.colors :as dc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.icons :as i] @@ -37,14 +38,11 @@ (= (:fill-opacity new-values) (:fill-opacity old-values))))) + (mf/defc fill-menu {::mf/wrap [#(mf/memo' % fill-menu-props-equals?)]} [{:keys [ids type values editor] :as props}] (let [locale (mf/deref i18n/locale) - shapes (deref (refs/objects-by-id ids)) - is-text? #(= (:type %) :text) - text-ids (map :id (filter is-text? shapes)) - other-ids (map :id (filter (comp not is-text?) shapes)) show? (not (nil? (:fill-color values))) label (case type @@ -61,51 +59,32 @@ (mf/use-callback (mf/deps ids) (fn [event] - (when-not (empty? other-ids) - (st/emit! (dwc/update-shapes other-ids #(assoc % :fill-color cp/default-color)))) - - (when-not (empty? text-ids) - (run! #(st/emit! (dwt/update-text-attrs - {:id % - :editor editor - :attrs {:fill cp/default-color}})) - text-ids)))) + (st/emit! (dc/change-fill ids cp/default-color nil nil)))) on-delete (mf/use-callback (mf/deps ids) (fn [event] - (when-not (empty? other-ids) - (st/emit! (dwc/update-shapes other-ids #(dissoc % :fill-color)))) - - (when-not (empty? text-ids) - (run! #(st/emit! (dwt/update-text-attrs - {:id % - :editor editor - :attrs {:fill nil}})) - text-ids)))) + (st/emit! (dc/change-fill ids nil nil nil)))) on-change (mf/use-callback (mf/deps ids) (fn [value opacity id file-id] - (let [change #(cond-> % - value (assoc :fill-color value - :fill-color-ref-id id - :fill-color-ref-file file-id) - opacity (assoc :fill-opacity opacity)) - converted-attrs (cond-> {} - value (assoc :fill value) - opacity (assoc :opacity opacity))] + (st/emit! (dc/change-fill ids value id file-id)))) + + on-open-picker + (mf/use-callback + (mf/deps ids) + (fn [value opacity id file-id] + (st/emit! dwc/start-undo-transaction))) + + on-close-picker + (mf/use-callback + (mf/deps ids) + (fn [value opacity id file-id] + (st/emit! dwc/commit-undo-transaction)))] - (when-not (empty? other-ids) - (st/emit! (dwc/update-shapes ids change))) - (when-not (empty? text-ids) - (run! #(st/emit! (dwt/update-text-attrs - {:id % - :editor editor - :attrs converted-attrs})) - text-ids)))))] (if show? [:div.element-set [:div.element-set-title @@ -113,10 +92,14 @@ [:div.add-page {:on-click on-delete} i/minus]] [:div.element-set-content - [:& color-row {:color color :on-change on-change}]]] + [:& color-row {:color color + :on-change on-change + :on-open on-open-picker + :on-close on-close-picker}]]] [:div.element-set [:div.element-set-title [:span label] [:div.add-page {:on-click on-add} i/close]]]))) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index 10cce3c661..04e6fe5e9e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -15,6 +15,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] [app.util.i18n :as i18n :refer [t]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]])) @@ -28,12 +29,25 @@ [{:keys [page-id] :as props}] (let [locale (i18n/use-locale) options (mf/deref refs/workspace-page-options) - handle-change-color (use-change-color page-id)] + handle-change-color (use-change-color page-id) + + on-open + (mf/use-callback + (mf/deps page-id) + #(st/emit! dwc/start-undo-transaction)) + + on-close + (mf/use-callback + (mf/deps page-id) + #(st/emit! dwc/commit-undo-transaction))] + [:div.element-set [:div.element-set-title (t locale "workspace.options.canvas-background")] [:div.element-set-content [:& color-row {:disable-opacity true :color {:value (get options :background "#E8E9EA") :opacity 1} - :on-change handle-change-color}]]])) + :on-change handle-change-color + :on-open on-open + :on-close on-close}]]])) 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 b41b9ae483..f039df99ff 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 @@ -19,16 +19,18 @@ [app.main.refs :as refs])) (defn color-picker-callback - [color handle-change-color disable-opacity] + [color handle-change-color handle-open handle-close disable-opacity] (fn [event] (let [x (.-clientX event) y (.-clientY event) props {:x x :y y :on-change handle-change-color + :on-close handle-close :value (:value color) :opacity (:opacity color) :disable-opacity disable-opacity}] + (handle-open) (modal/show! :colorpicker props)))) (defn value-to-background [value] @@ -57,7 +59,7 @@ (if (= v :multiple) nil v)) (mf/defc color-row - [{:keys [color on-change disable-opacity]}] + [{:keys [color on-change on-open on-close disable-opacity]}] (let [;; file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) @@ -91,6 +93,11 @@ (reset! state {:value new-value :opacity new-opacity}) (when on-change (on-change new-value new-opacity id file-id))) + handle-open (fn [] (when on-open (on-open))) + + 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) @@ -119,7 +126,7 @@ [:span.color-th {:class (when (:id color) "color-name") :style {:background-color (-> value value-to-background)} - :on-click (color-picker-callback @state handle-pick-color disable-opacity)} + :on-click (color-picker-callback @state handle-pick-color handle-open handle-close disable-opacity)} (when (= value :multiple) "?")] (if (:id color) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs index 4a54621e1c..29fb7b558b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs @@ -119,7 +119,19 @@ on-del-stroke (fn [event] - (st/emit! (dwc/update-shapes ids #(assoc % :stroke-style :none))))] + (st/emit! (dwc/update-shapes ids #(assoc % :stroke-style :none)))) + + on-open-picker + (mf/use-callback + (mf/deps ids) + (fn [value opacity id file-id] + (st/emit! dwc/start-undo-transaction))) + + on-close-picker + (mf/use-callback + (mf/deps ids) + (fn [value opacity id file-id] + (st/emit! dwc/commit-undo-transaction)))] (if show-options [:div.element-set @@ -130,7 +142,9 @@ [:div.element-set-content ;; Stroke Color [:& color-row {:color current-stroke-color - :on-change handle-change-stroke-color}] + :on-change handle-change-stroke-color + :on-open on-open-picker + :on-close on-close-picker}] ;; Stroke Width, Alignment & Style [:div.row-flex