diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index c30ed5bf2..e438a54f5 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -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 [] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 3239aff0a..362a461f3 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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 diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index c3b54d8bb..c98628bb4 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -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 diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 39d90d175..d4850808c 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -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])))] diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index ca0d1b0e2..aaa267bd5 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -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}])))]]))