Merge pull request #311 from uxbox/us/447/components

Us/447/components
This commit is contained in:
Andrey Antukh 2020-09-16 16:29:34 +02:00 committed by GitHub
commit 0f5ce3b836
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1133 additions and 235 deletions

View file

@ -182,6 +182,13 @@
(assoc m key (apply f found args))
m)))
(defn assoc-when
[m key v]
(let [found (get m key sentinel)]
(if-not (identical? sentinel found)
(assoc m key v)
m)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -44,6 +44,9 @@
(integer? %)
(>= % min-safe-int)
(<= % max-safe-int)))
(s/def ::component-id uuid?)
(s/def ::component-file uuid?)
(s/def ::shape-ref uuid?)
(s/def ::safe-number
#(and
@ -216,7 +219,10 @@
(s/def ::shape
(s/and ::minimal-shape ::shape-attrs
(s/keys :opt-un [::id])))
(s/keys :opt-un [::id
::component-id
::component-file
::shape-ref])))
(s/def :internal.page/objects (s/map-of uuid? ::shape))
@ -356,6 +362,18 @@
(defmethod change-spec :del-media [_]
(s/keys :req-un [::id]))
(s/def :internal.changes.add-component/shapes
(s/coll-of ::shape))
(defmethod change-spec :add-component [_]
(s/keys :req-un [::id ::name :internal.changes.add-component/shapes]))
(defmethod change-spec :del-component [_]
(s/keys :req-un [::id]))
(defmethod change-spec :update-component [_]
(s/keys :req-un [::id ::name :internal.changes.add-component/shapes]))
(s/def ::change (s/multi-spec change-spec :type))
(s/def ::changes (s/coll-of ::change))
@ -473,6 +491,18 @@
:points []
:segments [])))
(defn make-minimal-group
[frame-id selection-rect group-name]
{:id (uuid/next)
:type :group
:name group-name
:shapes []
:frame-id frame-id
:x (:x selection-rect)
:y (:y selection-rect)
:width (:width selection-rect)
:height (:height selection-rect)})
(defn make-file-data
([] (make-file-data (uuid/next)))
([id]
@ -745,6 +775,24 @@
[data {:keys [id]}]
(update data :media dissoc id))
(defmethod process-change :add-component
[data {:keys [id name shapes]}]
(assoc-in data [:components id]
{:id id
:name name
:objects (d/index-by :id shapes)}))
(defmethod process-change :del-component
[data {:keys [id]}]
(d/dissoc-in data [:components id]))
(defmethod process-change :update-component
[data {:keys [id name shapes]}]
(update-in data [:components id]
#(assoc %
:name name
:objects (d/index-by :id shapes))))
(defmethod process-operation :set
[shape op]
(let [attr (:attr op)

View file

@ -12,14 +12,53 @@
[app.common.data :as d]
[app.common.uuid :as uuid]))
(defn walk-pages
"Go through all pages of a file and apply a function to each one"
;; The function receives two parameters (page-id and page), and
;; returns the updated page.
[f data]
(update data :pages-index #(d/mapm f %)))
(defn select-objects
"Get a list of all objects in a page that satisfy a condition"
[f page]
(filter f (vals (get page :objects))))
(defn update-object-list
"Update multiple objects in a page at once"
[page objects-list]
(update page :objects
#(into % (d/index-by :id objects-list))))
(defn get-root-component
"Get the root shape linked to the component for this shape, if any"
[id objects]
(let [obj (get objects id)]
(if-let [component-id (:component-id obj)]
id
(if-let [parent-id (:parent-id obj)]
(get-root-component parent-id obj)
nil))))
(defn get-children
"Retrieve all children ids recursively for a given object"
[id objects]
(let [shapes (get-in objects [id :shapes])]
;; TODO: find why does this sometimes come as a list instead of vector
(let [shapes (vec (get-in objects [id :shapes]))]
(if shapes
(d/concat shapes (mapcat #(get-children % objects) shapes))
[])))
(defn get-children-objects
"Retrieve all children objects recursively for a given object"
[id objects]
(map #(get objects %) (get-children id objects)))
(defn get-object-with-children
"Retrieve a list with an object and all of its children"
[id objects]
(map #(get objects %) (concat [id] (get-children id objects))))
(defn is-shape-grouped
"Checks if a shape is inside a group"
[shape-id objects]
@ -113,3 +152,55 @@
(lazy-seq (loopfn (rest ids))))))]
(loopfn (:shapes root))))
(defn clone-object
"Gets a copy of the object and all its children, with new ids
and with the parent-children links correctly set. Admits functions
to make more transformations to the cloned objects and the
original ones.
Returns the cloned object, the list of all new objects (including
the cloned one), and possibly a list of original objects modified."
([object parent-id objects update-new-object]
(clone-object object parent-id objects update-new-object identity))
([object parent-id objects update-new-object update-original-object]
(let [new-id (uuid/next)]
(loop [child-ids (seq (:shapes object))
new-direct-children []
new-children []
updated-children []]
(if (empty? child-ids)
(let [new-object (cond-> object
true
(assoc :id new-id
:parent-id parent-id)
(some? (:shapes object))
(assoc :shapes (map :id new-direct-children)))
new-object (update-new-object new-object object)
new-objects (concat [new-object] new-children)
updated-object (update-original-object object new-object)
updated-objects (if (= object updated-object)
updated-children
(concat [updated-object] updated-children))]
[new-object new-objects updated-objects])
(let [child-id (first child-ids)
child (get objects child-id)
[new-child new-child-objects updated-child-objects]
(clone-object child new-id objects update-new-object update-original-object)]
(recur
(next child-ids)
(concat new-direct-children [new-child])
(concat new-children new-child-objects)
(concat updated-children updated-child-objects))))))))

View file

@ -52,4 +52,3 @@
;; (assoc obj :parent-id parent-id)))
;; objects)))))

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M385 15l112 222a30 30 0 010 27L385 486a23 23 0 01-22 14H137c-5 1-9-1-13-3-4-3-7-6-9-11L3 264a30 30 0 010-27L115 15c2-4 5-8 9-10 4-3 8-4 13-4h226c5 0 9 1 13 4 4 2 7 6 9 10zM152 445h196l98-194-98-195H152L54 251zm98-139c28 0 50-25 50-55 0-31-22-55-50-55s-50 24-50 55c0 30 22 55 50 55z"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View file

@ -288,7 +288,7 @@
}
},
"dashboard.grid.add-shared" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:146", "src/app/main/ui/dashboard/grid.cljs:166" ],
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:166", "src/app/main/ui/workspace/header.cljs:146" ],
"translations" : {
"en" : "Add as Shared Library",
"fr" : "",
@ -297,7 +297,7 @@
}
},
"dashboard.grid.add-shared-accept" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:69", "src/app/main/ui/dashboard/grid.cljs:95" ],
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:95", "src/app/main/ui/workspace/header.cljs:69" ],
"translations" : {
"en" : "Add as Shared Library",
"fr" : "",
@ -306,7 +306,7 @@
}
},
"dashboard.grid.add-shared-hint" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:68", "src/app/main/ui/dashboard/grid.cljs:94" ],
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:94", "src/app/main/ui/workspace/header.cljs:68" ],
"translations" : {
"en" : "Once added as Shared Library, the assets of this file library will be available to be used among the rest of your files.",
"fr" : "",
@ -315,7 +315,7 @@
}
},
"dashboard.grid.add-shared-message" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:67", "src/app/main/ui/dashboard/grid.cljs:93" ],
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:93", "src/app/main/ui/workspace/header.cljs:67" ],
"translations" : {
"en" : "Add “%s” as Shared Library",
"fr" : "",
@ -342,7 +342,7 @@
}
},
"dashboard.grid.remove-shared" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:144", "src/app/main/ui/dashboard/grid.cljs:165" ],
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:165", "src/app/main/ui/workspace/header.cljs:144" ],
"translations" : {
"en" : "Remove as Shared Library",
"fr" : "",
@ -351,7 +351,7 @@
}
},
"dashboard.grid.remove-shared-accept" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:78", "src/app/main/ui/dashboard/grid.cljs:114" ],
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:114", "src/app/main/ui/workspace/header.cljs:78" ],
"translations" : {
"en" : "Remove as Shared Library",
"fr" : "",
@ -360,7 +360,7 @@
}
},
"dashboard.grid.remove-shared-hint" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:77", "src/app/main/ui/dashboard/grid.cljs:113" ],
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:113", "src/app/main/ui/workspace/header.cljs:77" ],
"translations" : {
"en" : "Once removed as Shared Library, the File Library of this file will stop being available to be used among the rest of your files.",
"fr" : "",
@ -369,7 +369,7 @@
}
},
"dashboard.grid.remove-shared-message" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:76", "src/app/main/ui/dashboard/grid.cljs:112" ],
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:112", "src/app/main/ui/workspace/header.cljs:76" ],
"translations" : {
"en" : "Remove “%s” as Shared Library",
"fr" : "",
@ -621,6 +621,7 @@
"unused" : true
},
"ds.button.save" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:66" ],
"translations" : {
"en" : "Save",
"fr" : "Sauvegarder",
@ -774,7 +775,7 @@
}
},
"errors.media-type-mismatch" : {
"used-in" : [ "src/app/main/data/media.cljs:62", "src/app/main/data/workspace/persistence.cljs:352" ],
"used-in" : [ "src/app/main/data/workspace/persistence.cljs:352", "src/app/main/data/media.cljs:62" ],
"translations" : {
"en" : "Seems that the contents of the image does not match the file extension.",
"fr" : "",
@ -783,7 +784,7 @@
}
},
"errors.media-type-not-allowed" : {
"used-in" : [ "src/app/main/data/media.cljs:59", "src/app/main/data/workspace/persistence.cljs:349" ],
"used-in" : [ "src/app/main/data/workspace/persistence.cljs:349", "src/app/main/data/media.cljs:59" ],
"translations" : {
"en" : "Seems that this is not a valid image.",
"fr" : "",
@ -828,7 +829,7 @@
}
},
"errors.unexpected-error" : {
"used-in" : [ "src/app/main/data/media.cljs:65", "src/app/main/ui/settings/change_email.cljs:51", "src/app/main/ui/auth/register.cljs:54", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66" ],
"used-in" : [ "src/app/main/data/media.cljs:65", "src/app/main/ui/settings/change_email.cljs:51", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66", "src/app/main/ui/auth/register.cljs:54" ],
"translations" : {
"en" : "An unexpected error occurred.",
"fr" : "Une erreur inattendue c'est produite",
@ -873,7 +874,7 @@
}
},
"media.loading" : {
"used-in" : [ "src/app/main/data/media.cljs:44", "src/app/main/data/workspace/persistence.cljs:334" ],
"used-in" : [ "src/app/main/data/workspace/persistence.cljs:334", "src/app/main/data/media.cljs:44" ],
"translations" : {
"en" : "Loading image...",
"fr" : "Chargement de l'image...",
@ -882,6 +883,7 @@
}
},
"modal.create-color.new-color" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:59" ],
"translations" : {
"en" : "New Color",
"fr" : "Nouvelle couleur",
@ -1458,7 +1460,7 @@
}
},
"workspace.assets.assets" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:374" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:476" ],
"translations" : {
"en" : "Assets",
"fr" : "",
@ -1467,7 +1469,7 @@
}
},
"workspace.assets.box-filter-all" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:394" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:496" ],
"translations" : {
"en" : "All assets",
"fr" : "",
@ -1476,7 +1478,7 @@
}
},
"workspace.assets.box-filter-colors" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:396" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:498" ],
"translations" : {
"en" : "Colors",
"fr" : "",
@ -1485,7 +1487,7 @@
}
},
"workspace.assets.box-filter-graphics" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:395" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:497" ],
"translations" : {
"en" : "Graphics",
"fr" : "",
@ -1494,7 +1496,7 @@
}
},
"workspace.assets.colors" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:247" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:324" ],
"translations" : {
"en" : "Colors",
"fr" : "",
@ -1502,8 +1504,17 @@
"es" : "Colores"
}
},
"workspace.assets.components" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:106" ],
"translations" : {
"en" : "Components",
"fr" : "",
"ru" : "",
"es" : "Componentes"
}
},
"workspace.assets.delete" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:125", "src/app/main/ui/workspace/sidebar/assets.cljs:224" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:125", "src/app/main/ui/workspace/sidebar/assets.cljs:210", "src/app/main/ui/workspace/sidebar/assets.cljs:304" ],
"translations" : {
"en" : "Delete",
"fr" : "",
@ -1512,7 +1523,7 @@
}
},
"workspace.assets.edit" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:223" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:303" ],
"translations" : {
"en" : "Edit",
"fr" : "",
@ -1521,7 +1532,7 @@
}
},
"workspace.assets.file-library" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:309" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:401" ],
"translations" : {
"en" : "File library",
"fr" : "",
@ -1530,7 +1541,7 @@
}
},
"workspace.assets.graphics" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:99" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:184" ],
"translations" : {
"en" : "Graphics",
"fr" : "",
@ -1539,7 +1550,7 @@
}
},
"workspace.assets.libraries" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:377" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:479" ],
"translations" : {
"en" : "Libraries",
"fr" : "",
@ -1548,7 +1559,7 @@
}
},
"workspace.assets.not-found" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:339" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:440" ],
"translations" : {
"en" : "No assets found",
"fr" : "",
@ -1557,7 +1568,7 @@
}
},
"workspace.assets.rename" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:222" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:302" ],
"translations" : {
"en" : "Rename",
"fr" : "",
@ -1566,7 +1577,7 @@
}
},
"workspace.assets.search" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:381" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:483" ],
"translations" : {
"en" : "Search assets",
"fr" : "",
@ -1575,7 +1586,7 @@
}
},
"workspace.assets.shared" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:311" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:403" ],
"translations" : {
"en" : "SHARED",
"fr" : "",

View file

@ -20,6 +20,8 @@ $color-warning: #FC8802;
$color-danger: #E65244;
$color-info: #59b9e2;
$color-ocean: #4285f4;
$color-component: #76B0B8;
$color-component-highlight: #00E0FF;
// Gray scale
$color-gray-10: #E3E3E3;

View file

@ -170,6 +170,19 @@
grid-auto-rows: 7vh;
column-gap: 0.5rem;
row-gap: 0.5rem;
&.big {
grid-template-columns: 1fr 1fr;
grid-auto-rows: 10vh;
.grid-cell {
padding: $x-small;
& svg {
height: 10vh;
}
}
}
}
.grid-cell {

View file

@ -70,7 +70,6 @@
}
.element-icon {
svg {
fill: $color-primary;
}
@ -109,6 +108,41 @@
}
}
.element-list li.component {
.element-list-body {
span.element-name {
color: $color-component;
}
svg {
fill: $color-component;
}
&.selected {
span.element-name {
color: $color-component-highlight;
}
svg {
fill: $color-component-highlight;
}
}
&:hover {
background-color: $color-component-highlight;
span.element-name {
color: $color-gray-60;
}
svg {
fill: $color-gray-60;
}
}
}
}
.element-icon {
svg {
fill: $color-gray-30;

View file

@ -22,12 +22,13 @@
[app.config :as cfg]
[app.main.constants :as c]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.persistence :as dwp]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.colors :as dwl]
[app.main.data.colors :as mdc]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.streams :as ms]
@ -47,10 +48,6 @@
(s/def ::set-of-string
(s/every string? :kind set?))
;; --- Expose inner functions
(defn interrupt? [e] (= e :interrupt))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -949,7 +946,7 @@
ptk/WatchEvent
(watch [_ state stream]
(->> stream
(rx/filter interrupt?)
(rx/filter dwc/interrupt?)
(rx/take 1)
(rx/map (constantly clear-edition-mode))))))
@ -978,7 +975,7 @@
ptk/WatchEvent
(watch [_ state stream]
(let [cancel-event? (fn [event]
(interrupt? event))
(dwc/interrupt? event))
stoper (rx/filter (ptk/type? ::clear-drawing) stream)]
(->> (rx/filter cancel-event? stream)
(rx/take 1)
@ -1127,8 +1124,14 @@
(ptk/reify ::show-context-menu
ptk/UpdateEvent
(update [_ state]
(let [mdata {:position position
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component (:id shape) objects)
root-shape (get objects root-id)
mdata {:position position
:shape shape
:root-shape root-shape
:selected (get-in state [:workspace-local :selected])}]
(-> state
(assoc-in [:workspace-local :context-menu] mdata))))
@ -1260,70 +1263,19 @@
;; GROUPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn group-shape
[id frame-id selected selection-rect]
{:id id
:type :group
:name (name (gensym "Group-"))
:shapes []
:frame-id frame-id
:x (:x selection-rect)
:y (:y selection-rect)
:width (:width selection-rect)
:height (:height selection-rect)})
(def group-selected
(ptk/reify ::group-selected
ptk/WatchEvent
(watch [_ state stream]
(let [id (uuid/next)
page-id (:current-page-id state)
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
items (->> selected
(map #(get objects %))
(filter #(not= :frame (:type %)))
(map #(assoc % ::index (cph/position-on-parent (:id %) objects)))
(sort-by ::index))]
(when (not-empty items)
(let [selrect (geom/selection-rect items)
frame-id (-> items first :frame-id)
parent-id (-> items first :parent-id)
group (-> (group-shape id frame-id selected selrect)
(geom/setup selrect))
index (::index (first items))
rchanges [{:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:parent-id parent-id
:obj group
:index index}
{:type :mov-objects
:page-id page-id
:parent-id id
:shapes (->> items
(map :id)
(into #{})
(vec))}]
uchanges
(reduce (fn [res obj]
(conj res {:type :mov-objects
:page-id page-id
:parent-id (:parent-id obj)
:index (::index obj)
:shapes [(:id obj)]}))
[]
items)
uchanges (conj uchanges {:type :del-obj :id id :page-id page-id})]
shapes (dws/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [[group rchanges uchanges]
(dws/prepare-create-group page-id shapes "Group-" false)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set id)))))))))
(dws/select-shapes (d/ordered-set (:id group))))))))))
(def ungroup-selected
(ptk/reify ::ungroup-selected
@ -1336,34 +1288,11 @@
group (get objects group-id)]
(when (and (= 1 (count selected))
(= (:type group) :group))
(let [shapes (:shapes group)
parent-id (cph/get-parent group-id objects)
parent (get objects parent-id)
index-in-parent (->> (:shapes parent)
(map-indexed vector)
(filter #(#{group-id} (second %)))
(ffirst))
rchanges [{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes shapes
:index index-in-parent}]
uchanges [{:type :add-obj
:page-id page-id
:id group-id
:frame-id (:frame-id group)
:obj (assoc group :shapes [])}
{:type :mov-objects
:page-id page-id
:parent-id group-id
:shapes shapes}
{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes [group-id]
:index index-in-parent}]]
(let [[rchanges uchanges]
(dws/prepare-remove-group page-id group objects)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1506,6 +1435,7 @@
"+" #(st/emit! (increase-zoom nil))
"-" #(st/emit! (decrease-zoom nil))
"ctrl+g" #(st/emit! group-selected)
"ctrl+k" #(st/emit! dwl/add-component)
"shift+g" #(st/emit! ungroup-selected)
"shift+0" #(st/emit! reset-zoom)
"shift+1" #(st/emit! zoom-to-fit-all)
@ -1537,5 +1467,5 @@
"right" #(st/emit! (dwt/move-selected :right false))
"left" #(st/emit! (dwt/move-selected :left false))
"i" #(st/emit! (dwl/picker-for-selected-shape ))})
"i" #(st/emit! (mdc/picker-for-selected-shape ))})

View file

@ -44,6 +44,11 @@
([state page-id]
(get-in state [:workspace-data :pages-index page-id :options])))
(defn interrupt? [e] (= e :interrupt))
(defn lookup-component-objects
([state component-id]
(get-in state [:workspace-data :components component-id :objects])))
;; --- Changes Handling
@ -454,3 +459,4 @@
objects (lookup-page-objects state page-id)
[rchanges uchanges] (impl-gen-changes objects page-id (seq ids))]
(rx/of (commit-changes rchanges uchanges {:commit-local? true})))))))

View file

@ -12,12 +12,18 @@
[app.common.data :as d]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.common.pages-helpers :as cph]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.selection :as dws]
[app.common.pages :as cp]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.streams :as ms]
[app.util.color :as color]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
@ -68,7 +74,7 @@
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(defn delete-color
[{:keys [id] :as color}]
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-color
ptk/WatchEvent
@ -94,7 +100,7 @@
(defn delete-media
[{:keys [id] :as media}]
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-media
ptk/WatchEvent
@ -106,3 +112,502 @@
:object prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(declare make-component-shape)
(def add-component
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
shapes (dws/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
;; we need to create a group before creating the component.
[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dws/prepare-create-group page-id shapes "Component-" true))
[new-shape new-shapes updated-shapes]
(make-component-shape group nil objects)
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:shapes new-shapes})
rchanges (into rchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id updated-shape)}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val (:shape-ref updated-shape)}]})
updated-shapes))
uchanges (conj uchanges
{:type :del-component
:id (:id new-shape)})
uchanges (into uchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val nil}]})
updated-shapes))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(defn- make-component-shape
"Clone the shape and all children. Generate new ids and detach
from parent and frame. Update the original shapes to have links
to the new ones."
[shape parent-id objects]
(let [update-new-shape (fn [new-shape original-shape]
(assoc new-shape :frame-id nil))
update-original-shape (fn [original-shape new-shape]
(cond-> original-shape
true
(assoc :shape-ref (:id new-shape))
(nil? (:parent-id new-shape))
(assoc :component-id (:id new-shape))))]
(cph/clone-object shape parent-id objects update-new-shape update-original-shape)))
(defn delete-component
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (get-in state [:workspace-data :components id])
rchanges [{:type :del-component
:id id}]
uchanges [{:type :add-component
:id id
:name (:name component)
:shapes (vals (:objects component))}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn instantiate-component
[file-id component-id]
(us/assert (s/nilable ::us/uuid) file-id)
(us/assert ::us/uuid component-id)
(ptk/reify ::instantiate-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (if (nil? file-id)
(get-in state [:workspace-data :components component-id])
(get-in state [:workspace-libraries file-id :data :components component-id]))
component-shape (get-in component [:objects (:id component)])
orig-pos (gpt/point (:x component-shape) (:y component-shape))
mouse-pos @ms/mouse-position
delta (gpt/subtract mouse-pos orig-pos)
_ (js/console.log "orig-pos" (clj->js orig-pos))
_ (js/console.log "mouse-pos" (clj->js mouse-pos))
_ (js/console.log "delta" (clj->js delta))
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
unames (atom (dwc/retrieve-used-names objects))
all-frames (cph/select-frames objects)
update-new-shape
(fn [new-shape original-shape]
(let [new-name
(dwc/generate-unique-name @unames (:name new-shape))]
(swap! unames conj new-name)
(cond-> new-shape
true
(as-> $
(assoc $ :name new-name)
(geom/move $ delta)
(assoc $ :frame-id
(dwc/calculate-frame-overlap all-frames $))
(assoc $ :parent-id
(or (:parent-id $) (:frame-id $)))
(assoc $ :shape-ref (:id original-shape)))
(nil? (:parent-id original-shape))
(assoc :component-id (:id original-shape))
(and (nil? (:parent-id original-shape)) (some? file-id))
(assoc :component-file file-id))))
[new-shape new-shapes _]
(cph/clone-object component-shape
nil
(get component :objects)
update-new-shape)
rchanges (map (fn [obj]
{:type :add-obj
:id (:id obj)
:page-id page-id
:frame-id (:frame-id obj)
:parent-id (:parent-id obj)
:obj obj})
new-shapes)
uchanges (map (fn [obj]
{:type :del-obj
:id (:id obj)
:page-id page-id})
new-shapes)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id new-shape))))))))
(defn detach-component
[id]
(us/assert ::us/uuid id)
(ptk/reify ::detach-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component id objects)
shapes (cph/get-object-with-children root-id objects)
rchanges (map (fn [obj]
{:type :mod-obj
:page-id page-id
:id (:id obj)
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val nil}]})
shapes)
uchanges (map (fn [obj]
{:type :mod-obj
:page-id page-id
:id (:id obj)
:operations [{:type :set
:attr :component-id
:val (:component-id obj)}
{:type :set
:attr :component-file
:val (:component-file obj)}
{:type :set
:attr :shape-ref
:val (:shape-ref obj)}]})
shapes)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn nav-to-component-file
[file-id]
(us/assert ::us/uuid file-id)
(ptk/reify ::nav-to-component-file
ptk/WatchEvent
(watch [_ state stream]
(let [file (get-in state [:workspace-libraries file-id])
pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav-new-window :workspace pparams qparams))))))
(declare generate-sync-file)
(declare generate-sync-page)
(declare generate-sync-shape-and-children)
(declare generate-sync-shape)
(declare remove-component-and-ref)
(declare remove-ref)
(declare update-attrs)
(declare sync-attrs)
(declare calc-new-pos)
(defn reset-component
[id]
(us/assert ::us/uuid id)
(ptk/reify ::reset-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
page (get-in state [:workspace-data :pages-index page-id])
objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component id objects)
root-shape (get objects id)
file-id (get root-shape :component-file)
components
(if (nil? file-id)
(get-in state [:workspace-data :components])
(get-in state [:workspace-libraries file-id :data :components]))
[rchanges uchanges]
(generate-sync-shape-and-children root-shape page components)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn update-component
[id]
(us/assert ::us/uuid id)
(ptk/reify ::update-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component id objects)
root-shape (get objects id)
component-id (get root-shape :component-id)
component-objs (dwc/lookup-component-objects state component-id)
component-obj (get component-objs component-id)
;; Clone again the original shape and its children, maintaing
;; the ids of the cloned shapes. If the original shape has some
;; new child shapes, the cloned ones will have new generated ids.
update-new-shape (fn [new-shape original-shape]
(cond-> new-shape
true
(assoc :frame-id nil)
(some? (:shape-ref original-shape))
(assoc :id (:shape-ref original-shape))))
[new-shape new-shapes _]
(cph/clone-object root-shape nil objects update-new-shape)
rchanges [{:type :update-component
:id component-id
:name (:name new-shape)
:shapes new-shapes}]
uchanges [{:type :update-component
:id component-id
:name (:name component-obj)
:shapes (vals component-objs)}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn sync-file
[{:keys [file-id] :as params}]
(us/assert (s/nilable ::us/uuid) file-id)
(ptk/reify ::sync-file
ptk/WatchEvent
(watch [_ state stream]
(let [[rchanges uchanges] (generate-sync-file state file-id)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn- generate-sync-file
[state file-id]
(let [components
(if (nil? file-id)
(get-in state [:workspace-data :components])
(get-in state [:workspace-libraries file-id :data :components]))]
(loop [pages (seq (vals (get-in state [:workspace-data :pages-index])))
rchanges []
uchanges []]
(let [page (first pages)]
(if (nil? page)
[rchanges uchanges]
(let [[page-rchanges page-uchanges]
(generate-sync-page page components)]
(recur (next pages)
(concat rchanges page-rchanges)
(concat uchanges page-uchanges))))))))
(defn- generate-sync-page
[page components]
(let [linked-shapes
(cph/select-objects #(some? (:component-id %)) page)]
(loop [shapes (seq linked-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape-and-children shape page components)]
(recur (next shapes)
(concat rchanges shape-rchanges)
(concat uchanges shape-uchanges))))))))
(defn- generate-sync-shape-and-children
[root-shape page components]
(let [objects (get page :objects)
all-shapes (cph/get-object-with-children (:id root-shape) objects)
component (get components (:component-id root-shape))
root-component (get-in component [:objects (:shape-ref root-shape)])]
(loop [shapes (seq all-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape shape root-shape root-component page component)]
(recur (next shapes)
(concat rchanges shape-rchanges)
(concat uchanges shape-uchanges))))))))
(defn- generate-sync-shape
[shape root-shape root-component page component]
(if (nil? component)
(remove-component-and-ref shape page)
(let [component-shape (get (:objects component) (:shape-ref shape))]
(if (nil? component-shape)
(remove-ref shape page)
(update-attrs shape component-shape root-shape root-component page)))))
(defn- remove-component-and-ref
[shape page]
[[{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val nil}]}]
[{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations [{:type :set
:attr :component-id
:val (:component-id shape)}
{:type :set
:attr :component-file
:val (:component-file shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref shape)}]}]])
(defn- remove-ref
[shape page]
[[{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations [{:type :set
:attr :shape-ref
:val nil}]}]
[{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations [{:type :set
:attr :shape-ref
:val (:shape-ref shape)}]}]])
(defn- update-attrs
[shape component-shape root-shape root-component page]
(let [new-pos (calc-new-pos shape component-shape root-shape root-component)]
(loop [attrs (seq sync-attrs)
roperations [{:type :set
:attr :x
:val (:x new-pos)}
{:type :set
:attr :y
:val (:y new-pos)}]
uoperations [{:type :set
:attr :x
:val (:x shape)}
{:type :set
:attr :y
:val (:y shape)}]]
(let [attr (first attrs)]
(if (nil? attr)
(let [rchanges [{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations roperations}]
uchanges [{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations uoperations}]]
[rchanges uchanges])
(if-not (contains? shape attr)
(recur (next attrs)
roperations
uoperations)
(let [roperation {:type :set
:attr attr
:val (get component-shape attr)}
uoperation {:type :set
:attr attr
:val (get shape attr)}]
(recur (next attrs)
(conj roperations roperation)
(conj uoperations uoperation)))))))))
(def sync-attrs [:content
:fill-color
:fill-color-ref-file
:fill-color-ref-id
:fill-opacity
:font-family
:font-size
:font-style
:font-weight
:letter-spacing
:line-height
:proportion
:rx
:ry
:stroke-color
:stroke-color-ref-file
:stroke-color-ref-id
:stroke-opacity
:stroke-style
:stroke-width
:stroke-alignment
:text-align
:width
:height
:interactions
:points
:transform])
(defn- calc-new-pos
[shape component-shape root-shape root-component]
(let [root-pos (gpt/point (:x root-shape) (:y root-shape))
root-component-pos (gpt/point (:x root-component) (:y root-component))
component-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract component-pos root-component-pos)
shape-pos (gpt/point (:x shape) (:y shape))
new-pos (gpt/add root-pos delta)]
new-pos))

View file

@ -33,33 +33,6 @@
(s/def ::set-of-string
(s/every string? :kind set?))
;; Duplicate from workspace.
;; FIXME: Move these functions to a common place
(defn interrupt? [e] (= e :interrupt))
(defn- retrieve-used-names
[objects]
(into #{} (map :name) (vals objects)))
(defn- extract-numeric-suffix
[basename]
(if-let [[match p1 p2] (re-find #"(.*)-([0-9]+)$" basename)]
[p1 (+ 1 (d/parse-integer p2))]
[basename 1]))
(defn- generate-unique-name
"A unique name generator"
[used basename]
(s/assert ::set-of-string used)
(s/assert ::us/string basename)
(let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial]
(let [candidate (str prefix "-" counter)]
(if (contains? used candidate)
(recur (inc counter))
candidate)))))
;; --- Selection Rect
(declare select-shapes-by-current-selrect)
@ -88,7 +61,7 @@
(ptk/reify ::handle-selection
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter #(or (interrupt? %)
(let [stoper (rx/filter #(or (dwc/interrupt? %)
(ms/mouse-up? %))
stream)]
(rx/concat
@ -183,6 +156,88 @@
(rx/of deselect-all (select-shape (:id selected))))))))
;; --- Group shapes
(defn shapes-for-grouping
[objects selected]
(->> selected
(map #(get objects %))
(filter #(not= :frame (:type %)))
(map #(assoc % ::index (cph/position-on-parent (:id %) objects)))
(sort-by ::index)))
(defn- make-group
[shapes prefix keep-name]
(let [selrect (geom/selection-rect shapes)
frame-id (-> shapes first :frame-id)
parent-id (-> shapes first :parent-id)
group-name (if (and keep-name
(= (count shapes) 1)
(= (:type (first shapes)) :group))
(:name (first shapes))
(name (gensym prefix)))]
(-> (cp/make-minimal-group frame-id selrect group-name)
(geom/setup selrect)
(assoc :shapes (map :id shapes)))))
(defn prepare-create-group
[page-id shapes prefix keep-name]
(let [group (make-group shapes prefix keep-name)
rchanges [{:type :add-obj
:id (:id group)
:page-id page-id
:frame-id (:frame-id (first shapes))
:parent-id (:parent-id (first shapes))
:obj group
:index (::index (first shapes))}
{:type :mov-objects
:page-id page-id
:parent-id (:id group)
:shapes (map :id shapes)}]
uchanges (conj
(map (fn [obj] {:type :mov-objects
:page-id page-id
:parent-id (:parent-id obj)
:index (::index obj)
:shapes [(:id obj)]})
shapes)
{:type :del-obj
:id (:id group)
:page-id page-id})]
[group rchanges uchanges]))
(defn prepare-remove-group
[page-id group objects]
(let [shapes (:shapes group)
parent-id (cph/get-parent (:id group) objects)
parent (get objects parent-id)
index-in-parent (->> (:shapes parent)
(map-indexed vector)
(filter #(#{(:id group)} (second %)))
(ffirst))
rchanges [{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes shapes
:index index-in-parent}]
uchanges [{:type :add-obj
:page-id page-id
:id (:id group)
:frame-id (:frame-id group)
:obj (assoc group :shapes [])}
{:type :mov-objects
:page-id page-id
:parent-id (:id group)
:shapes shapes}
{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes [(:id group)]
:index index-in-parent}]]
[rchanges uchanges]))
;; --- Duplicate Shapes
(declare prepare-duplicate-change)
(declare prepare-duplicate-frame-change)
@ -218,7 +273,7 @@
(defn- prepare-duplicate-shape-change
[objects page-id names obj delta frame-id parent-id]
(let [id (uuid/next)
name (generate-unique-name names (:name obj))
name (dwc/generate-unique-name names (:name obj))
renamed-obj (assoc obj :id id :name name)
moved-obj (geom/move renamed-obj delta)
frames (cph/select-frames objects)
@ -258,7 +313,7 @@
(defn- prepare-duplicate-frame-change
[objects page-id names obj delta]
(let [frame-id (uuid/next)
frame-name (generate-unique-name names (:name obj))
frame-name (dwc/generate-unique-name names (:name obj))
sch (->> (map #(get objects %) (:shapes obj))
(mapcat #(prepare-duplicate-shape-change objects page-id names % delta frame-id frame-id)))
@ -287,7 +342,7 @@
selected (get-in state [:workspace-local :selected])
delta (gpt/point 0 0)
unames (retrieve-used-names objects)
unames (dwc/retrieve-used-names objects)
rchanges (prepare-duplicate-changes objects page-id unames selected delta)
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))

View file

@ -156,3 +156,34 @@
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& wrapper {:shape frame :view-box vbox}]]))
(mf/defc component-svg
{::mf/wrap [mf/memo]}
[{:keys [objects group zoom] :or {zoom 1} :as props}]
(let [modifier (-> (gpt/point (:x group) (:y group))
(gpt/negate)
(gmt/translate-matrix))
group-id (:id group)
modifier-ids (concat [group-id] (cph/get-children group-id objects))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
objects (reduce update-fn objects modifier-ids)
group (assoc-in group [:modifiers :displacement] modifier)
width (* (:width group) zoom)
height (* (:height group) zoom)
vbox (str "0 0 " (:width group 0)
" " (:height group 0))
wrapper (mf/use-memo
(mf/deps objects)
#(group-wrapper-factory objects))]
[:svg {:view-box vbox
:width width
:height height
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& wrapper {:shape group :view-box vbox}]]))

View file

@ -67,4 +67,4 @@
(defn ^:export dump-objects []
(let [page-id (get @state :current-page-id)]
(logjs "state" (get-in @state [:workspace-data page-id :objects]))))
(logjs "state" (get-in @state [:workspace-data :pages-index page-id :objects]))))

View file

@ -31,6 +31,7 @@
(def chat (icon-xref :chat))
(def circle (icon-xref :circle))
(def close (icon-xref :close))
(def component (icon-xref :component))
(def copy (icon-xref :copy))
(def curve (icon-xref :curve))
(def download (icon-xref :download))

View file

@ -20,6 +20,7 @@
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.ui.hooks :refer [use-rxsub]]
[app.main.ui.components.dropdown :refer [dropdown]]))
@ -45,6 +46,7 @@
[{:keys [mdata] :as props}]
(let [{:keys [id] :as shape} (:shape mdata)
selected (:selected mdata)
root-shape (:root-shape mdata)
do-duplicate #(st/emit! dw/duplicate-selected)
do-delete #(st/emit! dw/delete-selected)
@ -59,7 +61,15 @@
do-lock-shape #(st/emit! (dw/update-shape-flags id {:blocked true}))
do-unlock-shape #(st/emit! (dw/update-shape-flags id {:blocked false}))
do-create-group #(st/emit! dw/group-selected)
do-remove-group #(st/emit! dw/ungroup-selected)]
do-remove-group #(st/emit! dw/ungroup-selected)
do-add-component #(st/emit! dwl/add-component)
do-detach-component #(st/emit! (dwl/detach-component id))
do-reset-component #(st/emit! (dwl/reset-component id))
do-update-component #(do
(st/emit! (dwl/update-component id))
(st/emit! (dwl/sync-file {:file-id nil})))
do-navigate-component-file #(st/emit! (dwl/nav-to-component-file
(:component-file root-shape)))]
[:*
[:& menu-entry {:title "Copy"
:shortcut "Ctrl + c"
@ -101,13 +111,29 @@
[:& menu-entry {:title "Hide"
:on-click do-hide-shape}])
(if (:blocked shape)
[:& menu-entry {:title "Unlock"
:on-click do-unlock-shape}]
[:& menu-entry {:title "Lock"
:on-click do-lock-shape}])
[:& menu-separator]
(if (nil? (:shape-ref shape))
[:& menu-entry {:title "Create component"
:shortcut "Ctrl + K"
:on-click do-add-component}]
[:*
[:& menu-entry {:title "Detach instance"
:on-click do-detach-component}]
[:& menu-entry {:title "Reset overrides"
:on-click do-reset-component}]
(if (nil? (:component-file root-shape))
[:& menu-entry {:title "Update master component"
:on-click do-update-component}]
[:& menu-entry {:title "Go to master component file"
:on-click do-navigate-component-file}])])
[:& menu-separator]
[:& menu-entry {:title "Delete"
:shortcut "Supr"

View file

@ -34,10 +34,11 @@
(def resize-point-circle-radius 10)
(def resize-point-rect-size 8)
(def resize-side-height 8)
(def selection-rect-color "#1FDEA7")
(def selection-rect-color-normal "#1FDEA7")
(def selection-rect-color-component "#00E0FF")
(def selection-rect-width 1)
(mf/defc selection-rect [{:keys [transform rect zoom]}]
(mf/defc selection-rect [{:keys [transform rect zoom color]}]
(let [{:keys [x y width height]} rect]
[:rect.main
{:x x
@ -45,7 +46,7 @@
:width width
:height height
:transform transform
:style {:stroke selection-rect-color
:style {:stroke color
:stroke-width (/ selection-rect-width zoom)
:fill "transparent"}}]))
@ -125,7 +126,7 @@
:on-mouse-down on-rotate}]))
(mf/defc resize-point-handler
[{:keys [cx cy zoom position on-resize transform rotation]}]
[{:keys [cx cy zoom position on-resize transform rotation color]}]
(let [{cx' :x cy' :y} (gpt/transform (gpt/point cx cy) transform)
rot-square (case position
:top-left 0
@ -139,7 +140,7 @@
:vectorEffect "non-scaling-stroke"
}
:fill "#FFFFFF"
:stroke "#1FDEA7"
:stroke color
:cx cx'
:cy cy'}]
@ -173,6 +174,7 @@
[props]
(let [shape (obj/get props "shape")
zoom (obj/get props "zoom")
color (obj/get props "color")
on-resize (obj/get props "on-resize")
on-rotate (obj/get props "on-rotate")
current-transform (mf/deref refs/current-transform)
@ -186,8 +188,10 @@
;; Selection rect
[:& selection-rect {:rect selrect
:transform transform
:zoom zoom}]
[:& outline {:shape (geom/transform-shape shape)}]
:zoom zoom
:color color}]
[:& outline {:shape (geom/transform-shape shape)
:color color}]
;; Handlers
(for [{:keys [type position props]} (handlers-for-selection selrect)]
@ -197,7 +201,8 @@
:on-rotate on-rotate
:on-resize (partial on-resize position)
:transform transform
:rotation (:rotation shape)}
:rotation (:rotation shape)
:color color}
props (map->obj (merge common-props props))]
(case type
:rotation (when (not= :frame (:type shape)) [:> rotation-handler props])
@ -206,7 +211,7 @@
;; --- Selection Handlers (Component)
(mf/defc path-edition-selection-handlers
[{:keys [shape modifiers zoom] :as props}]
[{:keys [shape modifiers zoom color] :as props}]
(letfn [(on-mouse-down [event index]
(dom/stop-propagation event)
;; TODO: this need code ux refactor
@ -240,26 +245,26 @@
:key index
:on-mouse-down #(on-mouse-down % index)
:fill "#ffffff"
:stroke "#1FDEA7"
:stroke color
:style {:cursor cur/move-pointer}}]))])))
;; TODO: add specs for clarity
(mf/defc text-edition-selection-handlers
[{:keys [shape zoom] :as props}]
[{:keys [shape zoom color] :as props}]
(let [{:keys [x y width height]} shape]
[:g.controls
[:rect.main {:x x :y y
:transform (geom/transform-matrix shape)
:width width
:height height
:style {:stroke "#1FDEA7"
:style {:stroke color
:stroke-width "0.5"
:stroke-opacity "1"
:fill "transparent"}}]]))
(mf/defc multiple-selection-handlers
[{:keys [shapes selected zoom] :as props}]
[{:keys [shapes selected zoom color] :as props}]
(let [shape (geom/selection-rect shapes)
shape-center (geom/center shape)
on-resize (fn [current-position initial-position event]
@ -272,13 +277,14 @@
[:*
[:& controls {:shape shape
:zoom zoom
:color color
:on-resize on-resize
:on-rotate on-rotate}]
(when (debug? :selection-center)
[:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])]))
(mf/defc single-selection-handlers
[{:keys [shape zoom] :as props}]
[{:keys [shape zoom color] :as props}]
(let [shape-id (:id shape)
shape (geom/transform-shape shape)
shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape)
@ -293,6 +299,7 @@
[:*
[:& controls {:shape shape'
:zoom zoom
:color color
:on-rotate on-rotate
:on-resize on-resize}]]))
@ -304,7 +311,11 @@
shapes (->> (mf/deref (refs/objects-by-id selected))
(remove nil?))
num (count shapes)
{:keys [id type] :as shape} (first shapes)]
{:keys [id type] :as shape} (first shapes)
color (if (or (> num 1) (nil? (:shape-ref shape)))
selection-rect-color-normal
selection-rect-color-component)]
(cond
(zero? num)
nil
@ -312,18 +323,22 @@
(> num 1)
[:& multiple-selection-handlers {:shapes shapes
:selected selected
:zoom zoom}]
:zoom zoom
:color color}]
(and (= type :text)
(= edition (:id shape)))
[:& text-edition-selection-handlers {:shape shape
:zoom zoom}]
:zoom zoom
:color color}]
(and (or (= type :path)
(= type :curve))
(= edition (:id shape)))
[:& path-edition-selection-handlers {:shape shape
:zoom zoom}]
:zoom zoom
:color color}]
:else
[:& single-selection-handlers {:shape shape
:zoom zoom}])))
:zoom zoom
:color color}])))

View file

@ -158,7 +158,8 @@
:zoom zoom}]
(when dest-shape
[:& outline {:shape dest-shape}])])))
[:& outline {:shape dest-shape
:color "#31EFB8"}])])))
(mf/defc interaction-handle

View file

@ -22,6 +22,7 @@
[props]
(let [zoom (mf/deref refs/selected-zoom)
shape (unchecked-get props "shape")
color (unchecked-get props "color")
transform (gsh/transform-matrix shape)
{:keys [id x y width height]} shape
@ -31,7 +32,7 @@
"rect")
common {:fill "transparent"
:stroke "#31EFB8"
:stroke color
:strokeWidth (/ 1 zoom)
:pointerEvents "none"
:transform transform}

View file

@ -14,6 +14,7 @@
[app.common.geom.shapes :as geom]
[app.common.media :as cm]
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.workspace :as dw]
@ -21,6 +22,7 @@
[app.main.data.colors :as dc]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.exports :as exports]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
@ -38,6 +40,63 @@
[okulary.core :as l]
[rumext.alpha :as mf]))
(mf/defc components-box
[{:keys [file-id local? components] :as props}]
(let [state (mf/use-state {:menu-open false
:top nil
:left nil
:component-id nil})
on-delete
(mf/use-callback
(mf/deps state)
(fn []
(st/emit! (dwl/delete-component {:id (:component-id @state)}))
(st/emit! (dwl/sync-file {:file-id nil}))))
on-context-menu
(mf/use-callback
(fn [component-id]
(fn [event]
(when local?
(let [pos (dom/get-client-position event)
top (:y pos)
left (- (:x pos) 20)]
(dom/prevent-default event)
(swap! state assoc :menu-open true
:top top
:left left
:component-id component-id))))))
on-drag-start
(mf/use-callback
(fn [component-id event]
(dnd/set-data! event "app/component" {:file-id (if local? nil file-id)
:component-id component-id})
(dnd/set-allowed-effect! event "move")))]
[:div.asset-group
[:div.group-title
(tr "workspace.assets.components")
[:span (str "\u00A0(") (count components) ")"]] ;; Unicode 00A0 is non-breaking space
[:div.group-grid.big
(for [component components]
[:div.grid-cell {:key (:id component)
:draggable true
:on-context-menu (on-context-menu (:id component))
:on-drag-start (partial on-drag-start (:id component))}
[:& exports/component-svg {:group (get-in component [:objects (:id component)])
:objects (:objects component)}]
[:div.cell-name (:name component)]])
(when local?
[:& context-menu
{:selectable false
:show (:menu-open @state)
:on-close #(swap! state assoc :menu-open false)
:top (:top @state)
:left (:left @state)
:options [[(tr "workspace.assets.delete") on-delete]]}])]]))
(mf/defc graphics-box
[{:keys [file-id local? objects open? on-open on-close] :as props}]
(let [input-ref (mf/use-ref nil)
@ -126,7 +185,6 @@
:left (:left @state)
:options [[(tr "workspace.assets.delete") on-delete]]}])])]))
(mf/defc color-item
[{:keys [color local? locale file-id] :as props}]
(let [rename? (= (:color-for-rename @refs/workspace-local) (:id color))
@ -287,32 +345,45 @@
(vals (get-in state [:workspace-libraries id :data :media])))))
st/state =))
(defn file-components-ref
[id]
(l/derived (fn [state]
(let [wfile (:workspace-file state)]
(if (= (:id wfile) id)
(vals (get-in wfile [:data :components]))
(vals (get-in state [:workspace-libraries id :data :components])))))
st/state =))
(defn apply-filters
[coll filters]
(filter (fn [item]
(or (matches-search (:name item "!$!") (:term filters))
(matches-search (:value item "!$!") (:term filters))))
coll))
(->> coll
(filter (fn [item]
(or (matches-search (:name item "!$!") (:term filters))
(matches-search (:value item "!$!") (:term filters)))))
(sort-by #(str/lower (:name %)))))
(mf/defc file-library
[{:keys [file local? open? filters locale] :as props}]
(let [open? (mf/use-state open?)
shared? (:is-shared file)
router (mf/deref refs/router)
toggle-open #(swap! open? not)
(let [open? (mf/use-state open?)
shared? (:is-shared file)
router (mf/deref refs/router)
toggle-open #(swap! open? not)
toggles (mf/use-state #{:graphics :colors})
toggles (mf/use-state #{:graphics :colors})
url (rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
url (rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file)))
colors (apply-filters (mf/deref colors-ref) filters)
colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file)))
colors (apply-filters (mf/deref colors-ref) filters)
media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file)))
media (apply-filters (mf/deref media-ref) filters)]
media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file)))
media (apply-filters (mf/deref media-ref) filters)
components-ref (mf/use-memo (mf/deps (:id file)) #(file-components-ref (:id file)))
components (apply-filters (mf/deref components-ref) filters)]
[:div.tool-window
[:div.tool-window-bar
@ -332,15 +403,23 @@
[:a {:href (str "#" url) :target "_blank"} i/chain]]])]
(when @open?
(let [show-graphics? (and (or (= (:box filters) :all)
(= (:box filters) :graphics))
(or (> (count media) 0)
(str/empty? (:term filters))))
show-colors? (and (or (= (:box filters) :all)
(= (:box filters) :colors))
(or (> (count colors) 0)
(str/empty? (:term filters))))]
(let [show-components? (and (or (= (:box filters) :all)
(= (:box filters) :components))
(or (> (count components) 0)
(str/empty? (:term filters))))
show-graphics? (and (or (= (:box filters) :all)
(= (:box filters) :graphics))
(or (> (count media) 0)
(str/empty? (:term filters))))
show-colors? (and (or (= (:box filters) :all)
(= (:box filters) :colors))
(or (> (count colors) 0)
(str/empty? (:term filters))))]
[:div.tool-window-content
(when show-components?
[:& components-box {:file-id (:id file)
:local? local?
:components components}])
(when show-graphics?
[:& graphics-box {:file-id (:id file)
:local? local?
@ -357,10 +436,11 @@
:on-open #(swap! toggles conj :colors)
:on-close #(swap! toggles disj :colors)}])
(when (and (not show-graphics?) (not show-colors?))
(when (and (not show-components?) (not show-graphics?) (not show-colors?))
[:div.asset-group
[:div.group-title (t locale "workspace.assets.not-found")]])]))]))
(mf/defc assets-toolbox
[{:keys [team-id file] :as props}]
(let [libraries (mf/deref refs/workspace-libraries)

View file

@ -43,7 +43,9 @@
:rect i/box
:curve i/curve
:text i/text
:group i/folder
:group (if (nil? (:component-id shape))
i/folder
i/component)
nil))
;; --- Layer Name
@ -186,6 +188,7 @@
[:li {:on-context-menu on-context-menu
:ref dref
:class (dom/classnames
:component (not (nil? (:component-id item)))
:dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot)
@ -285,7 +288,18 @@
(defn- strip-objects
[objects]
(let [strip-data #(select-keys % [:id :name :blocked :hidden :shapes :type :content :parent-id :metadata])]
(let [strip-data #(select-keys % [:id
:name
:blocked
:hidden
:shapes
:type
:content
:parent-id
:component-id
:component-file
:shape-ref
:metadata])]
(persistent!
(reduce-kv (fn [res id obj]
(assoc! res id (strip-data obj)))

View file

@ -22,6 +22,7 @@
[app.common.data :as d]
[app.main.constants :as c]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.drawing :as dd]
[app.main.data.colors :as dwc]
[app.main.data.fetch :as mdf]
@ -132,12 +133,16 @@
hover (or (unchecked-get props "hover") #{})
outline? (set/union selected hover)
shapes (->> (vals objects) (filter (comp outline? :id)))
transform (mf/deref refs/current-transform)]
transform (mf/deref refs/current-transform)
color (if (or (> (count shapes) 1) (nil? (:shape-ref (first shapes))))
"#31EFB8"
"#00E0FF")]
(when (nil? transform)
[:g.outlines
(for [shape shapes]
[:& outline {:key (str "outline-" (:id shape))
:shape (gsh/transform-shape shape)}])])))
:shape (gsh/transform-shape shape)
:color color}])])))
(mf/defc frames
{::mf/wrap [mf/memo]
@ -454,6 +459,7 @@
on-drag-enter
(fn [e]
(when (or (dnd/has-type? e "app/shape")
(dnd/has-type? e "app/component")
(dnd/has-type? e "Files")
(dnd/has-type? e "text/uri-list"))
(dom/prevent-default e)))
@ -461,6 +467,7 @@
on-drag-over
(fn [e]
(when (or (dnd/has-type? e "app/shape")
(dnd/has-type? e "app/component")
(dnd/has-type? e "Files")
(dnd/has-type? e "text/uri-list"))
(dom/prevent-default e)))
@ -491,6 +498,10 @@
(assoc :x final-x)
(assoc :y final-y)))))
(dnd/has-type? event "app/component")
(let [{:keys [component-id file-id]} (dnd/get-data event "app/component")]
(st/emit! (dwl/instantiate-component file-id component-id)))
(dnd/has-type? event "text/uri-list")
(let [data (dnd/get-data event "text/uri-list")
lines (str/lines data)

View file

@ -16,6 +16,7 @@
[potok.core :as ptk]
[reitit.core :as r]
[app.common.data :as d]
[app.config :as cfg]
[app.util.browser-history :as bhistory]
[app.util.timers :as ts])
(:import
@ -112,6 +113,19 @@
(def navigate nav)
(deftype NavigateNewWindow [id params qparams]
ptk/EffectEvent
(effect [_ state stream]
(let [router (:router state)
path (resolve router id params qparams)
uri (str cfg/public-uri "/#" path)]
(js/window.open uri "_blank"))))
(defn nav-new-window
([id] (nav-new-window id nil nil))
([id params] (nav-new-window id params nil))
([id params qparams] (NavigateNewWindow. id params qparams)))
;; --- History API
(defn initialize-history