mirror of
https://github.com/penpot/penpot.git
synced 2025-06-07 01:21:38 +02:00
🎉 Share link & pages on viewer.
This commit is contained in:
parent
3532263af4
commit
c8102f4bff
58 changed files with 1837 additions and 1245 deletions
158
frontend/src/app/main/ui/viewer/comments.cljs
Normal file
158
frontend/src/app/main/ui/viewer/comments.cljs
Normal file
|
@ -0,0 +1,158 @@
|
|||
;; 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.ui.viewer.comments
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
||||
(mf/defc comments-menu
|
||||
[]
|
||||
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
update-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:mode mode}))))
|
||||
|
||||
update-show
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:show mode}))))]
|
||||
|
||||
[:div.view-options {:on-click toggle-dropdown}
|
||||
[:span.label (tr "labels.comments")]
|
||||
[:span.icon i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= :all cmode))
|
||||
:on-click #(update-mode :all)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.show-all-comments")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :yours cmode))
|
||||
:on-click #(update-mode :yours)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.show-your-comments")]]
|
||||
|
||||
[:hr]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :pending cshow))
|
||||
:on-click #(update-show (if (= :pending cshow) :all :pending))}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.hide-resolved-comments")]]]]]))
|
||||
|
||||
|
||||
(defn- frame-contains?
|
||||
[{:keys [x y width height]} {px :x py :y}]
|
||||
(let [x2 (+ x width)
|
||||
y2 (+ y height)]
|
||||
(and (<= x px x2)
|
||||
(<= y py y2))))
|
||||
|
||||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
(def comments-local-ref
|
||||
(l/derived :comments-local st/state))
|
||||
|
||||
(mf/defc comments-layer
|
||||
[{:keys [zoom file users frame page] :as props}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
|
||||
modifier1 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
modifier2 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gmt/translate-matrix))
|
||||
|
||||
threads-map (->> (mf/deref threads-ref)
|
||||
(d/mapm #(update %2 :position gpt/transform modifier1)))
|
||||
|
||||
cstate (mf/deref refs/comments-local)
|
||||
|
||||
mframe (geom/transform-shape frame)
|
||||
threads (->> (vals threads-map)
|
||||
(dcm/apply-filters cstate profile)
|
||||
(filter (fn [{:keys [position]}]
|
||||
(frame-contains? mframe position))))
|
||||
|
||||
on-bubble-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate)
|
||||
(fn [thread]
|
||||
(if (= (:open cstate) (:id thread))
|
||||
(st/emit! (dcm/close-thread))
|
||||
(st/emit! (dcm/open-thread thread)))))
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate frame page file)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (some? (:open cstate))
|
||||
(st/emit! (dcm/close-thread))
|
||||
(let [event (.-nativeEvent ^js event)
|
||||
position (-> (dom/get-offset-position event)
|
||||
(gpt/transform modifier2))
|
||||
params {:position position
|
||||
:page-id (:id page)
|
||||
:file-id (:id file)}]
|
||||
(st/emit! (dcm/create-draft params))))))
|
||||
|
||||
on-draft-cancel
|
||||
(mf/use-callback
|
||||
(mf/deps cstate)
|
||||
(st/emitf (dcm/close-thread)))
|
||||
|
||||
on-draft-submit
|
||||
(mf/use-callback
|
||||
(mf/deps frame)
|
||||
(fn [draft]
|
||||
(let [params (update draft :position gpt/transform modifier2)]
|
||||
(st/emit! (dcm/create-thread params)
|
||||
(dcm/close-thread)))))]
|
||||
|
||||
[:div.comments-section {:on-click on-click}
|
||||
[:div.viewer-comments-container
|
||||
[:div.threads
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open cstate))
|
||||
:key (:seqn item)}])
|
||||
|
||||
(when-let [id (:open cstate)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
[:& cmt/thread-comments {:thread thread
|
||||
:users users
|
||||
:zoom zoom}]))
|
||||
|
||||
(when-let [draft (:draft cstate)]
|
||||
[:& cmt/draft-thread {:draft (update draft :position gpt/transform modifier1)
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
:zoom zoom}])]]]))
|
68
frontend/src/app/main/ui/viewer/handoff.cljs
Normal file
68
frontend/src/app/main/ui/viewer/handoff.cljs
Normal file
|
@ -0,0 +1,68 @@
|
|||
;; 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.ui.viewer.handoff
|
||||
(:require
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]]
|
||||
[app.main.ui.viewer.handoff.render :refer [render-frame-svg]]
|
||||
[app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(defn handle-select-frame
|
||||
[frame]
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/select-shape (:id frame)))))
|
||||
|
||||
(mf/defc viewport
|
||||
[{:keys [local file page frame]}]
|
||||
(let [on-mouse-wheel
|
||||
(fn [event]
|
||||
(when (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
delta (+ (.-deltaY ^js event)
|
||||
(.-deltaX ^js event))]
|
||||
(if (pos? delta)
|
||||
(st/emit! dv/decrease-zoom)
|
||||
(st/emit! dv/increase-zoom)))))
|
||||
|
||||
on-mount
|
||||
(fn []
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
(let [key1 (events/listen goog/global EventType.WHEEL
|
||||
on-mouse-wheel #js {"passive" false})]
|
||||
(fn []
|
||||
(events/unlistenByKey key1))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps (:id frame))
|
||||
(fn []
|
||||
(st/emit! (dv/set-current-frame (:id frame))
|
||||
(dv/select-shape (:id frame)))))
|
||||
|
||||
[:*
|
||||
[:& left-sidebar {:frame frame
|
||||
:local local
|
||||
:page page}]
|
||||
[:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)}
|
||||
[:div.handoff-svg-container
|
||||
[:& render-frame-svg {:frame frame :page page :local local}]]]
|
||||
|
||||
[:& right-sidebar {:frame frame
|
||||
:selected (:selected local)
|
||||
:page page
|
||||
:file file}]]))
|
56
frontend/src/app/main/ui/viewer/handoff/attributes.cljs
Normal file
56
frontend/src/app/main/ui/viewer/handoff/attributes.cljs
Normal file
|
@ -0,0 +1,56 @@
|
|||
;; 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.ui.viewer.handoff.attributes
|
||||
(:require
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.image :refer [image-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.svg :refer [svg-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.text :refer [text-panel]]
|
||||
[app.main.ui.viewer.handoff.exports :refer [exports]]
|
||||
[app.util.i18n :as i18n]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def type->options
|
||||
{:multiple [:fill :stroke :image :text :shadow :blur]
|
||||
:frame [:layout :fill]
|
||||
:group [:layout :svg]
|
||||
:rect [:layout :fill :stroke :shadow :blur :svg]
|
||||
:circle [:layout :fill :stroke :shadow :blur :svg]
|
||||
:path [:layout :fill :stroke :shadow :blur :svg]
|
||||
:image [:image :layout :shadow :blur :svg]
|
||||
:text [:layout :text :shadow :blur]})
|
||||
|
||||
(mf/defc attributes
|
||||
[{:keys [page-id file-id shapes frame]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
shapes (->> shapes (map #(gsh/translate-to-frame % frame)))
|
||||
type (if (= (count shapes) 1) (-> shapes first :type) :multiple)
|
||||
options (type->options type)]
|
||||
[:div.element-options
|
||||
(for [option options]
|
||||
[:> (case option
|
||||
:layout layout-panel
|
||||
:fill fill-panel
|
||||
:stroke stroke-panel
|
||||
:shadow shadow-panel
|
||||
:blur blur-panel
|
||||
:image image-panel
|
||||
:text text-panel
|
||||
:svg svg-panel)
|
||||
{:shapes shapes
|
||||
:frame frame
|
||||
:locale locale}])
|
||||
(when-not (= :multiple type)
|
||||
[:& exports
|
||||
{:shape (first shapes)
|
||||
:page-id page-id
|
||||
:file-id file-id}])]))
|
38
frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs
Normal file
38
frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs
Normal file
|
@ -0,0 +1,38 @@
|
|||
;; 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.ui.viewer.handoff.attributes.blur
|
||||
(:require
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.i18n :refer [t]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn has-blur? [shape]
|
||||
(:blur shape))
|
||||
|
||||
(defn copy-data [shape]
|
||||
(cg/generate-css-props
|
||||
shape
|
||||
:blur
|
||||
{:to-prop "filter"
|
||||
:format #(str/fmt "blur(%spx)" (:value %))}))
|
||||
|
||||
(mf/defc blur-panel [{:keys [shapes locale]}]
|
||||
(let [shapes (->> shapes (filter has-blur?))]
|
||||
(when (seq shapes)
|
||||
[:div.attributes-block
|
||||
[:div.attributes-block-title
|
||||
[:div.attributes-block-title-text (t locale "handoff.attributes.blur")]
|
||||
(when (= (count shapes) 1)
|
||||
[:& copy-button {:data (copy-data (first shapes))}])]
|
||||
|
||||
(for [shape shapes]
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.blur.value")]
|
||||
[:div.attributes-value (-> shape :blur :value) "px"]
|
||||
[:& copy-button {:data (copy-data shape)}]])])))
|
|
@ -0,0 +1,73 @@
|
|||
;; 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.ui.viewer.handoff.attributes.common
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.color-bullet :refer [color-bullet color-name]]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.color :as uc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [t] :as i18n]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
||||
(def file-colors-ref
|
||||
(l/derived (l/in [:viewer-data :file :colors]) st/state))
|
||||
|
||||
(defn make-colors-library-ref [file-id]
|
||||
(let [get-library
|
||||
(fn [state]
|
||||
(get-in state [:viewer-libraries file-id :data :colors]))]
|
||||
#(l/derived get-library st/state)))
|
||||
|
||||
(mf/defc color-row [{:keys [color format copy-data on-change-format]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
|
||||
colors-library-ref (mf/use-memo
|
||||
(mf/deps (:file-id color))
|
||||
(make-colors-library-ref (:file-id color)))
|
||||
colors-library (mf/deref colors-library-ref)
|
||||
|
||||
file-colors (mf/deref file-colors-ref)
|
||||
|
||||
color-library-name (get-in (or colors-library file-colors) [(:id color) :name])]
|
||||
[:div.attributes-color-row
|
||||
(when color-library-name
|
||||
[:div.attributes-color-id
|
||||
[:& color-bullet {:color color}]
|
||||
[:div color-library-name]])
|
||||
|
||||
[:div.attributes-color-value {:class (when color-library-name "hide-color")}
|
||||
[:& color-bullet {:color color}]
|
||||
|
||||
(if (:gradient color)
|
||||
[:& color-name {:color color}]
|
||||
(case format
|
||||
:rgba (let [[r g b a] (->> (uc/hex->rgba (:color color) (:opacity color)) (map #(mth/precision % 2)))]
|
||||
[:div (str/fmt "%s, %s, %s, %s" r g b a)])
|
||||
:hsla (let [[h s l a] (->> (uc/hex->hsla (:color color) (:opacity color)) (map #(mth/precision % 2)))]
|
||||
[:div (str/fmt "%s, %s, %s, %s" h s l a)])
|
||||
[:*
|
||||
[:& color-name {:color color}]
|
||||
(when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])]))
|
||||
|
||||
(when-not (and on-change-format (:gradient color))
|
||||
[:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
|
||||
[:option {:value "hex"}
|
||||
(t locale "handoff.attributes.color.hex")]
|
||||
|
||||
[:option {:value "rgba"}
|
||||
(t locale "handoff.attributes.color.rgba")]
|
||||
|
||||
[:option {:value "hsla"}
|
||||
(t locale "handoff.attributes.color.hsla")]])]
|
||||
(when copy-data
|
||||
[:& copy-button {:data copy-data}])]))
|
||||
|
59
frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs
Normal file
59
frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs
Normal file
|
@ -0,0 +1,59 @@
|
|||
;; 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.ui.viewer.handoff.attributes.fill
|
||||
(:require
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def fill-attributes [:fill-color :fill-color-gradient])
|
||||
|
||||
(defn shape->color [shape]
|
||||
{:color (:fill-color shape)
|
||||
:opacity (:fill-opacity shape)
|
||||
:gradient (:fill-color-gradient shape)
|
||||
:id (:fill-color-ref-id shape)
|
||||
:file-id (:fill-color-ref-file shape)})
|
||||
|
||||
(defn has-color? [shape]
|
||||
(and
|
||||
(not (contains? #{:image :text :group} (:type shape)))
|
||||
(or (:fill-color shape)
|
||||
(:fill-color-gradient shape))))
|
||||
|
||||
(defn copy-data [shape]
|
||||
(cg/generate-css-props
|
||||
shape
|
||||
fill-attributes
|
||||
{:to-prop "background"
|
||||
:format #(uc/color->background (shape->color shape))}))
|
||||
|
||||
(mf/defc fill-block [{:keys [shape]}]
|
||||
(let [color-format (mf/use-state :hex)
|
||||
color (shape->color shape)]
|
||||
|
||||
[:& color-row {:color color
|
||||
:format @color-format
|
||||
:on-change-format #(reset! color-format %)
|
||||
:copy-data (copy-data shape)}]))
|
||||
|
||||
(mf/defc fill-panel
|
||||
[{:keys [shapes]}]
|
||||
(let [shapes (->> shapes (filter has-color?))]
|
||||
(when (seq shapes)
|
||||
[:div.attributes-block
|
||||
[:div.attributes-block-title
|
||||
[:div.attributes-block-title-text (tr "handoff.attributes.fill")]
|
||||
(when (= (count shapes) 1)
|
||||
[:& copy-button {:data (copy-data (first shapes))}])]
|
||||
|
||||
(for [shape shapes]
|
||||
[:& fill-block {:key (str "fill-block-" (:id shape))
|
||||
:shape shape}])])))
|
|
@ -0,0 +1,45 @@
|
|||
;; 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.ui.viewer.handoff.attributes.image
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn has-image? [shape]
|
||||
(= (:type shape) :image))
|
||||
|
||||
(mf/defc image-panel [{:keys [shapes]}]
|
||||
(let [shapes (->> shapes (filter has-image?))]
|
||||
(for [shape shapes]
|
||||
[:div.attributes-block {:key (str "image-" (:id shape))}
|
||||
[:div.attributes-image-row
|
||||
[:div.attributes-image
|
||||
[:img {:src (cfg/resolve-file-media (-> shape :metadata))}]]]
|
||||
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.image.width")]
|
||||
[:div.attributes-value (-> shape :metadata :width) "px"]
|
||||
[:& copy-button {:data (cg/generate-css-props shape :width)}]]
|
||||
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.image.height")]
|
||||
[:div.attributes-value (-> shape :metadata :height) "px"]
|
||||
[:& copy-button {:data (cg/generate-css-props shape :height)}]]
|
||||
|
||||
(let [mtype (-> shape :metadata :mtype)
|
||||
name (:name shape)
|
||||
extension (dom/mtype->extension mtype)]
|
||||
[:a.download-button {:target "_blank"
|
||||
:download (if extension
|
||||
(str name "." extension)
|
||||
name)
|
||||
:href (cfg/resolve-file-media (-> shape :metadata))}
|
||||
(tr "handoff.attributes.image.download")])])))
|
|
@ -0,0 +1,97 @@
|
|||
;; 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.ui.viewer.handoff.attributes.layout
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.i18n :refer [t]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def properties [:width :height :x :y :radius :rx :r1])
|
||||
|
||||
(def params
|
||||
{:to-prop {:x "left"
|
||||
:y "top"
|
||||
:rotation "transform"
|
||||
:rx "border-radius"
|
||||
:r1 "border-radius"}
|
||||
:format {:rotation #(str/fmt "rotate(%sdeg)" %)
|
||||
:r1 #(apply str/fmt "%spx, %spx, %spx, %spx" %)}
|
||||
:multi {:r1 [:r1 :r2 :r3 :r4]}})
|
||||
|
||||
(defn copy-data
|
||||
([shape]
|
||||
(apply copy-data shape properties))
|
||||
([shape & properties]
|
||||
(cg/generate-css-props shape properties params)))
|
||||
|
||||
(mf/defc layout-block
|
||||
[{:keys [shape locale]}]
|
||||
(let [selrect (:selrect shape)
|
||||
{:keys [width height x y]} selrect]
|
||||
[:*
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.width")]
|
||||
[:div.attributes-value (mth/precision width 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :width)}]]
|
||||
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.height")]
|
||||
[:div.attributes-value (mth/precision height 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :height)}]]
|
||||
|
||||
(when (not= (:x shape) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.left")]
|
||||
[:div.attributes-value (mth/precision x 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :x)}]])
|
||||
|
||||
(when (not= (:y shape) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.top")]
|
||||
[:div.attributes-value (mth/precision y 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :y)}]])
|
||||
|
||||
(when (and (:rx shape) (not= (:rx shape) 0))
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :rx)}]])
|
||||
|
||||
(when (and (:r1 shape)
|
||||
(or (not= (:r1 shape) 0)
|
||||
(not= (:r2 shape) 0)
|
||||
(not= (:r3 shape) 0)
|
||||
(not= (:r4 shape) 0)))
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:r1 shape) 2) ", "
|
||||
(mth/precision (:r2 shape) 2) ", "
|
||||
(mth/precision (:r3 shape) 2) ", "
|
||||
(mth/precision (:r4 shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :r1)}]])
|
||||
|
||||
(when (not= (:rotation shape 0) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
|
||||
[:div.attributes-value (mth/precision (:rotation shape) 2) "deg"]
|
||||
[:& copy-button {:data (copy-data shape :rotation)}]])]))
|
||||
|
||||
|
||||
(mf/defc layout-panel
|
||||
[{:keys [shapes locale]}]
|
||||
[:div.attributes-block
|
||||
[:div.attributes-block-title
|
||||
[:div.attributes-block-title-text (t locale "handoff.attributes.layout")]
|
||||
(when (= (count shapes) 1)
|
||||
[:& copy-button {:data (copy-data (first shapes))}])]
|
||||
|
||||
(for [shape shapes]
|
||||
[:& layout-block {:shape shape
|
||||
:locale locale}])])
|
|
@ -0,0 +1,74 @@
|
|||
;; 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.ui.viewer.handoff.attributes.shadow
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn has-shadow? [shape]
|
||||
(:shadow shape))
|
||||
|
||||
(defn shape-copy-data [shape]
|
||||
(cg/generate-css-props
|
||||
shape
|
||||
:shadow
|
||||
{:to-prop "box-shadow"
|
||||
:format #(str/join ", " (map cg/shadow->css (:shadow shape)))}))
|
||||
|
||||
(defn shadow-copy-data [shadow]
|
||||
(cg/generate-css-props
|
||||
shadow
|
||||
:style
|
||||
{:to-prop "box-shadow"
|
||||
:format #(cg/shadow->css shadow)}))
|
||||
|
||||
(mf/defc shadow-block [{:keys [shadow]}]
|
||||
(let [color-format (mf/use-state :hex)]
|
||||
[:div.attributes-shadow-block
|
||||
[:div.attributes-shadow-row
|
||||
[:div.attributes-label (->> shadow :style d/name (str "handoff.attributes.shadow.style.") (tr))]
|
||||
[:div.attributes-shadow
|
||||
[:div.attributes-label (tr "handoff.attributes.shadow.shorthand.offset-x")]
|
||||
[:div.attributes-value (str (:offset-x shadow))]]
|
||||
|
||||
[:div.attributes-shadow
|
||||
[:div.attributes-label (tr "handoff.attributes.shadow.shorthand.offset-y")]
|
||||
[:div.attributes-value (str (:offset-y shadow))]]
|
||||
|
||||
[:div.attributes-shadow
|
||||
[:div.attributes-label (tr "handoff.attributes.shadow.shorthand.blur")]
|
||||
[:div.attributes-value (str (:blur shadow))]]
|
||||
|
||||
[:div.attributes-shadow
|
||||
[:div.attributes-label (tr "handoff.attributes.shadow.shorthand.spread")]
|
||||
[:div.attributes-value (str (:spread shadow))]]
|
||||
|
||||
[:& copy-button {:data (shadow-copy-data shadow)}]]
|
||||
|
||||
[:& color-row {:color (:color shadow)
|
||||
:format @color-format
|
||||
:on-change-format #(reset! color-format %)}]]))
|
||||
|
||||
(mf/defc shadow-panel [{:keys [shapes]}]
|
||||
(let [shapes (->> shapes (filter has-shadow?))]
|
||||
(when (seq shapes)
|
||||
[:div.attributes-block
|
||||
[:div.attributes-block-title
|
||||
[:div.attributes-block-title-text (tr "handoff.attributes.shadow")]
|
||||
(when (= (count shapes) 1)
|
||||
[:& copy-button {:data (shape-copy-data (first shapes))}])]
|
||||
|
||||
[:div.attributes-shadow-blocks
|
||||
(for [shape shapes]
|
||||
(for [shadow (:shadow shape)]
|
||||
[:& shadow-block {:shape shape
|
||||
:shadow shadow}]))]])))
|
|
@ -0,0 +1,85 @@
|
|||
;; 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.ui.viewer.handoff.attributes.stroke
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [t]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn shape->color [shape]
|
||||
{:color (:stroke-color shape)
|
||||
:opacity (:stroke-opacity shape)
|
||||
:gradient (:stroke-color-gradient shape)
|
||||
:id (:stroke-color-ref-id shape)
|
||||
:file-id (:stroke-color-ref-file shape)})
|
||||
|
||||
(defn format-stroke [shape]
|
||||
(let [width (:stroke-width shape)
|
||||
style (d/name (:stroke-style shape))
|
||||
style (if (= style "svg") "solid" style)
|
||||
color (-> shape shape->color uc/color->background)]
|
||||
(str/format "%spx %s %s" width style color)))
|
||||
|
||||
(defn has-stroke? [{:keys [stroke-style]}]
|
||||
(and stroke-style
|
||||
(and (not= stroke-style :none)
|
||||
(not= stroke-style :svg))))
|
||||
|
||||
(defn copy-stroke-data [shape]
|
||||
(cg/generate-css-props
|
||||
shape
|
||||
:stroke-style
|
||||
{:to-prop "border"
|
||||
:format #(format-stroke shape)}))
|
||||
|
||||
(defn copy-color-data [shape]
|
||||
(cg/generate-css-props
|
||||
shape
|
||||
:stroke-color
|
||||
{:to-prop "border-color"
|
||||
:format #(uc/color->background (shape->color shape))}))
|
||||
|
||||
(mf/defc stroke-block
|
||||
[{:keys [shape locale]}]
|
||||
(let [color-format (mf/use-state :hex)
|
||||
color (shape->color shape)]
|
||||
[:*
|
||||
[:& color-row {:color color
|
||||
:format @color-format
|
||||
:copy-data (copy-color-data shape)
|
||||
:on-change-format #(reset! color-format %)}]
|
||||
|
||||
(let [{:keys [stroke-style stroke-alignment]} shape
|
||||
stroke-style (if (= stroke-style :svg) :solid stroke-style)
|
||||
stroke-alignment (or stroke-alignment :center)]
|
||||
[:div.attributes-stroke-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.stroke.width")]
|
||||
[:div.attributes-value (mth/precision (:stroke-width shape) 2) "px"]
|
||||
[:div.attributes-value (->> stroke-style d/name (str "handoff.attributes.stroke.style.") (t locale))]
|
||||
[:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (t locale))]
|
||||
[:& copy-button {:data (copy-stroke-data shape)}]])]))
|
||||
|
||||
(mf/defc stroke-panel
|
||||
[{:keys [shapes locale]}]
|
||||
(let [shapes (->> shapes (filter has-stroke?))]
|
||||
(when (seq shapes)
|
||||
[:div.attributes-block
|
||||
[:div.attributes-block-title
|
||||
[:div.attributes-block-title-text (t locale "handoff.attributes.stroke")]
|
||||
(when (= (count shapes) 1)
|
||||
[:& copy-button {:data (copy-stroke-data (first shapes))}])]
|
||||
|
||||
(for [shape shapes]
|
||||
[:& stroke-block {:key (str "stroke-color-" (:id shape))
|
||||
:shape shape
|
||||
:locale locale}])])))
|
54
frontend/src/app/main/ui/viewer/handoff/attributes/svg.cljs
Normal file
54
frontend/src/app/main/ui/viewer/handoff/attributes/svg.cljs
Normal file
|
@ -0,0 +1,54 @@
|
|||
;; 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.ui.viewer.handoff.attributes.svg
|
||||
(:require
|
||||
#_[app.common.math :as mth]
|
||||
#_[app.main.ui.icons :as i]
|
||||
#_[app.util.code-gen :as cg]
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
||||
(defn map->css [attr]
|
||||
(->> attr
|
||||
(map (fn [[attr-key attr-value]] (str (d/name attr-key) ":" attr-value)))
|
||||
(str/join "; ")))
|
||||
|
||||
(mf/defc svg-attr [{:keys [attr value]}]
|
||||
(if (map? value)
|
||||
[:*
|
||||
[:div.attributes-block-title
|
||||
[:div.attributes-block-title-text (d/name attr)]
|
||||
[:& copy-button {:data (map->css value)}]]
|
||||
|
||||
(for [[attr-key attr-value] value]
|
||||
[:& svg-attr {:attr attr-key :value attr-value}])]
|
||||
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (d/name attr)]
|
||||
[:div.attributes-value (str value)]
|
||||
[:& copy-button {:data (d/name value)}]]))
|
||||
|
||||
(mf/defc svg-block
|
||||
[{:keys [shape]}]
|
||||
[:*
|
||||
(for [[attr-key attr-value] (:svg-attrs shape)]
|
||||
[:& svg-attr {:attr attr-key :value attr-value}])] )
|
||||
|
||||
|
||||
(mf/defc svg-panel
|
||||
[{:keys [shapes]}]
|
||||
|
||||
(let [shape (first shapes)]
|
||||
(when (seq (:svg-attrs shape))
|
||||
[:div.attributes-block
|
||||
[:div.attributes-block-title
|
||||
[:div.attributes-block-title-text (tr "workspace.sidebar.options.svg-attrs.title")]]
|
||||
[:& svg-block {:shape shape}]])))
|
189
frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs
Normal file
189
frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs
Normal file
|
@ -0,0 +1,189 @@
|
|||
;; 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.ui.viewer.handoff.attributes.text
|
||||
(:require
|
||||
[app.common.text :as txt]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn has-text? [shape]
|
||||
(:content shape))
|
||||
|
||||
(def file-typographies-ref
|
||||
(l/derived (l/in [:viewer-data :file :typographies]) st/state))
|
||||
|
||||
(defn make-typographies-library-ref [file-id]
|
||||
(let [get-library
|
||||
(fn [state]
|
||||
(get-in state [:viewer-libraries file-id :data :typographies]))]
|
||||
#(l/derived get-library st/state)))
|
||||
|
||||
(def properties [:fill-color
|
||||
:fill-color-gradient
|
||||
:font-family
|
||||
:font-style
|
||||
:font-size
|
||||
:line-height
|
||||
:letter-spacing
|
||||
:text-decoration
|
||||
:text-transform])
|
||||
|
||||
(defn shape->color [shape]
|
||||
{:color (:fill-color shape)
|
||||
:opacity (:fill-opacity shape)
|
||||
:gradient (:fill-color-gradient shape)
|
||||
:id (:fill-color-ref-id shape)
|
||||
:file-id (:fill-color-ref-file shape)})
|
||||
|
||||
(def params
|
||||
{:to-prop {:fill-color "color"
|
||||
:fill-color-gradient "color"}
|
||||
:format {:font-family #(str "'" % "'")
|
||||
:font-style #(str "'" % "'")
|
||||
:font-size #(str % "px")
|
||||
:line-height #(str % "px")
|
||||
:letter-spacing #(str % "px")
|
||||
:text-decoration name
|
||||
:text-transform name
|
||||
:fill-color #(-> %2 shape->color uc/color->background)
|
||||
:fill-color-gradient #(-> %2 shape->color uc/color->background)}})
|
||||
|
||||
(defn copy-style-data
|
||||
([style]
|
||||
(cg/generate-css-props style properties params))
|
||||
([style & properties]
|
||||
(cg/generate-css-props style properties params)))
|
||||
|
||||
(mf/defc typography-block [{:keys [text style full-style]}]
|
||||
(let [typography-library-ref (mf/use-memo
|
||||
(mf/deps (:typography-ref-file style))
|
||||
(make-typographies-library-ref (:typography-ref-file style)))
|
||||
typography-library (mf/deref typography-library-ref)
|
||||
|
||||
file-typographies (mf/deref file-typographies-ref)
|
||||
|
||||
color-format (mf/use-state :hex)
|
||||
|
||||
typography (get (or typography-library file-typographies) (:typography-ref-id style))]
|
||||
|
||||
[:div.attributes-text-block
|
||||
(if (:typography-ref-id style)
|
||||
[:div.attributes-typography-name-row
|
||||
[:div.typography-entry
|
||||
[:div.typography-sample
|
||||
{:style {:font-family (:font-family typography)
|
||||
:font-weight (:font-weight typography)
|
||||
:font-style (:font-style typography)}}
|
||||
(tr "workspace.assets.typography.sample")]]
|
||||
[:div.typography-entry-name (:name typography)]
|
||||
[:& copy-button {:data (copy-style-data typography)}]]
|
||||
|
||||
[:div.attributes-typography-row
|
||||
[:div.typography-sample
|
||||
{:style {:font-family (:font-family full-style)
|
||||
:font-weight (:font-weight full-style)
|
||||
:font-style (:font-style full-style)}}
|
||||
(tr "workspace.assets.typography.sample")]
|
||||
[:& copy-button {:data (copy-style-data style)}]])
|
||||
|
||||
[:div.attributes-content-row
|
||||
[:pre.attributes-content (str/trim text)]
|
||||
[:& copy-button {:data (str/trim text)}]]
|
||||
|
||||
(when (or (:fill-color style) (:fill-color-gradient style))
|
||||
[:& color-row {:format @color-format
|
||||
:color (shape->color style)
|
||||
:copy-data (copy-style-data style :fill-color :fill-color-gradient)
|
||||
:on-change-format #(reset! color-format %)}])
|
||||
|
||||
(when (:font-id style)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.typography.font-family")]
|
||||
[:div.attributes-value (-> style :font-id fonts/get-font-data :name)]
|
||||
[:& copy-button {:data (copy-style-data style :font-family)}]])
|
||||
|
||||
(when (:font-style style)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.typography.font-style")]
|
||||
[:div.attributes-value (str (:font-style style))]
|
||||
[:& copy-button {:data (copy-style-data style :font-style)}]])
|
||||
|
||||
(when (:font-size style)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.typography.font-size")]
|
||||
[:div.attributes-value (str (:font-size style)) "px"]
|
||||
[:& copy-button {:data (copy-style-data style :font-size)}]])
|
||||
|
||||
(when (:line-height style)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.typography.line-height")]
|
||||
[:div.attributes-value (str (:line-height style)) "px"]
|
||||
[:& copy-button {:data (copy-style-data style :line-height)}]])
|
||||
|
||||
(when (:letter-spacing style)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.typography.letter-spacing")]
|
||||
[:div.attributes-value (str (:letter-spacing style)) "px"]
|
||||
[:& copy-button {:data (copy-style-data style :letter-spacing)}]])
|
||||
|
||||
(when (:text-decoration style)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.typography.text-decoration")]
|
||||
[:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (tr))]
|
||||
[:& copy-button {:data (copy-style-data style :text-decoration)}]])
|
||||
|
||||
(when (:text-transform style)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (tr "handoff.attributes.typography.text-transform")]
|
||||
[:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (tr))]
|
||||
[:& copy-button {:data (copy-style-data style :text-transform)}]])]))
|
||||
|
||||
|
||||
(defn- remove-equal-values
|
||||
[m1 m2]
|
||||
(if (and (map? m1) (map? m2) (not (nil? m1)) (not (nil? m2)))
|
||||
(->> m1
|
||||
(remove (fn [[k v]] (= (k m2) v)))
|
||||
(into {}))
|
||||
m1))
|
||||
|
||||
(mf/defc text-block [{:keys [shape]}]
|
||||
(let [style-text-blocks (->> (keys txt/default-text-attrs)
|
||||
(cg/parse-style-text-blocks (:content shape))
|
||||
(remove (fn [[_ text]] (str/empty? (str/trim text))))
|
||||
(mapv (fn [[style text]] (vector (merge txt/default-text-attrs style) text))))]
|
||||
|
||||
(for [[idx [full-style text]] (map-indexed vector style-text-blocks)]
|
||||
(let [previus-style (first (nth style-text-blocks (dec idx) nil))
|
||||
style (remove-equal-values full-style previus-style)
|
||||
|
||||
;; If the color is set we need to add opacity otherwise the display will not work
|
||||
style (cond-> style
|
||||
(:fill-color style)
|
||||
(assoc :fill-opacity (:fill-opacity full-style)))]
|
||||
[:& typography-block {:shape shape
|
||||
:full-style full-style
|
||||
:style style
|
||||
:text text}]))))
|
||||
|
||||
(mf/defc text-panel
|
||||
[{:keys [shapes]}]
|
||||
(when-let [shapes (seq (filter has-text? shapes))]
|
||||
[:div.attributes-block
|
||||
[:div.attributes-block-title
|
||||
[:div.attributes-block-title-text (tr "handoff.attributes.typography")]]
|
||||
|
||||
(for [shape shapes]
|
||||
[:& text-block {:shape shape}])]))
|
87
frontend/src/app/main/ui/viewer/handoff/code.cljs
Normal file
87
frontend/src/app/main/ui/viewer/handoff/code.cljs
Normal file
|
@ -0,0 +1,87 @@
|
|||
;; 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.ui.viewer.handoff.code
|
||||
(:require
|
||||
["js-beautify" :as beautify]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.components.code-block :refer [code-block]]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.dom :as dom]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn generate-markup-code [_type shapes]
|
||||
(let [frame (dom/query js/document "#svg-frame")
|
||||
markup-shape
|
||||
(fn [shape]
|
||||
(let [selector (str "#shape-" (:id shape) (when (= :text (:type shape)) " .root"))]
|
||||
(when-let [el (and frame (dom/query frame selector))]
|
||||
(str
|
||||
(str/fmt "<!-- %s -->" (:name shape))
|
||||
(.-outerHTML el)))))]
|
||||
(->> shapes
|
||||
(map markup-shape )
|
||||
(remove nil?)
|
||||
(str/join "\n\n"))))
|
||||
|
||||
(defn format-code [code type]
|
||||
(let [code (-> code
|
||||
(str/replace "<defs></defs>" "")
|
||||
(str/replace "><" ">\n<"))]
|
||||
(cond-> code
|
||||
(= type "svg") (beautify/html #js {"indent_size" 2}))))
|
||||
|
||||
(mf/defc code
|
||||
[{:keys [shapes frame on-expand]}]
|
||||
(let [style-type (mf/use-state "css")
|
||||
markup-type (mf/use-state "svg")
|
||||
shapes (->> shapes
|
||||
(map #(gsh/translate-to-frame % frame)))
|
||||
|
||||
style-code (-> (cg/generate-style-code @style-type shapes)
|
||||
(format-code "css"))
|
||||
|
||||
markup-code (-> (mf/use-memo (mf/deps shapes) #(generate-markup-code @markup-type shapes))
|
||||
(format-code "svg"))]
|
||||
[:div.element-options
|
||||
[:div.code-block
|
||||
[:div.code-row-lang
|
||||
[:select.code-selection
|
||||
[:option {:value "css"} "CSS"]
|
||||
#_[:option {:value "sass"} "SASS"]
|
||||
#_[:option {:value "less"} "Less"]
|
||||
#_[:option {:value "stylus"} "Stylus"]]
|
||||
|
||||
[:button.expand-button
|
||||
{:on-click on-expand }
|
||||
i/full-screen]
|
||||
|
||||
[:& copy-button { :data style-code }]]
|
||||
|
||||
[:div.code-row-display
|
||||
[:& code-block {:type @style-type
|
||||
:code style-code}]]]
|
||||
|
||||
[:div.code-block
|
||||
[:div.code-row-lang
|
||||
[:select.code-selection
|
||||
[:option "SVG"]
|
||||
[:option "HTML"]]
|
||||
|
||||
[:button.expand-button
|
||||
{:on-click on-expand}
|
||||
i/full-screen]
|
||||
|
||||
[:& copy-button { :data markup-code }]]
|
||||
|
||||
[:div.code-row-display
|
||||
[:& code-block {:type @markup-type
|
||||
:code markup-code}]]]
|
||||
|
||||
]))
|
127
frontend/src/app/main/ui/viewer/handoff/exports.cljs
Normal file
127
frontend/src/app/main/ui/viewer/handoff/exports.cljs
Normal file
|
@ -0,0 +1,127 @@
|
|||
;; 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.ui.viewer.handoff.exports
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.options.menus.exports :as we]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc exports
|
||||
[{:keys [shape page-id file-id] :as props}]
|
||||
(let [exports (mf/use-state (:exports shape []))
|
||||
loading? (mf/use-state false)
|
||||
|
||||
on-download
|
||||
(mf/use-callback
|
||||
(mf/deps shape @exports)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(swap! loading? not)
|
||||
(->> (we/request-export (assoc shape :page-id page-id :file-id file-id) @exports)
|
||||
(rx/subs
|
||||
(fn [{:keys [status body] :as response}]
|
||||
(js/console.log status body)
|
||||
(if (= status 200)
|
||||
(dom/trigger-download (:name shape) body)
|
||||
(st/emit! (dm/error (tr "errors.unexpected-error")))))
|
||||
(constantly nil)
|
||||
(fn []
|
||||
(swap! loading? not))))))
|
||||
|
||||
add-export
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(fn []
|
||||
(let [xspec {:type :png
|
||||
:suffix ""
|
||||
:scale 1}]
|
||||
(swap! exports conj xspec))))
|
||||
|
||||
delete-export
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(fn [index]
|
||||
(swap! exports (fn [exports]
|
||||
(let [[before after] (split-at index exports)]
|
||||
(d/concat [] before (rest after)))))))
|
||||
|
||||
on-scale-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)
|
||||
value (d/parse-double value)]
|
||||
(swap! exports assoc-in [index :scale] value))))
|
||||
|
||||
on-suffix-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)]
|
||||
(swap! exports assoc-in [index :suffix] value))))
|
||||
|
||||
on-type-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)
|
||||
value (keyword value)]
|
||||
(swap! exports assoc-in [index :type] value))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps shape)
|
||||
(fn []
|
||||
(reset! exports (:exports shape []))))
|
||||
|
||||
[:div.element-set.exports-options
|
||||
[:div.element-set-title
|
||||
[:span (tr "workspace.options.export")]
|
||||
[:div.add-page {:on-click add-export} i/close]]
|
||||
|
||||
(when (seq @exports)
|
||||
[:div.element-set-content
|
||||
(for [[index export] (d/enumerate @exports)]
|
||||
[:div.element-set-options-group
|
||||
{:key index}
|
||||
[:select.input-select {:on-change (partial on-scale-change index)
|
||||
:value (:scale export)}
|
||||
[:option {:value "0.5"} "0.5x"]
|
||||
[:option {:value "0.75"} "0.75x"]
|
||||
[:option {:value "1"} "1x"]
|
||||
[:option {:value "1.5"} "1.5x"]
|
||||
[:option {:value "2"} "2x"]
|
||||
[:option {:value "4"} "4x"]
|
||||
[:option {:value "6"} "6x"]]
|
||||
|
||||
[:input.input-text {:on-change (partial on-suffix-change index)
|
||||
:value (:suffix export)}]
|
||||
[:select.input-select {:on-change (partial on-type-change index)
|
||||
:value (d/name (:type export))}
|
||||
[:option {:value "png"} "PNG"]
|
||||
[:option {:value "jpeg"} "JPEG"]
|
||||
[:option {:value "svg"} "SVG"]]
|
||||
|
||||
[:div.delete-icon {:on-click (partial delete-export index)}
|
||||
i/minus]])
|
||||
|
||||
[:div.btn-icon-dark.download-button
|
||||
{:on-click (when-not @loading? on-download)
|
||||
:class (dom/classnames :btn-disabled @loading?)
|
||||
:disabled @loading?}
|
||||
(if @loading?
|
||||
(tr "workspace.options.exporting-object")
|
||||
(tr "workspace.options.export-object"))]])]))
|
||||
|
106
frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs
Normal file
106
frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs
Normal file
|
@ -0,0 +1,106 @@
|
|||
;; 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.ui.viewer.handoff.left-sidebar
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- make-collapsed-iref
|
||||
[id]
|
||||
#(-> (l/in [:viewer-local :collapsed id])
|
||||
(l/derived st/state)))
|
||||
|
||||
(mf/defc layer-item
|
||||
[{:keys [item selected objects disable-collapse?] :as props}]
|
||||
(let [id (:id item)
|
||||
selected? (contains? selected id)
|
||||
item-ref (mf/use-ref nil)
|
||||
|
||||
|
||||
collapsed-iref (mf/use-memo
|
||||
(mf/deps id)
|
||||
(make-collapsed-iref id))
|
||||
|
||||
expanded? (not (mf/deref collapsed-iref))
|
||||
|
||||
toggle-collapse
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/toggle-collapse id)))
|
||||
|
||||
select-shape
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(let [id (:id item)]
|
||||
(cond
|
||||
(or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(st/emit! (dv/toggle-selection id))
|
||||
|
||||
(kbd/shift? event)
|
||||
(st/emit! (dv/shift-select-to id))
|
||||
|
||||
:else
|
||||
(st/emit! (dv/select-shape id)))
|
||||
))
|
||||
]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps selected)
|
||||
(fn []
|
||||
(when (and (= (count selected) 1) selected?)
|
||||
(.scrollIntoView (mf/ref-val item-ref) false))))
|
||||
|
||||
[:li {:ref item-ref
|
||||
:class (dom/classnames
|
||||
:component (not (nil? (:component-id item)))
|
||||
:masked (:masked-group? item)
|
||||
:selected selected?)}
|
||||
|
||||
[:div.element-list-body {:class (dom/classnames :selected selected?
|
||||
:icon-layer (= (:type item) :icon))
|
||||
:on-click select-shape}
|
||||
[:& element-icon {:shape item}]
|
||||
[:& layer-name {:shape item}]
|
||||
|
||||
(when (and (not disable-collapse?) (:shapes item))
|
||||
[:span.toggle-content
|
||||
{:on-click toggle-collapse
|
||||
:class (when expanded? "inverse")}
|
||||
i/arrow-slide])]
|
||||
|
||||
(when (and (:shapes item) expanded?)
|
||||
[:ul.element-children
|
||||
(for [[index id] (reverse (d/enumerate (:shapes item)))]
|
||||
(when-let [item (get objects id)]
|
||||
[:& layer-item
|
||||
{:item item
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key (:id item)}]))])]))
|
||||
|
||||
(mf/defc left-sidebar
|
||||
[{:keys [frame page local]}]
|
||||
(let [selected (:selected local)
|
||||
objects (:objects page)]
|
||||
|
||||
[:aside.settings-bar.settings-bar-left
|
||||
[:div.settings-bar-inside
|
||||
[:ul.element-list
|
||||
[:& layer-item
|
||||
{:item frame
|
||||
:selected selected
|
||||
:index 0
|
||||
:objects objects
|
||||
:disable-collapse? true}]]]]))
|
189
frontend/src/app/main/ui/viewer/handoff/render.cljs
Normal file
189
frontend/src/app/main/ui/viewer/handoff/render.cljs
Normal file
|
@ -0,0 +1,189 @@
|
|||
;; 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.ui.viewer.handoff.render
|
||||
"The main container for a frame in handoff mode"
|
||||
(:require
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.shapes.circle :as circle]
|
||||
[app.main.ui.shapes.frame :as frame]
|
||||
[app.main.ui.shapes.group :as group]
|
||||
[app.main.ui.shapes.image :as image]
|
||||
[app.main.ui.shapes.path :as path]
|
||||
[app.main.ui.shapes.rect :as rect]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.svg-raw :as svg-raw]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[app.main.ui.viewer.handoff.selection-feedback :refer [selection-feedback]]
|
||||
[app.main.ui.viewer.interactions :refer [prepare-objects]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(declare shape-container-factory)
|
||||
|
||||
(defn handle-hover-shape
|
||||
[{:keys [type id]} hover?]
|
||||
(fn [event]
|
||||
(when-not (#{:group :frame} type)
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/hover-shape id hover?)))))
|
||||
|
||||
(defn select-shape [{:keys [type id]}]
|
||||
(fn [event]
|
||||
(when-not (#{:group :frame} type)
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(cond
|
||||
(.-shiftKey ^js event)
|
||||
(st/emit! (dv/toggle-selection id))
|
||||
|
||||
:else
|
||||
(st/emit! (dv/select-shape id))))))
|
||||
|
||||
(defn shape-wrapper-factory
|
||||
[component]
|
||||
(mf/fnc shape-wrapper
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (unchecked-get props "childs")
|
||||
frame (unchecked-get props "frame")
|
||||
render-wrapper? (or (not= :svg-raw (:type shape))
|
||||
(svg-raw/graphic-element? (get-in shape [:content :tag])))]
|
||||
|
||||
(if render-wrapper?
|
||||
[:> shape-container {:shape shape
|
||||
:on-mouse-enter (handle-hover-shape shape true)
|
||||
:on-mouse-leave (handle-hover-shape shape false)
|
||||
:on-click (select-shape shape)}
|
||||
[:& component {:shape shape
|
||||
:frame frame
|
||||
:childs childs
|
||||
:is-child-selected? true}]]
|
||||
|
||||
;; Don't wrap svg elements inside a <g> otherwise some can break
|
||||
[:& component {:shape shape
|
||||
:frame frame
|
||||
:childs childs}]))))
|
||||
|
||||
(defn frame-container-factory
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
frame-shape (frame/frame-shape shape-container)
|
||||
frame-wrapper (shape-wrapper-factory frame-shape)]
|
||||
(mf/fnc frame-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (mapv #(get objects %) (:shapes shape))
|
||||
shape (geom/transform-shape shape)
|
||||
|
||||
props (-> (obj/new)
|
||||
(obj/merge! props)
|
||||
(obj/merge! #js {:shape shape
|
||||
:childs childs}))]
|
||||
[:> frame-wrapper props]))))
|
||||
|
||||
(defn group-container-factory
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
group-shape (group/group-shape shape-container)
|
||||
group-wrapper (shape-wrapper-factory group-shape)]
|
||||
(mf/fnc group-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (mapv #(get objects %) (:shapes shape))
|
||||
props (-> (obj/new)
|
||||
(obj/merge! props)
|
||||
(obj/merge! #js {:childs childs}))]
|
||||
[:> group-wrapper props]))))
|
||||
|
||||
(defn svg-raw-container-factory
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
svg-raw-shape (svg-raw/svg-raw-shape shape-container)
|
||||
svg-raw-wrapper (shape-wrapper-factory svg-raw-shape)]
|
||||
(mf/fnc group-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (mapv #(get objects %) (:shapes shape))
|
||||
props (-> (obj/new)
|
||||
(obj/merge! props)
|
||||
(obj/merge! #js {:childs childs}))]
|
||||
[:> svg-raw-wrapper props]))))
|
||||
|
||||
(defn shape-container-factory
|
||||
[objects]
|
||||
(let [path-wrapper (shape-wrapper-factory path/path-shape)
|
||||
text-wrapper (shape-wrapper-factory text/text-shape)
|
||||
rect-wrapper (shape-wrapper-factory rect/rect-shape)
|
||||
image-wrapper (shape-wrapper-factory image/image-shape)
|
||||
circle-wrapper (shape-wrapper-factory circle/circle-shape)]
|
||||
(mf/fnc shape-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
frame (unchecked-get props "frame")
|
||||
group-container (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(group-container-factory objects))
|
||||
svg-raw-container (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(svg-raw-container-factory objects))]
|
||||
(when (and shape (not (:hidden shape)))
|
||||
(let [shape (-> (geom/transform-shape shape)
|
||||
(geom/translate-to-frame frame))
|
||||
opts #js {:shape shape
|
||||
:frame frame}]
|
||||
(case (:type shape)
|
||||
:text [:> text-wrapper opts]
|
||||
:rect [:> rect-wrapper opts]
|
||||
:path [:> path-wrapper opts]
|
||||
:image [:> image-wrapper opts]
|
||||
:circle [:> circle-wrapper opts]
|
||||
:group [:> group-container opts]
|
||||
:svg-raw [:> svg-raw-container opts])))))))
|
||||
|
||||
(mf/defc render-frame-svg
|
||||
[{:keys [page frame local]}]
|
||||
(let [objects (mf/use-memo
|
||||
(mf/deps page frame)
|
||||
(prepare-objects page frame))
|
||||
|
||||
|
||||
;; Retrieve frame again with correct modifier
|
||||
frame (get objects (:id frame))
|
||||
|
||||
zoom (:zoom local 1)
|
||||
width (* (:width frame) zoom)
|
||||
height (* (:height frame) zoom)
|
||||
vbox (str "0 0 " (:width frame 0) " " (:height frame 0))
|
||||
|
||||
render (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(frame-container-factory objects))]
|
||||
|
||||
[:svg
|
||||
{:id "svg-frame"
|
||||
:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
|
||||
[:& render {:shape frame :view-box vbox}]
|
||||
[:& selection-feedback
|
||||
{:frame frame
|
||||
:objects objects
|
||||
:local local}]]))
|
||||
|
55
frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs
Normal file
55
frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs
Normal file
|
@ -0,0 +1,55 @@
|
|||
;; 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.ui.viewer.handoff.right-sidebar
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.handoff.attributes :refer [attributes]]
|
||||
[app.main.ui.viewer.handoff.code :refer [code]]
|
||||
[app.main.ui.viewer.handoff.selection-feedback :refer [resolve-shapes]]
|
||||
[app.main.ui.workspace.sidebar.layers :refer [element-icon]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc right-sidebar
|
||||
[{:keys [frame page file selected]}]
|
||||
(let [expanded (mf/use-state false)
|
||||
section (mf/use-state :info #_:code)
|
||||
|
||||
shapes (resolve-shapes (:objects page) selected)
|
||||
selected-type (or (-> shapes first :type) :not-found)]
|
||||
|
||||
[:aside.settings-bar.settings-bar-right {:class (when @expanded "expanded")}
|
||||
[:div.settings-bar-inside
|
||||
(when (seq shapes)
|
||||
[:div.tool-window
|
||||
[:div.tool-window-bar.big
|
||||
(if (> (count shapes) 1)
|
||||
[:*
|
||||
[:span.tool-window-bar-icon i/layers]
|
||||
[:span.tool-window-bar-title (tr "handoff.tabs.code.selected.multiple" (count shapes))]]
|
||||
[:*
|
||||
[:span.tool-window-bar-icon
|
||||
[:& element-icon {:shape (-> shapes first)}]]
|
||||
[:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (tr))]])
|
||||
]
|
||||
[:div.tool-window-content
|
||||
[:& tab-container {:on-change-tab #(do
|
||||
(reset! expanded false)
|
||||
(reset! section %))
|
||||
:selected @section}
|
||||
[:& tab-element {:id :info :title (tr "handoff.tabs.info")}
|
||||
[:& attributes {:page-id (:id page)
|
||||
:file-id (:id file)
|
||||
:frame frame
|
||||
:shapes shapes}]]
|
||||
|
||||
[:& tab-element {:id :code :title (tr "handoff.tabs.code")}
|
||||
[:& code {:frame frame
|
||||
:shapes shapes
|
||||
:on-expand #(swap! expanded not)}]]]]])]]))
|
|
@ -0,0 +1,77 @@
|
|||
;; 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.ui.viewer.handoff.selection-feedback
|
||||
(:require
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.measurements :refer [selection-guides size-display measurement]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; CONSTANTS
|
||||
;; ------------------------------------------------
|
||||
|
||||
(def select-color "#1FDEA7")
|
||||
(def selection-rect-width 1)
|
||||
(def select-guide-width 1)
|
||||
(def select-guide-dasharray 5)
|
||||
|
||||
(defn resolve-shapes
|
||||
[objects ids]
|
||||
(let [resolve-shape #(get objects %)]
|
||||
(into [] (comp (map resolve-shape)
|
||||
(filter some?))
|
||||
ids)))
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; HELPERS
|
||||
;; ------------------------------------------------
|
||||
|
||||
(defn frame->bounds [frame]
|
||||
{:x 0
|
||||
:y 0
|
||||
:width (:width frame)
|
||||
:height (:height frame)})
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; COMPONENTS
|
||||
;; ------------------------------------------------
|
||||
|
||||
(mf/defc selection-rect [{:keys [selrect zoom]}]
|
||||
(let [{:keys [x y width height]} selrect
|
||||
selection-rect-width (/ selection-rect-width zoom)]
|
||||
[:g.selection-rect
|
||||
[:rect {:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:style {:fill "none"
|
||||
:stroke select-color
|
||||
:stroke-width selection-rect-width}}]]))
|
||||
|
||||
(mf/defc selection-feedback
|
||||
[{:keys [frame local objects]}]
|
||||
(let [{:keys [hover selected zoom]} local
|
||||
hover-shape (-> (or (first (resolve-shapes objects [hover])) frame)
|
||||
(gsh/translate-to-frame frame))
|
||||
selected-shapes (->> (resolve-shapes objects selected)
|
||||
(map #(gsh/translate-to-frame % frame)))
|
||||
|
||||
selrect (gsh/selection-rect selected-shapes)
|
||||
bounds (frame->bounds frame)]
|
||||
|
||||
|
||||
(when (seq selected-shapes)
|
||||
[:g.selection-feedback {:pointer-events "none"}
|
||||
[:g.selected-shapes
|
||||
[:& selection-guides {:bounds bounds :selrect selrect :zoom zoom}]
|
||||
[:& selection-rect {:selrect selrect :zoom zoom}]
|
||||
[:& size-display {:selrect selrect :zoom zoom}]]
|
||||
|
||||
[:& measurement {:bounds bounds
|
||||
:selected-shapes selected-shapes
|
||||
:hover-shape hover-shape
|
||||
:zoom zoom}]])))
|
|
@ -6,304 +6,155 @@
|
|||
|
||||
(ns app.main.ui.viewer.header
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.viewer.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[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.workspace.header :refer [zoom-widget]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.webapi :as wapi]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc zoom-widget
|
||||
{:wrap [mf/memo]}
|
||||
[{:keys [zoom
|
||||
on-increase
|
||||
on-decrease
|
||||
on-zoom-to-50
|
||||
on-zoom-to-100
|
||||
on-zoom-to-200
|
||||
on-fullscreen]
|
||||
:as props}]
|
||||
(let [show-dropdown? (mf/use-state false)]
|
||||
[:div.zoom-widget {:on-click #(reset! show-dropdown? true)}
|
||||
[:span {} (str (mth/round (* 100 zoom)) "%")]
|
||||
[:span.dropdown-button i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(reset! show-dropdown? false)}
|
||||
[:ul.dropdown.zoom-dropdown
|
||||
[:li {:on-click on-increase}
|
||||
"Zoom in" [:span (sc/get-tooltip :increase-zoom)]]
|
||||
[:li {:on-click on-decrease}
|
||||
"Zoom out" [:span (sc/get-tooltip :decrease-zoom)]]
|
||||
[:li {:on-click on-zoom-to-50}
|
||||
"Zoom to 50%" [:span (sc/get-tooltip :zoom-50)]]
|
||||
[:li {:on-click on-zoom-to-100}
|
||||
"Zoom to 100%" [:span (sc/get-tooltip :reset-zoom)]]
|
||||
[:li {:on-click on-zoom-to-200}
|
||||
"Zoom to 200%" [:span (sc/get-tooltip :zoom-200)]]
|
||||
[:li {:on-click on-fullscreen}
|
||||
"Full screen"]]]]))
|
||||
;; "Full screen" [:span (sc/get-tooltip :full-screen)]]]]]))
|
||||
|
||||
(mf/defc share-link
|
||||
[{:keys [token] :as props}]
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
dropdown-ref (mf/use-ref)
|
||||
create (st/emitf (dv/create-share-link))
|
||||
delete (st/emitf (dv/delete-share-link))
|
||||
|
||||
router (mf/deref refs/router)
|
||||
route (mf/deref refs/route)
|
||||
link (rt/resolve router
|
||||
:viewer
|
||||
(:path-params route)
|
||||
{:token token :index "0"})
|
||||
link (assoc cfg/public-uri :fragment link)
|
||||
|
||||
copy-link
|
||||
(fn [_]
|
||||
(wapi/write-to-clipboard (str link))
|
||||
(st/emit! (dm/show {:type :info
|
||||
:content "Link copied successfuly!"
|
||||
:timeout 3000})))]
|
||||
[:*
|
||||
[:span.btn-primary.btn-small
|
||||
{:alt (tr "viewer.header.share.title")
|
||||
:on-click #(swap! show-dropdown? not)}
|
||||
(tr "viewer.header.share.title")]
|
||||
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(swap! show-dropdown? not)
|
||||
:container dropdown-ref}
|
||||
[:div.dropdown.share-link-dropdown {:ref dropdown-ref}
|
||||
[:span.share-link-title (tr "viewer.header.share.title")]
|
||||
[:div.share-link-input
|
||||
(if (string? token)
|
||||
[:*
|
||||
[:span.link (str link)]
|
||||
[:span.link-button {:on-click copy-link}
|
||||
(tr "viewer.header.share.copy-link")]]
|
||||
[:span.link-placeholder (tr "viewer.header.share.placeholder")])]
|
||||
|
||||
[:span.share-link-subtitle (tr "viewer.header.share.subtitle")]
|
||||
[:div.share-link-buttons
|
||||
(if (string? token)
|
||||
[:button.btn-warning {:on-click delete}
|
||||
(tr "viewer.header.share.remove-link")]
|
||||
[:button.btn-primary {:on-click create}
|
||||
(tr "viewer.header.share.create-link")])]]]]))
|
||||
|
||||
(mf/defc interactions-menu
|
||||
[{:keys [state] :as props}]
|
||||
(let [imode (:interactions-mode state)
|
||||
|
||||
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-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dv/set-interactions-mode mode))))]
|
||||
|
||||
[:div.view-options
|
||||
[:div.view-options-dropdown {:on-click toggle-dropdown}
|
||||
[:span (tr "viewer.header.interactions")]
|
||||
i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= imode :hide))
|
||||
:on-click #(select-mode :hide)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.dont-show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= imode :show))
|
||||
:on-click #(select-mode :show)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= imode :show-on-click))
|
||||
:on-click #(select-mode :show-on-click)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.show-interactions-on-click")]]]]]))
|
||||
|
||||
(mf/defc comments-menu
|
||||
[]
|
||||
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
update-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:mode mode}))))
|
||||
|
||||
update-show
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:show mode}))))]
|
||||
|
||||
[:div.view-options
|
||||
[:div.icon {:on-click toggle-dropdown} i/eye]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= :all cmode))
|
||||
:on-click #(update-mode :all)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.show-all-comments")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :yours cmode))
|
||||
:on-click #(update-mode :yours)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.show-your-comments")]]
|
||||
|
||||
[:hr]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :pending cshow))
|
||||
:on-click #(update-show (if (= :pending cshow) :all :pending))}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.hide-resolved-comments")]]]]]))
|
||||
|
||||
(mf/defc file-menu
|
||||
[{:keys [project-id file-id page-id] :as props}]
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
on-edit
|
||||
(mf/use-callback
|
||||
(mf/deps project-id file-id page-id)
|
||||
(st/emitf (rt/nav :workspace
|
||||
{:project-id project-id
|
||||
:file-id file-id}
|
||||
{:page-id page-id})))]
|
||||
[:div.file-menu
|
||||
[:span.btn-icon-dark.btn-small {:on-click toggle-dropdown}
|
||||
i/actions
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown
|
||||
[:li {:on-click on-edit}
|
||||
[:span.label (tr "viewer.header.edit-file")]]]]]]))
|
||||
|
||||
(mf/defc header
|
||||
[{:keys [data index section state] :as props}]
|
||||
(let [{:keys [project file page frames]} data
|
||||
|
||||
fullscreen (mf/use-ctx fs/fullscreen-context)
|
||||
|
||||
total (count frames)
|
||||
profile (mf/deref refs/profile)
|
||||
teams (mf/deref refs/teams)
|
||||
|
||||
team-id (get-in data [:project :team-id])
|
||||
|
||||
has-permission? (and (not= uuid/zero (:id profile))
|
||||
(contains? teams team-id))
|
||||
|
||||
project-id (get-in data [:project :id])
|
||||
file-id (get-in data [:file :id])
|
||||
page-id (get-in data [:page :id])
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(st/emitf dv/toggle-thumbnails-panel))
|
||||
|
||||
on-goback
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(st/emitf (dv/go-to-dashboard project)))
|
||||
|
||||
navigate
|
||||
(mf/use-callback
|
||||
(mf/deps file-id page-id)
|
||||
(fn [section]
|
||||
(st/emit! (dv/go-to-section section))))
|
||||
(mf/defc header-options
|
||||
[{:keys [section zoom page file permissions]}]
|
||||
(let [fullscreen (mf/use-ctx fs/fullscreen-context)
|
||||
|
||||
toggle-fullscreen
|
||||
(mf/use-callback
|
||||
(mf/deps fullscreen)
|
||||
(fn []
|
||||
(if @fullscreen (fullscreen false) (fullscreen true))))]
|
||||
(mf/deps fullscreen)
|
||||
(fn []
|
||||
(if @fullscreen (fullscreen false) (fullscreen true))))
|
||||
|
||||
go-to-workspace
|
||||
(mf/use-callback
|
||||
(mf/deps page)
|
||||
(fn []
|
||||
(st/emit! (dv/go-to-workspace (:id page)))))
|
||||
|
||||
open-share-dialog
|
||||
(mf/use-callback
|
||||
(mf/deps page)
|
||||
(fn []
|
||||
(modal/show! :share-link {:page page :file file})))]
|
||||
|
||||
[:div.options-zone
|
||||
(case section
|
||||
:interactions [:& interactions-menu]
|
||||
:comments [:& comments-menu]
|
||||
|
||||
[:div.view-options])
|
||||
|
||||
[:& zoom-widget
|
||||
{:zoom zoom
|
||||
:on-increase (st/emitf dv/increase-zoom)
|
||||
:on-decrease (st/emitf dv/decrease-zoom)
|
||||
:on-zoom-to-50 (st/emitf dv/zoom-to-50)
|
||||
:on-zoom-to-100 (st/emitf dv/reset-zoom)
|
||||
:on-zoom-to-200 (st/emitf dv/zoom-to-200)
|
||||
:on-fullscreen toggle-fullscreen}]
|
||||
|
||||
[:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
|
||||
{:alt (tr "viewer.header.fullscreen")
|
||||
:on-click toggle-fullscreen}
|
||||
(if @fullscreen
|
||||
i/full-screen-off
|
||||
i/full-screen)]
|
||||
|
||||
(when (:edit permissions)
|
||||
[:span.btn-primary {:on-click open-share-dialog} (tr "labels.share-prototype")])
|
||||
|
||||
(when (:edit permissions)
|
||||
[:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")])]))
|
||||
|
||||
(mf/defc header-sitemap
|
||||
[{:keys [project file page frame] :as props}]
|
||||
(let [project-name (:name project)
|
||||
file-name (:name file)
|
||||
page-name (:name page)
|
||||
frame-name (:name frame)
|
||||
|
||||
toggle-thumbnails
|
||||
(fn []
|
||||
(st/emit! dv/toggle-thumbnails-panel))
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
|
||||
navigate-to
|
||||
(fn [page-id]
|
||||
(st/emit! (dv/go-to-page page-id))
|
||||
(reset! show-dropdown? false))
|
||||
]
|
||||
|
||||
[:div.sitemap-zone {:alt (tr "viewer.header.sitemap")}
|
||||
[:div.breadcrumb
|
||||
{:on-click #(swap! show-dropdown? not)}
|
||||
[:span.project-name project-name]
|
||||
[:span "/"]
|
||||
[:span.file-name file-name]
|
||||
[:span "/"]
|
||||
[:span.page-name page-name]
|
||||
[:span.icon i/arrow-down]
|
||||
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(swap! show-dropdown? not)}
|
||||
[:ul.dropdown
|
||||
(for [id (get-in file [:data :pages])]
|
||||
[:li {:id (str id)
|
||||
:on-click (partial navigate-to id)}
|
||||
(get-in file [:data :pages-index id :name])])]]]
|
||||
|
||||
[:div.current-frame
|
||||
{:on-click toggle-thumbnails}
|
||||
[:span.label "/"]
|
||||
[:span.label frame-name]
|
||||
[:span.icon i/arrow-down]]]))
|
||||
|
||||
(mf/defc header
|
||||
[{:keys [project file page frame zoom section permissions]}]
|
||||
(let [go-to-dashboard
|
||||
(st/emitf (dv/go-to-dashboard))
|
||||
|
||||
navigate
|
||||
(fn [section]
|
||||
(st/emit! (dv/go-to-section section)))]
|
||||
|
||||
|
||||
[:header.viewer-header
|
||||
[:div.main-icon
|
||||
[:a {:on-click on-goback
|
||||
[:a {:on-click go-to-dashboard
|
||||
;; If the user doesn't have permission we disable the link
|
||||
:style {:pointer-events (when-not has-permission? "none")}} i/logo-icon]]
|
||||
:style {:pointer-events (when-not permissions "none")}} i/logo-icon]]
|
||||
|
||||
[:div.sitemap-zone {:alt (tr "viewer.header.sitemap")
|
||||
:on-click on-click}
|
||||
[:span.project-name (:name project)]
|
||||
[:span "/"]
|
||||
[:span.file-name (:name file)]
|
||||
[:span "/"]
|
||||
[:span.page-name (:name page)]
|
||||
[:span.show-thumbnails-button i/arrow-down]
|
||||
[:span.counters (str (inc index) " / " total)]]
|
||||
[:& header-sitemap {:project project :file file :page page :frame frame}]
|
||||
|
||||
[:div.mode-zone
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :interactions)
|
||||
:class (dom/classnames :active (= section :interactions))
|
||||
:alt "View mode"}
|
||||
:alt (tr "viewer.header.interactions-section")}
|
||||
i/play]
|
||||
|
||||
(when has-permission?
|
||||
(when (:edit permissions)
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :comments)
|
||||
:class (dom/classnames :active (= section :comments))
|
||||
:alt "Comments"}
|
||||
:alt (tr "viewer.header.comments-section")}
|
||||
i/chat])
|
||||
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :handoff)
|
||||
:class (dom/classnames :active (= section :handoff))
|
||||
:alt "Code mode"}
|
||||
i/code]]
|
||||
(when (:read permissions)
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :handoff)
|
||||
:class (dom/classnames :active (= section :handoff))
|
||||
:alt (tr "viewer.header.handsoff-section")}
|
||||
i/code])]
|
||||
|
||||
[:div.options-zone
|
||||
(case section
|
||||
:interactions [:& interactions-menu {:state state}]
|
||||
:comments [:& comments-menu]
|
||||
nil)
|
||||
|
||||
(when has-permission?
|
||||
[:& share-link {:token (:token data)
|
||||
:page (:page data)}])
|
||||
|
||||
[:& zoom-widget
|
||||
{:zoom (:zoom state)
|
||||
:on-increase (st/emitf dv/increase-zoom)
|
||||
:on-decrease (st/emitf dv/decrease-zoom)
|
||||
:on-zoom-to-50 (st/emitf dv/zoom-to-50)
|
||||
:on-zoom-to-100 (st/emitf dv/reset-zoom)
|
||||
:on-zoom-to-200 (st/emitf dv/zoom-to-200)
|
||||
:on-fullscreen toggle-fullscreen}]
|
||||
|
||||
[:span.btn-icon-basic.btn-small.tooltip.tooltip-bottom-left
|
||||
{:alt (tr "viewer.header.fullscreen")
|
||||
:on-click toggle-fullscreen}
|
||||
(if @fullscreen
|
||||
i/full-screen-off
|
||||
i/full-screen)]
|
||||
|
||||
(when has-permission?
|
||||
[:& file-menu {:project-id project-id
|
||||
:file-id file-id
|
||||
:page-id page-id}])]]))
|
||||
[:& header-options {:section section
|
||||
:permissions permissions
|
||||
:page page
|
||||
:file file
|
||||
:zoom zoom}]]))
|
||||
|
||||
|
|
136
frontend/src/app/main/ui/viewer/interactions.cljs
Normal file
136
frontend/src/app/main/ui/viewer/interactions.cljs
Normal file
|
@ -0,0 +1,136 @@
|
|||
;; 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.ui.viewer.interactions
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.shapes :as shapes]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn prepare-objects
|
||||
[page frame]
|
||||
(fn []
|
||||
(let [objects (:objects page)
|
||||
frame-id (:id frame)
|
||||
modifier (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
update-fn #(d/update-when %1 %2 assoc-in [:modifiers :displacement] modifier)]
|
||||
|
||||
(->> (cp/get-children frame-id objects)
|
||||
(d/concat [frame-id])
|
||||
(reduce update-fn objects)))))
|
||||
|
||||
|
||||
(mf/defc viewport
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [local page frame size]}]
|
||||
(let [interactions? (:interactions-show? local)
|
||||
|
||||
objects (mf/use-memo
|
||||
(mf/deps page frame)
|
||||
(prepare-objects page frame))
|
||||
|
||||
wrapper (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(shapes/frame-container-factory objects interactions?))
|
||||
|
||||
;; Retrieve frame again with correct modifier
|
||||
frame (get objects (:id frame))
|
||||
|
||||
on-click
|
||||
(fn [_]
|
||||
(let [mode (:interactions-mode local)]
|
||||
(when (= mode :show-on-click)
|
||||
(st/emit! dv/flash-interactions))))
|
||||
|
||||
on-mouse-wheel
|
||||
(fn [event]
|
||||
(when (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
delta (+ (.-deltaY ^js event) (.-deltaX ^js event))]
|
||||
(if (pos? delta)
|
||||
(st/emit! dv/decrease-zoom)
|
||||
(st/emit! dv/increase-zoom)))))
|
||||
|
||||
on-key-down
|
||||
(fn [event]
|
||||
(when (kbd/esc? event)
|
||||
(st/emit! (dcm/close-thread))))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
(let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false})
|
||||
key2 (events/listen js/window "keydown" on-key-down)
|
||||
key3 (events/listen js/window "click" on-click)]
|
||||
(fn []
|
||||
(events/unlistenByKey key1)
|
||||
(events/unlistenByKey key2)
|
||||
(events/unlistenByKey key3)))))
|
||||
|
||||
[:svg {:view-box (:vbox size)
|
||||
:width (:width size)
|
||||
:height (:height size)
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
[:& wrapper {:shape frame
|
||||
:show-interactions? interactions?
|
||||
:view-box (:vbox size)}]]))
|
||||
|
||||
|
||||
(mf/defc interactions-menu
|
||||
[]
|
||||
(let [local (mf/deref refs/viewer-local)
|
||||
mode (:interactions-mode local)
|
||||
|
||||
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-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dv/set-interactions-mode mode))))]
|
||||
|
||||
[:div.view-options {:on-click toggle-dropdown}
|
||||
[:span.label (tr "viewer.header.interactions")]
|
||||
[:span.icon i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= mode :hide))
|
||||
:on-click #(select-mode :hide)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.dont-show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= mode :show))
|
||||
:on-click #(select-mode :show)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= mode :show-on-click))
|
||||
:on-click #(select-mode :show-on-click)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.show-interactions-on-click")]]]]]))
|
||||
|
|
@ -10,11 +10,11 @@
|
|||
[app.main.data.viewer :as dv]
|
||||
[app.main.exports :as exports]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown']]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[goog.object :as gobj]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as ts]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc thumbnails-content
|
||||
|
@ -50,7 +50,7 @@
|
|||
on-mount
|
||||
(fn []
|
||||
(let [dom (mf/ref-val container)]
|
||||
(reset! width (gobj/get dom "clientWidth"))))]
|
||||
(reset! width (obj/get dom "clientWidth"))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
(if expanded?
|
||||
|
@ -72,7 +72,8 @@
|
|||
[:span.btn-close {:on-click on-close} i/close]]])
|
||||
|
||||
(mf/defc thumbnail-item
|
||||
[{:keys [selected? frame on-click index objects] :as props}]
|
||||
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
|
||||
[{:keys [selected? frame on-click index objects]}]
|
||||
[:div.thumbnail-item {:on-click #(on-click % index)}
|
||||
[:div.thumbnail-preview
|
||||
{:class (dom/classnames :selected selected?)}
|
||||
|
@ -81,42 +82,39 @@
|
|||
[:span.name {:title (:name frame)} (:name frame)]]])
|
||||
|
||||
(mf/defc thumbnails-panel
|
||||
[{:keys [data index] :as props}]
|
||||
[{:keys [frames page index show?] :as props}]
|
||||
(let [expanded? (mf/use-state false)
|
||||
container (mf/use-ref)
|
||||
|
||||
on-close #(st/emit! dv/toggle-thumbnails-panel)
|
||||
selected (mf/use-var false)
|
||||
objects (:objects page)
|
||||
|
||||
on-mouse-leave
|
||||
(fn [_]
|
||||
(when @selected
|
||||
(on-close)))
|
||||
on-close #(st/emit! dv/toggle-thumbnails-panel)
|
||||
selected (mf/use-var false)
|
||||
|
||||
on-item-click
|
||||
(fn [_ index]
|
||||
(compare-and-set! selected false true)
|
||||
(st/emit! (dv/go-to-frame-by-index index))
|
||||
(when @expanded?
|
||||
(on-close)))]
|
||||
(mf/use-callback
|
||||
(mf/deps @expanded?)
|
||||
(fn [_ index]
|
||||
(compare-and-set! selected false true)
|
||||
(st/emit! (dv/go-to-frame-by-index index))
|
||||
(when @expanded?
|
||||
(on-close))))]
|
||||
|
||||
[:& dropdown' {:on-close on-close
|
||||
:container container
|
||||
:show true}
|
||||
[:section.viewer-thumbnails
|
||||
{:class (dom/classnames :expanded @expanded?)
|
||||
:ref container
|
||||
:on-mouse-leave on-mouse-leave}
|
||||
[:section.viewer-thumbnails
|
||||
{:class (dom/classnames :expanded @expanded?
|
||||
:invisible (not show?))
|
||||
|
||||
[:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not)
|
||||
:on-close on-close
|
||||
:total (count (:frames data))}]
|
||||
[:& thumbnails-content {:expanded? @expanded?
|
||||
:total (count (:frames data))}
|
||||
(for [[i frame] (d/enumerate (:frames data))]
|
||||
[:& thumbnail-item {:key i
|
||||
:index i
|
||||
:frame frame
|
||||
:objects (:objects data)
|
||||
:on-click on-item-click
|
||||
:selected? (= i index)}])]]]))
|
||||
:ref container
|
||||
}
|
||||
|
||||
[:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not)
|
||||
:on-close on-close
|
||||
:total (count frames)}]
|
||||
[:& thumbnails-content {:expanded? @expanded?
|
||||
:total (count frames)}
|
||||
(for [[i frame] (d/enumerate frames)]
|
||||
[:& thumbnail-item {:index i
|
||||
:frame frame
|
||||
:objects objects
|
||||
:on-click on-item-click
|
||||
:selected? (= i index)}])]]))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue