0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-12 18:18:24 -05:00

🎉 Create reusable components

This commit is contained in:
Andrés Moya 2020-09-01 15:09:57 +02:00
parent 8396357f36
commit 5e585201d3
17 changed files with 515 additions and 168 deletions

View file

@ -356,6 +356,12 @@
(defmethod change-spec :del-media [_]
(s/keys :req-un [::id]))
(defmethod change-spec :add-component [_]
(s/keys :req-un [::id ::name ::new-shapes]))
(defmethod change-spec :del-component [_]
(s/keys :req-un [::id]))
(s/def ::change (s/multi-spec change-spec :type))
(s/def ::changes (s/coll-of ::change))
@ -473,6 +479,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 +763,17 @@
[data {:keys [id]}]
(update data :media dissoc id))
(defmethod process-change :add-component
[data {:keys [id name new-shapes]}]
(assoc-in data [:components id]
{:id id
:name name
:objects (d/index-by :id new-shapes)}))
(defmethod process-change :del-component
[data {:keys [id]}]
(d/dissoc-in data [:components id]))
(defmethod process-operation :set
[shape op]
(let [attr (:attr op)

View file

@ -15,7 +15,7 @@
(defn get-children
"Retrieve all children ids recursively for a given object"
[id objects]
(let [shapes (get-in objects [id :shapes])]
(let [shapes (vec (get-in objects [id :shapes]))]
(if shapes
(d/concat shapes (mapcat #(get-children % objects) shapes))
[])))

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,21 @@
grid-auto-rows: 7vh;
column-gap: 0.5rem;
row-gap: 0.5rem;
&.big {
grid-template-columns: 1fr 1fr;
grid-auto-rows: 10vh;
.grid-cell {
background-color: transparent;
border: 1px solid $color-gray-40;
border-radius: 4px;
& svg {
height: 10vh;
}
}
}
}
.grid-cell {

View file

@ -20,42 +20,42 @@
margin-right: 8px;
width: 13px;
}
&.group {
&.open {
.toggle-content {
flex-shrink: 0;
svg {
transform: rotate(270deg);
}
}
}
}
&:hover {
background-color: $color-primary;
svg {
fill: $color-gray-60 !important;
}
.element-icon,
.element-actions {
svg {
fill: $color-gray-60;
}
}
.element-actions > * {
display: flex;
}
span {
color: $color-gray-60;
}
.toggle-content {
svg {
fill: $color-gray-60;
@ -64,13 +64,12 @@
}
&.selected {
svg {
fill: $color-primary;
}
.element-icon {
svg {
fill: $color-primary;
}
@ -79,10 +78,10 @@
span {
color: $color-primary;
}
&:hover {
background-color: $color-primary;
.element-icon,
.element-actions {
svg {
@ -95,20 +94,55 @@
}
}
}
&.drag-top {
border-top: 40px solid $color-gray-60 !important;
}
&.drag-bottom {
border-bottom: 40px solid $color-gray-60 !important;
}
&.drag-inside {
border: 2px solid $color-primary !important;
}
}
.element-list li.component {
.element-list-body {
.element-name {
color: $color-component;
}
svg {
fill: $color-component;
}
&.selected {
.element-name {
color: $color-component-highlight;
}
svg {
fill: $color-component-highlight;
}
}
&:hover {
background-color: $color-component-highlight;
.element-name {
color: $color-gray-60;
}
svg {
fill: $color-gray-60;
}
}
}
}
.element-icon {
svg {
fill: $color-gray-30;
@ -132,7 +166,7 @@ span.element-name {
white-space: nowrap;
width: 100%;
}
.element-actions {
display: flex;
flex-shrink: 0;
@ -149,13 +183,13 @@ span.element-name {
> * {
display: none;
}
.toggle-element,
.block-element {
left: 0;
position: absolute;
top: 0;
&.selected {
display: flex;
@ -177,17 +211,17 @@ span.element-name {
.toggle-content {
margin-left: auto;
width: 12px;
svg {
fill: $color-gray-20;
transform: rotate(90deg);
width: 10px;
}
&.inverse {
svg { transform: rotate(270deg); }
}
&:hover {
svg {
fill: $color-gray-60;

View file

@ -22,6 +22,7 @@
[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]
@ -1266,70 +1267,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
@ -1342,34 +1292,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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1512,6 +1439,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)

View file

@ -13,6 +13,7 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[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]
@ -68,7 +69,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 +95,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 +107,118 @@
:object prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(declare clone-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]
(clone-shape group nil objects)
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:new-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)}]})
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}]})
updated-shapes))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(defn- clone-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 [new-id (uuid/next)]
(if (nil? (:shapes shape))
; TODO: unify this case with the empty child-ids case.
(let [new-shape (assoc shape
:id new-id
:parent-id parent-id
:frame-id nil)]
[new-shape
[new-shape]
[(assoc shape :component-id (:id new-shape))]])
(loop [child-ids (seq (:shapes shape))
new-children []
updated-children []]
(if (empty? child-ids)
(let [new-shape (assoc shape
:id new-id
:parent-id parent-id
:frame-id nil
:shapes (map :id new-children))]
[new-shape
(conj new-children new-shape)
(conj updated-children
(assoc shape :component-id (:id new-shape)))])
(let [child-id (first child-ids)
child (get objects child-id)
[new-child new-child-shapes updated-child-shapes]
(clone-shape child new-id objects)]
(recur
(next child-ids)
(into new-children new-child-shapes)
(into updated-children updated-child-shapes))))))))
(defn delete-component
[{:keys [id] :as params}]
(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)
:new-shapes (:objects component)}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))

View file

@ -183,6 +183,86 @@
(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))
(: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)

View file

@ -156,3 +156,35 @@
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& wrapper {:shape frame :view-box vbox}]]))
;; TODO: unify with frame-svg?
(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]]))
@ -59,7 +60,8 @@
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)]
[:*
[:& menu-entry {:title "Copy"
:shortcut "Ctrl + c"
@ -101,13 +103,17 @@
[:& 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]
[:& menu-entry {:title "Create component"
:shortcut "Ctrl + K"
:on-click do-add-component}]
[:& menu-separator]
[:& menu-entry {:title "Delete"
:shortcut "Supr"

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,62 @@
[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 []
(let [params {:id (:component-id @state)}]
(st/emit! (dwl/delete-component params)))))
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 [path event]
(dnd/set-data! event "text/uri-list" (cfg/resolve-media-path path))
(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 (:path 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 +184,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 +344,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})
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 +402,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 +435,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,16 @@
(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
:metadata])]
(persistent!
(reduce-kv (fn [res id obj]
(assoc! res id (strip-data obj)))