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)) (assoc m key (apply f found args))
m))) 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 ;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -44,6 +44,9 @@
(integer? %) (integer? %)
(>= % min-safe-int) (>= % min-safe-int)
(<= % max-safe-int))) (<= % max-safe-int)))
(s/def ::component-id uuid?)
(s/def ::component-file uuid?)
(s/def ::shape-ref uuid?)
(s/def ::safe-number (s/def ::safe-number
#(and #(and
@ -216,7 +219,10 @@
(s/def ::shape (s/def ::shape
(s/and ::minimal-shape ::shape-attrs (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)) (s/def :internal.page/objects (s/map-of uuid? ::shape))
@ -356,6 +362,18 @@
(defmethod change-spec :del-media [_] (defmethod change-spec :del-media [_]
(s/keys :req-un [::id])) (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 ::change (s/multi-spec change-spec :type))
(s/def ::changes (s/coll-of ::change)) (s/def ::changes (s/coll-of ::change))
@ -473,6 +491,18 @@
:points [] :points []
:segments []))) :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 (defn make-file-data
([] (make-file-data (uuid/next))) ([] (make-file-data (uuid/next)))
([id] ([id]
@ -745,6 +775,24 @@
[data {:keys [id]}] [data {:keys [id]}]
(update data :media dissoc 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 (defmethod process-operation :set
[shape op] [shape op]
(let [attr (:attr op) (let [attr (:attr op)

View file

@ -12,14 +12,53 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.uuid :as uuid])) [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 (defn get-children
"Retrieve all children ids recursively for a given object" "Retrieve all children ids recursively for a given object"
[id objects] [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 (if shapes
(d/concat shapes (mapcat #(get-children % objects) 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 (defn is-shape-grouped
"Checks if a shape is inside a group" "Checks if a shape is inside a group"
[shape-id objects] [shape-id objects]
@ -113,3 +152,55 @@
(lazy-seq (loopfn (rest ids))))))] (lazy-seq (loopfn (rest ids))))))]
(loopfn (:shapes root)))) (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))) ;; (assoc obj :parent-id parent-id)))
;; objects))))) ;; 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" : { "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" : { "translations" : {
"en" : "Add as Shared Library", "en" : "Add as Shared Library",
"fr" : "", "fr" : "",
@ -297,7 +297,7 @@
} }
}, },
"dashboard.grid.add-shared-accept" : { "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" : { "translations" : {
"en" : "Add as Shared Library", "en" : "Add as Shared Library",
"fr" : "", "fr" : "",
@ -306,7 +306,7 @@
} }
}, },
"dashboard.grid.add-shared-hint" : { "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" : { "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.", "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" : "", "fr" : "",
@ -315,7 +315,7 @@
} }
}, },
"dashboard.grid.add-shared-message" : { "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" : { "translations" : {
"en" : "Add “%s” as Shared Library", "en" : "Add “%s” as Shared Library",
"fr" : "", "fr" : "",
@ -342,7 +342,7 @@
} }
}, },
"dashboard.grid.remove-shared" : { "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" : { "translations" : {
"en" : "Remove as Shared Library", "en" : "Remove as Shared Library",
"fr" : "", "fr" : "",
@ -351,7 +351,7 @@
} }
}, },
"dashboard.grid.remove-shared-accept" : { "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" : { "translations" : {
"en" : "Remove as Shared Library", "en" : "Remove as Shared Library",
"fr" : "", "fr" : "",
@ -360,7 +360,7 @@
} }
}, },
"dashboard.grid.remove-shared-hint" : { "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" : { "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.", "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" : "", "fr" : "",
@ -369,7 +369,7 @@
} }
}, },
"dashboard.grid.remove-shared-message" : { "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" : { "translations" : {
"en" : "Remove “%s” as Shared Library", "en" : "Remove “%s” as Shared Library",
"fr" : "", "fr" : "",
@ -621,6 +621,7 @@
"unused" : true "unused" : true
}, },
"ds.button.save" : { "ds.button.save" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:66" ],
"translations" : { "translations" : {
"en" : "Save", "en" : "Save",
"fr" : "Sauvegarder", "fr" : "Sauvegarder",
@ -774,7 +775,7 @@
} }
}, },
"errors.media-type-mismatch" : { "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" : { "translations" : {
"en" : "Seems that the contents of the image does not match the file extension.", "en" : "Seems that the contents of the image does not match the file extension.",
"fr" : "", "fr" : "",
@ -783,7 +784,7 @@
} }
}, },
"errors.media-type-not-allowed" : { "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" : { "translations" : {
"en" : "Seems that this is not a valid image.", "en" : "Seems that this is not a valid image.",
"fr" : "", "fr" : "",
@ -828,7 +829,7 @@
} }
}, },
"errors.unexpected-error" : { "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" : { "translations" : {
"en" : "An unexpected error occurred.", "en" : "An unexpected error occurred.",
"fr" : "Une erreur inattendue c'est produite", "fr" : "Une erreur inattendue c'est produite",
@ -873,7 +874,7 @@
} }
}, },
"media.loading" : { "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" : { "translations" : {
"en" : "Loading image...", "en" : "Loading image...",
"fr" : "Chargement de l'image...", "fr" : "Chargement de l'image...",
@ -882,6 +883,7 @@
} }
}, },
"modal.create-color.new-color" : { "modal.create-color.new-color" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:59" ],
"translations" : { "translations" : {
"en" : "New Color", "en" : "New Color",
"fr" : "Nouvelle couleur", "fr" : "Nouvelle couleur",
@ -1458,7 +1460,7 @@
} }
}, },
"workspace.assets.assets" : { "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" : { "translations" : {
"en" : "Assets", "en" : "Assets",
"fr" : "", "fr" : "",
@ -1467,7 +1469,7 @@
} }
}, },
"workspace.assets.box-filter-all" : { "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" : { "translations" : {
"en" : "All assets", "en" : "All assets",
"fr" : "", "fr" : "",
@ -1476,7 +1478,7 @@
} }
}, },
"workspace.assets.box-filter-colors" : { "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" : { "translations" : {
"en" : "Colors", "en" : "Colors",
"fr" : "", "fr" : "",
@ -1485,7 +1487,7 @@
} }
}, },
"workspace.assets.box-filter-graphics" : { "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" : { "translations" : {
"en" : "Graphics", "en" : "Graphics",
"fr" : "", "fr" : "",
@ -1494,7 +1496,7 @@
} }
}, },
"workspace.assets.colors" : { "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" : { "translations" : {
"en" : "Colors", "en" : "Colors",
"fr" : "", "fr" : "",
@ -1502,8 +1504,17 @@
"es" : "Colores" "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" : { "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" : { "translations" : {
"en" : "Delete", "en" : "Delete",
"fr" : "", "fr" : "",
@ -1512,7 +1523,7 @@
} }
}, },
"workspace.assets.edit" : { "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" : { "translations" : {
"en" : "Edit", "en" : "Edit",
"fr" : "", "fr" : "",
@ -1521,7 +1532,7 @@
} }
}, },
"workspace.assets.file-library" : { "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" : { "translations" : {
"en" : "File library", "en" : "File library",
"fr" : "", "fr" : "",
@ -1530,7 +1541,7 @@
} }
}, },
"workspace.assets.graphics" : { "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" : { "translations" : {
"en" : "Graphics", "en" : "Graphics",
"fr" : "", "fr" : "",
@ -1539,7 +1550,7 @@
} }
}, },
"workspace.assets.libraries" : { "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" : { "translations" : {
"en" : "Libraries", "en" : "Libraries",
"fr" : "", "fr" : "",
@ -1548,7 +1559,7 @@
} }
}, },
"workspace.assets.not-found" : { "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" : { "translations" : {
"en" : "No assets found", "en" : "No assets found",
"fr" : "", "fr" : "",
@ -1557,7 +1568,7 @@
} }
}, },
"workspace.assets.rename" : { "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" : { "translations" : {
"en" : "Rename", "en" : "Rename",
"fr" : "", "fr" : "",
@ -1566,7 +1577,7 @@
} }
}, },
"workspace.assets.search" : { "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" : { "translations" : {
"en" : "Search assets", "en" : "Search assets",
"fr" : "", "fr" : "",
@ -1575,7 +1586,7 @@
} }
}, },
"workspace.assets.shared" : { "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" : { "translations" : {
"en" : "SHARED", "en" : "SHARED",
"fr" : "", "fr" : "",

View file

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

View file

@ -170,6 +170,19 @@
grid-auto-rows: 7vh; grid-auto-rows: 7vh;
column-gap: 0.5rem; column-gap: 0.5rem;
row-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 { .grid-cell {

View file

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

View file

@ -22,12 +22,13 @@
[app.config :as cfg] [app.config :as cfg]
[app.main.constants :as c] [app.main.constants :as c]
[app.main.data.workspace.common :as dwc] [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.notifications :as dwn]
[app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.persistence :as dwp]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt] [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.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.streams :as ms] [app.main.streams :as ms]
@ -47,10 +48,6 @@
(s/def ::set-of-string (s/def ::set-of-string
(s/every string? :kind set?)) (s/every string? :kind set?))
;; --- Expose inner functions
(defn interrupt? [e] (= e :interrupt))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace Initialization ;; Workspace Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -949,7 +946,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(->> stream (->> stream
(rx/filter interrupt?) (rx/filter dwc/interrupt?)
(rx/take 1) (rx/take 1)
(rx/map (constantly clear-edition-mode)))))) (rx/map (constantly clear-edition-mode))))))
@ -978,7 +975,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [cancel-event? (fn [event] (let [cancel-event? (fn [event]
(interrupt? event)) (dwc/interrupt? event))
stoper (rx/filter (ptk/type? ::clear-drawing) stream)] stoper (rx/filter (ptk/type? ::clear-drawing) stream)]
(->> (rx/filter cancel-event? stream) (->> (rx/filter cancel-event? stream)
(rx/take 1) (rx/take 1)
@ -1127,8 +1124,14 @@
(ptk/reify ::show-context-menu (ptk/reify ::show-context-menu
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (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 :shape shape
:root-shape root-shape
:selected (get-in state [:workspace-local :selected])}] :selected (get-in state [:workspace-local :selected])}]
(-> state (-> state
(assoc-in [:workspace-local :context-menu] mdata)))) (assoc-in [:workspace-local :context-menu] mdata))))
@ -1260,70 +1263,19 @@
;; GROUPS ;; 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 (def group-selected
(ptk/reify ::group-selected (ptk/reify ::group-selected
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [id (uuid/next) (let [page-id (:current-page-id state)
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id) objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected]) selected (get-in state [:workspace-local :selected])
items (->> selected shapes (dws/shapes-for-grouping objects selected)]
(map #(get objects %)) (when-not (empty? shapes)
(filter #(not= :frame (:type %))) (let [[group rchanges uchanges]
(map #(assoc % ::index (cph/position-on-parent (:id %) objects))) (dws/prepare-create-group page-id shapes "Group-" false)]
(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})]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) (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 (def ungroup-selected
(ptk/reify ::ungroup-selected (ptk/reify ::ungroup-selected
@ -1336,34 +1288,11 @@
group (get objects group-id)] group (get objects group-id)]
(when (and (= 1 (count selected)) (when (and (= 1 (count selected))
(= (:type group) :group)) (= (:type group) :group))
(let [shapes (:shapes group) (let [[rchanges uchanges]
parent-id (cph/get-parent group-id objects) (dws/prepare-remove-group page-id group 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}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))))) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactions ;; Interactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1506,6 +1435,7 @@
"+" #(st/emit! (increase-zoom nil)) "+" #(st/emit! (increase-zoom nil))
"-" #(st/emit! (decrease-zoom nil)) "-" #(st/emit! (decrease-zoom nil))
"ctrl+g" #(st/emit! group-selected) "ctrl+g" #(st/emit! group-selected)
"ctrl+k" #(st/emit! dwl/add-component)
"shift+g" #(st/emit! ungroup-selected) "shift+g" #(st/emit! ungroup-selected)
"shift+0" #(st/emit! reset-zoom) "shift+0" #(st/emit! reset-zoom)
"shift+1" #(st/emit! zoom-to-fit-all) "shift+1" #(st/emit! zoom-to-fit-all)
@ -1537,5 +1467,5 @@
"right" #(st/emit! (dwt/move-selected :right false)) "right" #(st/emit! (dwt/move-selected :right false))
"left" #(st/emit! (dwt/move-selected :left 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] ([state page-id]
(get-in state [:workspace-data :pages-index page-id :options]))) (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 ;; --- Changes Handling
@ -454,3 +459,4 @@
objects (lookup-page-objects state page-id) objects (lookup-page-objects state page-id)
[rchanges uchanges] (impl-gen-changes objects page-id (seq ids))] [rchanges uchanges] (impl-gen-changes objects page-id (seq ids))]
(rx/of (commit-changes rchanges uchanges {:commit-local? true}))))))) (rx/of (commit-changes rchanges uchanges {:commit-local? true})))))))

View file

@ -12,12 +12,18 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [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.common :as dwc]
[app.main.data.workspace.selection :as dws]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.streams :as ms]
[app.util.color :as color] [app.util.color :as color]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.router :as rt]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[potok.core :as ptk])) [potok.core :as ptk]))
@ -68,7 +74,7 @@
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(defn delete-color (defn delete-color
[{:keys [id] :as color}] [{:keys [id] :as params}]
(us/assert ::us/uuid id) (us/assert ::us/uuid id)
(ptk/reify ::delete-color (ptk/reify ::delete-color
ptk/WatchEvent ptk/WatchEvent
@ -94,7 +100,7 @@
(defn delete-media (defn delete-media
[{:keys [id] :as media}] [{:keys [id] :as params}]
(us/assert ::us/uuid id) (us/assert ::us/uuid id)
(ptk/reify ::delete-media (ptk/reify ::delete-media
ptk/WatchEvent ptk/WatchEvent
@ -106,3 +112,502 @@
:object prev}] :object prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) (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/def ::set-of-string
(s/every string? :kind set?)) (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 ;; --- Selection Rect
(declare select-shapes-by-current-selrect) (declare select-shapes-by-current-selrect)
@ -88,7 +61,7 @@
(ptk/reify ::handle-selection (ptk/reify ::handle-selection
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [stoper (rx/filter #(or (interrupt? %) (let [stoper (rx/filter #(or (dwc/interrupt? %)
(ms/mouse-up? %)) (ms/mouse-up? %))
stream)] stream)]
(rx/concat (rx/concat
@ -183,6 +156,88 @@
(rx/of deselect-all (select-shape (:id selected)))))))) (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 ;; --- Duplicate Shapes
(declare prepare-duplicate-change) (declare prepare-duplicate-change)
(declare prepare-duplicate-frame-change) (declare prepare-duplicate-frame-change)
@ -218,7 +273,7 @@
(defn- prepare-duplicate-shape-change (defn- prepare-duplicate-shape-change
[objects page-id names obj delta frame-id parent-id] [objects page-id names obj delta frame-id parent-id]
(let [id (uuid/next) (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) renamed-obj (assoc obj :id id :name name)
moved-obj (geom/move renamed-obj delta) moved-obj (geom/move renamed-obj delta)
frames (cph/select-frames objects) frames (cph/select-frames objects)
@ -258,7 +313,7 @@
(defn- prepare-duplicate-frame-change (defn- prepare-duplicate-frame-change
[objects page-id names obj delta] [objects page-id names obj delta]
(let [frame-id (uuid/next) (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)) sch (->> (map #(get objects %) (:shapes obj))
(mapcat #(prepare-duplicate-shape-change objects page-id names % delta frame-id frame-id))) (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]) selected (get-in state [:workspace-local :selected])
delta (gpt/point 0 0) 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) rchanges (prepare-duplicate-changes objects page-id unames selected delta)
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %)) 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" :xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"} :xmlns "http://www.w3.org/2000/svg"}
[:& wrapper {:shape frame :view-box vbox}]])) [:& 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 [] (defn ^:export dump-objects []
(let [page-id (get @state :current-page-id)] (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 chat (icon-xref :chat))
(def circle (icon-xref :circle)) (def circle (icon-xref :circle))
(def close (icon-xref :close)) (def close (icon-xref :close))
(def component (icon-xref :component))
(def copy (icon-xref :copy)) (def copy (icon-xref :copy))
(def curve (icon-xref :curve)) (def curve (icon-xref :curve))
(def download (icon-xref :download)) (def download (icon-xref :download))

View file

@ -20,6 +20,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.ui.hooks :refer [use-rxsub]] [app.main.ui.hooks :refer [use-rxsub]]
[app.main.ui.components.dropdown :refer [dropdown]])) [app.main.ui.components.dropdown :refer [dropdown]]))
@ -45,6 +46,7 @@
[{:keys [mdata] :as props}] [{:keys [mdata] :as props}]
(let [{:keys [id] :as shape} (:shape mdata) (let [{:keys [id] :as shape} (:shape mdata)
selected (:selected mdata) selected (:selected mdata)
root-shape (:root-shape mdata)
do-duplicate #(st/emit! dw/duplicate-selected) do-duplicate #(st/emit! dw/duplicate-selected)
do-delete #(st/emit! dw/delete-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-lock-shape #(st/emit! (dw/update-shape-flags id {:blocked true}))
do-unlock-shape #(st/emit! (dw/update-shape-flags id {:blocked false})) do-unlock-shape #(st/emit! (dw/update-shape-flags id {:blocked false}))
do-create-group #(st/emit! dw/group-selected) 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" [:& menu-entry {:title "Copy"
:shortcut "Ctrl + c" :shortcut "Ctrl + c"
@ -101,13 +111,29 @@
[:& menu-entry {:title "Hide" [:& menu-entry {:title "Hide"
:on-click do-hide-shape}]) :on-click do-hide-shape}])
(if (:blocked shape) (if (:blocked shape)
[:& menu-entry {:title "Unlock" [:& menu-entry {:title "Unlock"
:on-click do-unlock-shape}] :on-click do-unlock-shape}]
[:& menu-entry {:title "Lock" [:& menu-entry {:title "Lock"
:on-click do-lock-shape}]) :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-separator]
[:& menu-entry {:title "Delete" [:& menu-entry {:title "Delete"
:shortcut "Supr" :shortcut "Supr"

View file

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

View file

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

View file

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

View file

@ -14,6 +14,7 @@
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.common.media :as cm] [app.common.media :as cm]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cfg] [app.config :as cfg]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
@ -21,6 +22,7 @@
[app.main.data.colors :as dc] [app.main.data.colors :as dc]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.exports :as exports]
[app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.components.tab-container :refer [tab-container tab-element]]
@ -38,6 +40,63 @@
[okulary.core :as l] [okulary.core :as l]
[rumext.alpha :as mf])) [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 (mf/defc graphics-box
[{:keys [file-id local? objects open? on-open on-close] :as props}] [{:keys [file-id local? objects open? on-open on-close] :as props}]
(let [input-ref (mf/use-ref nil) (let [input-ref (mf/use-ref nil)
@ -126,7 +185,6 @@
:left (:left @state) :left (:left @state)
:options [[(tr "workspace.assets.delete") on-delete]]}])])])) :options [[(tr "workspace.assets.delete") on-delete]]}])])]))
(mf/defc color-item (mf/defc color-item
[{:keys [color local? locale file-id] :as props}] [{:keys [color local? locale file-id] :as props}]
(let [rename? (= (:color-for-rename @refs/workspace-local) (:id color)) (let [rename? (= (:color-for-rename @refs/workspace-local) (:id color))
@ -287,32 +345,45 @@
(vals (get-in state [:workspace-libraries id :data :media]))))) (vals (get-in state [:workspace-libraries id :data :media])))))
st/state =)) 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 (defn apply-filters
[coll filters] [coll filters]
(filter (fn [item] (->> coll
(or (matches-search (:name item "!$!") (:term filters)) (filter (fn [item]
(matches-search (:value item "!$!") (:term filters)))) (or (matches-search (:name item "!$!") (:term filters))
coll)) (matches-search (:value item "!$!") (:term filters)))))
(sort-by #(str/lower (:name %)))))
(mf/defc file-library (mf/defc file-library
[{:keys [file local? open? filters locale] :as props}] [{:keys [file local? open? filters locale] :as props}]
(let [open? (mf/use-state open?) (let [open? (mf/use-state open?)
shared? (:is-shared file) shared? (:is-shared file)
router (mf/deref refs/router) router (mf/deref refs/router)
toggle-open #(swap! open? not) toggle-open #(swap! open? not)
toggles (mf/use-state #{:graphics :colors}) toggles (mf/use-state #{:graphics :colors})
url (rt/resolve router :workspace url (rt/resolve router :workspace
{:project-id (:project-id file) {:project-id (:project-id file)
:file-id (:id file)} :file-id (:id file)}
{:page-id (get-in file [:data :pages 0])}) {:page-id (get-in file [:data :pages 0])})
colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file))) colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file)))
colors (apply-filters (mf/deref colors-ref) filters) colors (apply-filters (mf/deref colors-ref) filters)
media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file))) media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file)))
media (apply-filters (mf/deref media-ref) filters)] 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
[:div.tool-window-bar [:div.tool-window-bar
@ -332,15 +403,23 @@
[:a {:href (str "#" url) :target "_blank"} i/chain]]])] [:a {:href (str "#" url) :target "_blank"} i/chain]]])]
(when @open? (when @open?
(let [show-graphics? (and (or (= (:box filters) :all) (let [show-components? (and (or (= (:box filters) :all)
(= (:box filters) :graphics)) (= (:box filters) :components))
(or (> (count media) 0) (or (> (count components) 0)
(str/empty? (:term filters)))) (str/empty? (:term filters))))
show-colors? (and (or (= (:box filters) :all) show-graphics? (and (or (= (:box filters) :all)
(= (:box filters) :colors)) (= (:box filters) :graphics))
(or (> (count colors) 0) (or (> (count media) 0)
(str/empty? (:term filters))))] (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 [:div.tool-window-content
(when show-components?
[:& components-box {:file-id (:id file)
:local? local?
:components components}])
(when show-graphics? (when show-graphics?
[:& graphics-box {:file-id (:id file) [:& graphics-box {:file-id (:id file)
:local? local? :local? local?
@ -357,10 +436,11 @@
:on-open #(swap! toggles conj :colors) :on-open #(swap! toggles conj :colors)
:on-close #(swap! toggles disj :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.asset-group
[:div.group-title (t locale "workspace.assets.not-found")]])]))])) [:div.group-title (t locale "workspace.assets.not-found")]])]))]))
(mf/defc assets-toolbox (mf/defc assets-toolbox
[{:keys [team-id file] :as props}] [{:keys [team-id file] :as props}]
(let [libraries (mf/deref refs/workspace-libraries) (let [libraries (mf/deref refs/workspace-libraries)

View file

@ -43,7 +43,9 @@
:rect i/box :rect i/box
:curve i/curve :curve i/curve
:text i/text :text i/text
:group i/folder :group (if (nil? (:component-id shape))
i/folder
i/component)
nil)) nil))
;; --- Layer Name ;; --- Layer Name
@ -186,6 +188,7 @@
[:li {:on-context-menu on-context-menu [:li {:on-context-menu on-context-menu
:ref dref :ref dref
:class (dom/classnames :class (dom/classnames
:component (not (nil? (:component-id item)))
:dnd-over (= (:over dprops) :center) :dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top) :dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot) :dnd-over-bot (= (:over dprops) :bot)
@ -285,7 +288,18 @@
(defn- strip-objects (defn- strip-objects
[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! (persistent!
(reduce-kv (fn [res id obj] (reduce-kv (fn [res id obj]
(assoc! res id (strip-data obj))) (assoc! res id (strip-data obj)))

View file

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

View file

@ -16,6 +16,7 @@
[potok.core :as ptk] [potok.core :as ptk]
[reitit.core :as r] [reitit.core :as r]
[app.common.data :as d] [app.common.data :as d]
[app.config :as cfg]
[app.util.browser-history :as bhistory] [app.util.browser-history :as bhistory]
[app.util.timers :as ts]) [app.util.timers :as ts])
(:import (:import
@ -112,6 +113,19 @@
(def navigate nav) (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 ;; --- History API
(defn initialize-history (defn initialize-history