penpot/frontend/src/app/main/data/workspace/variants.cljs

439 lines
17 KiB
Clojure

;; 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.data.workspace.variants
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.geom.point :as gpt]
[app.common.logic.variant-properties :as clvp]
[app.common.logic.variants :as clv]
[app.common.types.component :as ctc]
[app.common.types.components-list :as ctkl]
[app.common.types.shape.layout :as ctsl]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.colors :as cl]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.main.features :as features]
[app.util.dom :as dom]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn update-properties-names-and-values
"Compares the previous properties with the updated ones and executes the correspondent action
for each one depending on if it needs to be removed, updated or added"
[component-id variant-id previous-properties updated-properties]
(ptk/reify ::update-properties-names-and-values
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :shape-for-rename))
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
data (dsh/lookup-file-data state)
objects (-> (dsh/get-page data page-id)
(get :objects))
properties-to-remove (ctv/find-properties-to-remove previous-properties updated-properties)
properties-to-add (ctv/find-properties-to-add previous-properties updated-properties)
properties-to-update (ctv/find-properties-to-update previous-properties updated-properties)
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/with-library-data data))
changes (reduce
(fn [changes {:keys [name]}]
(-> changes
(clvp/generate-update-property-value component-id (ctv/find-index-for-property-name previous-properties name) "")))
changes
properties-to-remove)
changes (reduce
(fn [changes {:keys [name value]}]
(-> changes
(clvp/generate-update-property-value component-id (ctv/find-index-for-property-name previous-properties name) value)))
changes
properties-to-update)
changes (reduce
(fn [changes [idx {:keys [name value]}]]
(-> changes
(clvp/generate-add-new-property variant-id {:property-name name})
(clvp/generate-update-property-value component-id (+ idx (count previous-properties)) value)))
changes
(map-indexed vector properties-to-add))
undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dwu/commit-undo-transaction undo-id))))))
(defn update-property-name
"Update the variant property name on the position pos
in all the components with this variant-id"
[variant-id pos new-name]
(ptk/reify ::update-property-name
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
data (dsh/lookup-file-data state)
objects (-> (dsh/get-page data page-id)
(get :objects))
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/with-library-data data)
(clvp/generate-update-property-name variant-id pos new-name))
undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dwu/commit-undo-transaction undo-id))))))
(defn update-property-value
"Updates the variant property value on the position pos in a component"
[component-id pos value]
(ptk/reify ::update-property-value
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
data (dsh/lookup-file-data state)
objects (-> (dsh/get-page data page-id)
(get :objects))
changes (-> (pcb/empty-changes it page-id)
(pcb/with-library-data data)
(pcb/with-objects objects)
(clvp/generate-update-property-value component-id pos value))
undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dwu/commit-undo-transaction undo-id))))))
(defn remove-property
"Remove the variant property on the position pos
in all the components with this variant-id"
[variant-id pos]
(ptk/reify ::remove-property
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
data (dsh/lookup-file-data state)
objects (-> (dsh/get-page data page-id)
(get :objects))
changes (-> (pcb/empty-changes it page-id)
(pcb/with-library-data data)
(pcb/with-objects objects)
(clvp/generate-remove-property variant-id pos))
undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dwu/commit-undo-transaction undo-id))))))
(defn add-new-property
"Add a new variant property to all the components with this variant-id"
[variant-id & [options]]
(ptk/reify ::add-new-property
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
data (dsh/lookup-file-data state)
objects (-> (dsh/get-page data page-id)
(get :objects))
changes (-> (pcb/empty-changes it page-id)
(pcb/with-library-data data)
(pcb/with-objects objects)
(clvp/generate-add-new-property variant-id options))
undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dwu/commit-undo-transaction undo-id))))))
(defn- set-variant-id
"Sets the variant-id on a component"
[component-id variant-id]
(ptk/reify ::set-variant-id
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it page-id)
(pcb/with-library-data data)
(pcb/update-component component-id #(assoc % :variant-id variant-id)))
undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dwu/commit-undo-transaction undo-id))))))
(defn- focus-property
[shape-id prop-num]
(ptk/reify ::focus-property
ptk/EffectEvent
(effect [_ _ _]
(dom/focus! (dom/get-element (str "variant-prop-" shape-id prop-num))))))
(defn- resposition-and-resize-variant
"Resize the variant container, and move the shape (that is a variant) to the right"
[shape-id]
(ptk/reify ::resposition-and-resize-variant
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
shape (get objects shape-id)
container (get objects (:parent-id shape))
width (+ (:width container) (:width shape) 20) ;; 20 is the default gap for variants
x (- width (+ (:width shape) 30))] ;; 30 is the default margin for variants
(rx/of
(dwt/update-dimensions [(:parent-id shape)] :width width)
(dwt/update-position shape-id
{:x x}
{:absolute? false}))))))
(defn add-new-variant
"Create a new variant and add it to the variant-container"
[shape-id]
(ptk/reify ::add-new-variant
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
data (dsh/lookup-file-data state)
objects (-> (dsh/get-page data page-id)
(get :objects))
shape (get objects shape-id)
shape (if (ctc/is-variant-container? shape)
(get objects (last (:shapes shape)))
shape)
component-id (:component-id shape)
component (ctkl/get-component data component-id)
container-id (:parent-id shape)
variant-container (get objects container-id)
has-layout? (ctsl/any-layout? variant-container)
new-component-id (uuid/next)
new-shape-id (uuid/next)
prop-num (dec (count (:variant-properties component)))
changes (-> (pcb/empty-changes it page-id)
(pcb/with-library-data data)
(pcb/with-objects objects)
(pcb/with-page-id page-id)
(clv/generate-add-new-variant shape (:variant-id component) new-component-id new-shape-id prop-num))
undo-id (js/Symbol)]
(rx/concat
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(when-not has-layout?
(resposition-and-resize-variant new-shape-id))
(dwu/commit-undo-transaction undo-id)
(ptk/data-event :layout/update {:ids [(:parent-id shape)]})
(dws/select-shape new-shape-id))
(->> (rx/of (focus-property new-shape-id prop-num))
(rx/delay 250)))))))
(defn transform-in-variant
"Given the id of a main shape of a component, creates a variant structure for
that component"
[main-instance-id]
(ptk/reify ::transform-in-variant
ptk/WatchEvent
(watch [_ state _]
(let [variant-id (uuid/next)
variant-vec [variant-id]
file-id (:current-file-id state)
page-id (:current-page-id state)
objects (dsh/lookup-page-objects state file-id page-id)
main (get objects main-instance-id)
parent (get objects (:parent-id main))
component-id (:component-id main)
cpath (cfh/split-path (:name main))
name (first cpath)
num-props (max 1 (dec (count cpath)))
cont-props {:layout-item-h-sizing :auto
:layout-item-v-sizing :auto
:layout-padding {:p1 30 :p2 30 :p3 30 :p4 30}
:layout-gap {:row-gap 0 :column-gap 20}
:name name
:r1 20
:r2 20
:r3 20
:r4 20
:is-variant-container true}
main-props {:layout-item-h-sizing :fix
:layout-item-v-sizing :fix
:variant-id variant-id
:name name}
stroke-props {:stroke-alignment :inner
:stroke-style :solid
:stroke-color "#bb97d8" ;; todo use color var?
:stroke-opacity 1
:stroke-width 2}
;; Move the position of the variant container so the main shape doesn't
;; change its position
delta (if (ctsl/any-layout? parent)
(gpt/point 0 0)
(gpt/point -30 -30))
undo-id (js/Symbol)]
;;TODO Refactor all called methods in order to be able to
;;generate changes instead of call the events
(rx/concat
(rx/of
(dwu/start-undo-transaction undo-id)
(when (not= name (:name main))
(dwl/rename-component component-id name))
;; Create variant container
(dwsh/create-artboard-from-selection variant-id nil nil nil delta)
(cl/remove-all-fills variant-vec {:color clr/black :opacity 1})
(dwsl/create-layout-from-id variant-id :flex)
(dwsh/update-shapes variant-vec #(merge % cont-props))
(dwsh/update-shapes [main-instance-id] #(merge % main-props))
(cl/add-stroke variant-vec stroke-props)
(set-variant-id component-id variant-id))
;; Add the necessary number of new properties, with default values
(rx/from
(repeatedly num-props
#(add-new-property variant-id {:fill-values? true})))
;; When the component has path, set the path items as properties values
(when (> (count cpath) 1)
(rx/from
(map
#(update-property-value component-id % (nth cpath (inc %)))
(range num-props))))
(rx/of
(add-new-variant main-instance-id)
(dwu/commit-undo-transaction undo-id)
(ptk/data-event :layout/update {:ids [variant-id]})))))))
(defn add-component-or-variant
"Manage the shared shortcut, and do the pertinent action"
[]
(ptk/reify ::add-component-or-variant
ptk/WatchEvent
(watch [_ state _]
(let [variants? (features/active-feature? state "variants/v1")
objects (dsh/lookup-page-objects state)
selected-ids (dsh/lookup-selected state)
selected-shapes (map (d/getf objects) selected-ids)
single? (= 1 (count selected-ids))
first-shape (first selected-shapes)
transform-in-variant? (and variants?
single?
(not (ctc/is-variant? first-shape))
(ctc/main-instance? first-shape))
add-new-variant? (and variants?
(every? ctc/is-variant? selected-shapes))
undo-id (js/Symbol)]
(cond
transform-in-variant?
(rx/of (transform-in-variant (:id first-shape)))
add-new-variant?
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(rx/from (map add-new-variant selected-ids))
(rx/of (dwu/commit-undo-transaction undo-id)))
:else
(rx/of (dwl/add-component)))))))
(defn duplicate-or-add-variant
"Manage the shared shortcut, and do the pertinent action"
[]
(ptk/reify ::duplicate-or-add-variant
ptk/WatchEvent
(watch [_ state _]
(let [variants? (features/active-feature? state "variants/v1")
objects (dsh/lookup-page-objects state)
selected-ids (dsh/lookup-selected state)
selected-shapes (map (d/getf objects) selected-ids)
add-new-variant? (and variants?
(every? ctc/is-variant? selected-shapes))
undo-id (js/Symbol)]
(if add-new-variant?
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(rx/from (map add-new-variant selected-ids))
(rx/of (dwu/commit-undo-transaction undo-id)))
(rx/of (dws/duplicate-selected true)))))))
(defn rename-variant
[variant-id name]
(ptk/reify ::rename-variant
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
data (dsh/lookup-file-data state)
objects (-> (dsh/get-page data page-id)
(get :objects))
variant-components (cfv/find-variant-components data objects variant-id)
clean-name (cfh/clean-path name)
undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/update-shapes [variant-id] #(assoc % :name clean-name)))
(rx/from (map
#(dwl/rename-component-and-main-instance (:id %) clean-name)
variant-components))
(rx/of (dwu/commit-undo-transaction undo-id)))))))
(defn rename-comp-or-variant-and-main
[component-id name]
(ptk/reify ::rename-comp-or-variant-and-main
ptk/WatchEvent
(watch [_ state _]
(let [data (dsh/lookup-file-data state)
component (ctkl/get-component data component-id)]
(if (ctc/is-variant? component)
(rx/of (rename-variant (:variant-id component) name))
(rx/of (dwl/rename-component-and-main-instance component-id name)))))))