mirror of
https://github.com/penpot/penpot.git
synced 2025-02-03 04:49:03 -05:00
🎉 Add interaction overlays
This commit is contained in:
parent
c7252a950b
commit
b4b2f91363
5 changed files with 209 additions and 94 deletions
|
@ -31,6 +31,7 @@
|
|||
:comments-show :unresolved
|
||||
:selected #{}
|
||||
:collapsed #{}
|
||||
:overlays []
|
||||
:hover nil})
|
||||
|
||||
(declare fetch-comment-threads)
|
||||
|
@ -286,7 +287,7 @@
|
|||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :interactions-show?] false))))
|
||||
|
||||
;; --- Navigation
|
||||
;; --- Navigation inside page
|
||||
|
||||
(defn go-to-frame-by-index
|
||||
[index]
|
||||
|
@ -324,6 +325,35 @@
|
|||
qparams (:query-params route)]
|
||||
(rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
|
||||
|
||||
;; --- Overlays
|
||||
|
||||
(defn open-overlay
|
||||
[frame-id]
|
||||
(us/verify ::us/uuid frame-id)
|
||||
(ptk/reify ::open-overlay
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
frames (get-in state [:viewer :pages page-id :frames])
|
||||
frame (d/seek #(= (:id %) frame-id) frames)
|
||||
overlays (get-in state [:viewer-local :overlays])]
|
||||
(if-not (some #(= % frame) overlays)
|
||||
(update-in state [:viewer-local :overlays] conj frame)
|
||||
state)))))
|
||||
|
||||
(defn close-overlay
|
||||
[frame-id]
|
||||
(ptk/reify ::close-overlay
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:viewer-local :overlays]
|
||||
(fn [overlays]
|
||||
(remove #(= (:id %) frame-id) overlays))))))
|
||||
|
||||
;; --- Objects selection
|
||||
|
||||
(defn deselect-all []
|
||||
(ptk/reify ::deselect-all
|
||||
ptk/UpdateEvent
|
||||
|
@ -397,7 +427,7 @@
|
|||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :hover] (when hover? id)))))
|
||||
|
||||
;; --- NAV
|
||||
;; --- Navigation outside page
|
||||
|
||||
(defn go-to-dashboard
|
||||
[]
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
:selected (d/ordered-set)
|
||||
:expanded {}
|
||||
:tooltip nil
|
||||
:options-mode :prototype ; OJOOOOOOOOOOOOOOOOOOOOOO============== :design
|
||||
:options-mode :design
|
||||
:draw-interaction-to nil
|
||||
:left-sidebar? true
|
||||
:right-sidebar? true
|
||||
|
@ -1751,12 +1751,16 @@
|
|||
;; Interactions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare move-create-interaction)
|
||||
(declare finish-create-interaction)
|
||||
(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))
|
||||
|
||||
(defn start-create-interaction
|
||||
[]
|
||||
(ptk/reify ::start-create-interaction
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [initial-pos @ms/mouse-position
|
||||
|
@ -1766,12 +1770,12 @@
|
|||
(rx/concat
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until stopper)
|
||||
(rx/map #(move-create-interaction initial-pos %)))
|
||||
(rx/of (finish-create-interaction initial-pos))))))))
|
||||
(rx/map #(move-edit-interaction initial-pos %)))
|
||||
(rx/of (finish-edit-interaction index initial-pos))))))))
|
||||
|
||||
(defn move-create-interaction
|
||||
(defn move-edit-interaction
|
||||
[initial-pos position]
|
||||
(ptk/reify ::move-create-interaction
|
||||
(ptk/reify ::move-edit-interaction
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (:current-page-id state)
|
||||
|
@ -1785,12 +1789,13 @@
|
|||
(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-create-interaction
|
||||
[initial-pos]
|
||||
(ptk/reify ::finish-create-interaction
|
||||
(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)))
|
||||
|
||||
|
@ -1809,14 +1814,22 @@
|
|||
(fn [shape]
|
||||
(update shape :interactions
|
||||
(fn [interactions]
|
||||
(if (and frame
|
||||
(not= (:id frame) (:id shape))
|
||||
(not= (:id frame) (:frame-id shape)))
|
||||
(conj interactions
|
||||
(assoc spec/default-interaction
|
||||
:destination (:id frame)))
|
||||
(vec (remove #(= (:action-type %) :navigate)
|
||||
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 index
|
||||
(assoc-in interactions [index :destination] (:id frame))
|
||||
(conj (or interactions [])
|
||||
(assoc spec/default-interaction
|
||||
:destination (:id frame))))))))))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; CANVAS OPTIONS
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.shapes.filters :as filters]
|
||||
[app.main.ui.share-link]
|
||||
[app.main.ui.static :as static]
|
||||
[app.main.ui.viewer.comments :refer [comments-layer]]
|
||||
|
@ -27,9 +28,15 @@
|
|||
|
||||
(defn- calculate-size
|
||||
[frame zoom]
|
||||
{:width (* (:width frame) zoom)
|
||||
:height (* (:height frame) zoom)
|
||||
:vbox (str "0 0 " (:width frame 0) " " (:height frame 0))})
|
||||
(let [{:keys [_ _ width height]} (filters/get-filters-bounds frame)]
|
||||
{:width (* width zoom)
|
||||
:height (* height zoom)
|
||||
:vbox (str "0 0 " width " " height)}))
|
||||
|
||||
(defn- position-overlay
|
||||
[size size-over]
|
||||
{:x (/ (- (:width size) (:width size-over)) 2)
|
||||
:y (/ (- (:height size) (:height size-over)) 2)})
|
||||
|
||||
(mf/defc viewer
|
||||
[{:keys [params data]}]
|
||||
|
@ -137,7 +144,24 @@
|
|||
:page page
|
||||
:file file
|
||||
:users users
|
||||
:local local}]]))]]]))
|
||||
:local local}]
|
||||
|
||||
(for [overlay (:overlays local)]
|
||||
(let [size-over (calculate-size overlay zoom)
|
||||
pos-over (position-overlay size size-over)]
|
||||
[:div.viewport-container
|
||||
{:style {:width (:width size-over)
|
||||
:height (:height size-over)
|
||||
:position "absolute"
|
||||
:left (:x pos-over)
|
||||
:top (:y pos-over)}}
|
||||
[:& interactions/viewport
|
||||
{:frame overlay
|
||||
:size size-over
|
||||
:page page
|
||||
:file file
|
||||
:users users
|
||||
:local local}]]))]))]]]))
|
||||
|
||||
;; --- Component: Viewer Page
|
||||
|
||||
|
|
|
@ -28,14 +28,27 @@
|
|||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn on-mouse-down
|
||||
[event interactions]
|
||||
(let [interaction (first (filter #(= (:event-type %) :click) interactions))]
|
||||
[event shape]
|
||||
(doseq [interaction (->> (:interactions shape)
|
||||
(filter #(= (:event-type %) :click)))]
|
||||
|
||||
(case (:action-type interaction)
|
||||
:navigate
|
||||
(let [frame-id (:destination interaction)]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/go-to-frame frame-id)))
|
||||
|
||||
:open-overlay
|
||||
(let [frame-id (:destination interaction)]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/open-overlay frame-id)))
|
||||
|
||||
:close-overlay
|
||||
(let [frame-id (or (:destination interaction)
|
||||
(:frame-id shape))]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/close-overlay frame-id)))
|
||||
|
||||
nil)))
|
||||
|
||||
(mf/defc interaction
|
||||
|
@ -61,17 +74,15 @@
|
|||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
objects (unchecked-get props "objects")
|
||||
childs (unchecked-get props "childs")
|
||||
frame (unchecked-get props "frame")
|
||||
|
||||
interactions (->> (:interactions shape)
|
||||
(filter #(contains? objects (:destination %))))
|
||||
interactions (:interactions shape)
|
||||
|
||||
on-mouse-down (mf/use-callback
|
||||
(mf/deps interactions)
|
||||
(mf/deps shape)
|
||||
(fn [event]
|
||||
(on-mouse-down event interactions)))
|
||||
(on-mouse-down event shape)))
|
||||
|
||||
svg-element? (and (= :svg-raw (:type shape))
|
||||
(not= :svg (get-in shape [:content :tag])))]
|
||||
|
|
|
@ -7,24 +7,20 @@
|
|||
(ns app.main.ui.workspace.viewport.interactions
|
||||
"Visually show shape interactions in workspace"
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.viewport.outline :refer [outline]]
|
||||
[app.util.dom :as dom]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]
|
||||
))
|
||||
|
||||
(defn- get-click-interaction
|
||||
[shape]
|
||||
(first (filter #(= (:event-type %) :click) (:interactions shape))))
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- on-mouse-down
|
||||
[event {:keys [id] :as shape}]
|
||||
[event index {:keys [id] :as shape}]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dw/select-shape id))
|
||||
(st/emit! (dw/start-create-interaction)))
|
||||
(st/emit! (dw/start-edit-interaction index)))
|
||||
|
||||
(defn connect-to-shape
|
||||
"Calculate the best position to draw an interaction line
|
||||
|
@ -84,38 +80,56 @@
|
|||
|
||||
|
||||
(mf/defc interaction-marker
|
||||
[{:keys [x y arrow-dir zoom] :as props}]
|
||||
(let [arrow-pdata (case arrow-dir
|
||||
:right "M -5 0 l 8 0 l -4 -4 m 4 4 l -4 4"
|
||||
:left "M 5 0 l -8 0 l 4 -4 m -4 4 l 4 4"
|
||||
[])
|
||||
[{:keys [x y stroke action-type arrow-dir zoom] :as props}]
|
||||
(let [icon-pdata (case action-type
|
||||
:navigate (case arrow-dir
|
||||
:right "M -5 0 l 8 0 l -4 -4 m 4 4 l -4 4"
|
||||
:left "M 5 0 l -8 0 l 4 -4 m -4 4 l 4 4"
|
||||
nil)
|
||||
:open-overlay (case arrow-dir
|
||||
;; TODO: have a different icon for open overlay?
|
||||
:right "M -5 0 l 8 0 l -4 -4 m 4 4 l -4 4"
|
||||
:left "M 5 0 l -8 0 l 4 -4 m -4 4 l 4 4"
|
||||
nil)
|
||||
|
||||
:close-overlay "M -4 -4 L 4 4 M -4 4 L 4 -4"
|
||||
|
||||
nil)
|
||||
inv-zoom (/ 1 zoom)]
|
||||
[:*
|
||||
[:circle {:cx 0
|
||||
:cy 0
|
||||
:r 8
|
||||
:stroke "#31EFB8"
|
||||
:stroke stroke
|
||||
:stroke-width 2
|
||||
:fill "#FFFFFF"
|
||||
:transform (str
|
||||
"scale(" inv-zoom ", " inv-zoom ") "
|
||||
"translate(" (* zoom x) ", " (* zoom y) ")")}]
|
||||
(when arrow-dir
|
||||
[:path {:stroke "#31EFB8"
|
||||
(when icon-pdata
|
||||
[:path {:stroke stroke
|
||||
:fill "none"
|
||||
:stroke-width 2
|
||||
:d arrow-pdata
|
||||
:d icon-pdata
|
||||
:transform (str
|
||||
"scale(" inv-zoom ", " inv-zoom ") "
|
||||
"translate(" (* zoom x) ", " (* zoom y) ")")}])]))
|
||||
|
||||
|
||||
(mf/defc interaction-path
|
||||
[{:keys [orig-shape dest-shape dest-point selected? zoom] :as props}]
|
||||
[{:keys [index orig-shape dest-shape dest-point selected? action-type zoom] :as props}]
|
||||
(let [[orig-pos orig-x orig-y dest-pos dest-x dest-y]
|
||||
(if dest-shape
|
||||
(cond
|
||||
dest-shape
|
||||
(connect-to-shape orig-shape dest-shape)
|
||||
(connect-to-point orig-shape dest-point))
|
||||
|
||||
dest-point
|
||||
(connect-to-point orig-shape dest-point)
|
||||
|
||||
:else
|
||||
(connect-to-point orig-shape
|
||||
{:x (+ (:x2 (:selrect orig-shape)) 100)
|
||||
:y (- (:y1 (:selrect orig-shape)) 50)}))
|
||||
|
||||
orig-dx (if (= orig-pos :right) 100 -100)
|
||||
dest-dx (if (= dest-pos :right) 100 -100)
|
||||
|
@ -126,25 +140,38 @@
|
|||
arrow-dir (if (= dest-pos :left) :right :left)]
|
||||
|
||||
(if-not selected?
|
||||
[:path {:stroke "#B1B2B5"
|
||||
:fill "none"
|
||||
:pointer-events "visible"
|
||||
:stroke-width (/ 2 zoom)
|
||||
:d pdata
|
||||
:on-mouse-down #(on-mouse-down % orig-shape)}]
|
||||
[:g {:on-mouse-down #(on-mouse-down % index orig-shape)}
|
||||
[:path {:stroke "#B1B2B5"
|
||||
:fill "none"
|
||||
:pointer-events "visible"
|
||||
:stroke-width (/ 2 zoom)
|
||||
:d pdata}]
|
||||
(when (and (not dest-shape)
|
||||
(= action-type :close-overlay))
|
||||
[:& interaction-marker {:index index
|
||||
:x dest-x
|
||||
:y dest-y
|
||||
:stroke "#B1B2B5"
|
||||
:action-type action-type
|
||||
:arrow-dir arrow-dir
|
||||
:zoom zoom}])]
|
||||
|
||||
[:g {:on-mouse-down #(on-mouse-down % orig-shape)}
|
||||
[:g {:on-mouse-down #(on-mouse-down % index orig-shape)}
|
||||
[:path {:stroke "#31EFB8"
|
||||
:fill "none"
|
||||
:pointer-events "visible"
|
||||
:stroke-width (/ 2 zoom)
|
||||
:d pdata}]
|
||||
[:& interaction-marker {:x orig-x
|
||||
[:& interaction-marker {:index index
|
||||
:x orig-x
|
||||
:y orig-y
|
||||
:arrow-dir nil
|
||||
:stroke "#31EFB8"
|
||||
:zoom zoom}]
|
||||
[:& interaction-marker {:x dest-x
|
||||
[:& interaction-marker {:index index
|
||||
:x dest-x
|
||||
:y dest-y
|
||||
:stroke "#31EFB8"
|
||||
:action-type action-type
|
||||
:arrow-dir arrow-dir
|
||||
:zoom zoom}]
|
||||
|
||||
|
@ -154,13 +181,15 @@
|
|||
|
||||
|
||||
(mf/defc interaction-handle
|
||||
[{:keys [shape zoom] :as props}]
|
||||
[{:keys [index shape zoom] :as props}]
|
||||
(let [shape-rect (:selrect shape)
|
||||
handle-x (+ (:x shape-rect) (:width shape-rect))
|
||||
handle-y (+ (:y shape-rect) (/ (:height shape-rect) 2))]
|
||||
[:g {:on-mouse-down #(on-mouse-down % shape)}
|
||||
[:g {:on-mouse-down #(on-mouse-down % index shape)}
|
||||
[:& interaction-marker {:x handle-x
|
||||
:y handle-y
|
||||
:stroke "#31EFB8"
|
||||
:action-type :navigate
|
||||
:arrow-dir :right
|
||||
:zoom zoom}]]))
|
||||
|
||||
|
@ -171,8 +200,9 @@
|
|||
zoom (mf/deref refs/selected-zoom)
|
||||
current-transform (:transform local)
|
||||
objects (mf/deref refs/workspace-page-objects)
|
||||
active-shapes (filter #(first (get-click-interaction %)) (vals objects))
|
||||
active-shapes (filter #(seq (:interactions %)) (vals objects))
|
||||
selected-shapes (map #(get objects %) selected)
|
||||
editing-interaction-index (:editing-interaction-index local)
|
||||
draw-interaction-to (:draw-interaction-to local)
|
||||
draw-interaction-to-frame (:draw-interaction-to-frame local)
|
||||
first-selected (first selected-shapes)]
|
||||
|
@ -180,39 +210,46 @@
|
|||
[:g.interactions
|
||||
[:g.non-selected
|
||||
(for [shape active-shapes]
|
||||
(let [interaction (get-click-interaction shape)
|
||||
dest-shape (get objects (:destination interaction))
|
||||
selected? (contains? selected (:id shape))]
|
||||
(when-not (or selected? (not dest-shape))
|
||||
[:& interaction-path {:key (:id shape)
|
||||
:orig-shape shape
|
||||
:dest-shape dest-shape
|
||||
:selected selected
|
||||
:selected? false
|
||||
:zoom zoom}])))]
|
||||
(for [[index interaction] (d/enumerate (:interactions shape))]
|
||||
(let [dest-shape (get objects (:destination interaction))
|
||||
selected? (contains? selected (:id shape))]
|
||||
(when-not selected?
|
||||
[:& interaction-path {:key (str (:id shape) "-" index)
|
||||
:index index
|
||||
:orig-shape shape
|
||||
:dest-shape dest-shape
|
||||
:selected selected
|
||||
:selected? false
|
||||
:action-type (:action-type interaction)
|
||||
:zoom zoom}]))))]
|
||||
|
||||
[:g.selected
|
||||
(if (and draw-interaction-to first-selected)
|
||||
(when (and draw-interaction-to first-selected)
|
||||
[:& interaction-path {:key "interactive"
|
||||
:index nil
|
||||
:orig-shape first-selected
|
||||
:dest-point draw-interaction-to
|
||||
:dest-shape draw-interaction-to-frame
|
||||
:selected? true
|
||||
:zoom zoom}]
|
||||
|
||||
(for [shape selected-shapes]
|
||||
(let [interaction (get-click-interaction shape)
|
||||
dest-shape (get objects (:destination interaction))]
|
||||
(if dest-shape
|
||||
[:& interaction-path {:key (:id shape)
|
||||
:orig-shape shape
|
||||
:dest-shape dest-shape
|
||||
:action-type :navigate
|
||||
:zoom zoom}])
|
||||
(for [shape selected-shapes]
|
||||
(if (seq (:interactions shape))
|
||||
(for [[index interaction] (d/enumerate (:interactions shape))]
|
||||
(when-not (= index editing-interaction-index)
|
||||
(let [dest-shape (get objects (:destination interaction))]
|
||||
[:& interaction-path {:key (str (:id shape) "-" index)
|
||||
:index index
|
||||
:orig-shape shape
|
||||
:dest-shape dest-shape
|
||||
:selected selected
|
||||
:selected? true
|
||||
:action-type (:action-type interaction)
|
||||
:zoom zoom}])))
|
||||
(when (not (#{:move :rotate} current-transform))
|
||||
[:& interaction-handle {:key (:id shape)
|
||||
:index nil
|
||||
:shape shape
|
||||
:selected selected
|
||||
:selected? true
|
||||
:zoom zoom}]
|
||||
(when (not (#{:move :rotate} current-transform))
|
||||
[:& interaction-handle {:key (:id shape)
|
||||
:shape shape
|
||||
:selected selected
|
||||
:zoom zoom}])))))]]))
|
||||
:zoom zoom}])))]]))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue