0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-11 23:31:21 -05:00

🎉 Add interaction flows

This commit is contained in:
Andrés Moya 2021-09-30 16:24:52 +02:00 committed by Alonso Torres
parent f3bb5c55f5
commit 0159eea526
22 changed files with 856 additions and 257 deletions

View file

@ -159,6 +159,11 @@
([mfn coll]
(into {} (mapm mfn) coll)))
(defn removev
"Returns a vector of the items in coll for which (fn item) returns logical false"
[fn coll]
(filterv (comp not fn) coll))
(defn filterm
"Filter values of a map that satisfy a predicate"
[pred coll]

View file

@ -10,6 +10,7 @@
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.common.types.interactions :as cti]
[app.common.types.page-options :as cto]
[app.common.uuid :as uuid]
[clojure.set :as set]
[clojure.spec.alpha :as s]))
@ -144,43 +145,6 @@
:internal.blur/value
:internal.blur/hidden]))
;; Page Options
(s/def :internal.page.grid.color/value string?)
(s/def :internal.page.grid.color/opacity ::us/safe-number)
(s/def :internal.page.grid/size ::us/safe-integer)
(s/def :internal.page.grid/color
(s/keys :req-un [:internal.page.grid.color/value
:internal.page.grid.color/opacity]))
(s/def :internal.page.grid/type #{:stretch :left :center :right})
(s/def :internal.page.grid/item-length (s/nilable ::us/safe-integer))
(s/def :internal.page.grid/gutter (s/nilable ::us/safe-integer))
(s/def :internal.page.grid/margin (s/nilable ::us/safe-integer))
(s/def :internal.page.grid/square
(s/keys :req-un [:internal.page.grid/size
:internal.page.grid/color]))
(s/def :internal.page.grid/column
(s/keys :req-un [:internal.page.grid/size
:internal.page.grid/color
:internal.page.grid/type
:internal.page.grid/item-length
:internal.page.grid/gutter
:internal.page.grid/margin]))
(s/def :internal.page.grid/row :internal.page.grid/column)
(s/def :internal.page.options/background string?)
(s/def :internal.page.options/saved-grids
(s/keys :req-un [:internal.page.grid/square
:internal.page.grid/row
:internal.page.grid/column]))
(s/def :internal.page/options
(s/keys :opt-un [:internal.page.options/background]))
;; Size constraints
(s/def :internal.shape/constraints-h #{:left :right :leftright :center :scale})
@ -370,7 +334,7 @@
(s/def ::page
(s/keys :req-un [::id
::name
:internal.page/options
::cto/options
:internal.page/objects]))

View file

@ -10,9 +10,14 @@
[app.common.spec :as us]
[clojure.spec.alpha :as s]))
(s/def ::point
(s/and (s/keys :req-un [::x ::y])
gpt/point?))
;; WARNING: options are not deleted when changing event or action type, so it can be
;; restored if the user changes it back later.
;;
;; But that means that an interaction may have for example a delay or
;; destination, even if its type does not require it (but a previous type did).
;;
;; So make sure to use has-delay/has-destination... functions, or similar,
;; before reading them.
;; -- Options depending on event type
@ -54,7 +59,7 @@
:bottom-left
:bottom-right
:bottom-center})
(s/def ::overlay-position ::point)
(s/def ::overlay-position ::us/point)
(s/def ::url ::us/string)
(s/def ::close-click-outside ::us/boolean)
(s/def ::background-overlay ::us/boolean)

View file

@ -0,0 +1,94 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.common.types.page-options
(:require
[app.common.data :as d]
[app.common.spec :as us]
[clojure.spec.alpha :as s]))
;; --- Grid options
(s/def :artboard-grid.color/value ::us/string)
(s/def :artboard-grid.color/opacity ::us/safe-number)
(s/def :artboard-grid/size ::us/safe-integer)
(s/def :artboard-grid/color (s/keys :req-un [:artboard-grid.color/value
:artboard-grid.color/opacity]))
(s/def :artboard-grid/type #{:stretch :left :center :right})
(s/def :artboard-grid/item-length (s/nilable ::us/safe-integer))
(s/def :artboard-grid/gutter (s/nilable ::us/safe-integer))
(s/def :artboard-grid/margin (s/nilable ::us/safe-integer))
(s/def :artboard-grid/square
(s/keys :req-un [:artboard-grid/size
:artboard-grid/color]))
(s/def :artboard-grid/column
(s/keys :req-un [:artboard-grid/size
:artboard-grid/color
:artboard-grid/type
:artboard-grid/item-length
:artboard-grid/gutter
:artboard-grid/margin]))
(s/def :artboard-grid/row :artboard-grid/column)
(s/def ::saved-grids
(s/keys :req-un [:artboard-grid/square
:artboard-grid/row
:artboard-grid/column]))
;; --- Background options
(s/def ::background string?)
;; --- Flow options
(s/def :interactions-flow/id ::us/uuid)
(s/def :interactions-flow/name ::us/string)
(s/def :interactions-flow/starting-frame ::us/uuid)
(s/def ::flow
(s/keys :req-un [:interactions-flow/id
:interactions-flow/name
:interactions-flow/starting-frame]))
(s/def ::flows
(s/coll-of ::flow :kind vector?))
;; --- Options
(s/def ::options
(s/keys :opt-un [::background
::saved-grids
::flows]))
;; --- Helpers for flow
(defn rename-flow
[flow name]
(assoc flow :name name))
;; --- Helpers for flows
(defn add-flow
[flows flow]
(conj flows flow))
(defn remove-flow
[flows flow-id]
(vec (remove #(= (:id %) flow-id) flows)))
(defn update-flow
[flows flow-id update-fn]
(let [index (d/index-of-pred flows #(= (:id %) flow-id))]
(update flows index update-fn)))
(defn get-frame-flow
[flows frame-id]
(d/seek #(= (:starting-frame %) frame-id) flows))

View file

@ -937,6 +937,10 @@
.element-set-options-group {
flex-wrap: wrap;
}
&:not(:first-child) {
border-top: 1px solid $color-gray-60;
}
}
.exports-options,

View file

@ -75,7 +75,84 @@
}
svg {
width: 18px;
height: 18px;
width: 18px;
}
}
.flow-element {
display: flex;
align-items: center;
padding: $size-1;
.element-label {
font-size: $fs11;
}
.flow-name {
cursor: pointer;
}
& input.element-name {
background: transparent;
border-color: $color-primary;
color: $color-white;
font-size: $fs11;
}
}
.flow-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
margin-right: $size-2;
& svg {
height: 12px;
width: 12px;
fill: $color-gray-20;
}
&:hover svg {
fill: $color-primary;
}
}
.flow-badge {
display: flex;
& .content {
align-items: center;
background-color: $color-gray-50;
border-radius: 4px;
display: flex;
height: 24px;
& svg {
height: 12px;
margin: 0 $size-2;
width: 12px;
fill: $color-gray-20;
}
& span {
color: $color-gray-20;
font-size: $fs12;
margin-right: $size-4;
}
}
&.selected .content {
background-color: $color-primary;
& svg {
fill: $color-gray-60;
}
& span {
color: $color-gray-60;
}
}
}

View file

@ -68,7 +68,7 @@
align-items: center;
cursor: pointer;
display: flex;
width: 90px;
position: relative;
> span {
color: $color-gray-10;
@ -96,7 +96,7 @@
}
.dropdown {
min-width: 260px;
min-width: 295px;
top: 45px;
left: -25px;
}

View file

@ -238,10 +238,6 @@
font-size: $fs12;
}
.selected .workspace-frame-label {
fill: $color-primary-dark;
}
.multiuser-cursor {
align-items: center;
display: flex;

View file

@ -111,6 +111,7 @@
(rx/of (df/fonts-fetched fonts)
(bundle-fetched (merge bundle params))))))))))
(declare go-to-frame-auto)
(defn bundle-fetched
[{:keys [project file share-links libraries users permissions] :as bundle}]
@ -130,7 +131,15 @@
:permissions permissions
:project project
:pages pages
:file file}))))))
:file file})))
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
qparams (:query-params route)
index (:index qparams)]
(when (nil? index)
(rx/of (go-to-frame-auto))))))))
(defn fetch-comment-threads
[{:keys [file-id page-id] :as params}]
@ -329,6 +338,20 @@
(when index
(rx/of (go-to-frame-by-index index)))))))
(defn go-to-frame-auto
[]
(ptk/reify ::go-to-frame-auto
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
qparams (:query-params route)
page-id (:page-id qparams)
flows (get-in state [:viewer :pages page-id :options :flows])]
(if (seq flows)
(let [frame-id (:starting-frame (first flows))]
(rx/of (go-to-frame frame-id)))
(rx/of (go-to-frame-by-index 0)))))))
(defn go-to-section
[section]
(ptk/reify ::go-to-section
@ -391,7 +414,7 @@
:background-overlay background-overlay})
(update-in state [:viewer-local :overlays]
(fn [overlays]
(remove #(= (:id (:frame %)) frame-id) overlays))))))))
(d/removev #(= (:id (:frame %)) frame-id) overlays))))))))
(defn close-overlay
[frame-id]
@ -400,7 +423,7 @@
(update [_ state]
(update-in state [:viewer-local :overlays]
(fn [overlays]
(remove #(= (:id (:frame %)) frame-id) overlays))))))
(d/removev #(= (:id (:frame %)) frame-id) overlays))))))
;; --- Objects selection

View file

@ -18,7 +18,6 @@
[app.common.pages.spec :as spec]
[app.common.spec :as us]
[app.common.transit :as t]
[app.common.types.interactions :as cti]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.events :as ev]
@ -28,6 +27,7 @@
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.interactions :as dwi]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.path :as dwdp]
@ -1285,8 +1285,7 @@
(watch [_ state _]
(let [{:keys [current-file-id current-page-id]} state
pparams {:file-id (or file-id current-file-id)}
qparams {:page-id (or page-id current-page-id)
:index 0}]
qparams {:page-id (or page-id current-page-id)}]
(rx/of ::dwp/force-persist
(rt/nav-new-window* {:rname :viewer
:path-params pparams
@ -1754,166 +1753,12 @@
;; Interactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare move-edit-interaction)
(declare finish-edit-interaction)
(defn start-edit-interaction
[index]
(ptk/reify ::start-edit-interaction
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :editing-interaction-index] index))
ptk/WatchEvent
(watch [_ state stream]
(let [initial-pos @ms/mouse-position
selected (wsh/lookup-selected state)
stopper (rx/filter ms/mouse-up? stream)]
(when (= 1 (count selected))
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-edit-interaction initial-pos %)))
(rx/of (finish-edit-interaction index initial-pos))))))))
(defn move-edit-interaction
[initial-pos position]
(ptk/reify ::move-edit-interaction
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected-shape-id (-> state wsh/lookup-selected first)
selected-shape (get objects selected-shape-id)
selected-shape-frame-id (:frame-id selected-shape)
start-frame (get objects selected-shape-frame-id)
end-frame (dwc/get-frame-at-point objects position)]
(cond-> state
(not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position)
(not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame))))))
(defn finish-edit-interaction
[index initial-pos]
(ptk/reify ::finish-edit-interaction
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :editing-interaction-index] nil)
(assoc-in [:workspace-local :draw-interaction-to] nil)
(assoc-in [:workspace-local :draw-interaction-to-frame] nil)))
ptk/WatchEvent
(watch [_ state _]
(let [position @ms/mouse-position
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
frame (dwc/get-frame-at-point objects position)
shape-id (-> state wsh/lookup-selected first)
shape (get objects shape-id)]
(when (and shape (not (= position initial-pos)))
(rx/of (dch/update-shapes [shape-id]
(fn [shape]
(update shape :interactions
(fn [interactions]
(if-not frame
;; Drop in an empty space -> remove interaction
(if index
(into (subvec interactions 0 index)
(subvec interactions (inc index)))
interactions)
(let [frame (if (or (= (:id frame) (:id shape))
(= (:id frame) (:frame-id shape)))
nil ;; Drop onto self frame -> set destination to none
frame)]
;; Update or create interaction
(if (and index (cti/has-destination (get interactions index)))
(update interactions index
#(cti/set-destination % (:id frame)))
(conj (or interactions [])
(cti/set-destination cti/default-interaction
(:id frame))))))))))))))))
(declare move-overlay-pos)
(declare finish-move-overlay-pos)
(defn start-move-overlay-pos
[index]
(ptk/reify ::start-move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :move-overlay-to] nil)
(assoc-in [:workspace-local :move-overlay-index] index)))
ptk/WatchEvent
(watch [_ state stream]
(let [initial-pos @ms/mouse-position
selected (wsh/lookup-selected state)
stopper (rx/filter ms/mouse-up? stream)]
(when (= 1 (count selected))
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shape (->> state
wsh/lookup-selected
first
(get objects))
overlay-pos (-> shape
(get-in [:interactions index])
:overlay-position)
orig-frame (cph/get-frame shape objects)
frame-pos (gpt/point (:x orig-frame) (:y orig-frame))
offset (-> initial-pos
(gpt/subtract overlay-pos)
(gpt/subtract frame-pos))]
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-overlay-pos % frame-pos offset)))
(rx/of (finish-move-overlay-pos index frame-pos offset)))))))))
(defn move-overlay-pos
[pos frame-pos offset]
(ptk/reify ::move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(let [pos (-> pos
(gpt/subtract frame-pos)
(gpt/subtract offset))]
(assoc-in state [:workspace-local :move-overlay-to] pos)))))
(defn finish-move-overlay-pos
[index frame-pos offset]
(ptk/reify ::finish-move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(-> state
(d/dissoc-in [:workspace-local :move-overlay-to])
(d/dissoc-in [:workspace-local :move-overlay-index])))
ptk/WatchEvent
(watch [_ state _]
(let [pos @ms/mouse-position
overlay-pos (-> pos
(gpt/subtract frame-pos)
(gpt/subtract offset))
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shape (->> state
wsh/lookup-selected
first
(get objects))
interactions (:interactions shape)
new-interactions
(update interactions index
#(cti/set-overlay-position % overlay-pos))]
(rx/of (update-shape (:id shape) {:interactions new-interactions}))))))
(d/export dwi/start-edit-interaction)
(d/export dwi/move-edit-interaction)
(d/export dwi/finish-edit-interaction)
(d/export dwi/start-move-overlay-pos)
(d/export dwi/move-overlay-pos)
(d/export dwi/finish-move-overlay-pos)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CANVAS OPTIONS

View file

@ -12,6 +12,8 @@
[app.common.logging :as log]
[app.common.pages :as cp]
[app.common.spec :as us]
[app.common.types.interactions :as cti]
[app.common.types.page-options :as cto]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
@ -379,8 +381,10 @@
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
options (wsh/lookup-page-options state page-id)
ids (cp/clean-loops objects ids)
flows (:flows options)
groups-to-unmask
(reduce (fn [group-ids id]
@ -399,9 +403,14 @@
interacting-shapes
(filter (fn [shape]
(let [interactions (:interactions shape)]
(some ids (map :destination interactions))))
(some #(and (cti/has-destination %)
(contains? ids (:destination %)))
interactions)))
(vals objects))
starting-flows
(filter #(contains? ids (:starting-frame %)) flows)
empty-parents-xform
(comp
(map (fn [id] (get objects id)))
@ -467,7 +476,8 @@
:operations [{:type :set
:attr :interactions
:val (vec (remove (fn [interaction]
(contains? ids (:destination interaction)))
(and (cti/has-destination interaction)
(contains? ids (:destination interaction))))
(:interactions obj)))}]})))
mk-mod-int-add-xf
(comp (filter some?)
@ -479,6 +489,22 @@
:attr :interactions
:val (:interactions obj)}]})))
mk-mod-del-flow-xf
(comp (filter some?)
(map (fn [flow]
{:type :set-option
:page-id page-id
:option :flows
:value (cto/remove-flow flows (:id flow))})))
mk-mod-add-flow-xf
(comp (filter some?)
(map (fn [_]
{:type :set-option
:page-id page-id
:option :flows
:value flows})))
mk-mod-unmask-xf
(comp (filter (partial contains? objects))
(map (fn [id]
@ -508,7 +534,8 @@
:page-id page-id
:shapes (vec all-parents)})
(into mk-mod-unmask-xf groups-to-unmask)
(into mk-mod-int-del-xf interacting-shapes))
(into mk-mod-int-del-xf interacting-shapes)
(into mk-mod-del-flow-xf starting-flows))
uchanges
(-> []
@ -520,8 +547,8 @@
:shapes (vec all-parents)})
(into mk-mod-touched-xf (reverse all-parents))
(into mk-mod-mask-xf groups-to-unmask)
(into mk-mod-int-add-xf interacting-shapes))
]
(into mk-mod-int-add-xf interacting-shapes)
(into mk-mod-add-flow-xf starting-flows))]
;; (println "================ rchanges")
;; (cljs.pprint/pprint rchanges)

View file

@ -0,0 +1,284 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.data.workspace.interactions
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.types.interactions :as cti]
[app.common.types.page-options :as cto]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.streams :as ms]
[beicon.core :as rx]
[potok.core :as ptk]))
;; --- Flows
(defn add-flow-selected-frame
[]
(ptk/reify ::add-flow-selected-frame
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
flows (get-in state [:workspace-data
:pages-index
page-id
:options
:flows] [])
selected (wsh/lookup-selected state)
unames (into #{} (map :name flows))
name (dwc/generate-unique-name unames "Flow-1")
new-flow {:id (uuid/next)
:name name
:starting-frame (first selected)}]
(rx/of (dch/commit-changes
{:redo-changes [{:type :set-option
:page-id page-id
:option :flows
:value (cto/add-flow flows new-flow)}]
:undo-changes [{:type :set-option
:page-id page-id
:option :flows
:value flows}]
:origin it}))))))
(defn remove-flow
[flow-id]
(us/verify ::us/uuid flow-id)
(ptk/reify ::remove-flow
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
flows (get-in state [:workspace-data
:pages-index
page-id
:options
:flows] [])]
(rx/of (dch/commit-changes
{:redo-changes [{:type :set-option
:page-id page-id
:option :flows
:value (cto/remove-flow flows flow-id)}]
:undo-changes [{:type :set-option
:page-id page-id
:option :flows
:value flows}]
:origin it}))))))
(defn rename-flow
[flow-id name]
(us/verify ::us/uuid flow-id)
(us/verify ::us/string name)
(ptk/reify ::rename-flow
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
flows (get-in state [:workspace-data
:pages-index
page-id
:options
:flows] [])]
(rx/of (dch/commit-changes
{:redo-changes [{:type :set-option
:page-id page-id
:option :flows
:value (cto/update-flow flows flow-id
#(cto/rename-flow % name))}]
:undo-changes [{:type :set-option
:page-id page-id
:option :flows
:value flows}]
:origin it}))))))
(defn start-rename-flow
[id]
(us/verify ::us/uuid id)
(ptk/reify ::start-rename-flow
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :flow-for-rename] id))))
(defn end-rename-flow
[]
(ptk/reify ::end-rename-flow
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :flow-for-rename))))
;; --- Interactions
(declare move-edit-interaction)
(declare finish-edit-interaction)
(defn start-edit-interaction
[index]
(ptk/reify ::start-edit-interaction
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :editing-interaction-index] index))
ptk/WatchEvent
(watch [_ state stream]
(let [initial-pos @ms/mouse-position
selected (wsh/lookup-selected state)
stopper (rx/filter ms/mouse-up? stream)]
(when (= 1 (count selected))
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-edit-interaction initial-pos %)))
(rx/of (finish-edit-interaction index initial-pos))))))))
(defn move-edit-interaction
[initial-pos position]
(ptk/reify ::move-edit-interaction
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected-shape-id (-> state wsh/lookup-selected first)
selected-shape (get objects selected-shape-id)
selected-shape-frame-id (:frame-id selected-shape)
start-frame (get objects selected-shape-frame-id)
end-frame (dwc/get-frame-at-point objects position)]
(cond-> state
(not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position)
(not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame))))))
(defn finish-edit-interaction
[index initial-pos]
(ptk/reify ::finish-edit-interaction
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :editing-interaction-index] nil)
(assoc-in [:workspace-local :draw-interaction-to] nil)
(assoc-in [:workspace-local :draw-interaction-to-frame] nil)))
ptk/WatchEvent
(watch [_ state _]
(let [position @ms/mouse-position
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
frame (dwc/get-frame-at-point objects position)
shape-id (-> state wsh/lookup-selected first)
shape (get objects shape-id)]
(when (and shape (not (= position initial-pos)))
(rx/of (dch/update-shapes [shape-id]
(fn [shape]
(update shape :interactions
(fn [interactions]
(if-not frame
;; Drop in an empty space -> remove interaction
(if index
(into (subvec interactions 0 index)
(subvec interactions (inc index)))
interactions)
(let [frame (if (or (= (:id frame) (:id shape))
(= (:id frame) (:frame-id shape)))
nil ;; Drop onto self frame -> set destination to none
frame)]
;; Update or create interaction
(if (and index (cti/has-destination (get interactions index)))
(update interactions index
#(cti/set-destination % (:id frame)))
(conj (or interactions [])
(cti/set-destination cti/default-interaction
(:id frame))))))))))))))))
;; --- Overlays
(declare move-overlay-pos)
(declare finish-move-overlay-pos)
(defn start-move-overlay-pos
[index]
(ptk/reify ::start-move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :move-overlay-to] nil)
(assoc-in [:workspace-local :move-overlay-index] index)))
ptk/WatchEvent
(watch [_ state stream]
(let [initial-pos @ms/mouse-position
selected (wsh/lookup-selected state)
stopper (rx/filter ms/mouse-up? stream)]
(when (= 1 (count selected))
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shape (->> state
wsh/lookup-selected
first
(get objects))
overlay-pos (-> shape
(get-in [:interactions index])
:overlay-position)
orig-frame (cph/get-frame shape objects)
frame-pos (gpt/point (:x orig-frame) (:y orig-frame))
offset (-> initial-pos
(gpt/subtract overlay-pos)
(gpt/subtract frame-pos))]
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-overlay-pos % frame-pos offset)))
(rx/of (finish-move-overlay-pos index frame-pos offset)))))))))
(defn move-overlay-pos
[pos frame-pos offset]
(ptk/reify ::move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(let [pos (-> pos
(gpt/subtract frame-pos)
(gpt/subtract offset))]
(assoc-in state [:workspace-local :move-overlay-to] pos)))))
(defn finish-move-overlay-pos
[index frame-pos offset]
(ptk/reify ::finish-move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(-> state
(d/dissoc-in [:workspace-local :move-overlay-to])
(d/dissoc-in [:workspace-local :move-overlay-index])))
ptk/WatchEvent
(watch [_ state _]
(let [pos @ms/mouse-position
overlay-pos (-> pos
(gpt/subtract frame-pos)
(gpt/subtract offset))
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shape (->> state
wsh/lookup-selected
first
(get objects))
interactions (:interactions shape)
new-interactions
(update interactions index
#(cti/set-overlay-position % overlay-pos))]
(rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions})))))))

View file

@ -41,8 +41,7 @@
(s/keys :req-un [::file-id]))
(s/def ::viewer-query-params
(s/keys :req-un [::index]
:opt-un [::share-id ::section ::page-id]))
(s/keys :opt-un [::index ::share-id ::section ::page-id]))
(def routes
[["/auth"

View file

@ -13,14 +13,14 @@
[app.main.ui.components.fullscreen :as fs]
[app.main.ui.icons :as i]
[app.main.ui.viewer.comments :refer [comments-menu]]
[app.main.ui.viewer.interactions :refer [interactions-menu]]
[app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]]
[app.main.ui.workspace.header :refer [zoom-widget]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
(mf/defc header-options
[{:keys [section zoom page file permissions]}]
[{:keys [section zoom page file index permissions]}]
(let [fullscreen (mf/use-ctx fs/fullscreen-context)
toggle-fullscreen
@ -43,7 +43,9 @@
[:div.options-zone
(case section
:interactions [:& interactions-menu]
:interactions [:*
[:& flows-menu {:page page :index index}]
[:& interactions-menu]]
:comments [:& comments-menu]
[:div.view-options])
@ -133,7 +135,6 @@
(fn [section]
(st/emit! (dv/go-to-section section)))]
[:header.viewer-header
[:div.main-icon
[:a {:on-click go-to-dashboard
@ -167,5 +168,6 @@
:permissions permissions
:page page
:file file
:index index
:zoom zoom}]]))

View file

@ -10,6 +10,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.pages :as cp]
[app.common.types.page-options :as cto]
[app.main.data.comments :as dcm]
[app.main.data.viewer :as dv]
[app.main.refs :as refs]
@ -99,6 +100,38 @@
:view-box (:vbox size)}]]]]))
(mf/defc flows-menu
{::mf/wrap [mf/memo]}
[{:keys [page index]}]
(let [flows (get-in page [:options :flows])
frames (:frames page)
frame (get frames index)
current-flow (cto/get-frame-flow flows (:id frame))
show-dropdown? (mf/use-state false)
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
select-flow
(mf/use-callback
(fn [flow]
(st/emit! (dv/go-to-frame (:starting-frame flow)))))]
(when (seq flows)
[:div.view-options {:on-click toggle-dropdown}
[:span.icon i/play]
[:span.label (:name current-flow)]
[:span.icon i/arrow-down]
[:& dropdown {:show @show-dropdown?
:on-close hide-dropdown}
[:ul.dropdown.with-check
(for [flow flows]
[:li {:class (dom/classnames :selected (= (:id flow) (:id current-flow)))
:on-click #(select-flow flow)}
[:span.icon i/tick]
[:span.label (:name flow)]])]]])))
(mf/defc interactions-menu
[]
(let [local (mf/deref refs/viewer-local)

View file

@ -7,8 +7,10 @@
(ns app.main.ui.workspace.context-menu
"A workspace specific context menu (mouse right click)."
(:require
[app.common.types.page-options :as cto]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.shortcuts :as sc]
[app.main.data.workspace.undo :as dwu]
@ -95,6 +97,11 @@
is-group? (and (some? shape) (= :group (:type shape)))
is-bool? (and (some? shape) (= :bool (:type shape)))
options (mf/deref refs/workspace-page-options)
flows (:flows options)
options-mode (mf/deref refs/options-mode)
set-bool
(fn [bool-type]
#(cond
@ -122,6 +129,8 @@
do-hide-shape (st/emitf (dw/update-shape-flags id {:hidden true}))
do-lock-shape (st/emitf (dw/update-shape-flags id {:blocked true}))
do-unlock-shape (st/emitf (dw/update-shape-flags id {:blocked false}))
do-add-flow (st/emitf (dwi/add-flow-selected-frame))
do-remove-flow #(st/emitf (dwi/remove-flow (:id %)))
do-create-group (st/emitf dw/group-selected)
do-remove-group (st/emitf dw/ungroup-selected)
do-mask-group (st/emitf dw/mask-group)
@ -262,6 +271,14 @@
[:& menu-entry {:title (tr "workspace.shape.menu.lock")
:on-click do-lock-shape}])
(when (and (= options-mode :prototype) (= (:type shape) :frame))
(let [flow (cto/get-frame-flow flows (:id shape))]
(if (nil? flow)
[:& menu-entry {:title (tr "workspace.shape.menu.flow-start")
:on-click do-add-flow}]
[:& menu-entry {:title (tr "workspace.shape.menu.delete-flow-start")
:on-click (do-remove-flow flow)}])))
(when (and (or (nil? (:shape-ref shape))
(> (count selected) 1))
(not= (:type shape) :frame))

View file

@ -19,6 +19,7 @@
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.timers :as ts]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]))
@ -68,7 +69,8 @@
(on-stop-edit)
(swap! local assoc :edition false)
(st/emit! (dw/end-rename-shape)
(dw/update-shape (:id shape) {:name name}))))
(when-not (str/empty? name)
(dw/update-shape (:id shape) {:name name})))))
cancel-edit (fn []
(on-stop-edit)

View file

@ -9,14 +9,19 @@
[app.common.data :as d]
[app.common.pages :as cp]
[app.common.types.interactions :as cti]
[app.common.types.page-options :as cto]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.numeric-input :refer [numeric-input]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]))
(defn- event-type-names
@ -68,6 +73,92 @@
:bottom-right (tr "workspace.options.interaction-pos-bottom-right")
:bottom-center (tr "workspace.options.interaction-pos-bottom-center")})
(def flow-for-rename-ref
(l/derived (l/in [:workspace-local :flow-for-rename]) st/state))
(mf/defc flow-item
[{:keys [flow]}]
(let [editing? (mf/use-state false)
flow-for-rename (mf/deref flow-for-rename-ref)
name-ref (mf/use-ref)
start-edit (fn []
(reset! editing? true))
accept-edit (fn []
(let [name-input (mf/ref-val name-ref)
name (dom/get-value name-input)]
(reset! editing? false)
(st/emit! (dwi/end-rename-flow)
(when-not (str/empty? name)
(dwi/rename-flow (:id flow) name)))))
cancel-edit (fn []
(reset! editing? false)
(st/emit! (dwi/end-rename-flow)))
on-key-down (fn [event]
(when (kbd/enter? event) (accept-edit))
(when (kbd/esc? event) (cancel-edit)))]
(mf/use-effect
(fn []
#(when editing?
(cancel-edit))))
(mf/use-effect
(mf/deps flow-for-rename)
#(when (and (= flow-for-rename (:id flow))
(not @editing?))
(start-edit)))
(mf/use-effect
(mf/deps @editing?)
#(when @editing?
(let [name-input (mf/ref-val name-ref)]
(dom/select-text! name-input))
nil))
[:div.flow-element
[:div.flow-button {:on-click (st/emitf (dw/select-shape (:starting-frame flow)))}
i/play]
(if @editing?
[:input.element-name
{:type "text"
:ref name-ref
:on-blur accept-edit
:on-key-down on-key-down
:auto-focus true
:default-value (:name flow "")}]
[:span.element-label.flow-name
{:on-double-click (st/emitf (dwi/start-rename-flow (:id flow)))}
(:name flow)])
[:div.add-page {:on-click (st/emitf (dwi/remove-flow (:id flow)))}
i/minus]]))
(mf/defc page-flows
[{:keys [flows]}]
(when (seq flows)
[:div.element-set.interactions-options
[:div.element-set-title
[:span (tr "workspace.options.flows.flow-starts")]]
(for [flow flows]
[:& flow-item {:flow flow :key (str (:id flow))}])]))
(mf/defc shape-flows
[{:keys [flows shape]}]
(when (= (:type shape) :frame)
(let [flow (cto/get-frame-flow flows (:id shape))]
[:div.element-set.interactions-options
[:div.element-set-title
[:span (tr "workspace.options.flows.flow-start")]]
(if (nil? flow)
[:div.flow-element
[:span.element-label (tr "workspace.options.flows.add-flow-start")]
[:div.add-page {:on-click (st/emitf (dwi/add-flow-selected-frame))}
i/plus]]
[:& flow-item {:flow flow :key (str (:id flow))}])])))
(mf/defc interaction-entry
[{:keys [index shape interaction update-interaction remove-interaction]}]
(let [objects (deref refs/workspace-page-objects)
@ -184,7 +275,9 @@
[:select.input-select
{:value (str (:destination interaction))
:on-change change-destination}
[:option {:value ""} (tr "workspace.options.interaction-none")]
(if (= (:action-type interaction) :close-overlay)
[:option {:value ""} (tr "workspace.options.interaction-self")]
[:option {:value ""} (tr "workspace.options.interaction-none")])
(for [frame frames]
(when (and (not= (:id frame) (:id shape)) ; A frame cannot navigate to itself
(not= (:id frame) (:frame-id shape))) ; nor a shape to its container frame
@ -263,6 +356,9 @@
[{:keys [shape] :as props}]
(let [interactions (get shape :interactions [])
options (mf/deref refs/workspace-page-options)
flows (:flows options)
add-interaction
(fn [_]
(let [new-interactions (conj interactions cti/default-interaction)]
@ -280,13 +376,18 @@
(let [new-interactions (update interactions index update-fn)]
(st/emit! (dw/update-shape (:id shape) {:interactions new-interactions})))) ]
[:*
(if shape
[:& shape-flows {:flows flows
:shape shape}]
[:& page-flows {:flows flows}])
[:div.element-set.interactions-options
(when shape
[:div.element-set-title
[:span (tr "workspace.options.interactions")]
[:div.add-page {:on-click add-interaction}
i/plus]])
[:div.element-set-content
(when (= (count interactions) 0)
[:*
@ -304,5 +405,5 @@
:shape shape
:interaction interaction
:update-interaction update-interaction
:remove-interaction remove-interaction}])]]))
:remove-interaction remove-interaction}])]]]))

View file

@ -159,13 +159,9 @@
(hooks/setup-shortcuts node-editing? drawing-path?)
(hooks/setup-active-frames objects vbox hover active-frames)
[:div.viewport
[:div.viewport-overlays
[:& wtr/frame-renderer {:objects objects
:background background}]
@ -273,6 +269,17 @@
:on-frame-leave on-frame-leave
:on-frame-select on-frame-select}]
(when show-prototypes?
[:& widgets/frame-flows
{:flows (:flows options)
:objects objects
:selected selected
:zoom zoom
:modifiers modifiers
:on-frame-enter on-frame-enter
:on-frame-leave on-frame-leave
:on-frame-select on-frame-select}])
(when show-gradient-handlers?
[:& gradients/gradient-handlers
{:id (first selected)

View file

@ -10,10 +10,12 @@
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.streams :as ms]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.main.ui.workspace.viewport.path-actions :refer [path-actions]]
[app.util.dom :as dom]
[rumext.alpha :as mf]))
@ -156,3 +158,75 @@
:on-frame-enter on-frame-enter
:on-frame-leave on-frame-leave
:on-frame-select on-frame-select}])]))
(mf/defc frame-flow
[{:keys [flow frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}]
(let [{:keys [x y]} (gsh/transform-shape frame)
flow-pos (gpt/point x (- y (/ 35 zoom)))
on-mouse-down
(mf/use-callback
(mf/deps (:id frame) on-frame-select)
(fn [bevent]
(let [event (.-nativeEvent bevent)]
(when (= 1 (.-which event))
(dom/prevent-default event)
(dom/stop-propagation event)
(on-frame-select event (:id frame))))))
on-double-click
(mf/use-callback
(mf/deps (:id frame))
(st/emitf (dwi/start-rename-flow (:id flow))))
on-pointer-enter
(mf/use-callback
(mf/deps (:id frame) on-frame-enter)
(fn [_]
(on-frame-enter (:id frame))))
on-pointer-leave
(mf/use-callback
(mf/deps (:id frame) on-frame-leave)
(fn [_]
(on-frame-leave (:id frame))))]
[:foreignObject {:x 0
:y -15
:width 100000
:height 24
:transform (str (when (and selected? modifiers)
(str (:displacement modifiers) " " ))
(text-transform flow-pos zoom))}
[:div.flow-badge {:class (dom/classnames :selected selected?)}
[:div.content {:on-mouse-down on-mouse-down
:on-double-click on-double-click
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
i/play
[:span (:name flow)]]]]))
(mf/defc frame-flows
{::mf/wrap-props false}
[props]
(let [flows (unchecked-get props "flows")
objects (unchecked-get props "objects")
zoom (unchecked-get props "zoom")
modifiers (unchecked-get props "modifiers")
selected (or (unchecked-get props "selected") #{})
on-frame-enter (unchecked-get props "on-frame-enter")
on-frame-leave (unchecked-get props "on-frame-leave")
on-frame-select (unchecked-get props "on-frame-select")]
[:g.frame-flows
(for [flow flows]
(let [frame (get objects (:starting-frame flow))]
[:& frame-flow {:flow flow
:frame frame
:selected? (contains? selected (:id frame))
:zoom zoom
:modifiers modifiers
:on-frame-enter on-frame-enter
:on-frame-leave on-frame-leave
:on-frame-select on-frame-select}]))]))

View file

@ -2422,6 +2422,18 @@ msgstr "Rotation"
msgid "workspace.options.add-interaction"
msgstr "Click the + button to add interactions."
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.flows.add-flow-start"
msgstr "Add flow start"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.flows.flow-start"
msgstr "Flow start"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.flows.flow-starts"
msgstr "Flow starts"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-action"
msgstr "Action"
@ -2857,6 +2869,10 @@ msgstr "Cut"
msgid "workspace.shape.menu.delete"
msgstr "Delete"
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.delete-flow-start"
msgstr "Delete flow start"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.detach-instance"
msgstr "Detach instance"
@ -2877,6 +2893,10 @@ msgstr "Flip horizontal"
msgid "workspace.shape.menu.flip-vertical"
msgstr "Flip vertical"
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.flow-start"
msgstr "Flow start"
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.forward"
msgstr "Bring forward"

View file

@ -2308,6 +2308,18 @@ msgstr "Rotación"
msgid "workspace.options.add-interaction"
msgstr "Pulsa el botón + para añadir interacciones."
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.flows.add-flow-start"
msgstr "Añadir inicio de flujo"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.flows.flow-start"
msgstr "Inicio de flujo"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.flows.flow-starts"
msgstr "Inicios de flujo"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-action"
msgstr "Acción"
@ -2745,6 +2757,10 @@ msgstr "Cortar"
msgid "workspace.shape.menu.delete"
msgstr "Eliminar"
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.delete-flow-start"
msgstr "Eliminar inicio de flujo"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.detach-instance"
msgstr "Desacoplar instancia"
@ -2765,6 +2781,10 @@ msgstr "Voltear horizontal"
msgid "workspace.shape.menu.flip-vertical"
msgstr "Voltear vertical"
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.flow-start"
msgstr "Inicio de flujo"
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.forward"
msgstr "Mover hacia delante"