mirror of
https://github.com/penpot/penpot.git
synced 2025-07-08 17:17:16 +02:00
Merge branch 'staging'
This commit is contained in:
commit
78d1c57b7c
306 changed files with 14686 additions and 4386 deletions
|
@ -54,22 +54,11 @@
|
|||
:browser
|
||||
:webworker))
|
||||
|
||||
(def available-flags
|
||||
#{:registration
|
||||
:audit-log
|
||||
:demo-users
|
||||
:user-feedback
|
||||
:demo-warning
|
||||
:login-with-ldap})
|
||||
|
||||
(def default-flags
|
||||
#{:registration :demo-users})
|
||||
|
||||
(defn- parse-flags
|
||||
[global]
|
||||
(let [flags (obj/get global "penpotFlags" "")
|
||||
flags (into #{} (map keyword) (str/words flags))]
|
||||
(flags/parse default-flags flags)))
|
||||
(flags/parse flags flags/default)))
|
||||
|
||||
(defn- parse-version
|
||||
[global]
|
||||
|
@ -88,6 +77,7 @@
|
|||
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
(def translations (obj/get global "penpotTranslations"))
|
||||
(def themes (obj/get global "penpotThemes"))
|
||||
(def sentry-dsn (obj/get global "penpotSentryDsn"))
|
||||
|
||||
(def flags (atom (parse-flags global)))
|
||||
(def version (atom (parse-version global)))
|
||||
|
@ -103,7 +93,8 @@
|
|||
(when (false? registration)
|
||||
(swap! flags disj :registration)))
|
||||
|
||||
(def public-uri
|
||||
(defn get-public-uri
|
||||
[]
|
||||
(let [uri (u/uri (or (obj/get global "penpotPublicURI")
|
||||
(.-origin ^js location)))]
|
||||
;; Ensure that the path always ends with "/"; this ensures that
|
||||
|
@ -112,9 +103,7 @@
|
|||
(not (str/ends-with? (:path uri) "/"))
|
||||
(update :path #(str % "/")))))
|
||||
|
||||
(when (= :browser @target)
|
||||
(js/console.log
|
||||
(str/format "Welcome to penpot! version='%s' base-uri='%s'." (:full @version) (str public-uri))))
|
||||
(def public-uri (get-public-uri))
|
||||
|
||||
;; --- Helper Functions
|
||||
|
||||
|
|
|
@ -6,25 +6,23 @@
|
|||
|
||||
(ns app.main
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.logging :as log]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.errors]
|
||||
[app.main.sentry :as sentry]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui :as ui]
|
||||
[app.main.ui.confirm]
|
||||
[app.main.ui.modal :refer [modal]]
|
||||
[app.main.ui.routes :as rt]
|
||||
[app.main.worker]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.logging :as log]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.theme :as theme]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
@ -32,79 +30,38 @@
|
|||
(log/set-level! :root :warn)
|
||||
(log/set-level! :app :info)
|
||||
|
||||
(when (= :browser @cf/target)
|
||||
(log/info :message "Welcome to penpot" :version (:full @cf/version) :public-uri (str cf/public-uri)))
|
||||
|
||||
(declare reinit)
|
||||
|
||||
(s/def ::any any?)
|
||||
|
||||
(defn match-path
|
||||
[router path]
|
||||
(when-let [match (rt/match router path)]
|
||||
(if-let [conform (get-in match [:data :conform])]
|
||||
(let [spath (get conform :path-params ::any)
|
||||
squery (get conform :query-params ::any)]
|
||||
(try
|
||||
(-> (dissoc match :params)
|
||||
(assoc :path-params (us/conform spath (get match :path-params))
|
||||
:query-params (us/conform squery (get match :query-params))))
|
||||
(catch :default _
|
||||
nil)))
|
||||
match)))
|
||||
|
||||
(defn on-navigate
|
||||
[router path]
|
||||
(let [match (match-path router path)
|
||||
profile (:profile @storage)
|
||||
nopath? (or (= path "") (= path "/"))
|
||||
authed? (and (not (nil? profile))
|
||||
(not= (:id profile) uuid/zero))]
|
||||
|
||||
(cond
|
||||
(and nopath? authed? (nil? match))
|
||||
(if (not= uuid/zero profile)
|
||||
(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)}))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
|
||||
(and (not authed?) (nil? match))
|
||||
(st/emit! (rt/nav :auth-login))
|
||||
|
||||
(nil? match)
|
||||
(st/emit! (dm/assign-exception {:type :not-found}))
|
||||
|
||||
:else
|
||||
(st/emit! (rt/navigated match)))))
|
||||
|
||||
(defn init-ui
|
||||
[]
|
||||
(mf/mount (mf/element ui/app) (dom/get-element "app"))
|
||||
(mf/mount (mf/element modal) (dom/get-element "modal")))
|
||||
|
||||
|
||||
(defn initialize
|
||||
[]
|
||||
(letfn [(on-profile [_profile]
|
||||
(rx/of (rt/initialize-router ui/routes)
|
||||
(rt/initialize-history on-navigate)))]
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :session-id (uuid/next)))
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :session-id (uuid/next)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(rx/merge
|
||||
(rx/of
|
||||
(ptk/event ::ev/initialize)
|
||||
(du/initialize-profile))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::du/profile-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/mapcat on-profile)))))))
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(rx/merge
|
||||
(rx/of (ptk/event ::ev/initialize)
|
||||
(du/initialize-profile))
|
||||
(->> stream
|
||||
(rx/filter du/profile-fetched?)
|
||||
(rx/take 1)
|
||||
(rx/map #(rt/init-routes)))))))
|
||||
|
||||
(defn ^:export init
|
||||
[]
|
||||
(i18n/init! cfg/translations)
|
||||
(theme/init! cfg/themes)
|
||||
(sentry/init!)
|
||||
(i18n/init! cf/translations)
|
||||
(theme/init! cf/themes)
|
||||
(init-ui)
|
||||
(st/emit! (initialize)))
|
||||
|
||||
|
@ -114,11 +71,14 @@
|
|||
(mf/unmount (dom/get-element "modal"))
|
||||
(init-ui))
|
||||
|
||||
(add-watch i18n/locale "locale" (fn [_ _ o v]
|
||||
(when (not= o v)
|
||||
(reinit))))
|
||||
|
||||
(defn ^:dev/after-load after-load
|
||||
[]
|
||||
(reinit))
|
||||
|
||||
;; Reload the UI when the language changes
|
||||
(add-watch
|
||||
i18n/locale "locale"
|
||||
(fn [_ _ old-value current-value]
|
||||
(when (not= old-value current-value)
|
||||
(reinit))))
|
||||
|
||||
|
|
|
@ -77,7 +77,8 @@
|
|||
(watch [_ _ _]
|
||||
(->> (rp/mutation :create-comment-thread params)
|
||||
(rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %)}))
|
||||
(rx/map #(partial created %)))))))
|
||||
(rx/map #(partial created %))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))))))
|
||||
|
||||
(defn update-comment-thread-status
|
||||
[{:keys [id] :as thread}]
|
||||
|
@ -87,7 +88,8 @@
|
|||
(watch [_ _ _]
|
||||
(let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0)]
|
||||
(->> (rp/mutation :update-comment-thread-status {:id id})
|
||||
(rx/map (constantly done)))))))
|
||||
(rx/map (constantly done))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))))))
|
||||
|
||||
|
||||
(defn update-comment-thread
|
||||
|
@ -104,6 +106,7 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/mutation :update-comment-thread {:id id :is-resolved is-resolved})
|
||||
(rx/catch #(rx/throw {:type :comment-error}))
|
||||
(rx/ignore)))))
|
||||
|
||||
|
||||
|
@ -118,7 +121,8 @@
|
|||
(watch [_ _ _]
|
||||
(rx/concat
|
||||
(->> (rp/mutation :add-comment {:thread-id (:id thread) :content content})
|
||||
(rx/map #(partial created %)))
|
||||
(rx/map #(partial created %))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))
|
||||
(rx/of (refresh-comment-thread thread)))))))
|
||||
|
||||
(defn update-comment
|
||||
|
@ -132,6 +136,7 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/mutation :update-comment {:id id :content content})
|
||||
(rx/catch #(rx/throw {:type :comment-error}))
|
||||
(rx/ignore)))))
|
||||
|
||||
(defn delete-comment-thread
|
||||
|
@ -147,6 +152,7 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/mutation :delete-comment-thread {:id id})
|
||||
(rx/catch #(rx/throw {:type :comment-error}))
|
||||
(rx/ignore)))))
|
||||
|
||||
(defn delete-comment
|
||||
|
@ -160,6 +166,7 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/mutation :delete-comment {:id id})
|
||||
(rx/catch #(rx/throw {:type :comment-error}))
|
||||
(rx/ignore)))))
|
||||
|
||||
(defn refresh-comment-thread
|
||||
|
@ -171,7 +178,8 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/query :comment-thread {:file-id file-id :id id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
(rx/map #(partial fetched %))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))))))
|
||||
|
||||
(defn retrieve-comment-threads
|
||||
[file-id]
|
||||
|
@ -182,7 +190,8 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/query :comment-threads {:file-id file-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
(rx/map #(partial fetched %))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))))))
|
||||
|
||||
(defn retrieve-comments
|
||||
[thread-id]
|
||||
|
@ -193,7 +202,8 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/query :comments {:thread-id thread-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
(rx/map #(partial fetched %))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))))))
|
||||
|
||||
(defn retrieve-unread-comment-threads
|
||||
"A event used mainly in dashboard for retrieve all unread threads of a team."
|
||||
|
@ -204,7 +214,8 @@
|
|||
(watch [_ _ _]
|
||||
(let [fetched #(assoc %2 :comment-threads (d/index-by :id %1))]
|
||||
(->> (rp/query :unread-comment-threads {:team-id team-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
(rx/map #(partial fetched %))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(du/set-current-team! id)
|
||||
(let [prev-team-id (:current-team-id state)]
|
||||
(cond-> state
|
||||
(not= prev-team-id id)
|
||||
|
@ -749,7 +750,6 @@
|
|||
(ptk/reify ::go-to-projects-1
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(du/set-current-team! team-id)
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
|
||||
(defn go-to-team-members
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
(:require
|
||||
["opentype.js" :as ot]
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as log]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.logging :as log]
|
||||
[app.util.webapi :as wa]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
|
|
|
@ -128,12 +128,3 @@
|
|||
:controls controls
|
||||
:actions actions
|
||||
:tag tag})))
|
||||
|
||||
(defn assign-exception
|
||||
[error]
|
||||
(ptk/reify ::assign-exception
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(if (nil? error)
|
||||
(dissoc state :exception)
|
||||
(assoc state :exception error)))))
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
(:refer-clojure :exclude [meta reset!])
|
||||
(:require
|
||||
["mousetrap" :as mousetrap]
|
||||
[app.common.logging :as log]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.util.logging :as log]
|
||||
[app.config :as cf]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
|||
"Adds the control/command modifier to a shortcuts depending on the
|
||||
operating system for the user"
|
||||
[shortcut]
|
||||
(if (cfg/check-platform? :macos)
|
||||
(if (cf/check-platform? :macos)
|
||||
(str "command+" shortcut)
|
||||
(str "ctrl+" shortcut)))
|
||||
|
||||
|
@ -55,12 +55,12 @@
|
|||
[key]
|
||||
;; If the key is "+" we need to surround with quotes
|
||||
;; otherwise will not be very readable
|
||||
(let [key (if (and (not (cfg/check-platform? :macos))
|
||||
(let [key (if (and (not (cf/check-platform? :macos))
|
||||
(= key "+"))
|
||||
"\"+\""
|
||||
key)]
|
||||
(str
|
||||
(if (cfg/check-platform? :macos)
|
||||
(if (cf/check-platform? :macos)
|
||||
mac-command
|
||||
"Ctrl+")
|
||||
key)))
|
||||
|
@ -68,7 +68,7 @@
|
|||
(defn shift
|
||||
[key]
|
||||
(str
|
||||
(if (cfg/check-platform? :macos)
|
||||
(if (cf/check-platform? :macos)
|
||||
mac-shift
|
||||
"Shift+")
|
||||
key))
|
||||
|
@ -76,7 +76,7 @@
|
|||
(defn alt
|
||||
[key]
|
||||
(str
|
||||
(if (cfg/check-platform? :macos)
|
||||
(if (cf/check-platform? :macos)
|
||||
mac-option
|
||||
"Alt+")
|
||||
key))
|
||||
|
@ -91,19 +91,19 @@
|
|||
|
||||
(defn supr
|
||||
[]
|
||||
(if (cfg/check-platform? :macos)
|
||||
(if (cf/check-platform? :macos)
|
||||
mac-delete
|
||||
"Supr"))
|
||||
|
||||
(defn esc
|
||||
[]
|
||||
(if (cfg/check-platform? :macos)
|
||||
(if (cf/check-platform? :macos)
|
||||
mac-esc
|
||||
"Escape"))
|
||||
|
||||
(defn enter
|
||||
[]
|
||||
(if (cfg/check-platform? :macos)
|
||||
(if (cf/check-platform? :macos)
|
||||
mac-enter
|
||||
"Enter"))
|
||||
|
||||
|
|
|
@ -62,14 +62,26 @@
|
|||
|
||||
(defn teams-fetched
|
||||
[teams]
|
||||
(let [teams (d/index-by :id teams)]
|
||||
(let [teams (d/index-by :id teams)
|
||||
ids (into #{} (keys teams))]
|
||||
|
||||
(ptk/reify ::teams-fetched
|
||||
IDeref
|
||||
(-deref [_] teams)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :teams teams)))))
|
||||
(assoc state :teams teams))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; Check if current team-id is part of available teams
|
||||
;; if not, dissoc it from storage.
|
||||
(when-let [ctid (::current-team-id @storage)]
|
||||
(when-not (contains? ids ctid)
|
||||
(swap! storage dissoc ::current-team-id)))))))
|
||||
|
||||
|
||||
|
||||
(defn fetch-teams
|
||||
[]
|
||||
|
@ -81,6 +93,9 @@
|
|||
|
||||
;; --- EVENT: fetch-profile
|
||||
|
||||
(def profile-fetched?
|
||||
(ptk/type? ::profile-fetched))
|
||||
|
||||
(defn profile-fetched
|
||||
[{:keys [id] :as profile}]
|
||||
(us/verify ::profile profile)
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
:comments-show :unresolved
|
||||
:selected #{}
|
||||
:collapsed #{}
|
||||
:overlays []
|
||||
:hover nil})
|
||||
|
||||
(declare fetch-comment-threads)
|
||||
|
@ -110,6 +111,7 @@
|
|||
(rx/of (df/fonts-fetched fonts)
|
||||
(bundle-fetched (merge bundle params))))))))))
|
||||
|
||||
(declare go-to-frame-auto)
|
||||
|
||||
(defn bundle-fetched
|
||||
[{:keys [project file share-links libraries users permissions] :as bundle}]
|
||||
|
@ -129,7 +131,15 @@
|
|||
:permissions permissions
|
||||
:project project
|
||||
:pages pages
|
||||
:file file}))))))
|
||||
:file file})))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
index (:index qparams)]
|
||||
(when (nil? index)
|
||||
(rx/of (go-to-frame-auto))))))))
|
||||
|
||||
(defn fetch-comment-threads
|
||||
[{:keys [file-id page-id] :as params}]
|
||||
|
@ -218,6 +228,12 @@
|
|||
(update [_ state]
|
||||
(update-in state [:viewer-local :show-thumbnails] not))))
|
||||
|
||||
(def close-thumbnails-panel
|
||||
(ptk/reify ::close-thumbnails-panel
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :show-thumbnails] false))))
|
||||
|
||||
(def select-prev-frame
|
||||
(ptk/reify ::select-prev-frame
|
||||
ptk/WatchEvent
|
||||
|
@ -286,11 +302,15 @@
|
|||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :interactions-show?] false))))
|
||||
|
||||
;; --- Navigation
|
||||
;; --- Navigation inside page
|
||||
|
||||
(defn go-to-frame-by-index
|
||||
[index]
|
||||
(ptk/reify ::go-to-frame-by-index
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :overlays] []))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
|
@ -303,6 +323,10 @@
|
|||
[frame-id]
|
||||
(us/verify ::us/uuid frame-id)
|
||||
(ptk/reify ::go-to-frame
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :overlays] []))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
|
@ -314,9 +338,27 @@
|
|||
(when index
|
||||
(rx/of (go-to-frame-by-index index)))))))
|
||||
|
||||
(defn go-to-frame-auto
|
||||
[]
|
||||
(ptk/reify ::go-to-frame-auto
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
flows (get-in state [:viewer :pages page-id :options :flows])]
|
||||
(if (seq flows)
|
||||
(let [frame-id (:starting-frame (first flows))]
|
||||
(rx/of (go-to-frame frame-id)))
|
||||
(rx/of (go-to-frame-by-index 0)))))))
|
||||
|
||||
(defn go-to-section
|
||||
[section]
|
||||
(ptk/reify ::go-to-section
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :overlays] []))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
|
@ -324,6 +366,67 @@
|
|||
qparams (:query-params route)]
|
||||
(rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
|
||||
|
||||
;; --- Overlays
|
||||
|
||||
(defn open-overlay
|
||||
[frame-id position close-click-outside background-overlay]
|
||||
(us/verify ::us/uuid frame-id)
|
||||
(us/verify ::us/point position)
|
||||
(us/verify (s/nilable ::us/boolean) close-click-outside)
|
||||
(us/verify (s/nilable ::us/boolean) background-overlay)
|
||||
(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 %) frame) overlays)
|
||||
(update-in state [:viewer-local :overlays] conj
|
||||
{:frame frame
|
||||
:position position
|
||||
:close-click-outside close-click-outside
|
||||
:background-overlay background-overlay})
|
||||
state)))))
|
||||
|
||||
(defn toggle-overlay
|
||||
[frame-id position close-click-outside background-overlay]
|
||||
(us/verify ::us/uuid frame-id)
|
||||
(us/verify ::us/point position)
|
||||
(us/verify (s/nilable ::us/boolean) close-click-outside)
|
||||
(us/verify (s/nilable ::us/boolean) background-overlay)
|
||||
(ptk/reify ::toggle-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 %) frame) overlays)
|
||||
(update-in state [:viewer-local :overlays] conj
|
||||
{:frame frame
|
||||
:position position
|
||||
:close-click-outside close-click-outside
|
||||
:background-overlay background-overlay})
|
||||
(update-in state [:viewer-local :overlays]
|
||||
(fn [overlays]
|
||||
(d/removev #(= (:id (:frame %)) frame-id) overlays))))))))
|
||||
|
||||
(defn close-overlay
|
||||
[frame-id]
|
||||
(ptk/reify ::close-overlay
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:viewer-local :overlays]
|
||||
(fn [overlays]
|
||||
(d/removev #(= (:id (:frame %)) frame-id) overlays))))))
|
||||
|
||||
;; --- Objects selection
|
||||
|
||||
(defn deselect-all []
|
||||
(ptk/reify ::deselect-all
|
||||
ptk/UpdateEvent
|
||||
|
@ -397,7 +500,7 @@
|
|||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :hover] (when hover? id)))))
|
||||
|
||||
;; --- NAV
|
||||
;; --- Navigation outside page
|
||||
|
||||
(defn go-to-dashboard
|
||||
[]
|
||||
|
@ -411,6 +514,10 @@
|
|||
(defn go-to-page
|
||||
[page-id]
|
||||
(ptk/reify ::go-to-page
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :overlays] []))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
|
|
|
@ -22,13 +22,16 @@
|
|||
[app.config :as cfg]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace.booleans :as dwb]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.notifications :as dwn]
|
||||
[app.main.data.workspace.path :as dwdp]
|
||||
[app.main.data.workspace.path.shapes-to-path :as dwps]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
|
@ -49,8 +52,6 @@
|
|||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; (log/set-level! :trace)
|
||||
|
||||
(s/def ::shape-attrs ::cp/shape-attrs)
|
||||
(s/def ::set-of-string
|
||||
(s/every string? :kind set?))
|
||||
|
@ -1096,7 +1097,7 @@
|
|||
:text
|
||||
(rx/of (dwc/start-edition-mode id))
|
||||
|
||||
:group
|
||||
(:group :bool)
|
||||
(rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)])))
|
||||
|
||||
:svg-raw
|
||||
|
@ -1282,8 +1283,7 @@
|
|||
(watch [_ state _]
|
||||
(let [{:keys [current-file-id current-page-id]} state
|
||||
pparams {:file-id (or file-id current-file-id)}
|
||||
qparams {:page-id (or page-id current-page-id)
|
||||
:index 0}]
|
||||
qparams {:page-id (or page-id current-page-id)}]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav-new-window* {:rname :viewer
|
||||
:path-params pparams
|
||||
|
@ -1322,10 +1322,33 @@
|
|||
(ptk/reify ::show-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [mdata (cond-> params
|
||||
(some? shape)
|
||||
(assoc :selected
|
||||
(wsh/lookup-selected state)))]
|
||||
(let [selected (wsh/lookup-selected state)
|
||||
objects (wsh/lookup-page-objects state)
|
||||
|
||||
selected-with-children
|
||||
(into []
|
||||
(mapcat #(cp/get-object-with-children % objects))
|
||||
selected)
|
||||
|
||||
head (get objects (first selected))
|
||||
|
||||
first-not-group-like?
|
||||
(and (= (count selected) 1)
|
||||
(not (contains? #{:group :bool} (:type head))))
|
||||
|
||||
has-invalid-shapes? (->> selected-with-children
|
||||
(some (comp #{:frame :text} :type)))
|
||||
|
||||
disable-booleans? (or (empty? selected) has-invalid-shapes? first-not-group-like?)
|
||||
disable-flatten? (or (empty? selected) has-invalid-shapes?)
|
||||
|
||||
mdata
|
||||
(-> params
|
||||
(assoc :disable-booleans? disable-booleans?)
|
||||
(assoc :disable-flatten? disable-flatten?)
|
||||
(cond-> (some? shape)
|
||||
(assoc :selected selected)))]
|
||||
|
||||
(assoc-in state [:workspace-local :context-menu] mdata)))))
|
||||
|
||||
(defn show-shape-context-menu
|
||||
|
@ -1534,8 +1557,12 @@
|
|||
(= :frame (get-in objects [(first selected) :type])))))
|
||||
|
||||
(defn- paste-shape
|
||||
[{:keys [selected objects images] :as data} in-viewport?] ;; TODO: perhaps rename 'objects' to 'shapes', because it contains only
|
||||
(letfn [;; Given a file-id and img (part generated by the ;; the shapes to paste, not the whole page tree of shapes
|
||||
[{selected :selected
|
||||
paste-objects :objects ;; rename this because here comes only the clipboard shapes,
|
||||
images :images ;; not the whole page tree of shapes.
|
||||
:as data}
|
||||
in-viewport?]
|
||||
(letfn [;; Given a file-id and img (part generated by the
|
||||
;; copy-selected event), uploads the new media.
|
||||
(upload-media [file-id imgpart]
|
||||
(->> (http/send! {:uri (:file-data imgpart)
|
||||
|
@ -1567,7 +1594,7 @@
|
|||
|
||||
(calculate-paste-position [state mouse-pos in-viewport?]
|
||||
(let [page-objects (wsh/lookup-page-objects state)
|
||||
selected-objs (map #(get objects %) selected)
|
||||
selected-objs (map #(get paste-objects %) selected)
|
||||
has-frame? (d/seek #(= (:type %) :frame) selected-objs)
|
||||
page-selected (wsh/lookup-selected state)
|
||||
wrapper (gsh/selection-rect selected-objs)
|
||||
|
@ -1594,12 +1621,12 @@
|
|||
[frame-id parent-id delta index]))))
|
||||
|
||||
;; Change the indexes if the paste is done with an element selected
|
||||
(change-add-obj-index [objects selected index change]
|
||||
(change-add-obj-index [paste-objects selected index change]
|
||||
(let [set-index (fn [[result index] id]
|
||||
[(assoc result id index) (inc index)])
|
||||
|
||||
map-ids (when index
|
||||
(->> (vals objects)
|
||||
(->> (vals paste-objects)
|
||||
(filter #(not (selected (:parent-id %))))
|
||||
(map :id)
|
||||
(reduce set-index [{} (inc index)])
|
||||
|
@ -1611,8 +1638,8 @@
|
|||
|
||||
;; Check if the shape is an instance whose master is defined in a
|
||||
;; library that is not linked to the current file
|
||||
(foreign-instance? [shape objects state]
|
||||
(let [root (cph/get-root-shape shape objects)
|
||||
(foreign-instance? [shape paste-objects state]
|
||||
(let [root (cph/get-root-shape shape paste-objects)
|
||||
root-file-id (:component-file root)]
|
||||
(and (some? root)
|
||||
(not= root-file-id (:current-file-id state))
|
||||
|
@ -1620,34 +1647,37 @@
|
|||
|
||||
;; Procceed with the standard shape paste procediment.
|
||||
(do-paste [it state mouse-pos media]
|
||||
(let [media-idx (d/index-by :prev-id media)
|
||||
(let [page-objects (wsh/lookup-page-objects state)
|
||||
media-idx (d/index-by :prev-id media)
|
||||
|
||||
;; Calculate position for the pasted elements
|
||||
[frame-id parent-id delta index] (calculate-paste-position state mouse-pos in-viewport?)
|
||||
|
||||
objects (->> objects
|
||||
(d/mapm (fn [_ shape]
|
||||
(-> shape
|
||||
(assoc :frame-id frame-id)
|
||||
(assoc :parent-id parent-id)
|
||||
paste-objects (->> paste-objects
|
||||
(d/mapm (fn [_ shape]
|
||||
(-> shape
|
||||
(assoc :frame-id frame-id)
|
||||
(assoc :parent-id parent-id)
|
||||
|
||||
(cond->
|
||||
;; if foreign instance, detach the shape
|
||||
(foreign-instance? shape objects state)
|
||||
(dissoc :component-id
|
||||
:component-file
|
||||
:component-root?
|
||||
:remote-synced?
|
||||
:shape-ref
|
||||
:touched))))))
|
||||
(cond->
|
||||
;; if foreign instance, detach the shape
|
||||
(foreign-instance? shape paste-objects state)
|
||||
(dissoc :component-id
|
||||
:component-file
|
||||
:component-root?
|
||||
:remote-synced?
|
||||
:shape-ref
|
||||
:touched))))))
|
||||
|
||||
all-objects (merge page-objects paste-objects)
|
||||
|
||||
page-id (:current-page-id state)
|
||||
unames (-> (wsh/lookup-page-objects state page-id)
|
||||
(dwc/retrieve-used-names)) ;; TODO: move this calculation inside prepare-duplcate-changes?
|
||||
|
||||
rchanges (->> (dws/prepare-duplicate-changes objects page-id unames selected delta)
|
||||
rchanges (->> (dws/prepare-duplicate-changes all-objects page-id unames selected delta)
|
||||
(mapv (partial process-rchange media-idx))
|
||||
(mapv (partial change-add-obj-index objects selected index)))
|
||||
(mapv (partial change-add-obj-index paste-objects selected index)))
|
||||
|
||||
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))
|
||||
(reverse rchanges))
|
||||
|
@ -1751,69 +1781,12 @@
|
|||
;; Interactions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare move-create-interaction)
|
||||
(declare finish-create-interaction)
|
||||
|
||||
(defn start-create-interaction
|
||||
[]
|
||||
(ptk/reify ::start-create-interaction
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [initial-pos @ms/mouse-position
|
||||
selected (wsh/lookup-selected state)
|
||||
stopper (rx/filter ms/mouse-up? stream)]
|
||||
(when (= 1 (count selected))
|
||||
(rx/concat
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until stopper)
|
||||
(rx/map #(move-create-interaction initial-pos %)))
|
||||
(rx/of (finish-create-interaction initial-pos))))))))
|
||||
|
||||
(defn move-create-interaction
|
||||
[initial-pos position]
|
||||
(ptk/reify ::move-create-interaction
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected-shape-id (-> state wsh/lookup-selected first)
|
||||
selected-shape (get objects selected-shape-id)
|
||||
selected-shape-frame-id (:frame-id selected-shape)
|
||||
start-frame (get objects selected-shape-frame-id)
|
||||
end-frame (dwc/get-frame-at-point objects position)]
|
||||
(cond-> state
|
||||
(not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position)
|
||||
(not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame))))))
|
||||
|
||||
(defn finish-create-interaction
|
||||
[initial-pos]
|
||||
(ptk/reify ::finish-create-interaction
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :draw-interaction-to] nil)
|
||||
(assoc-in [:workspace-local :draw-interaction-to-frame] nil)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [position @ms/mouse-position
|
||||
page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
frame (dwc/get-frame-at-point objects position)
|
||||
|
||||
shape-id (-> state wsh/lookup-selected first)
|
||||
shape (get objects shape-id)]
|
||||
|
||||
(when-not (= position initial-pos)
|
||||
(if (and frame shape-id
|
||||
(not= (:id frame) (:id shape))
|
||||
(not= (:id frame) (:frame-id shape)))
|
||||
(rx/of (update-shape shape-id
|
||||
{:interactions [{:event-type :click
|
||||
:action-type :navigate
|
||||
:destination (:id frame)}]}))
|
||||
(rx/of (update-shape shape-id
|
||||
{:interactions []}))))))))
|
||||
(d/export dwi/start-edit-interaction)
|
||||
(d/export dwi/move-edit-interaction)
|
||||
(d/export dwi/finish-edit-interaction)
|
||||
(d/export dwi/start-move-overlay-pos)
|
||||
(d/export dwi/move-overlay-pos)
|
||||
(d/export dwi/finish-move-overlay-pos)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; CANVAS OPTIONS
|
||||
|
@ -1887,3 +1860,12 @@
|
|||
(d/export dwg/unmask-group)
|
||||
(d/export dwg/group-selected)
|
||||
(d/export dwg/ungroup-selected)
|
||||
|
||||
;; Boolean
|
||||
(d/export dwb/create-bool)
|
||||
(d/export dwb/group-to-bool)
|
||||
(d/export dwb/bool-to-group)
|
||||
(d/export dwb/change-bool-type)
|
||||
|
||||
;; Shapes to path
|
||||
(d/export dwps/convert-selected-to-path)
|
||||
|
|
131
frontend/src/app/main/data/workspace/booleans.cljs
Normal file
131
frontend/src/app/main/data/workspace/booleans.cljs
Normal file
|
@ -0,0 +1,131 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.booleans
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.changes-builder :as cb]
|
||||
[app.common.path.shapes-to-path :as stp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(defn selected-shapes
|
||||
[state]
|
||||
(let [objects (wsh/lookup-page-objects state)]
|
||||
(->> (wsh/lookup-selected state)
|
||||
(cp/clean-loops objects)
|
||||
(map #(get objects %))
|
||||
(filter #(not= :frame (:type %)))
|
||||
(map #(assoc % ::index (cp/position-on-parent (:id %) objects)))
|
||||
(sort-by ::index))))
|
||||
|
||||
(defn create-bool-data
|
||||
[bool-type name shapes objects]
|
||||
(let [shapes (mapv #(stp/convert-to-path % objects) shapes)
|
||||
head (if (= bool-type :difference) (first shapes) (last shapes))
|
||||
head (cond-> head
|
||||
(and (contains? head :svg-attrs) (nil? (:fill-color head)))
|
||||
(assoc :fill-color "#000000"))
|
||||
|
||||
head-data (select-keys head stp/style-properties)]
|
||||
[(-> {:id (uuid/next)
|
||||
:type :bool
|
||||
:bool-type bool-type
|
||||
:frame-id (:frame-id head)
|
||||
:parent-id (:parent-id head)
|
||||
:name name
|
||||
:shapes []}
|
||||
(merge head-data)
|
||||
(gsh/update-bool-selrect shapes objects))
|
||||
(cp/position-on-parent (:id head) objects)]))
|
||||
|
||||
(defn group->bool
|
||||
[group bool-type objects]
|
||||
|
||||
(let [shapes (->> (:shapes group)
|
||||
(map #(get objects %))
|
||||
(mapv #(stp/convert-to-path % objects)))
|
||||
head (if (= bool-type :difference) (first shapes) (last shapes))
|
||||
head (cond-> head
|
||||
(and (contains? head :svg-attrs) (nil? (:fill-color head)))
|
||||
(assoc :fill-color "#000000"))
|
||||
head-data (select-keys head stp/style-properties)]
|
||||
|
||||
(-> group
|
||||
(assoc :type :bool)
|
||||
(assoc :bool-type bool-type)
|
||||
(merge head-data)
|
||||
(gsh/update-bool-selrect shapes objects))))
|
||||
|
||||
(defn bool->group
|
||||
[shape objects]
|
||||
|
||||
(let [children (->> (:shapes shape)
|
||||
(mapv #(get objects %)))]
|
||||
(-> shape
|
||||
(assoc :type :group)
|
||||
(dissoc :bool-type)
|
||||
(d/without-keys stp/style-group-properties)
|
||||
(gsh/update-group-selrect children))))
|
||||
|
||||
(defn create-bool
|
||||
[bool-type]
|
||||
(ptk/reify ::create-bool-union
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state)
|
||||
base-name (-> bool-type d/name str/capital (str "-1"))
|
||||
name (-> (dwc/retrieve-used-names objects)
|
||||
(dwc/generate-unique-name base-name))
|
||||
shapes (selected-shapes state)]
|
||||
|
||||
(when-not (empty? shapes)
|
||||
(let [[boolean-data index] (create-bool-data bool-type name shapes objects)
|
||||
shape-id (:id boolean-data)
|
||||
changes (-> (cb/empty-changes it page-id)
|
||||
(cb/with-objects objects)
|
||||
(cb/add-obj boolean-data index)
|
||||
(cb/change-parent shape-id shapes))]
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(dwc/select-shapes (d/ordered-set shape-id)))))))))
|
||||
|
||||
(defn group-to-bool
|
||||
[shape-id bool-type]
|
||||
(ptk/reify ::group-to-bool
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
change-to-bool
|
||||
(fn [shape] (group->bool shape bool-type objects))]
|
||||
(rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true}))))))
|
||||
|
||||
(defn bool-to-group
|
||||
[shape-id]
|
||||
(ptk/reify ::bool-to-group
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
change-to-group
|
||||
(fn [shape] (bool->group shape objects))]
|
||||
(rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true}))))))
|
||||
|
||||
|
||||
(defn change-bool-type
|
||||
[shape-id bool-type]
|
||||
(ptk/reify ::change-bool-type
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [change-type
|
||||
(fn [shape] (assoc shape :bool-type bool-type))]
|
||||
(rx/of (dch/update-shapes [shape-id] change-type {:reg-objects? true}))))))
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.data.workspace.changes
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as log]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.spec :as spec]
|
||||
[app.common.spec :as us]
|
||||
|
@ -14,7 +15,6 @@
|
|||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.store :as st]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.logging :as log]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]))
|
||||
|
|
|
@ -9,15 +9,17 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.geom.proportions :as gpr]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.interactions :as cti]
|
||||
[app.common.types.page-options :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.logging :as log]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]))
|
||||
|
@ -379,8 +381,10 @@
|
|||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
options (wsh/lookup-page-options state page-id)
|
||||
|
||||
ids (cp/clean-loops objects ids)
|
||||
flows (:flows options)
|
||||
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
|
@ -399,9 +403,14 @@
|
|||
interacting-shapes
|
||||
(filter (fn [shape]
|
||||
(let [interactions (:interactions shape)]
|
||||
(some ids (map :destination interactions))))
|
||||
(some #(and (cti/has-destination %)
|
||||
(contains? ids (:destination %)))
|
||||
interactions)))
|
||||
(vals objects))
|
||||
|
||||
starting-flows
|
||||
(filter #(contains? ids (:starting-frame %)) flows)
|
||||
|
||||
empty-parents-xform
|
||||
(comp
|
||||
(map (fn [id] (get objects id)))
|
||||
|
@ -467,7 +476,8 @@
|
|||
:operations [{:type :set
|
||||
:attr :interactions
|
||||
:val (vec (remove (fn [interaction]
|
||||
(contains? ids (:destination interaction)))
|
||||
(and (cti/has-destination interaction)
|
||||
(contains? ids (:destination interaction))))
|
||||
(:interactions obj)))}]})))
|
||||
mk-mod-int-add-xf
|
||||
(comp (filter some?)
|
||||
|
@ -479,6 +489,22 @@
|
|||
:attr :interactions
|
||||
:val (:interactions obj)}]})))
|
||||
|
||||
mk-mod-del-flow-xf
|
||||
(comp (filter some?)
|
||||
(map (fn [flow]
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option :flows
|
||||
:value (cto/remove-flow flows (:id flow))})))
|
||||
|
||||
mk-mod-add-flow-xf
|
||||
(comp (filter some?)
|
||||
(map (fn [_]
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option :flows
|
||||
:value flows})))
|
||||
|
||||
mk-mod-unmask-xf
|
||||
(comp (filter (partial contains? objects))
|
||||
(map (fn [id]
|
||||
|
@ -508,7 +534,8 @@
|
|||
:page-id page-id
|
||||
:shapes (vec all-parents)})
|
||||
(into mk-mod-unmask-xf groups-to-unmask)
|
||||
(into mk-mod-int-del-xf interacting-shapes))
|
||||
(into mk-mod-int-del-xf interacting-shapes)
|
||||
(into mk-mod-del-flow-xf starting-flows))
|
||||
|
||||
uchanges
|
||||
(-> []
|
||||
|
@ -520,8 +547,8 @@
|
|||
:shapes (vec all-parents)})
|
||||
(into mk-mod-touched-xf (reverse all-parents))
|
||||
(into mk-mod-mask-xf groups-to-unmask)
|
||||
(into mk-mod-int-add-xf interacting-shapes))
|
||||
]
|
||||
(into mk-mod-int-add-xf interacting-shapes)
|
||||
(into mk-mod-add-flow-xf starting-flows))]
|
||||
|
||||
;; (println "================ rchanges")
|
||||
;; (cljs.pprint/pprint rchanges)
|
||||
|
|
|
@ -198,7 +198,7 @@
|
|||
group-id (first selected)
|
||||
group (get objects group-id)]
|
||||
(when (and (= 1 (count selected))
|
||||
(= (:type group) :group))
|
||||
(contains? #{:group :bool} (:type group)))
|
||||
(let [[rchanges uchanges]
|
||||
(prepare-remove-group page-id group objects)]
|
||||
(rx/of (dch/commit-changes {:redo-changes rchanges
|
||||
|
|
332
frontend/src/app/main/data/workspace/interactions.cljs
Normal file
332
frontend/src/app/main/data/workspace/interactions.cljs
Normal file
|
@ -0,0 +1,332 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.interactions
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.interactions :as cti]
|
||||
[app.common.types.page-options :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.streams :as ms]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; --- Flows
|
||||
|
||||
(defn add-flow
|
||||
[starting-frame]
|
||||
(ptk/reify ::add-flow
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
flows (get-in state [:workspace-data
|
||||
:pages-index
|
||||
page-id
|
||||
:options
|
||||
:flows] [])
|
||||
|
||||
unames (into #{} (map :name flows))
|
||||
name (dwc/generate-unique-name unames "Flow-1")
|
||||
|
||||
new-flow {:id (uuid/next)
|
||||
:name name
|
||||
:starting-frame starting-frame}]
|
||||
|
||||
(rx/of (dch/commit-changes
|
||||
{:redo-changes [{:type :set-option
|
||||
:page-id page-id
|
||||
:option :flows
|
||||
:value (cto/add-flow flows new-flow)}]
|
||||
:undo-changes [{:type :set-option
|
||||
:page-id page-id
|
||||
:option :flows
|
||||
:value flows}]
|
||||
:origin it}))))))
|
||||
|
||||
(defn add-flow-selected-frame
|
||||
[]
|
||||
(ptk/reify ::add-flow-selected-frame
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [selected (wsh/lookup-selected state)]
|
||||
(rx/of (add-flow (first selected)))))))
|
||||
|
||||
(defn remove-flow
|
||||
[flow-id]
|
||||
(us/verify ::us/uuid flow-id)
|
||||
(ptk/reify ::remove-flow
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
flows (get-in state [:workspace-data
|
||||
:pages-index
|
||||
page-id
|
||||
:options
|
||||
:flows] [])]
|
||||
(rx/of (dch/commit-changes
|
||||
{:redo-changes [{:type :set-option
|
||||
:page-id page-id
|
||||
:option :flows
|
||||
:value (cto/remove-flow flows flow-id)}]
|
||||
:undo-changes [{:type :set-option
|
||||
:page-id page-id
|
||||
:option :flows
|
||||
:value flows}]
|
||||
:origin it}))))))
|
||||
|
||||
(defn rename-flow
|
||||
[flow-id name]
|
||||
(us/verify ::us/uuid flow-id)
|
||||
(us/verify ::us/string name)
|
||||
(ptk/reify ::rename-flow
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
flows (get-in state [:workspace-data
|
||||
:pages-index
|
||||
page-id
|
||||
:options
|
||||
:flows] [])]
|
||||
(rx/of (dch/commit-changes
|
||||
{:redo-changes [{:type :set-option
|
||||
:page-id page-id
|
||||
:option :flows
|
||||
:value (cto/update-flow flows flow-id
|
||||
#(cto/rename-flow % name))}]
|
||||
:undo-changes [{:type :set-option
|
||||
:page-id page-id
|
||||
:option :flows
|
||||
:value flows}]
|
||||
:origin it}))))))
|
||||
|
||||
|
||||
(defn start-rename-flow
|
||||
[id]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::start-rename-flow
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :flow-for-rename] id))))
|
||||
|
||||
(defn end-rename-flow
|
||||
[]
|
||||
(ptk/reify ::end-rename-flow
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-local dissoc :flow-for-rename))))
|
||||
|
||||
;; --- Interactions
|
||||
|
||||
(defn add-new-interaction
|
||||
([shape] (add-new-interaction shape nil))
|
||||
([shape destination]
|
||||
(ptk/reify ::add-new-interaction
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
frame (cph/get-frame shape objects)
|
||||
flows (get-in state [:workspace-data
|
||||
:pages-index
|
||||
page-id
|
||||
:options
|
||||
:flows] [])
|
||||
flow (cto/get-frame-flow flows (:id frame))]
|
||||
(rx/concat
|
||||
(rx/of (dch/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(let [new-interaction (cti/set-destination
|
||||
cti/default-interaction
|
||||
destination)]
|
||||
(update shape :interactions
|
||||
cti/add-interaction new-interaction)))))
|
||||
(when (and (not (cph/connected-frame? (:id frame) objects))
|
||||
(nil? flow))
|
||||
(rx/of (add-flow (:id frame))))))))))
|
||||
|
||||
(defn remove-interaction
|
||||
[shape index]
|
||||
(ptk/reify ::remove-interaction
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dch/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(update shape :interactions
|
||||
cti/remove-interaction index)))))))
|
||||
|
||||
(defn update-interaction
|
||||
[shape index update-fn]
|
||||
(ptk/reify ::update-interaction
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dch/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(update shape :interactions
|
||||
cti/update-interaction index update-fn)))))))
|
||||
|
||||
(declare move-edit-interaction)
|
||||
(declare finish-edit-interaction)
|
||||
|
||||
(defn start-edit-interaction
|
||||
[index]
|
||||
(ptk/reify ::start-edit-interaction
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :editing-interaction-index] index))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [initial-pos @ms/mouse-position
|
||||
selected (wsh/lookup-selected state)
|
||||
stopper (rx/filter ms/mouse-up? stream)]
|
||||
(when (= 1 (count selected))
|
||||
(rx/concat
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until stopper)
|
||||
(rx/map #(move-edit-interaction initial-pos %)))
|
||||
(rx/of (finish-edit-interaction index initial-pos))))))))
|
||||
|
||||
(defn move-edit-interaction
|
||||
[initial-pos position]
|
||||
(ptk/reify ::move-edit-interaction
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected-shape-id (-> state wsh/lookup-selected first)
|
||||
selected-shape (get objects selected-shape-id)
|
||||
selected-shape-frame-id (:frame-id selected-shape)
|
||||
start-frame (get objects selected-shape-frame-id)
|
||||
end-frame (dwc/get-frame-at-point objects position)]
|
||||
(cond-> state
|
||||
(not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position)
|
||||
(not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame))))))
|
||||
|
||||
(defn finish-edit-interaction
|
||||
[index initial-pos]
|
||||
(ptk/reify ::finish-edit-interaction
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :editing-interaction-index] nil)
|
||||
(assoc-in [:workspace-local :draw-interaction-to] nil)
|
||||
(assoc-in [:workspace-local :draw-interaction-to-frame] nil)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [position @ms/mouse-position
|
||||
page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
frame (dwc/get-frame-at-point objects position)
|
||||
|
||||
shape-id (-> state wsh/lookup-selected first)
|
||||
shape (get objects shape-id)]
|
||||
|
||||
(when (and shape (not (= position initial-pos)))
|
||||
(if (nil? frame)
|
||||
(when index
|
||||
(rx/of (remove-interaction shape index)))
|
||||
(let [frame (if (or (= (:id frame) (:id shape))
|
||||
(= (:id frame) (:frame-id shape)))
|
||||
nil ;; Drop onto self frame -> set destination to none
|
||||
frame)]
|
||||
(if (nil? index)
|
||||
(rx/of (add-new-interaction shape (:id frame)))
|
||||
(rx/of (update-interaction shape index
|
||||
(fn [interaction]
|
||||
(cond-> interaction
|
||||
(not (cti/has-destination interaction))
|
||||
(cti/set-action-type :navigate)
|
||||
|
||||
:always
|
||||
(cti/set-destination (:id frame))))))))))))))
|
||||
;; --- Overlays
|
||||
|
||||
(declare move-overlay-pos)
|
||||
(declare finish-move-overlay-pos)
|
||||
|
||||
(defn start-move-overlay-pos
|
||||
[index]
|
||||
(ptk/reify ::start-move-overlay-pos
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :move-overlay-to] nil)
|
||||
(assoc-in [:workspace-local :move-overlay-index] index)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [initial-pos @ms/mouse-position
|
||||
selected (wsh/lookup-selected state)
|
||||
stopper (rx/filter ms/mouse-up? stream)]
|
||||
(when (= 1 (count selected))
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
shape (->> state
|
||||
wsh/lookup-selected
|
||||
first
|
||||
(get objects))
|
||||
overlay-pos (-> shape
|
||||
(get-in [:interactions index])
|
||||
:overlay-position)
|
||||
orig-frame (cph/get-frame shape objects)
|
||||
frame-pos (gpt/point (:x orig-frame) (:y orig-frame))
|
||||
offset (-> initial-pos
|
||||
(gpt/subtract overlay-pos)
|
||||
(gpt/subtract frame-pos))]
|
||||
(rx/concat
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until stopper)
|
||||
(rx/map #(move-overlay-pos % frame-pos offset)))
|
||||
(rx/of (finish-move-overlay-pos index frame-pos offset)))))))))
|
||||
|
||||
(defn move-overlay-pos
|
||||
[pos frame-pos offset]
|
||||
(ptk/reify ::move-overlay-pos
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pos (-> pos
|
||||
(gpt/subtract frame-pos)
|
||||
(gpt/subtract offset))]
|
||||
(assoc-in state [:workspace-local :move-overlay-to] pos)))))
|
||||
|
||||
(defn finish-move-overlay-pos
|
||||
[index frame-pos offset]
|
||||
(ptk/reify ::finish-move-overlay-pos
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(d/dissoc-in [:workspace-local :move-overlay-to])
|
||||
(d/dissoc-in [:workspace-local :move-overlay-index])))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [pos @ms/mouse-position
|
||||
overlay-pos (-> pos
|
||||
(gpt/subtract frame-pos)
|
||||
(gpt/subtract offset))
|
||||
|
||||
page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
shape (->> state
|
||||
wsh/lookup-selected
|
||||
first
|
||||
(get objects))
|
||||
|
||||
interactions (:interactions shape)
|
||||
|
||||
new-interactions
|
||||
(update interactions index
|
||||
#(cti/set-overlay-position % overlay-pos))]
|
||||
|
||||
(rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions})))))))
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.logging :as log]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -18,10 +19,10 @@
|
|||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.libraries-helpers :as dwlh]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.logging :as log]
|
||||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[beicon.core :as rx]
|
||||
|
@ -134,10 +135,12 @@
|
|||
:color color}
|
||||
uchg {:type :mod-color
|
||||
:color prev}]
|
||||
(rx/of (dch/commit-changes {:redo-changes [rchg]
|
||||
(rx/of (dwu/start-undo-transaction)
|
||||
(dch/commit-changes {:redo-changes [rchg]
|
||||
:undo-changes [uchg]
|
||||
:origin it})
|
||||
(sync-file (:current-file-id state) file-id))))))
|
||||
(sync-file (:current-file-id state) file-id)
|
||||
(dwu/commit-undo-transaction))))))
|
||||
|
||||
(defn delete-color
|
||||
[{:keys [id] :as params}]
|
||||
|
@ -244,10 +247,12 @@
|
|||
:typography typography}
|
||||
uchg {:type :mod-typography
|
||||
:typography prev}]
|
||||
(rx/of (dch/commit-changes {:redo-changes [rchg]
|
||||
(rx/of (dwu/start-undo-transaction)
|
||||
(dch/commit-changes {:redo-changes [rchg]
|
||||
:undo-changes [uchg]
|
||||
:origin it})
|
||||
(sync-file (:current-file-id state) file-id))))))
|
||||
(sync-file (:current-file-id state) file-id)
|
||||
(dwu/commit-undo-transaction))))))
|
||||
|
||||
(defn delete-typography
|
||||
[id]
|
||||
|
@ -516,12 +521,14 @@
|
|||
(ptk/reify ::nav-to-component-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file (get-in state [:workspace-libraries file-id])
|
||||
pparams {:project-id (:project-id file)
|
||||
:file-id (:id file)}
|
||||
qparams {:page-id (first (get-in file [:data :pages]))
|
||||
:layout :assets}]
|
||||
(rx/of (rt/nav-new-window :workspace pparams qparams))))))
|
||||
(let [file (get-in state [:workspace-libraries file-id])
|
||||
path-params {:project-id (:project-id file)
|
||||
:file-id (:id file)}
|
||||
query-params {:page-id (first (get-in file [:data :pages]))
|
||||
:layout :assets}]
|
||||
(rx/of (rt/nav-new-window* {:rname :workspace
|
||||
:path-params path-params
|
||||
:query-params query-params}))))))
|
||||
|
||||
(defn ext-library-changed
|
||||
[file-id modified-at revn changes]
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.logging :as log]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.text :as txt]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.util.logging :as log]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.set :as set]))
|
||||
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
(ns app.main.data.workspace.path.drawing
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as upg]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.path.commands :as upc]
|
||||
[app.common.path.shapes-to-path :as upsp]
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
|
@ -21,9 +24,6 @@
|
|||
[app.main.data.workspace.path.undo :as undo]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.path.commands :as upc]
|
||||
[app.util.path.geom :as upg]
|
||||
[app.util.path.shapes-to-path :as upsp]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as upg]
|
||||
[app.common.path.commands :as upc]
|
||||
[app.common.path.shapes-to-path :as upsp]
|
||||
[app.common.path.subpaths :as ups]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.path.changes :as changes]
|
||||
|
@ -19,10 +23,6 @@
|
|||
[app.main.data.workspace.path.undo :as undo]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.path.commands :as upc]
|
||||
[app.util.path.geom :as upg]
|
||||
[app.util.path.shapes-to-path :as upsp]
|
||||
[app.util.path.subpaths :as ups]
|
||||
[app.util.path.tools :as upt]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.path.commands :as upc]
|
||||
[app.common.path.subpaths :as ups]
|
||||
[app.main.data.workspace.path.common :as common]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.path.commands :as upc]
|
||||
[app.util.path.subpaths :as ups]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(defn end-path-event? [event]
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.path.shapes-to-path
|
||||
(:require
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.changes-builder :as cb]
|
||||
[app.common.path.shapes-to-path :as upsp]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(defn convert-selected-to-path []
|
||||
(ptk/reify ::convert-selected-to-path
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state)
|
||||
selected (wsh/lookup-selected state)
|
||||
|
||||
children-ids
|
||||
(into #{}
|
||||
(mapcat #(cp/get-children % objects))
|
||||
selected)
|
||||
|
||||
changes
|
||||
(-> (cb/empty-changes it page-id)
|
||||
(cb/with-objects objects)
|
||||
(cb/remove-objects children-ids)
|
||||
(cb/update-shapes selected #(upsp/convert-to-path % objects)))]
|
||||
|
||||
(rx/of (dch/commit-changes changes))))))
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.main.data.workspace.path.state
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.util.path.shapes-to-path :as upsp]))
|
||||
[app.common.path.shapes-to-path :as upsp]))
|
||||
|
||||
(defn get-path-id
|
||||
"Retrieves the currently editing path id"
|
||||
|
@ -31,7 +31,8 @@
|
|||
[state & ks]
|
||||
(let [path-loc (get-path-location state)
|
||||
shape (-> (get-in state path-loc)
|
||||
(upsp/convert-to-path))]
|
||||
;; Empty map because we know the current shape will not have children
|
||||
(upsp/convert-to-path {}))]
|
||||
|
||||
(if (empty? ks)
|
||||
shape
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
(ns app.main.data.workspace.path.streams
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as upg]
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.workspace.path.state :as state]
|
||||
[app.main.snap :as snap]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.path.geom :as upg]
|
||||
[beicon.core :as rx]
|
||||
[okulary.core :as l]
|
||||
[potok.core :as ptk]))
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
|
||||
(ns app.main.data.workspace.path.tools
|
||||
(:require
|
||||
[app.common.path.shapes-to-path :as upsp]
|
||||
[app.common.path.subpaths :as ups]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.path.changes :as changes]
|
||||
[app.main.data.workspace.path.state :as st]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.util.path.shapes-to-path :as upsp]
|
||||
[app.util.path.subpaths :as ups]
|
||||
[app.util.path.tools :as upt]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.interactions :as cti]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.modal :as md]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
|
@ -288,12 +289,17 @@
|
|||
fit."
|
||||
[objects page-id unames ids delta]
|
||||
(let [unames (volatile! unames)
|
||||
update-unames! (fn [new-name] (vswap! unames conj new-name))]
|
||||
update-unames! (fn [new-name] (vswap! unames conj new-name))
|
||||
all-ids (reduce (fn [ids-set id]
|
||||
(into ids-set (cons id (cp/get-children id objects))))
|
||||
#{}
|
||||
ids)
|
||||
ids-map (into {} (map #(vector % (uuid/next)) all-ids))]
|
||||
(loop [ids (seq ids)
|
||||
chgs []]
|
||||
(if ids
|
||||
(let [id (first ids)
|
||||
result (prepare-duplicate-change objects page-id unames update-unames! id delta)
|
||||
result (prepare-duplicate-change objects page-id unames update-unames! ids-map id delta)
|
||||
result (if (vector? result) result [result])]
|
||||
(recur
|
||||
(next ids)
|
||||
|
@ -313,22 +319,27 @@
|
|||
(-> changes (update-indices index-map))))
|
||||
|
||||
(defn- prepare-duplicate-change
|
||||
[objects page-id unames update-unames! id delta]
|
||||
[objects page-id unames update-unames! ids-map id delta]
|
||||
(let [obj (get objects id)]
|
||||
(if (= :frame (:type obj))
|
||||
(prepare-duplicate-frame-change objects page-id unames update-unames! obj delta)
|
||||
(prepare-duplicate-shape-change objects page-id unames update-unames! obj delta (:frame-id obj) (:parent-id obj)))))
|
||||
(prepare-duplicate-frame-change objects page-id unames update-unames! ids-map obj delta)
|
||||
(prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta (:frame-id obj) (:parent-id obj)))))
|
||||
|
||||
(defn- prepare-duplicate-shape-change
|
||||
[objects page-id unames update-unames! obj delta frame-id parent-id]
|
||||
[objects page-id unames update-unames! ids-map obj delta frame-id parent-id]
|
||||
(when (some? obj)
|
||||
(let [id (uuid/next)
|
||||
(let [new-id (ids-map (:id obj))
|
||||
parent-id (or parent-id frame-id)
|
||||
name (dwc/generate-unique-name @unames (:name obj))
|
||||
_ (update-unames! name)
|
||||
|
||||
renamed-obj (assoc obj :id id :name name)
|
||||
moved-obj (geom/move renamed-obj delta)
|
||||
parent-id (or parent-id frame-id)
|
||||
new-obj (-> obj
|
||||
(assoc :id new-id
|
||||
:name name
|
||||
:frame-id frame-id)
|
||||
(dissoc :shapes)
|
||||
(geom/move delta)
|
||||
(d/update-when :interactions #(cti/remap-interactions % ids-map objects)))
|
||||
|
||||
children-changes
|
||||
(loop [result []
|
||||
|
@ -337,47 +348,45 @@
|
|||
(if (nil? cid)
|
||||
result
|
||||
(let [obj (get objects cid)
|
||||
changes (prepare-duplicate-shape-change objects page-id unames update-unames! obj delta frame-id id)]
|
||||
changes (prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta frame-id new-id)]
|
||||
(recur
|
||||
(into result changes)
|
||||
(first cids)
|
||||
(rest cids)))))
|
||||
(rest cids)))))]
|
||||
|
||||
reframed-obj (-> moved-obj
|
||||
(assoc :frame-id frame-id)
|
||||
(dissoc :shapes))]
|
||||
(into [{:type :add-obj
|
||||
:id id
|
||||
:id new-id
|
||||
:page-id page-id
|
||||
:old-id (:id obj)
|
||||
:frame-id frame-id
|
||||
:parent-id parent-id
|
||||
:ignore-touched true
|
||||
:obj (dissoc reframed-obj :shapes)}]
|
||||
:obj new-obj}]
|
||||
children-changes))))
|
||||
|
||||
(defn- prepare-duplicate-frame-change
|
||||
[objects page-id unames update-unames! obj delta]
|
||||
(let [frame-id (uuid/next)
|
||||
[objects page-id unames update-unames! ids-map obj delta]
|
||||
(let [new-id (ids-map (:id obj))
|
||||
frame-name (dwc/generate-unique-name @unames (:name obj))
|
||||
_ (update-unames! frame-name)
|
||||
|
||||
sch (->> (map #(get objects %) (:shapes obj))
|
||||
(mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! % delta frame-id frame-id)))
|
||||
(mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! ids-map % delta new-id new-id)))
|
||||
|
||||
frame (-> obj
|
||||
(assoc :id frame-id)
|
||||
(assoc :name frame-name)
|
||||
(assoc :frame-id uuid/zero)
|
||||
(assoc :shapes [])
|
||||
(geom/move delta))
|
||||
new-frame (-> obj
|
||||
(assoc :id new-id
|
||||
:name frame-name
|
||||
:frame-id uuid/zero
|
||||
:shapes [])
|
||||
(geom/move delta)
|
||||
(d/update-when :interactions #(cti/remap-interactions % ids-map objects)))
|
||||
|
||||
fch {:type :add-obj
|
||||
:old-id (:id obj)
|
||||
:page-id page-id
|
||||
:id frame-id
|
||||
:id new-id
|
||||
:frame-id uuid/zero
|
||||
:obj frame}]
|
||||
:obj new-frame}]
|
||||
|
||||
(into [fch] sch)))
|
||||
|
||||
|
@ -420,48 +429,49 @@
|
|||
(gpt/point (+ (:width obj) 50) 0)
|
||||
(gpt/point 0 0))
|
||||
|
||||
(let [obj-original (get objects id-original)
|
||||
obj-duplicated (get objects id-duplicated)
|
||||
distance (gpt/subtract (gpt/point obj-duplicated)
|
||||
(gpt/point obj-original))
|
||||
new-pos (gpt/add (gpt/point obj-duplicated) distance)
|
||||
delta (gpt/subtract new-pos (gpt/point obj))]
|
||||
delta))))
|
||||
(let [pt-original (-> (get objects id-original) :selrect gpt/point)
|
||||
pt-duplicated (-> (get objects id-duplicated) :selrect gpt/point)
|
||||
pt-obj (-> obj :selrect gpt/point)
|
||||
distance (gpt/subtract pt-duplicated pt-original)
|
||||
new-pos (gpt/add pt-duplicated distance)]
|
||||
|
||||
(def duplicate-selected
|
||||
(gpt/subtract new-pos pt-obj)))))
|
||||
|
||||
(defn duplicate-selected [move-delta?]
|
||||
(ptk/reify ::duplicate-selected
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected (wsh/lookup-selected state)
|
||||
delta (if (= (count selected) 1)
|
||||
(let [obj (get objects (first selected))]
|
||||
(calc-duplicate-delta obj state objects))
|
||||
(gpt/point 0 0))
|
||||
(when (nil? (get-in state [:workspace-local :transform]))
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected (wsh/lookup-selected state)
|
||||
delta (if (and move-delta? (= (count selected) 1))
|
||||
(let [obj (get objects (first selected))]
|
||||
(calc-duplicate-delta obj state objects))
|
||||
(gpt/point 0 0))
|
||||
|
||||
unames (dwc/retrieve-used-names objects)
|
||||
unames (dwc/retrieve-used-names objects)
|
||||
|
||||
rchanges (->> (prepare-duplicate-changes objects page-id unames selected delta)
|
||||
(duplicate-changes-update-indices objects selected))
|
||||
rchanges (->> (prepare-duplicate-changes objects page-id unames selected delta)
|
||||
(duplicate-changes-update-indices objects selected))
|
||||
|
||||
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))
|
||||
(reverse rchanges))
|
||||
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))
|
||||
(reverse rchanges))
|
||||
|
||||
id-original (when (= (count selected) 1) (first selected))
|
||||
id-original (when (= (count selected) 1) (first selected))
|
||||
|
||||
selected (->> rchanges
|
||||
(filter #(selected (:old-id %)))
|
||||
(map #(get-in % [:obj :id]))
|
||||
(into (d/ordered-set)))
|
||||
selected (->> rchanges
|
||||
(filter #(selected (:old-id %)))
|
||||
(map #(get-in % [:obj :id]))
|
||||
(into (d/ordered-set)))
|
||||
|
||||
id-duplicated (when (= (count selected) 1) (first selected))]
|
||||
id-duplicated (when (= (count selected) 1) (first selected))]
|
||||
|
||||
(rx/of (dch/commit-changes {:redo-changes rchanges
|
||||
:undo-changes uchanges
|
||||
:origin it})
|
||||
(select-shapes selected)
|
||||
(memorize-duplicated id-original id-duplicated))))))
|
||||
(rx/of (dch/commit-changes {:redo-changes rchanges
|
||||
:undo-changes uchanges
|
||||
:origin it})
|
||||
(select-shapes selected)
|
||||
(memorize-duplicated id-original id-duplicated)))))))
|
||||
|
||||
(defn change-hover-state
|
||||
[id value]
|
||||
|
|
|
@ -119,7 +119,7 @@
|
|||
|
||||
:duplicate {:tooltip (ds/meta "D")
|
||||
:command (ds/c-mod "d")
|
||||
:fn #(st/emit! dw/duplicate-selected)}
|
||||
:fn #(st/emit! (dw/duplicate-selected true))}
|
||||
|
||||
:undo {:tooltip (ds/meta "Z")
|
||||
:command (ds/c-mod "z")
|
||||
|
@ -260,6 +260,23 @@
|
|||
:command ["alt" "."]
|
||||
:type "keyup"
|
||||
:fn #(st/emit! (dw/toggle-distances-display false))}
|
||||
|
||||
:boolean-union {:tooltip (ds/alt "U")
|
||||
:command "alt+u"
|
||||
:fn #(st/emit! (dw/create-bool :union))}
|
||||
|
||||
:boolean-difference {:tooltip (ds/alt "D")
|
||||
:command "alt+d"
|
||||
:fn #(st/emit! (dw/create-bool :difference))}
|
||||
|
||||
:boolean-intersection {:tooltip (ds/alt "I")
|
||||
:command "alt+i"
|
||||
:fn #(st/emit! (dw/create-bool :intersection))}
|
||||
|
||||
:boolean-exclude {:tooltip (ds/alt "E")
|
||||
:command "alt+e"
|
||||
:fn #(st/emit! (dw/create-bool :exclude))}
|
||||
|
||||
})
|
||||
|
||||
(defn get-tooltip [shortcut]
|
||||
|
|
|
@ -178,7 +178,8 @@
|
|||
:y (+ y offset-y)}
|
||||
(gsh/setup-selrect)
|
||||
(assoc :svg-attrs (-> (:attrs svg-data)
|
||||
(dissoc :viewBox :xmlns))))))
|
||||
(dissoc :viewBox :xmlns)
|
||||
(d/without-keys usvg/inheritable-props))))))
|
||||
|
||||
(defn create-group [name frame-id svg-data {:keys [attrs]}]
|
||||
(let [svg-transform (usvg/parse-transform (:transform attrs))
|
||||
|
@ -368,16 +369,16 @@
|
|||
;; SVG graphic elements
|
||||
;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use
|
||||
(let [shape (-> (case tag
|
||||
(:g :a :svg) (create-group name frame-id svg-data element-data)
|
||||
:rect (create-rect-shape name frame-id svg-data element-data)
|
||||
(:g :a :svg) (create-group name frame-id svg-data element-data)
|
||||
:rect (create-rect-shape name frame-id svg-data element-data)
|
||||
(:circle
|
||||
:ellipse) (create-circle-shape name frame-id svg-data element-data)
|
||||
:path (create-path-shape name frame-id svg-data element-data)
|
||||
:polyline (create-path-shape name frame-id svg-data (-> element-data usvg/polyline->path))
|
||||
:polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path))
|
||||
:line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path))
|
||||
:image (create-image-shape name frame-id svg-data element-data)
|
||||
#_other (create-raw-svg name frame-id svg-data element-data))
|
||||
:ellipse) (create-circle-shape name frame-id svg-data element-data)
|
||||
:path (create-path-shape name frame-id svg-data element-data)
|
||||
:polyline (create-path-shape name frame-id svg-data (-> element-data usvg/polyline->path))
|
||||
:polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path))
|
||||
:line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path))
|
||||
:image (create-image-shape name frame-id svg-data element-data)
|
||||
#_other (create-raw-svg name frame-id svg-data element-data))
|
||||
|
||||
)
|
||||
shape (when (some? shape)
|
||||
|
@ -387,7 +388,7 @@
|
|||
(setup-stroke)))
|
||||
|
||||
children (cond->> (:content element-data)
|
||||
(= tag :g)
|
||||
(or (= tag :g) (= tag :svg))
|
||||
(mapv #(usvg/inherit-attributes attrs %)))]
|
||||
[shape children]))))
|
||||
|
||||
|
@ -487,11 +488,15 @@
|
|||
;; Creates the root shape
|
||||
changes (dwc/add-shape-changes page-id objects selected root-shape false)
|
||||
|
||||
root-attrs (-> (:attrs svg-data)
|
||||
(usvg/format-styles))
|
||||
|
||||
;; Reduces the children to create the changes to add the children shapes
|
||||
[_ [rchanges uchanges]]
|
||||
(reduce (partial add-svg-child-changes page-id objects selected frame-id root-id svg-data)
|
||||
[unames changes]
|
||||
(d/enumerate (:content svg-data)))
|
||||
(d/enumerate (->> (:content svg-data)
|
||||
(mapv #(usvg/inherit-attributes root-attrs %)))))
|
||||
|
||||
reg-objects-action {:type :reg-objects
|
||||
:page-id page-id
|
||||
|
|
|
@ -180,12 +180,10 @@
|
|||
shape (get objects id)
|
||||
|
||||
merge-fn (fn [node attrs]
|
||||
(reduce-kv (fn [node k v]
|
||||
(if (= (get node k) v)
|
||||
(dissoc node k)
|
||||
(assoc node k v)))
|
||||
node
|
||||
attrs))
|
||||
(reduce-kv
|
||||
(fn [node k v] (assoc node k v))
|
||||
node
|
||||
attrs))
|
||||
|
||||
update-fn #(update-shape % txt/is-paragraph-node? merge-fn attrs)
|
||||
shape-ids (cond (= (:type shape) :text) [id]
|
||||
|
|
|
@ -70,23 +70,19 @@
|
|||
(defn- fix-init-point
|
||||
"Fix the initial point so the resizes are accurate"
|
||||
[initial handler shape]
|
||||
(let [{:keys [x y width height]} (:selrect shape)
|
||||
{:keys [rotation]} shape
|
||||
rotation (or rotation 0)]
|
||||
(if (= rotation 0)
|
||||
(cond-> initial
|
||||
(contains? #{:left :top-left :bottom-left} handler)
|
||||
(assoc :x x)
|
||||
(let [{:keys [x y width height]} (:selrect shape)]
|
||||
(cond-> initial
|
||||
(contains? #{:left :top-left :bottom-left} handler)
|
||||
(assoc :x x)
|
||||
|
||||
(contains? #{:right :top-right :bottom-right} handler)
|
||||
(assoc :x (+ x width))
|
||||
(contains? #{:right :top-right :bottom-right} handler)
|
||||
(assoc :x (+ x width))
|
||||
|
||||
(contains? #{:top :top-right :top-left} handler)
|
||||
(assoc :y y)
|
||||
(contains? #{:top :top-right :top-left} handler)
|
||||
(assoc :y y)
|
||||
|
||||
(contains? #{:bottom :bottom-right :bottom-left} handler)
|
||||
(assoc :y (+ y height)))
|
||||
initial)))
|
||||
(contains? #{:bottom :bottom-right :bottom-left} handler)
|
||||
(assoc :y (+ y height)))))
|
||||
|
||||
(defn finish-transform []
|
||||
(ptk/reify ::finish-transform
|
||||
|
@ -117,8 +113,9 @@
|
|||
(declare clear-local-transform)
|
||||
|
||||
(defn- set-modifiers
|
||||
([ids] (set-modifiers ids nil))
|
||||
([ids modifiers]
|
||||
([ids] (set-modifiers ids nil false))
|
||||
([ids modifiers] (set-modifiers ids modifiers false))
|
||||
([ids modifiers ignore-constraints]
|
||||
(us/verify (s/coll-of uuid?) ids)
|
||||
(ptk/reify ::set-modifiers
|
||||
ptk/UpdateEvent
|
||||
|
@ -136,7 +133,8 @@
|
|||
(get objects id)
|
||||
modifiers
|
||||
nil
|
||||
nil)))
|
||||
nil
|
||||
ignore-constraints)))
|
||||
state
|
||||
ids))))))
|
||||
|
||||
|
@ -201,7 +199,7 @@
|
|||
(dwu/commit-undo-transaction))))))
|
||||
|
||||
(defn- set-modifiers-recursive
|
||||
[modif-tree objects shape modifiers root transformed-root]
|
||||
[modif-tree objects shape modifiers root transformed-root ignore-constraints]
|
||||
(let [children (->> (get shape :shapes [])
|
||||
(map #(get objects %)))
|
||||
|
||||
|
@ -215,13 +213,15 @@
|
|||
set-child (fn [modif-tree child]
|
||||
(let [child-modifiers (gsh/calc-child-modifiers shape
|
||||
child
|
||||
modifiers)]
|
||||
modifiers
|
||||
ignore-constraints)]
|
||||
(set-modifiers-recursive modif-tree
|
||||
objects
|
||||
child
|
||||
child-modifiers
|
||||
root
|
||||
transformed-root)))]
|
||||
transformed-root
|
||||
ignore-constraints)))]
|
||||
(reduce set-child
|
||||
(assoc-in modif-tree [(:id shape) :modifiers] modifiers)
|
||||
children)))
|
||||
|
@ -285,10 +285,19 @@
|
|||
(letfn [(resize [shape initial layout [point lock? center? point-snap]]
|
||||
(let [{:keys [width height]} (:selrect shape)
|
||||
{:keys [rotation]} shape
|
||||
|
||||
shape-center (gsh/center-shape shape)
|
||||
shape-transform (:transform shape (gmt/matrix))
|
||||
shape-transform-inverse (:transform-inverse shape (gmt/matrix))
|
||||
|
||||
rotation (or rotation 0)
|
||||
|
||||
initial (gsh/transform-point-center initial shape-center shape-transform-inverse)
|
||||
initial (fix-init-point initial handler shape)
|
||||
|
||||
point (gsh/transform-point-center (if (= rotation 0) point-snap point)
|
||||
shape-center shape-transform-inverse)
|
||||
|
||||
shapev (-> (gpt/point width height))
|
||||
|
||||
scale-text (:scale-text layout)
|
||||
|
@ -300,8 +309,7 @@
|
|||
handler-mult (let [[x y] (handler-multipliers handler)] (gpt/point x y))
|
||||
|
||||
;; Difference between the origin point in the coordinate system of the rotation
|
||||
deltav (-> (gpt/to-vec initial (if (= rotation 0) point-snap point))
|
||||
(gpt/transform (gmt/rotate-matrix (- rotation)))
|
||||
deltav (-> (gpt/to-vec initial point)
|
||||
(gpt/multiply handler-mult))
|
||||
|
||||
;; Resize vector
|
||||
|
@ -317,26 +325,25 @@
|
|||
scalev)
|
||||
|
||||
;; Resize origin point given the selected handler
|
||||
origin (handler-resize-origin (:selrect shape) handler)
|
||||
handler-origin (handler-resize-origin (:selrect shape) handler)
|
||||
|
||||
shape-center (gsh/center-shape shape)
|
||||
shape-transform (:transform shape (gmt/matrix))
|
||||
shape-transform-inverse (:transform-inverse shape (gmt/matrix))
|
||||
|
||||
;; If we want resize from center, displace the shape
|
||||
;; so it is still centered after resize.
|
||||
displacement (when center?
|
||||
(-> shape-center
|
||||
(gpt/subtract origin)
|
||||
(gpt/multiply scalev)
|
||||
(gpt/add origin)
|
||||
(gpt/subtract shape-center)
|
||||
(gpt/multiply (gpt/point -1 -1))
|
||||
(gpt/transform shape-transform)))
|
||||
displacement
|
||||
(when center?
|
||||
(-> shape-center
|
||||
(gpt/subtract handler-origin)
|
||||
(gpt/multiply scalev)
|
||||
(gpt/add handler-origin)
|
||||
(gpt/subtract shape-center)
|
||||
(gpt/multiply (gpt/point -1 -1))
|
||||
(gpt/transform shape-transform)))
|
||||
|
||||
origin (cond-> (gsh/transform-point-center origin shape-center shape-transform)
|
||||
(some? displacement)
|
||||
(gpt/add displacement))
|
||||
resize-origin
|
||||
(cond-> (gsh/transform-point-center handler-origin shape-center shape-transform)
|
||||
(some? displacement)
|
||||
(gpt/add displacement))
|
||||
|
||||
displacement (when (some? displacement)
|
||||
(gmt/translate-matrix displacement))]
|
||||
|
@ -344,7 +351,7 @@
|
|||
(rx/of (set-modifiers ids
|
||||
{:displacement displacement
|
||||
:resize-vector scalev
|
||||
:resize-origin origin
|
||||
:resize-origin resize-origin
|
||||
:resize-transform shape-transform
|
||||
:resize-scale-text scale-text
|
||||
:resize-transform-inverse shape-transform-inverse}))))
|
||||
|
@ -407,7 +414,8 @@
|
|||
shape
|
||||
modifiers
|
||||
nil
|
||||
nil))))
|
||||
nil
|
||||
false))))
|
||||
state
|
||||
ids)))
|
||||
|
||||
|
@ -505,7 +513,7 @@
|
|||
(if alt?
|
||||
;; When alt is down we start a duplicate+move
|
||||
(rx/of (start-move-duplicate initial)
|
||||
dws/duplicate-selected)
|
||||
(dws/duplicate-selected false))
|
||||
;; Otherwise just plain old move
|
||||
(rx/of (start-move initial selected)))))))))))
|
||||
|
||||
|
@ -712,7 +720,8 @@
|
|||
(rx/of (set-modifiers selected
|
||||
{:resize-vector (gpt/point -1.0 1.0)
|
||||
:resize-origin origin
|
||||
:displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))})
|
||||
:displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))}
|
||||
true)
|
||||
(apply-modifiers selected))))))
|
||||
|
||||
(defn flip-vertical-selected []
|
||||
|
@ -728,7 +737,8 @@
|
|||
(rx/of (set-modifiers selected
|
||||
{:resize-vector (gpt/point 1.0 -1.0)
|
||||
:resize-origin origin
|
||||
:displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))})
|
||||
:displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))}
|
||||
true)
|
||||
(apply-modifiers selected))))))
|
||||
|
||||
|
||||
|
|
172
frontend/src/app/main/errors.cljs
Normal file
172
frontend/src/app/main/errors.cljs
Normal file
|
@ -0,0 +1,172 @@
|
|||
;; 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.errors
|
||||
"Generic error handling"
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.sentry :as sentry]
|
||||
[app.main.store :as st]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as ts]
|
||||
[cljs.pprint :refer [pprint]]
|
||||
[cuerdas.core :as str]
|
||||
[expound.alpha :as expound]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(defn on-error
|
||||
"A general purpose error handler."
|
||||
[error]
|
||||
(cond
|
||||
(instance? ExceptionInfo error)
|
||||
(-> error sentry/capture-exception ex-data ptk/handle-error)
|
||||
|
||||
(map? error)
|
||||
(ptk/handle-error error)
|
||||
|
||||
:else
|
||||
(let [hint (ex-message error)
|
||||
msg (str "Internal Error: " hint)]
|
||||
(sentry/capture-exception error)
|
||||
(ts/schedule (st/emitf (rt/assign-exception error)))
|
||||
|
||||
(js/console.group msg)
|
||||
(ex/ignoring (js/console.error error))
|
||||
(js/console.groupEnd msg))))
|
||||
|
||||
;; Set the main potok error handler
|
||||
(reset! st/on-error on-error)
|
||||
|
||||
;; We receive a explicit authentication error; this explicitly clears
|
||||
;; all profile data and redirect the user to the login page. This is
|
||||
;; here and not in app.main.errors because of circular dependency.
|
||||
(defmethod ptk/handle-error :authentication
|
||||
[_]
|
||||
(ts/schedule (st/emitf (du/logout))))
|
||||
|
||||
|
||||
;; That are special case server-errors that should be treated
|
||||
;; differently.
|
||||
(derive :not-found ::exceptional-state)
|
||||
(derive :bad-gateway ::exceptional-state)
|
||||
(derive :service-unavailable ::exceptional-state)
|
||||
|
||||
(defmethod ptk/handle-error ::exceptional-state
|
||||
[error]
|
||||
(ts/schedule
|
||||
(st/emitf (rt/assign-exception error))))
|
||||
|
||||
;; Error that happens on an active bussines model validation does not
|
||||
;; passes an validation (example: profile can't leave a team). From
|
||||
;; the user perspective a error flash message should be visualized but
|
||||
;; user can continue operate on the application.
|
||||
(defmethod ptk/handle-error :validation
|
||||
[error]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "Unexpected validation error."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
|
||||
;; Print to the console some debug info.
|
||||
(js/console.group "Validation Error:")
|
||||
(ex/ignoring
|
||||
(js/console.info
|
||||
(with-out-str
|
||||
(pprint (dissoc error :explain))))
|
||||
(when-let [explain (:explain error)]
|
||||
(js/console.error explain)))
|
||||
(js/console.groupEnd "Validation Error:"))
|
||||
|
||||
|
||||
;; Error on parsing an SVG
|
||||
(defmethod ptk/handle-error :svg-parser
|
||||
[_]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "SVG is invalid or malformed"
|
||||
:type :error
|
||||
:timeout 3000}))))
|
||||
|
||||
(defmethod ptk/handle-error :comment-error
|
||||
[_]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "There was an error with the comment"
|
||||
:type :error
|
||||
:timeout 3000}))))
|
||||
|
||||
;; This is a pure frontend error that can be caused by an active
|
||||
;; assertion (assertion that is preserved on production builds). From
|
||||
;; the user perspective this should be treated as internal error.
|
||||
(defmethod ptk/handle-error :assertion
|
||||
[{:keys [data stack message hint context] :as error}]
|
||||
(let [message (or message hint)
|
||||
message (str "Internal Assertion Error: " message)
|
||||
context (str/fmt "ns: '%s'\nname: '%s'\nfile: '%s:%s'"
|
||||
(:ns context)
|
||||
(:name context)
|
||||
(str cf/public-uri "js/cljs-runtime/" (:file context))
|
||||
(:line context))]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "Internal error: assertion."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
|
||||
;; Print to the console some debugging info
|
||||
(js/console.group message)
|
||||
(js/console.info context)
|
||||
(js/console.groupCollapsed "Stack Trace")
|
||||
(js/console.info stack)
|
||||
(js/console.groupEnd "Stack Trace")
|
||||
(js/console.error (with-out-str (expound/printer data)))
|
||||
(js/console.groupEnd message)))
|
||||
|
||||
;; This happens when the backed server fails to process the
|
||||
;; request. This can be caused by an internal assertion or any other
|
||||
;; uncontrolled error.
|
||||
(defmethod ptk/handle-error :server-error
|
||||
[{:keys [data hint] :as error}]
|
||||
(let [hint (or hint (:hint data) (:message data))
|
||||
info (with-out-str (pprint (dissoc data :explain)))
|
||||
expl (:explain data)
|
||||
msg (str "Internal Server Error: " hint)]
|
||||
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "Something wrong has happened (on backend)."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
|
||||
(js/console.group msg)
|
||||
(js/console.info info)
|
||||
(when expl (js/console.error expl))
|
||||
(js/console.groupEnd msg)))
|
||||
|
||||
(defn on-unhandled-error
|
||||
[error]
|
||||
(if (instance? ExceptionInfo error)
|
||||
(-> error sentry/capture-exception ex-data ptk/handle-error)
|
||||
(let [hint (ex-message error)
|
||||
msg (str "Unhandled Internal Error: " hint)]
|
||||
(sentry/capture-exception error)
|
||||
(ts/schedule (st/emitf (rt/assign-exception error)))
|
||||
(js/console.group msg)
|
||||
(ex/ignoring (js/console.error error))
|
||||
(js/console.groupEnd msg))))
|
||||
|
||||
(defonce uncaught-error-handler
|
||||
(letfn [(on-error [event]
|
||||
(.preventDefault ^js event)
|
||||
(some-> (unchecked-get event "error")
|
||||
(on-unhandled-error)))]
|
||||
(.addEventListener js/window "error" on-error)
|
||||
(fn []
|
||||
(.removeEventListener js/window "error" on-error))))
|
|
@ -14,6 +14,7 @@
|
|||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.ui.shapes.bool :as bool]
|
||||
[app.main.ui.shapes.circle :as circle]
|
||||
[app.main.ui.shapes.embed :as embed]
|
||||
[app.main.ui.shapes.export :as use]
|
||||
|
@ -81,6 +82,18 @@
|
|||
:is-child-selected? true
|
||||
:childs childs}]))))
|
||||
|
||||
(defn bool-wrapper-factory
|
||||
[objects]
|
||||
(let [shape-wrapper (shape-wrapper-factory objects)
|
||||
bool-shape (bool/bool-shape shape-wrapper)]
|
||||
(mf/fnc bool-wrapper
|
||||
[{:keys [shape frame] :as props}]
|
||||
(let [childs (->> (cp/get-children (:id shape) objects)
|
||||
(select-keys objects))]
|
||||
[:& bool-shape {:frame frame
|
||||
:shape shape
|
||||
:childs childs}]))))
|
||||
|
||||
(defn svg-raw-wrapper-factory
|
||||
[objects]
|
||||
(let [shape-wrapper (shape-wrapper-factory objects)
|
||||
|
@ -104,9 +117,10 @@
|
|||
[objects]
|
||||
(mf/fnc shape-wrapper
|
||||
[{:keys [frame shape] :as props}]
|
||||
(let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects))
|
||||
(let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects))
|
||||
svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects))
|
||||
frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))]
|
||||
bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects))
|
||||
frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))]
|
||||
(when (and shape (not (:hidden shape)))
|
||||
(let [shape (-> (gsh/transform-shape shape)
|
||||
(gsh/translate-to-frame frame))
|
||||
|
@ -122,6 +136,7 @@
|
|||
:circle [:> circle/circle-shape opts]
|
||||
:frame [:> frame-wrapper {:shape shape}]
|
||||
:group [:> group-wrapper {:shape shape :frame frame}]
|
||||
:bool [:> bool-wrapper {:shape shape :frame frame}]
|
||||
nil)]
|
||||
|
||||
;; Don't wrap svg elements inside a <g> otherwise some can break
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
(:require-macros [app.main.fonts :refer [preload-gfonts]])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as log]
|
||||
[app.common.text :as txt]
|
||||
[app.config :as cf]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.logging :as log]
|
||||
[app.util.object :as obj]
|
||||
[beicon.core :as rx]
|
||||
[clojure.set :as set]
|
||||
|
@ -23,7 +23,7 @@
|
|||
[okulary.core :as l]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(log/set-level! :trace)
|
||||
(log/set-level! :warn)
|
||||
|
||||
(def google-fonts
|
||||
(preload-gfonts "fonts/gfonts.2020.04.23.json"))
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
;; 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/.
|
||||
|
@ -11,6 +10,7 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.path.commands :as upc]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.store :as st]
|
||||
[okulary.core :as l]))
|
||||
|
@ -122,6 +122,10 @@
|
|||
:show-distances?])
|
||||
workspace-local =))
|
||||
|
||||
(def local-displacement
|
||||
(l/derived #(select-keys % [:modifiers :selected])
|
||||
workspace-local =))
|
||||
|
||||
(def selected-zoom
|
||||
(l/derived :zoom workspace-local))
|
||||
|
||||
|
@ -239,16 +243,44 @@
|
|||
|
||||
([ids {:keys [with-modifiers?]
|
||||
:or { with-modifiers? false }}]
|
||||
(l/derived (fn [state]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
modifiers (:workspace-modifiers state)
|
||||
objects (cond-> objects
|
||||
with-modifiers?
|
||||
(gsh/merge-modifiers modifiers))
|
||||
xform (comp (map #(get objects %))
|
||||
(remove nil?))]
|
||||
(into [] xform ids)))
|
||||
st/state =)))
|
||||
(let [selector
|
||||
(fn [state]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
modifiers (:workspace-modifiers state)
|
||||
objects (cond-> objects
|
||||
with-modifiers?
|
||||
(gsh/merge-modifiers modifiers))
|
||||
xform (comp (map #(get objects %))
|
||||
(remove nil?))]
|
||||
(into [] xform ids)))]
|
||||
(l/derived selector st/state =))))
|
||||
|
||||
(defn- set-content-modifiers [state]
|
||||
(fn [id shape]
|
||||
(let [content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers])]
|
||||
(if (some? content-modifiers)
|
||||
(update shape :content upc/apply-content-modifiers content-modifiers)
|
||||
shape))))
|
||||
|
||||
(defn select-children [id]
|
||||
(let [selector
|
||||
(fn [state]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
|
||||
modifiers (-> (:workspace-modifiers state))
|
||||
{selected :selected disp-modifiers :modifiers}
|
||||
(-> (:workspace-local state)
|
||||
(select-keys [:modifiers :selected]))
|
||||
|
||||
modifiers
|
||||
(d/deep-merge
|
||||
modifiers
|
||||
(into {} (map #(vector % {:modifiers disp-modifiers})) selected))]
|
||||
|
||||
(as-> (cp/select-children id objects) $
|
||||
(gsh/merge-modifiers $ modifiers)
|
||||
(d/mapm (set-content-modifiers state) $))))]
|
||||
(l/derived selector st/state =)))
|
||||
|
||||
(def selected-data
|
||||
(l/derived #(let [selected (wsh/lookup-selected %)
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
[id params]
|
||||
(->> (http/send! {:method :get
|
||||
:uri (u/join base-uri "api/rpc/query/" (name id))
|
||||
:credentials "include"
|
||||
:query params})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
@ -58,6 +59,7 @@
|
|||
[id params]
|
||||
(->> (http/send! {:method :post
|
||||
:uri (u/join base-uri "api/rpc/mutation/" (name id))
|
||||
:credentials "include"
|
||||
:body (http/transit-data params)})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
@ -87,7 +89,10 @@
|
|||
[_ {:keys [provider] :as params}]
|
||||
(let [uri (u/join base-uri "api/auth/oauth/" (d/name provider))
|
||||
params (dissoc params :provider)]
|
||||
(->> (http/send! {:method :post :uri uri :query params})
|
||||
(->> (http/send! {:method :post
|
||||
:uri uri
|
||||
:credentials "include"
|
||||
:query params})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response))))
|
||||
|
||||
|
@ -95,6 +100,7 @@
|
|||
[_ params]
|
||||
(->> (http/send! {:method :post
|
||||
:uri (u/join base-uri "api/feedback")
|
||||
:credentials "include"
|
||||
:body (http/transit-data params)})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
@ -104,6 +110,7 @@
|
|||
(->> (http/send! {:method :post
|
||||
:uri (u/join base-uri "export")
|
||||
:body (http/transit-data params)
|
||||
:credentials "include"
|
||||
:response-type :blob})
|
||||
(rx/mapcat handle-response)))
|
||||
|
||||
|
@ -112,6 +119,7 @@
|
|||
(->> (http/send! {:method :post
|
||||
:uri (u/join base-uri "export-frames")
|
||||
:body (http/transit-data params)
|
||||
:credentials "include"
|
||||
:response-type :blob})
|
||||
(rx/mapcat handle-response)))
|
||||
|
||||
|
@ -123,6 +131,7 @@
|
|||
[id params]
|
||||
(->> (http/send! {:method :post
|
||||
:uri (u/join base-uri "api/rpc/mutation/" (name id))
|
||||
:credentials "include"
|
||||
:body (http/form-data params)})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
|
60
frontend/src/app/main/sentry.cljs
Normal file
60
frontend/src/app/main/sentry.cljs
Normal file
|
@ -0,0 +1,60 @@
|
|||
;; 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.sentry
|
||||
"Sentry integration."
|
||||
(:require
|
||||
["@sentry/browser" :as sentry]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.refs :as refs]))
|
||||
|
||||
(defn- setup-profile!
|
||||
[profile]
|
||||
(if (or (= uuid/zero (:id profile))
|
||||
(nil? profile))
|
||||
(sentry/setUser nil)
|
||||
(sentry/setUser #js {:id (str (:id profile))})))
|
||||
|
||||
(defn init!
|
||||
[]
|
||||
(setup-profile! @refs/profile)
|
||||
(when cf/sentry-dsn
|
||||
(sentry/init
|
||||
#js {:dsn cf/sentry-dsn
|
||||
:autoSessionTracking false
|
||||
:attachStacktrace false
|
||||
:release (str "frontend@" (:base @cf/version))
|
||||
:maxBreadcrumbs 20
|
||||
:beforeBreadcrumb (fn [breadcrumb _hint]
|
||||
(let [category (.-category ^js breadcrumb)]
|
||||
(if (= category "navigate")
|
||||
breadcrumb
|
||||
nil)))
|
||||
:tracesSampleRate 1.0})
|
||||
|
||||
(add-watch refs/profile ::profile
|
||||
(fn [_ _ _ profile]
|
||||
(setup-profile! profile)))
|
||||
|
||||
(add-watch refs/route ::route
|
||||
(fn [_ _ _ route]
|
||||
(sentry/addBreadcrumb
|
||||
#js {:category "navigate",
|
||||
:message (str "path: " (:path route))
|
||||
:level (.-Info ^js sentry/Severity)})))))
|
||||
|
||||
(defn capture-exception
|
||||
[err]
|
||||
(when cf/sentry-dsn
|
||||
(when (ex/ex-info? err)
|
||||
(sentry/setContext "ex-data", (clj->js (ex-data err))))
|
||||
(sentry/captureException err))
|
||||
err)
|
||||
|
||||
|
||||
|
|
@ -17,11 +17,15 @@
|
|||
|
||||
(enable-console-print!)
|
||||
|
||||
(def ^:dynamic *on-error* identity)
|
||||
|
||||
(defonce loader (l/atom false))
|
||||
(defonce state (ptk/store {:resolve ptk/resolve}))
|
||||
(defonce stream (ptk/input-stream state))
|
||||
(defonce on-error (l/atom identity))
|
||||
|
||||
(defonce state
|
||||
(ptk/store {:resolve ptk/resolve
|
||||
:on-error (fn [e] (@on-error e))}))
|
||||
|
||||
(defonce stream
|
||||
(ptk/input-stream state))
|
||||
|
||||
(defonce last-events
|
||||
(let [buffer (atom #queue [])
|
||||
|
|
|
@ -6,12 +6,6 @@
|
|||
|
||||
(ns app.main.ui
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth :refer [auth]]
|
||||
|
@ -28,77 +22,12 @@
|
|||
[app.main.ui.static :as static]
|
||||
[app.main.ui.viewer :as viewer]
|
||||
[app.main.ui.workspace :as workspace]
|
||||
[app.util.timers :as ts]
|
||||
[cljs.pprint :refer [pprint]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[expound.alpha :as expound]
|
||||
[potok.core :as ptk]
|
||||
[app.util.router :as rt]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
;; --- Routes
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::section ::us/keyword)
|
||||
(s/def ::index ::us/integer)
|
||||
(s/def ::token (s/nilable ::us/not-empty-string))
|
||||
(s/def ::share-id ::us/uuid)
|
||||
|
||||
(s/def ::viewer-path-params
|
||||
(s/keys :req-un [::file-id]))
|
||||
|
||||
(s/def ::viewer-query-params
|
||||
(s/keys :req-un [::index]
|
||||
:opt-un [::share-id ::section ::page-id]))
|
||||
|
||||
(def routes
|
||||
[["/auth"
|
||||
["/login" :auth-login]
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register" :auth-register])
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register/validate" :auth-register-validate])
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register/success" :auth-register-success])
|
||||
["/recovery/request" :auth-recovery-request]
|
||||
["/recovery" :auth-recovery]
|
||||
["/verify-token" :auth-verify-token]]
|
||||
|
||||
["/settings"
|
||||
["/profile" :settings-profile]
|
||||
["/password" :settings-password]
|
||||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]]
|
||||
|
||||
["/view/:file-id"
|
||||
{:name :viewer
|
||||
:conform
|
||||
{:path-params ::viewer-path-params
|
||||
:query-params ::viewer-query-params}}]
|
||||
|
||||
(when *assert*
|
||||
["/debug/icons-preview" :debug-icons-preview])
|
||||
|
||||
;; Used for export
|
||||
["/render-object/:file-id/:page-id/:object-id" :render-object]
|
||||
["/render-sprite/:file-id" :render-sprite]
|
||||
|
||||
["/dashboard/team/:team-id"
|
||||
["/members" :dashboard-team-members]
|
||||
["/settings" :dashboard-team-settings]
|
||||
["/projects" :dashboard-projects]
|
||||
["/search" :dashboard-search]
|
||||
["/fonts" :dashboard-fonts]
|
||||
["/fonts/providers" :dashboard-font-providers]
|
||||
["/libraries" :dashboard-libraries]
|
||||
["/projects/:project-id" :dashboard-files]]
|
||||
|
||||
["/workspace/:project-id/:file-id" :workspace]])
|
||||
|
||||
(mf/defc on-main-error
|
||||
[{:keys [error] :as props}]
|
||||
(mf/use-effect #(ptk/handle-error error))
|
||||
(mf/use-effect (st/emitf (rt/assign-exception error)))
|
||||
[:span "Internal application errror"])
|
||||
|
||||
(mf/defc main-page
|
||||
|
@ -161,12 +90,14 @@
|
|||
|
||||
:render-object
|
||||
(do
|
||||
(let [file-id (uuid (get-in route [:path-params :file-id]))
|
||||
page-id (uuid (get-in route [:path-params :page-id]))
|
||||
object-id (uuid (get-in route [:path-params :object-id]))]
|
||||
(let [file-id (uuid (get-in route [:path-params :file-id]))
|
||||
page-id (uuid (get-in route [:path-params :page-id]))
|
||||
object-id (uuid (get-in route [:path-params :object-id]))
|
||||
render-texts (get-in route [:query-params :render-texts])]
|
||||
[:& render/render-object {:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id object-id}]))
|
||||
:object-id object-id
|
||||
:render-texts? (and (some? render-texts) (= render-texts "true"))}]))
|
||||
|
||||
:render-sprite
|
||||
(do
|
||||
|
@ -199,143 +130,3 @@
|
|||
[:& msgs/notifications]
|
||||
(when route
|
||||
[:& main-page {:route route}])])]))
|
||||
|
||||
;; --- Error Handling
|
||||
|
||||
;; That are special case server-errors that should be treated
|
||||
;; differently.
|
||||
(derive :not-found ::exceptional-state)
|
||||
(derive :bad-gateway ::exceptional-state)
|
||||
(derive :service-unavailable ::exceptional-state)
|
||||
|
||||
(defmethod ptk/handle-error ::exceptional-state
|
||||
[error]
|
||||
(ts/schedule
|
||||
(st/emitf (dm/assign-exception error))))
|
||||
|
||||
;; We receive a explicit authentication error; this explicitly clears
|
||||
;; all profile data and redirect the user to the login page.
|
||||
(defmethod ptk/handle-error :authentication
|
||||
[_]
|
||||
(ts/schedule (st/emitf (du/logout))))
|
||||
|
||||
;; Error that happens on an active bussines model validation does not
|
||||
;; passes an validation (example: profile can't leave a team). From
|
||||
;; the user perspective a error flash message should be visualized but
|
||||
;; user can continue operate on the application.
|
||||
(defmethod ptk/handle-error :validation
|
||||
[error]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "Unexpected validation error."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
|
||||
;; Print to the console some debug info.
|
||||
(js/console.group "Validation Error")
|
||||
(ex/ignoring
|
||||
(js/console.info
|
||||
(with-out-str
|
||||
(pprint (dissoc error :explain))))
|
||||
(when-let [explain (:explain error)]
|
||||
(js/console.error explain)))
|
||||
(js/console.groupEnd "Validation Error"))
|
||||
|
||||
;; Error on parsing an SVG
|
||||
(defmethod ptk/handle-error :svg-parser
|
||||
[_]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "SVG is invalid or malformed"
|
||||
:type :error
|
||||
:timeout 3000}))))
|
||||
|
||||
;; This is a pure frontend error that can be caused by an active
|
||||
;; assertion (assertion that is preserved on production builds). From
|
||||
;; the user perspective this should be treated as internal error.
|
||||
(defmethod ptk/handle-error :assertion
|
||||
[{:keys [data stack message hint context] :as error}]
|
||||
(let [message (or message hint)
|
||||
context (str/fmt "ns: '%s'\nname: '%s'\nfile: '%s:%s'"
|
||||
(:ns context)
|
||||
(:name context)
|
||||
(str cf/public-uri "js/cljs-runtime/" (:file context))
|
||||
(:line context))]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "Internal error: assertion."
|
||||
:type :error
|
||||
:timeout 3000})
|
||||
(ptk/event ::ev/event
|
||||
{::ev/type "exception"
|
||||
::ev/name "assertion-error"
|
||||
:message message
|
||||
:context context
|
||||
:trace stack})))
|
||||
|
||||
;; Print to the console some debugging info
|
||||
(js/console.group message)
|
||||
(js/console.info context)
|
||||
(js/console.groupCollapsed "Stack Trace")
|
||||
(js/console.info stack)
|
||||
(js/console.groupEnd "Stack Trace")
|
||||
(js/console.error (with-out-str (expound/printer data)))
|
||||
(js/console.groupEnd message)))
|
||||
|
||||
;; This happens when the backed server fails to process the
|
||||
;; request. This can be caused by an internal assertion or any other
|
||||
;; uncontrolled error.
|
||||
(defmethod ptk/handle-error :server-error
|
||||
[{:keys [data hint] :as error}]
|
||||
(let [hint (or hint (:hint data) (:message data))
|
||||
info (with-out-str (pprint (dissoc data :explain)))
|
||||
expl (:explain data)]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "Something wrong has happened (on backend)."
|
||||
:type :error
|
||||
:timeout 3000})
|
||||
(ptk/event ::ev/event
|
||||
{::ev/type "exception"
|
||||
::ev/name "server-error"
|
||||
:hint hint
|
||||
:info info
|
||||
:explain expl})))
|
||||
|
||||
(js/console.group "Internal Server Error:")
|
||||
(js/console.error "hint:" hint)
|
||||
(js/console.info info)
|
||||
(when expl (js/console.error expl))
|
||||
(js/console.groupEnd "Internal Server Error:")))
|
||||
|
||||
(defmethod ptk/handle-error :default
|
||||
[error]
|
||||
(if (instance? ExceptionInfo error)
|
||||
(ptk/handle-error (ex-data error))
|
||||
(let [stack (.-stack error)
|
||||
hint (or (ex-message error)
|
||||
(:hint error)
|
||||
(:message error))]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/assign-exception error)
|
||||
(ptk/event ::ev/event
|
||||
{::ev/type "exception"
|
||||
::ev/name "unexpected-error"
|
||||
:message hint
|
||||
:trace (.-stack error)})))
|
||||
|
||||
(js/console.group "Internal error:")
|
||||
(js/console.log "hint:" hint)
|
||||
(ex/ignoring
|
||||
(js/console.error (clj->js error))
|
||||
(js/console.error "stack:" stack))
|
||||
(js/console.groupEnd "Internal error:"))))
|
||||
|
||||
(defonce uncaught-error-handler
|
||||
(letfn [(on-error [event]
|
||||
(ptk/handle-error (unchecked-get event "error"))
|
||||
(.preventDefault ^js event))]
|
||||
(.addEventListener js/window "error" on-error)
|
||||
(fn []
|
||||
(.removeEventListener js/window "error" on-error))))
|
||||
|
|
|
@ -210,8 +210,13 @@
|
|||
[:div.fields-row
|
||||
[:& fm/input {:name :accept-terms-and-privacy
|
||||
:class "check-primary"
|
||||
:label (tr "auth.terms-privacy-agreement")
|
||||
:type "checkbox"}]]
|
||||
:type "checkbox"}
|
||||
[:span
|
||||
(tr "auth.terms-privacy-agreement")
|
||||
[:div
|
||||
[:a {:href "https://penpot.app/terms.html" :target "_blank"} (tr "auth.terms-of-service")]
|
||||
[:span ",\u00A0"]
|
||||
[:a {:href "https://penpot.app/privacy.html" :target "_blank"} (tr "auth.privacy-policy")]]]]]
|
||||
|
||||
;; (when (contains? @cf/flags :newsletter-registration-check)
|
||||
;; [:div.fields-row
|
||||
|
|
|
@ -302,21 +302,22 @@
|
|||
(when-let [node (mf/ref-val ref)]
|
||||
(.scrollIntoViewIfNeeded ^js node))))
|
||||
|
||||
[:div.thread-content
|
||||
{:style {:top (str pos-y "px")
|
||||
:left (str pos-x "px")}
|
||||
:on-click dom/stop-propagation}
|
||||
(when (some? comment)
|
||||
[:div.thread-content
|
||||
{:style {:top (str pos-y "px")
|
||||
:left (str pos-x "px")}
|
||||
:on-click dom/stop-propagation}
|
||||
|
||||
[:div.comments
|
||||
[:& comment-item {:comment comment
|
||||
:users users
|
||||
:thread thread}]
|
||||
(for [item (rest comments)]
|
||||
[:*
|
||||
[:hr]
|
||||
[:& comment-item {:comment item :users users}]])
|
||||
[:div {:ref ref}]]
|
||||
[:& reply-form {:thread thread}]]))
|
||||
[:div.comments
|
||||
[:& comment-item {:comment comment
|
||||
:users users
|
||||
:thread thread}]
|
||||
(for [item (rest comments)]
|
||||
[:*
|
||||
[:hr]
|
||||
[:& comment-item {:comment item :users users}]])
|
||||
[:div {:ref ref}]]
|
||||
[:& reply-form {:thread thread}]])))
|
||||
|
||||
(mf/defc thread-bubble
|
||||
{::mf/wrap [mf/memo]}
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
|
||||
handle-key-down
|
||||
(mf/use-callback
|
||||
(mf/deps set-value)
|
||||
(fn [event]
|
||||
(when (= type "number")
|
||||
(let [up? (kbd/up-arrow? event)
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
(def use-form fm/use-form)
|
||||
|
||||
(mf/defc input
|
||||
[{:keys [label help-icon disabled form hint trim] :as props}]
|
||||
[{:keys [label help-icon disabled form hint trim children] :as props}]
|
||||
(let [input-type (get props :type "text")
|
||||
input-name (get props :name)
|
||||
more-classes (get props :class)
|
||||
|
@ -82,7 +82,7 @@
|
|||
(swap! form assoc-in [:touched input-name] true)))
|
||||
|
||||
props (-> props
|
||||
(dissoc :help-icon :form :trim)
|
||||
(dissoc :help-icon :form :trim :children)
|
||||
(assoc :id (name input-name)
|
||||
:value value
|
||||
:auto-focus auto-focus?
|
||||
|
@ -97,7 +97,13 @@
|
|||
{:class klass}
|
||||
[:*
|
||||
[:> :input props]
|
||||
[:label {:for (name input-name)} label]
|
||||
(cond
|
||||
(some? label)
|
||||
[:label {:for (name input-name)} label]
|
||||
|
||||
(some? children)
|
||||
[:label {:for (name input-name)} children])
|
||||
|
||||
(when help-icon'
|
||||
[:div.help-icon
|
||||
{:style {:cursor "pointer"}
|
||||
|
|
|
@ -74,10 +74,12 @@
|
|||
|
||||
on-new-tab
|
||||
(fn [_]
|
||||
(let [pparams {:project-id (:project-id file)
|
||||
:file-id (:id file)}
|
||||
qparams {:page-id (first (get-in file [:data :pages]))}]
|
||||
(st/emit! (rt/nav-new-window :workspace pparams qparams))))
|
||||
(let [path-params {:project-id (:project-id file)
|
||||
:file-id (:id file)}
|
||||
query-params {:page-id (first (get-in file [:data :pages]))}]
|
||||
(st/emit! (rt/nav-new-window* {:rname :workspace
|
||||
:path-params path-params
|
||||
:query-params query-params}))))
|
||||
|
||||
on-duplicate
|
||||
(fn [_]
|
||||
|
|
|
@ -18,13 +18,10 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.logging :as log]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(log/set-level! :trace)
|
||||
|
||||
(defn- use-set-page-title
|
||||
[team section]
|
||||
(mf/use-effect
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.dashboard.import
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as log]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
|
@ -16,12 +17,11 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.logging :as log]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(log/set-level! :debug)
|
||||
(log/set-level! :warn)
|
||||
|
||||
(def ^:const emit-delay 1000)
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
|
@ -69,7 +70,8 @@
|
|||
(mf/use-callback
|
||||
(mf/deps item)
|
||||
(fn [name]
|
||||
(st/emit! (dd/rename-project (assoc item :name name)))
|
||||
(st/emit! (-> (dd/rename-project (assoc item :name name))
|
||||
(with-meta {::ev/origin "dashboard:sidebar"})))
|
||||
(swap! local assoc :edition? false)))
|
||||
|
||||
on-drag-enter
|
||||
|
|
|
@ -26,12 +26,13 @@
|
|||
|
||||
(mf/defc header
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [section] :as props}]
|
||||
[{:keys [section team] :as props}]
|
||||
(let [go-members (st/emitf (dd/go-to-team-members))
|
||||
go-settings (st/emitf (dd/go-to-team-settings))
|
||||
invite-member (st/emitf (modal/show {:type ::invite-member}))
|
||||
invite-member (st/emitf (modal/show {:type ::invite-member :team team}))
|
||||
members-section? (= section :dashboard-team-members)
|
||||
settings-section? (= section :dashboard-team-settings)]
|
||||
settings-section? (= section :dashboard-team-settings)
|
||||
permissions (:permissions team)]
|
||||
|
||||
[:header.dashboard-header
|
||||
[:div.dashboard-title
|
||||
|
@ -46,20 +47,21 @@
|
|||
[:li {:class (when settings-section? "active")}
|
||||
[:a {:on-click go-settings} (tr "labels.settings")]]]]
|
||||
|
||||
(if members-section?
|
||||
(if (and members-section? (:is-admin permissions))
|
||||
[:a.btn-secondary.btn-small {:on-click invite-member}
|
||||
(tr "dashboard.invite-profile")]
|
||||
[:div])]))
|
||||
|
||||
(defn get-available-roles
|
||||
[]
|
||||
[{:value "" :label (tr "labels.role")}
|
||||
{:value "admin" :label (tr "labels.admin")}
|
||||
{:value "editor" :label (tr "labels.editor")}
|
||||
;; Temporarily disabled viewer role
|
||||
;; https://tree.taiga.io/project/uxboxproject/issue/1083
|
||||
;; {:value "viewer" :label (tr "labels.viewer")}
|
||||
])
|
||||
[permissions]
|
||||
(->> [{:value "editor" :label (tr "labels.editor")}
|
||||
(when (:is-admin permissions)
|
||||
{:value "admin" :label (tr "labels.admin")})
|
||||
;; Temporarily disabled viewer role
|
||||
;; https://tree.taiga.io/project/uxboxproject/issue/1083
|
||||
;; {:value "viewer" :label (tr "labels.viewer")}
|
||||
]
|
||||
(filterv identity)))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::role ::us/keyword)
|
||||
|
@ -69,8 +71,9 @@
|
|||
(mf/defc invite-member-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as ::invite-member}
|
||||
[]
|
||||
(let [roles (mf/use-memo get-available-roles)
|
||||
[{:keys [team]}]
|
||||
(let [perms (:permissions team)
|
||||
roles (mf/use-memo (mf/deps perms) #(get-available-roles perms))
|
||||
initial (mf/use-memo (constantly {:role "editor"}))
|
||||
form (fm/use-form :spec ::invite-member-form
|
||||
:initial initial)
|
||||
|
@ -262,6 +265,7 @@
|
|||
(tr "dashboard.your-penpot")
|
||||
(:name team))))))
|
||||
|
||||
|
||||
(mf/use-effect
|
||||
(st/emitf (dd/fetch-team-members)
|
||||
(dd/fetch-team-stats)))
|
||||
|
|
|
@ -11,13 +11,10 @@
|
|||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.dom.dnd :as dnd]
|
||||
[app.util.logging :as log]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(log/set-level! :warn)
|
||||
|
||||
(defn use-rxsub
|
||||
[ob]
|
||||
(let [[state reset-state!] (mf/useState @ob)]
|
||||
|
@ -101,7 +98,6 @@
|
|||
subscribe-to-drag-end
|
||||
(fn []
|
||||
(when (nil? (:subscr @state))
|
||||
;; (js/console.log "subscribing" (:name data))
|
||||
(swap! state
|
||||
#(assoc % :subscr (rx/sub! global-drag-end cleanup)))))
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
(:require-macros [app.main.ui.icons :refer [icon-xref]])
|
||||
(:require [rumext.alpha :as mf]))
|
||||
|
||||
;; Keep the list of icons sorted
|
||||
|
||||
(def action (icon-xref :action))
|
||||
(def actions (icon-xref :actions))
|
||||
(def align-bottom (icon-xref :align-bottom))
|
||||
|
@ -23,6 +25,11 @@
|
|||
(def auto-fix (icon-xref :auto-fix))
|
||||
(def auto-height (icon-xref :auto-height))
|
||||
(def auto-width (icon-xref :auto-width))
|
||||
(def boolean-difference (icon-xref :boolean-difference))
|
||||
(def boolean-exclude (icon-xref :boolean-exclude))
|
||||
(def boolean-flatten (icon-xref :boolean-flatten))
|
||||
(def boolean-intersection (icon-xref :boolean-intersection))
|
||||
(def boolean-union (icon-xref :boolean-union))
|
||||
(def box (icon-xref :box))
|
||||
(def chain (icon-xref :chain))
|
||||
(def chat (icon-xref :chat))
|
||||
|
@ -67,8 +74,8 @@
|
|||
(def listing-thumbs (icon-xref :listing-thumbs))
|
||||
(def loader (icon-xref :loader))
|
||||
(def lock (icon-xref :lock))
|
||||
(def logo (icon-xref :uxbox-logo))
|
||||
(def logo-icon (icon-xref :uxbox-logo-icon))
|
||||
(def logo (icon-xref :penpot-logo))
|
||||
(def logo-icon (icon-xref :penpot-logo-icon))
|
||||
(def logout (icon-xref :logout))
|
||||
(def lowercase (icon-xref :lowercase))
|
||||
(def mail (icon-xref :mail))
|
||||
|
@ -101,6 +108,13 @@
|
|||
(def play (icon-xref :play))
|
||||
(def plus (icon-xref :plus))
|
||||
(def pointer-inner (icon-xref :pointer-inner))
|
||||
(def position-bottom-center (icon-xref :position-bottom-center))
|
||||
(def position-bottom-left (icon-xref :position-bottom-left))
|
||||
(def position-bottom-right (icon-xref :position-bottom-right))
|
||||
(def position-center (icon-xref :position-center))
|
||||
(def position-top-center (icon-xref :position-top-center))
|
||||
(def position-top-left (icon-xref :position-top-left))
|
||||
(def position-top-right (icon-xref :position-top-right))
|
||||
(def radius (icon-xref :radius))
|
||||
(def radius-1 (icon-xref :radius-1))
|
||||
(def radius-4 (icon-xref :radius-4))
|
||||
|
@ -145,6 +159,7 @@
|
|||
(def uppercase (icon-xref :uppercase))
|
||||
(def user (icon-xref :user))
|
||||
|
||||
|
||||
(def loader-pencil
|
||||
(mf/html
|
||||
[:svg
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
[app.main.ui.releases.v1-6]
|
||||
[app.main.ui.releases.v1-7]
|
||||
[app.main.ui.releases.v1-8]
|
||||
[app.main.ui.releases.v1-9]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
|
@ -298,5 +299,5 @@
|
|||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "1.8")))
|
||||
(rc/render-release-notes (assoc params :version "1.9")))
|
||||
|
||||
|
|
108
frontend/src/app/main/ui/releases/v1_9.cljs
Normal file
108
frontend/src/app/main/ui/releases/v1_9.cljs
Normal file
|
@ -0,0 +1,108 @@
|
|||
;; 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.releases.v1-9
|
||||
(:require
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defmethod c/render-release-notes "1.9"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(mf/html
|
||||
(case @slide
|
||||
:start
|
||||
[:div.modal-overlay
|
||||
[:div.animated {:class @klass}
|
||||
[:div.modal-container.onboarding.feature
|
||||
[:div.modal-left
|
||||
[:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.9"}]]
|
||||
[:div.modal-right
|
||||
[:div.modal-title
|
||||
[:h2 "What's new?"]]
|
||||
[:span.release "Alpha version " version]
|
||||
[:div.modal-content
|
||||
[:p "Penpot continues growing with new features that improve performance, user experience and visual design."]
|
||||
[:p "We are happy to show you a sneak peak of the most important stuff that the Alpha 1.9 version brings."]]
|
||||
[:div.modal-navigation
|
||||
[:button.btn-secondary {:on-click next} "Continue"]]]
|
||||
[:img.deco {:src "images/deco-left.png" :border "0"}]
|
||||
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]]
|
||||
|
||||
0
|
||||
[:div.modal-overlay
|
||||
[:div.animated {:class @klass}
|
||||
[:div.modal-container.onboarding.feature
|
||||
[:div.modal-left
|
||||
[:img {:src "images/features/advanced-proto.gif" :border "0" :alt "Advanced interactions"}]]
|
||||
[:div.modal-right
|
||||
[:div.modal-title
|
||||
[:h2 "Prototyping triggers and actions"]]
|
||||
[:div.modal-content
|
||||
[:p "Prototyping options at last! Different triggers (like mouse events or time delays) and actions allow you to add complexity to the interactions of your prototypes."]
|
||||
[:p "Create overlays, back buttons or links to URLs to mimic the behavior of the product you’re designing."]]
|
||||
[:div.modal-navigation
|
||||
[:button.btn-secondary {:on-click next} "Continue"]
|
||||
[:& c/navigation-bullets
|
||||
{:slide @slide
|
||||
:navigate navigate
|
||||
:total 4}]]]]]]
|
||||
|
||||
1
|
||||
[:div.modal-overlay
|
||||
[:div.animated {:class @klass}
|
||||
[:div.modal-container.onboarding.feature
|
||||
[:div.modal-left
|
||||
[:img {:src "images/features/flows-proto.gif" :border "0" :alt "Multiple flows"}]]
|
||||
[:div.modal-right
|
||||
[:div.modal-title
|
||||
[:h2 "Multiple flows"]]
|
||||
[:div.modal-content
|
||||
[:p "Design projects usually need to define multiple casuistics for different devices and user journeys."]
|
||||
[:p "Flows allow you to define multiple starting points within the same page so you can better organize and present your prototypes."]]
|
||||
[:div.modal-navigation
|
||||
[:button.btn-secondary {:on-click next} "Continue"]
|
||||
[:& c/navigation-bullets
|
||||
{:slide @slide
|
||||
:navigate navigate
|
||||
:total 4}]]]]]]
|
||||
|
||||
2
|
||||
[:div.modal-overlay
|
||||
[:div.animated {:class @klass}
|
||||
[:div.modal-container.onboarding.feature
|
||||
[:div.modal-left
|
||||
[:img {:src "images/features/booleans.gif" :border "0" :alt "Boolean shapes"}]]
|
||||
[:div.modal-right
|
||||
[:div.modal-title
|
||||
[:h2 "Boolean operations"]]
|
||||
[:div.modal-content
|
||||
[:p "Now in Penpot you can combine shapes in different ways. There are five options: Union, difference, intersection, exclusion and flatten."]
|
||||
[:p "Using boolean operations will lead to countless graphic possibilities for your designs."]]
|
||||
[:div.modal-navigation
|
||||
[:button.btn-secondary {:on-click next} "Continue"]
|
||||
[:& c/navigation-bullets
|
||||
{:slide @slide
|
||||
:navigate navigate
|
||||
:total 4}]]]]]]
|
||||
|
||||
3
|
||||
[:div.modal-overlay
|
||||
[:div.animated {:class @klass}
|
||||
[:div.modal-container.onboarding.feature
|
||||
[:div.modal-left
|
||||
[:img {:src "images/features/libraries-feature.gif" :border "0" :alt "Libraries & templates"}]]
|
||||
[:div.modal-right
|
||||
[:div.modal-title
|
||||
[:h2 "Libraries & templates"]]
|
||||
[:div.modal-content
|
||||
[:p "We’ve created a new space on Penpot where you can share your libraries and templates and download the ones you like. Material Design, Cocomaterial or Penpot’s Design System are among them (and a lot more to come!)."]
|
||||
[:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates.html"} "Explore libraries & templates"]]]
|
||||
[:div.modal-navigation
|
||||
[:button.btn-secondary {:on-click finish} "Start!"]
|
||||
[:& c/navigation-bullets
|
||||
{:slide @slide
|
||||
:navigate navigate
|
||||
:total 4}]]]]]])))
|
|
@ -25,9 +25,34 @@
|
|||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn calc-bounds
|
||||
[object objects]
|
||||
|
||||
(let [xf-get-bounds (comp (map #(get objects %)) (map #(calc-bounds % objects)))
|
||||
padding (filters/calculate-padding object)
|
||||
obj-bounds
|
||||
(-> (filters/get-filters-bounds object)
|
||||
(update :x - padding)
|
||||
(update :y - padding)
|
||||
(update :width + (* 2 padding))
|
||||
(update :height + (* 2 padding)))]
|
||||
|
||||
(cond
|
||||
(and (= :group (:type object))
|
||||
(:masked-group? object))
|
||||
(calc-bounds (get objects (first (:shapes object))) objects)
|
||||
|
||||
(= :group (:type object))
|
||||
(->> (:shapes object)
|
||||
(into [obj-bounds] xf-get-bounds)
|
||||
(gsh/join-rects))
|
||||
|
||||
:else
|
||||
obj-bounds)))
|
||||
|
||||
(mf/defc object-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects object-id zoom] :or {zoom 1} :as props}]
|
||||
[{:keys [objects object-id zoom render-texts?] :or {zoom 1} :as props}]
|
||||
(let [object (get objects object-id)
|
||||
frame-id (if (= :frame (:type object))
|
||||
(:id object)
|
||||
|
@ -47,20 +72,10 @@
|
|||
objects (reduce updt-fn objects mod-ids)
|
||||
object (get objects object-id)
|
||||
|
||||
;; We need to get the shadows/blurs paddings to create the viewbox properly
|
||||
{:keys [x y width height]} (filters/get-filters-bounds object)
|
||||
{:keys [x y width height] :as bs} (calc-bounds object objects)
|
||||
[_ _ width height :as coords] (->> [x y width height] (map #(* % zoom)))
|
||||
|
||||
x (* x zoom)
|
||||
y (* y zoom)
|
||||
width (* width zoom)
|
||||
height (* height zoom)
|
||||
|
||||
padding (* (filters/calculate-padding object) zoom)
|
||||
|
||||
vbox (str/join " " [(- x padding)
|
||||
(- y padding)
|
||||
(+ width padding padding)
|
||||
(+ height padding padding)])
|
||||
vbox (str/join " " coords)
|
||||
|
||||
frame-wrapper
|
||||
(mf/use-memo
|
||||
|
@ -76,18 +91,22 @@
|
|||
(mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(exports/shape-wrapper-factory objects))
|
||||
]
|
||||
|
||||
text-shapes
|
||||
(->> objects
|
||||
(filter (fn [[_ shape]] (= :text (:type shape))))
|
||||
(mapv second))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps width height)
|
||||
#(dom/set-page-style {:size (str (mth/ceil (+ width padding padding)) "px "
|
||||
(mth/ceil (+ height padding padding)) "px")}))
|
||||
#(dom/set-page-style {:size (str (mth/ceil width) "px "
|
||||
(mth/ceil height) "px")}))
|
||||
|
||||
[:& (mf/provider embed/context) {:value true}
|
||||
[:svg {:id "screenshot"
|
||||
:view-box vbox
|
||||
:width (+ width padding padding)
|
||||
:height (+ height padding padding)
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
|
@ -100,7 +119,19 @@
|
|||
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
||||
:group [:> shape-container {:shape object}
|
||||
[:& group-wrapper {:shape object}]]
|
||||
[:& shape-wrapper {:shape object}])]]))
|
||||
[:& shape-wrapper {:shape object}])]
|
||||
|
||||
;; Auxiliary SVG for rendering text-shapes
|
||||
(when render-texts?
|
||||
(for [object text-shapes]
|
||||
[:svg {:id (str "screenshot-text-" (:id object))
|
||||
:view-box (str "0 0 " (:width object) " " (:height object))
|
||||
:width (:width object)
|
||||
:height (:height object)
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"}
|
||||
[:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]))]))
|
||||
|
||||
(defn- adapt-root-frame
|
||||
[objects object-id]
|
||||
|
@ -120,7 +151,7 @@
|
|||
;; backend entry point for download only the data of single page.
|
||||
|
||||
(mf/defc render-object
|
||||
[{:keys [file-id page-id object-id] :as props}]
|
||||
[{:keys [file-id page-id object-id render-texts?] :as props}]
|
||||
(let [objects (mf/use-state nil)]
|
||||
(mf/use-effect
|
||||
(mf/deps file-id page-id object-id)
|
||||
|
@ -140,6 +171,7 @@
|
|||
(when @objects
|
||||
[:& object-svg {:objects @objects
|
||||
:object-id object-id
|
||||
:render-texts? render-texts?
|
||||
:zoom 1}])))
|
||||
|
||||
(mf/defc render-sprite
|
||||
|
|
122
frontend/src/app/main/ui/routes.cljs
Normal file
122
frontend/src/app/main/ui/routes.cljs
Normal file
|
@ -0,0 +1,122 @@
|
|||
;; 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.routes
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::section ::us/keyword)
|
||||
(s/def ::index ::us/integer)
|
||||
(s/def ::token (s/nilable ::us/not-empty-string))
|
||||
(s/def ::share-id ::us/uuid)
|
||||
|
||||
(s/def ::viewer-path-params
|
||||
(s/keys :req-un [::file-id]))
|
||||
|
||||
(s/def ::viewer-query-params
|
||||
(s/keys :opt-un [::index ::share-id ::section ::page-id]))
|
||||
|
||||
(s/def ::any any?)
|
||||
|
||||
(def routes
|
||||
[["/auth"
|
||||
["/login" :auth-login]
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register" :auth-register])
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register/validate" :auth-register-validate])
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register/success" :auth-register-success])
|
||||
["/recovery/request" :auth-recovery-request]
|
||||
["/recovery" :auth-recovery]
|
||||
["/verify-token" :auth-verify-token]]
|
||||
|
||||
["/settings"
|
||||
["/profile" :settings-profile]
|
||||
["/password" :settings-password]
|
||||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]]
|
||||
|
||||
["/view/:file-id"
|
||||
{:name :viewer
|
||||
:conform
|
||||
{:path-params ::viewer-path-params
|
||||
:query-params ::viewer-query-params}}]
|
||||
|
||||
(when *assert*
|
||||
["/debug/icons-preview" :debug-icons-preview])
|
||||
|
||||
;; Used for export
|
||||
["/render-object/:file-id/:page-id/:object-id" :render-object]
|
||||
["/render-sprite/:file-id" :render-sprite]
|
||||
|
||||
["/dashboard/team/:team-id"
|
||||
["/members" :dashboard-team-members]
|
||||
["/settings" :dashboard-team-settings]
|
||||
["/projects" :dashboard-projects]
|
||||
["/search" :dashboard-search]
|
||||
["/fonts" :dashboard-fonts]
|
||||
["/fonts/providers" :dashboard-font-providers]
|
||||
["/libraries" :dashboard-libraries]
|
||||
["/projects/:project-id" :dashboard-files]]
|
||||
|
||||
["/workspace/:project-id/:file-id" :workspace]])
|
||||
|
||||
(defn- match-path
|
||||
[router path]
|
||||
(when-let [match (rt/match router path)]
|
||||
(if-let [conform (get-in match [:data :conform])]
|
||||
(let [spath (get conform :path-params ::any)
|
||||
squery (get conform :query-params ::any)]
|
||||
(try
|
||||
(-> (dissoc match :params)
|
||||
(assoc :path-params (us/conform spath (get match :path-params))
|
||||
:query-params (us/conform squery (get match :query-params))))
|
||||
(catch :default _
|
||||
nil)))
|
||||
match)))
|
||||
|
||||
(defn on-navigate
|
||||
[router path]
|
||||
(let [match (match-path router path)
|
||||
profile (:profile @storage)
|
||||
nopath? (or (= path "") (= path "/"))
|
||||
authed? (and (not (nil? profile))
|
||||
(not= (:id profile) uuid/zero))]
|
||||
|
||||
(cond
|
||||
(and nopath? authed? (nil? match))
|
||||
(if (not= uuid/zero profile)
|
||||
(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)}))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
|
||||
(and (not authed?) (nil? match))
|
||||
(st/emit! (rt/nav :auth-login))
|
||||
|
||||
(nil? match)
|
||||
(st/emit! (rt/assign-exception {:type :not-found}))
|
||||
|
||||
:else
|
||||
(st/emit! (rt/navigated match)))))
|
||||
|
||||
(defn init-routes
|
||||
[]
|
||||
(ptk/reify ::init-routes
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (rt/initialize-router routes)
|
||||
(rt/initialize-history on-navigate)))))
|
|
@ -14,12 +14,14 @@
|
|||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- stroke-type->dasharray
|
||||
[style]
|
||||
(case style
|
||||
:mixed "5,5,1,5"
|
||||
:dotted "5,5"
|
||||
:dashed "10,10"
|
||||
nil))
|
||||
[width style]
|
||||
(let [values (case style
|
||||
:mixed [5 5 1 5]
|
||||
:dotted [5 5]
|
||||
:dashed [10 10]
|
||||
nil)]
|
||||
|
||||
(->> values (map #(+ % width)) (str/join ","))))
|
||||
|
||||
(defn- truncate-side
|
||||
[shape ra-attr rb-attr dimension-attr]
|
||||
|
@ -102,10 +104,11 @@
|
|||
|
||||
(defn add-stroke [attrs shape render-id]
|
||||
(let [stroke-style (:stroke-style shape :none)
|
||||
stroke-color-gradient-id (str "stroke-color-gradient_" render-id)]
|
||||
stroke-color-gradient-id (str "stroke-color-gradient_" render-id)
|
||||
stroke-width (:stroke-width shape 1)]
|
||||
(if (not= stroke-style :none)
|
||||
(let [stroke-attrs
|
||||
(cond-> {:strokeWidth (:stroke-width shape 1)}
|
||||
(cond-> {:strokeWidth stroke-width}
|
||||
(:stroke-color-gradient shape)
|
||||
(assoc :stroke (str/format "url(#%s)" stroke-color-gradient-id))
|
||||
|
||||
|
@ -118,7 +121,7 @@
|
|||
(assoc :strokeOpacity (:stroke-opacity shape nil))
|
||||
|
||||
(not= stroke-style :svg)
|
||||
(assoc :strokeDasharray (stroke-type->dasharray stroke-style))
|
||||
(assoc :strokeDasharray (stroke-type->dasharray stroke-width stroke-style))
|
||||
|
||||
;; For simple line caps we use svg stroke-line-cap attribute. This
|
||||
;; only works if all caps are the same and we are not using the tricks
|
||||
|
|
114
frontend/src/app/main/ui/shapes/bool.cljs
Normal file
114
frontend/src/app/main/ui/shapes/bool.cljs
Normal file
|
@ -0,0 +1,114 @@
|
|||
;; 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.shapes.bool
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.path.bool :as pb]
|
||||
[app.common.path.shapes-to-path :as stp]
|
||||
[app.main.ui.hooks :refer [use-equal-memo]]
|
||||
[app.main.ui.shapes.export :as use]
|
||||
[app.main.ui.shapes.path :refer [path-shape]]
|
||||
[app.util.object :as obj]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc debug-bool
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
(let [frame (obj/get props "frame")
|
||||
shape (obj/get props "shape")
|
||||
childs (obj/get props "childs")
|
||||
|
||||
[content-a content-b]
|
||||
(mf/use-memo
|
||||
(mf/deps shape childs)
|
||||
(fn []
|
||||
(let [childs (d/mapm #(-> %2 (gsh/translate-to-frame frame) gsh/transform-shape) childs)
|
||||
[content-a content-b]
|
||||
(->> (:shapes shape)
|
||||
(map #(get childs %))
|
||||
(filter #(not (:hidden %)))
|
||||
(map #(stp/convert-to-path % childs))
|
||||
(map :content)
|
||||
(map pb/close-paths)
|
||||
(map pb/add-previous))]
|
||||
(pb/content-intersect-split content-a content-b))))]
|
||||
[:g.debug-bool
|
||||
[:g.shape-a
|
||||
[:& path-shape {:shape (-> shape
|
||||
(assoc :type :path)
|
||||
(assoc :stroke-color "blue")
|
||||
(assoc :stroke-opacity 1)
|
||||
(assoc :stroke-width 1)
|
||||
(assoc :stroke-style :solid)
|
||||
(dissoc :fill-color :fill-opacity)
|
||||
(assoc :content content-b))
|
||||
:frame frame}]
|
||||
(for [{:keys [x y]} (gsp/content->points (pb/close-paths content-b))]
|
||||
[:circle {:cx x
|
||||
:cy y
|
||||
:r 2.5
|
||||
:style {:fill "blue"}}])]
|
||||
|
||||
[:g.shape-b
|
||||
[:& path-shape {:shape (-> shape
|
||||
(assoc :type :path)
|
||||
(assoc :stroke-color "red")
|
||||
(assoc :stroke-opacity 1)
|
||||
(assoc :stroke-width 0.5)
|
||||
(assoc :stroke-style :solid)
|
||||
(dissoc :fill-color :fill-opacity)
|
||||
(assoc :content content-a))
|
||||
:frame frame}]
|
||||
(for [{:keys [x y]} (gsp/content->points (pb/close-paths content-a))]
|
||||
[:circle {:cx x
|
||||
:cy y
|
||||
:r 1.25
|
||||
:style {:fill "red"}}])]])
|
||||
)
|
||||
|
||||
|
||||
(defn bool-shape
|
||||
[shape-wrapper]
|
||||
(mf/fnc bool-shape
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [frame (obj/get props "frame")
|
||||
shape (obj/get props "shape")
|
||||
childs (obj/get props "childs")
|
||||
|
||||
childs (use-equal-memo childs)
|
||||
|
||||
include-metadata? (mf/use-ctx use/include-metadata-ctx)
|
||||
|
||||
bool-content
|
||||
(mf/use-memo
|
||||
(mf/deps shape childs)
|
||||
(fn []
|
||||
(let [childs (d/mapm #(-> %2 gsh/transform-shape (gsh/translate-to-frame frame)) childs)]
|
||||
(->> (:shapes shape)
|
||||
(map #(get childs %))
|
||||
(filter #(not (:hidden %)))
|
||||
(map #(stp/convert-to-path % childs))
|
||||
(mapv :content)
|
||||
(pb/content-bool (:bool-type shape))))))]
|
||||
|
||||
[:*
|
||||
[:& path-shape {:shape (assoc shape :content bool-content)}]
|
||||
|
||||
(when include-metadata?
|
||||
[:> "penpot:bool" {}
|
||||
(for [item (->> (:shapes shape) (mapv #(get childs %)))]
|
||||
[:& shape-wrapper {:frame frame
|
||||
:shape item
|
||||
:key (:id item)}])])
|
||||
|
||||
#_[:& debug-bool {:frame frame
|
||||
:shape shape
|
||||
:childs childs}]])))
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.shapes.custom-stroke
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -34,8 +35,22 @@
|
|||
[{:keys [shape render-id]}]
|
||||
(let [stroke-mask-id (str "outer-stroke-" render-id)
|
||||
shape-id (str "stroke-shape-" render-id)
|
||||
stroke-width (:stroke-width shape 0)]
|
||||
[:mask {:id stroke-mask-id}
|
||||
stroke-width (case (:stroke-alignment shape :center)
|
||||
:center (/ (:stroke-width shape 0) 2)
|
||||
:outer (:stroke-width shape 0)
|
||||
0)
|
||||
margin (gsh/shape-stroke-margin shape stroke-width)
|
||||
bounding-box (-> (gsh/points->selrect (:points shape))
|
||||
(update :x - (+ stroke-width margin))
|
||||
(update :y - (+ stroke-width margin))
|
||||
(update :width + (* 2 (+ stroke-width margin)))
|
||||
(update :height + (* 2 (+ stroke-width margin))))]
|
||||
[:mask {:id stroke-mask-id
|
||||
:x (:x bounding-box)
|
||||
:y (:y bounding-box)
|
||||
:width (:width bounding-box)
|
||||
:height (:height bounding-box)
|
||||
:maskUnits "userSpaceOnUse"}
|
||||
[:use {:xlinkHref (str "#" shape-id)
|
||||
:style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}]
|
||||
|
||||
|
@ -60,8 +75,8 @@
|
|||
:viewBox "0 0 3 6"
|
||||
:refX "2"
|
||||
:refY "3"
|
||||
:markerWidth "3"
|
||||
:markerHeight "6"
|
||||
:markerWidth "8.5"
|
||||
:markerHeight "8.5"
|
||||
:orient "auto-start-reverse"
|
||||
:fill stroke-color
|
||||
:fillOpacity stroke-opacity}
|
||||
|
@ -72,8 +87,8 @@
|
|||
:viewBox "0 0 3 6"
|
||||
:refX "2"
|
||||
:refY "3"
|
||||
:markerWidth "3"
|
||||
:markerHeight "6"
|
||||
:markerWidth "8.5"
|
||||
:markerHeight "8.5"
|
||||
:orient "auto-start-reverse"
|
||||
:fill stroke-color
|
||||
:fillOpacity stroke-opacity}
|
||||
|
@ -82,10 +97,10 @@
|
|||
(when (or (= cap-start :square-marker) (= cap-end :square-marker))
|
||||
[:marker {:id (str marker-id-prefix "-square-marker")
|
||||
:viewBox "0 0 6 6"
|
||||
:refX "5"
|
||||
:refX "3"
|
||||
:refY "3"
|
||||
:markerWidth "6"
|
||||
:markerHeight "6"
|
||||
:markerWidth "4.2426" ;; diagonal length of a 3x3 square
|
||||
:markerHeight "4.2426"
|
||||
:orient "auto-start-reverse"
|
||||
:fill stroke-color
|
||||
:fillOpacity stroke-opacity}
|
||||
|
@ -94,10 +109,10 @@
|
|||
(when (or (= cap-start :circle-marker) (= cap-end :circle-marker))
|
||||
[:marker {:id (str marker-id-prefix "-circle-marker")
|
||||
:viewBox "0 0 6 6"
|
||||
:refX "5"
|
||||
:refX "3"
|
||||
:refY "3"
|
||||
:markerWidth "6"
|
||||
:markerHeight "6"
|
||||
:markerWidth "4"
|
||||
:markerHeight "4"
|
||||
:orient "auto-start-reverse"
|
||||
:fill stroke-color
|
||||
:fillOpacity stroke-opacity}
|
||||
|
@ -106,7 +121,7 @@
|
|||
(when (or (= cap-start :diamond-marker) (= cap-end :diamond-marker))
|
||||
[:marker {:id (str marker-id-prefix "-diamond-marker")
|
||||
:viewBox "0 0 6 6"
|
||||
:refX "5"
|
||||
:refX "3"
|
||||
:refY "3"
|
||||
:markerWidth "6"
|
||||
:markerHeight "6"
|
||||
|
@ -145,22 +160,24 @@
|
|||
|
||||
(mf/defc stroke-defs
|
||||
[{:keys [shape render-id]}]
|
||||
(cond
|
||||
(and (= :inner (:stroke-alignment shape :center))
|
||||
(> (:stroke-width shape 0) 0))
|
||||
[:& inner-stroke-clip-path {:shape shape
|
||||
:render-id render-id}]
|
||||
(when (or (not= (:type shape) :path)
|
||||
(not (gsh/open-path? shape)))
|
||||
(cond
|
||||
(and (= :inner (:stroke-alignment shape :center))
|
||||
(> (:stroke-width shape 0) 0))
|
||||
[:& inner-stroke-clip-path {:shape shape
|
||||
:render-id render-id}]
|
||||
|
||||
(and (= :outer (:stroke-alignment shape :center))
|
||||
(> (:stroke-width shape 0) 0))
|
||||
[:& outer-stroke-mask {:shape shape
|
||||
:render-id render-id}]
|
||||
(and (= :outer (:stroke-alignment shape :center))
|
||||
(> (:stroke-width shape 0) 0))
|
||||
[:& outer-stroke-mask {:shape shape
|
||||
:render-id render-id}]
|
||||
|
||||
(and (or (some? (:stroke-cap-start shape))
|
||||
(some? (:stroke-cap-end shape)))
|
||||
(= (:stroke-alignment shape) :center))
|
||||
[:& cap-markers {:shape shape
|
||||
:render-id render-id}]))
|
||||
(and (or (some? (:stroke-cap-start shape))
|
||||
(some? (:stroke-cap-end shape)))
|
||||
(= (:stroke-alignment shape) :center))
|
||||
[:& cap-markers {:shape shape
|
||||
:render-id render-id}])))
|
||||
|
||||
;; Outer alingmnent: display the shape in two layers. One
|
||||
;; without stroke (only fill), and another one only with stroke
|
||||
|
@ -253,15 +270,17 @@
|
|||
stroke-position (:stroke-alignment shape :center)
|
||||
has-stroke? (and (> stroke-width 0)
|
||||
(not= stroke-style :none))
|
||||
inner? (= :inner stroke-position)
|
||||
outer? (= :outer stroke-position)]
|
||||
closed? (or (not= :path (:type shape))
|
||||
(not (gsh/open-path? shape)))
|
||||
inner? (= :inner stroke-position)
|
||||
outer? (= :outer stroke-position)]
|
||||
|
||||
(cond
|
||||
(and has-stroke? inner?)
|
||||
(and has-stroke? inner? closed?)
|
||||
[:& inner-stroke {:shape shape}
|
||||
child]
|
||||
|
||||
(and has-stroke? outer?)
|
||||
(and has-stroke? outer? closed?)
|
||||
[:& outer-stroke {:shape shape}
|
||||
child]
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
text? (= :text (:type shape))
|
||||
path? (= :path (:type shape))
|
||||
mask? (and group? (:masked-group? shape))
|
||||
bool? (= :bool (:type shape))
|
||||
center (gsh/center-shape shape)]
|
||||
(-> props
|
||||
(add! :name)
|
||||
|
@ -102,7 +103,10 @@
|
|||
(add! :content (comp json/encode uuid->string))))
|
||||
|
||||
(cond-> mask?
|
||||
(obj/set! "penpot:masked-group" "true")))))
|
||||
(obj/set! "penpot:masked-group" "true"))
|
||||
|
||||
(cond-> bool?
|
||||
(add! :bool-type)))))
|
||||
|
||||
|
||||
(defn add-library-refs [props shape]
|
||||
|
@ -127,30 +131,41 @@
|
|||
|
||||
(mf/defc export-grid-data
|
||||
[{:keys [grids]}]
|
||||
(when-not (empty? grids)
|
||||
[:> "penpot:grids" #js {}
|
||||
(for [{:keys [type display params]} grids]
|
||||
(let [props (->> (d/without-keys params [:color])
|
||||
(prefix-keys)
|
||||
(clj->js))]
|
||||
[:> "penpot:grid"
|
||||
(-> props
|
||||
(obj/set! "penpot:color" (get-in params [:color :color]))
|
||||
(obj/set! "penpot:opacity" (get-in params [:color :opacity]))
|
||||
(obj/set! "penpot:type" (d/name type))
|
||||
(cond-> (some? display)
|
||||
(obj/set! "penpot:display" (str display))))]))]))
|
||||
[:> "penpot:grids" #js {}
|
||||
(for [{:keys [type display params]} grids]
|
||||
(let [props (->> (d/without-keys params [:color])
|
||||
(prefix-keys)
|
||||
(clj->js))]
|
||||
[:> "penpot:grid"
|
||||
(-> props
|
||||
(obj/set! "penpot:color" (get-in params [:color :color]))
|
||||
(obj/set! "penpot:opacity" (get-in params [:color :opacity]))
|
||||
(obj/set! "penpot:type" (d/name type))
|
||||
(cond-> (some? display)
|
||||
(obj/set! "penpot:display" (str display))))]))])
|
||||
|
||||
(mf/defc export-flows
|
||||
[{:keys [flows]}]
|
||||
[:> "penpot:flows" #js {}
|
||||
(for [{:keys [id name starting-frame]} flows]
|
||||
[:> "penpot:flow" #js {:id id
|
||||
:name name
|
||||
:starting-frame starting-frame}])])
|
||||
|
||||
(mf/defc export-page
|
||||
[{:keys [options]}]
|
||||
(let [saved-grids (get options :saved-grids)]
|
||||
(when-not (empty? saved-grids)
|
||||
(let [parse-grid
|
||||
(fn [[type params]]
|
||||
{:type type :params params})
|
||||
grids (->> saved-grids (mapv parse-grid))]
|
||||
[:> "penpot:page" #js {}
|
||||
[:& export-grid-data {:grids grids}]]))))
|
||||
(let [saved-grids (get options :saved-grids)
|
||||
flows (get options :flows)]
|
||||
(when (or (seq saved-grids) (seq flows))
|
||||
(let [parse-grid
|
||||
(fn [[type params]]
|
||||
{:type type :params params})
|
||||
grids (->> saved-grids (mapv parse-grid))]
|
||||
[:> "penpot:page" #js {}
|
||||
(when (seq saved-grids)
|
||||
[:& export-grid-data {:grids grids}])
|
||||
(when (seq flows)
|
||||
[:& export-flows {:flows flows}])]))))
|
||||
|
||||
(mf/defc export-shadow-data
|
||||
[{:keys [shadow]}]
|
||||
|
@ -216,11 +231,18 @@
|
|||
[{:keys [interactions]}]
|
||||
(when-not (empty? interactions)
|
||||
[:> "penpot:interactions" #js {}
|
||||
(for [{:keys [action-type destination event-type]} interactions]
|
||||
(for [interaction interactions]
|
||||
[:> "penpot:interaction"
|
||||
#js {:penpot:action-type (d/name action-type)
|
||||
:penpot:destination (str destination)
|
||||
:penpot:event-type (d/name event-type)}])]))
|
||||
#js {:penpot:event-type (d/name (:event-type interaction))
|
||||
:penpot:action-type (d/name (:action-type interaction))
|
||||
:penpot:delay ((d/nilf str) (:delay interaction))
|
||||
:penpot:destination ((d/nilf str) (:destination interaction))
|
||||
:penpot:overlay-pos-type ((d/nilf d/name) (:overlay-pos-type interaction))
|
||||
:penpot:overlay-position-x ((d/nilf get-in) interaction [:overlay-position :x])
|
||||
:penpot:overlay-position-y ((d/nilf get-in) interaction [:overlay-position :y])
|
||||
:penpot:url (:url interaction)
|
||||
:penpot:close-click-outside ((d/nilf str) (:close-click-outside interaction))
|
||||
:penpot:background-overlay ((d/nilf str) (:background-overlay interaction))}])]))
|
||||
|
||||
(mf/defc export-data
|
||||
[{:keys [shape]}]
|
||||
|
|
|
@ -202,17 +202,12 @@
|
|||
:height (- y2 y1)})))))
|
||||
|
||||
(defn calculate-padding [shape]
|
||||
(let [{:keys [stroke-style stroke-alignment stroke-width]} shape]
|
||||
(cond
|
||||
(and (not= stroke-style :none)
|
||||
(= stroke-alignment :outer))
|
||||
stroke-width
|
||||
|
||||
(and (not= stroke-style :none)
|
||||
(= stroke-alignment :center))
|
||||
(mth/ceil (/ stroke-width 2))
|
||||
|
||||
:else 0)))
|
||||
(let [stroke-width (case (:stroke-alignment shape :center)
|
||||
:center (/ (:stroke-width shape 0) 2)
|
||||
:outer (:stroke-width shape 0)
|
||||
0)
|
||||
margin (gsh/shape-stroke-margin shape stroke-width)]
|
||||
(+ stroke-width margin)))
|
||||
|
||||
(mf/defc filters
|
||||
[{:keys [filter-id shape]}]
|
||||
|
@ -221,9 +216,7 @@
|
|||
|
||||
;; Adds the previous filter as `filter-in` parameter
|
||||
filters (map #(assoc %1 :filter-in %2) filters (cons nil (map :id filters)))
|
||||
|
||||
bounds (get-filters-bounds shape filters (or (-> shape :blur :value) 0))
|
||||
|
||||
padding (calculate-padding shape)]
|
||||
|
||||
[:*
|
||||
|
|
|
@ -27,21 +27,31 @@
|
|||
[(first childs) (rest childs)]
|
||||
[nil childs])
|
||||
|
||||
;; We need to separate mask and clip into two because a bug in Firefox
|
||||
;; breaks when the group has clip+mask+foreignObject
|
||||
;; Clip and mask separated will work in every platform
|
||||
; Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805
|
||||
[clip-wrapper clip-props]
|
||||
(if masked-group?
|
||||
["g" (-> (obj/new)
|
||||
(obj/set! "clipPath" (clip-url render-id mask)))]
|
||||
[mf/Fragment nil])
|
||||
|
||||
[mask-wrapper mask-props]
|
||||
(if masked-group?
|
||||
["g" (-> (obj/new)
|
||||
(obj/set! "clipPath" (clip-url render-id mask))
|
||||
(obj/set! "mask" (mask-url render-id mask)))]
|
||||
[mf/Fragment nil])]
|
||||
|
||||
[:> mask-wrapper mask-props
|
||||
(when masked-group?
|
||||
[:> render-mask #js {:frame frame :mask mask}])
|
||||
[:> clip-wrapper clip-props
|
||||
[:> mask-wrapper mask-props
|
||||
(when masked-group?
|
||||
[:> render-mask #js {:frame frame :mask mask}])
|
||||
|
||||
(for [item childs]
|
||||
[:& shape-wrapper {:frame frame
|
||||
:shape item
|
||||
:key (:id item)}])]))))
|
||||
(for [item childs]
|
||||
[:& shape-wrapper {:frame frame
|
||||
:shape item
|
||||
:key (:id item)}])]]))))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
[:> wrapper-tag wrapper-props
|
||||
(when include-metadata?
|
||||
[:& ed/export-data {:shape shape}])
|
||||
|
||||
[:defs
|
||||
[:& defs/svg-defs {:shape shape :render-id render-id}]
|
||||
[:& filters/filters {:shape shape :filter-id filter-id}]
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
(let [valign (:vertical-align node "top")
|
||||
width (some-> (:width shape) (+ 1))
|
||||
base #js {:height (or (:height shape) "100%")
|
||||
:width (or width "100%")}]
|
||||
:width (or width "100%")
|
||||
:fontFamily "sourcesanspro"}]
|
||||
(cond-> base
|
||||
(= valign "top") (obj/set! "justifyContent" "flex-start")
|
||||
(= valign "center") (obj/set! "justifyContent" "center")
|
||||
|
@ -40,6 +41,7 @@
|
|||
:justifyContent "inherit"
|
||||
:minHeight (when-not (or auto-width? auto-height?) "100%")
|
||||
:minWidth (when-not auto-width? "100%")
|
||||
:marginRight "1px"
|
||||
:verticalAlign "top"}))
|
||||
|
||||
(defn generate-paragraph-styles
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.share-link
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as log]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dc]
|
||||
[app.main.data.messages :as dm]
|
||||
|
@ -16,12 +17,11 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.logging :as log]
|
||||
[app.util.router :as rt]
|
||||
[app.util.webapi :as wapi]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(log/set-level! :debug)
|
||||
(log/set-level! :warn)
|
||||
|
||||
(defn prepare-params
|
||||
[{:keys [sections pages pages-mode]}]
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
(ns app.main.ui.static
|
||||
(:require
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
|
@ -72,7 +71,7 @@
|
|||
[:div.desc-message (tr "labels.internal-error.desc-message")]
|
||||
[:div.sign-info
|
||||
[:a.btn-primary.btn-small
|
||||
{:on-click (st/emitf (dm/assign-exception nil))}
|
||||
{:on-click (st/emitf (rt/assign-exception nil))}
|
||||
(tr "labels.retry")]]])
|
||||
|
||||
(mf/defc exception-page
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
(ns app.main.ui.viewer
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.viewer.shortcuts :as sc]
|
||||
|
@ -13,6 +15,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,22 +30,19 @@
|
|||
|
||||
(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)}))
|
||||
|
||||
(mf/defc viewer
|
||||
[{:keys [params data]}]
|
||||
|
||||
(let [{:keys [page-id section index]} params
|
||||
{:keys [file users project permissions]} data
|
||||
|
||||
local (mf/deref refs/viewer-local)
|
||||
|
||||
file (:file data)
|
||||
users (:users data)
|
||||
project (:project data)
|
||||
perms (:permissions data)
|
||||
|
||||
page-id (or page-id (-> file :data :pages first))
|
||||
|
||||
page (mf/use-memo
|
||||
|
@ -58,15 +58,26 @@
|
|||
(mf/deps frame zoom)
|
||||
(fn [] (calculate-size frame zoom)))
|
||||
|
||||
interactions-mode
|
||||
(:interactions-mode local)
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(mf/deps section)
|
||||
(fn [_]
|
||||
(when (= section :comments)
|
||||
(st/emit! (dcm/close-thread)))))]
|
||||
(st/emit! (dcm/close-thread)))))
|
||||
|
||||
close-overlay
|
||||
(mf/use-callback
|
||||
(fn [frame]
|
||||
(st/emit! (dv/close-overlay (:id frame)))))]
|
||||
|
||||
(hooks/use-shortcuts ::viewer sc/shortcuts)
|
||||
|
||||
(when (nil? page)
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
;; Set the page title
|
||||
(mf/use-effect
|
||||
(mf/deps (:name file))
|
||||
|
@ -90,8 +101,8 @@
|
|||
:file file
|
||||
:page page
|
||||
:frame frame
|
||||
:permissions perms
|
||||
:zoom (:zoom local)
|
||||
:permissions permissions
|
||||
:zoom zoom
|
||||
:section section}]
|
||||
|
||||
[:div.viewer-content
|
||||
|
@ -118,7 +129,6 @@
|
|||
:section section
|
||||
:local local}]
|
||||
|
||||
|
||||
[:div.viewport-container
|
||||
{:style {:width (:width size)
|
||||
:height (:height size)
|
||||
|
@ -133,11 +143,43 @@
|
|||
|
||||
[:& interactions/viewport
|
||||
{:frame frame
|
||||
:base-frame frame
|
||||
:frame-offset (gpt/point 0 0)
|
||||
:size size
|
||||
:page page
|
||||
:file file
|
||||
:users users
|
||||
:local local}]]))]]]))
|
||||
:interactions-mode interactions-mode}]
|
||||
|
||||
(for [overlay (:overlays local)]
|
||||
(let [size-over (calculate-size (:frame overlay) zoom)]
|
||||
[:*
|
||||
(when (or (:close-click-outside overlay)
|
||||
(:background-overlay overlay))
|
||||
[:div.viewer-overlay-background
|
||||
{:class (dom/classnames
|
||||
:visible (:background-overlay overlay))
|
||||
:style {:width (:width frame)
|
||||
:height (:height frame)
|
||||
:position "absolute"
|
||||
:left 0
|
||||
:top 0}
|
||||
:on-click #(when (:close-click-outside overlay)
|
||||
(close-overlay (:frame overlay)))}])
|
||||
[:div.viewport-container.viewer-overlay
|
||||
{:style {:width (:width size-over)
|
||||
:height (:height size-over)
|
||||
:left (* (:x (:position overlay)) zoom)
|
||||
:top (* (:y (:position overlay)) zoom)}}
|
||||
[:& interactions/viewport
|
||||
{:frame (:frame overlay)
|
||||
:base-frame frame
|
||||
:frame-offset (:position overlay)
|
||||
:size size-over
|
||||
:page page
|
||||
:file file
|
||||
:users users
|
||||
:interactions-mode interactions-mode}]]]))]))]]]))
|
||||
|
||||
;; --- Component: Viewer Page
|
||||
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
"The main container for a frame in handoff mode"
|
||||
(:require
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.shapes.bool :as bool]
|
||||
[app.main.ui.shapes.circle :as circle]
|
||||
[app.main.ui.shapes.frame :as frame]
|
||||
[app.main.ui.shapes.group :as group]
|
||||
|
@ -106,6 +108,22 @@
|
|||
(obj/merge! #js {:childs childs}))]
|
||||
[:> group-wrapper props]))))
|
||||
|
||||
(defn bool-container-factory
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
bool-shape (bool/bool-shape shape-container)
|
||||
bool-wrapper (shape-wrapper-factory bool-shape)]
|
||||
(mf/fnc bool-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
children-ids (cp/get-children (:id shape) objects)
|
||||
childs (select-keys objects children-ids)
|
||||
props (-> (obj/new)
|
||||
(obj/merge! props)
|
||||
(obj/merge! #js {:childs childs}))]
|
||||
[:> bool-wrapper props]))))
|
||||
|
||||
(defn svg-raw-container-factory
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
|
@ -133,12 +151,18 @@
|
|||
[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))]
|
||||
|
||||
group-container
|
||||
(mf/use-memo (mf/deps objects)
|
||||
#(group-container-factory objects))
|
||||
|
||||
bool-container
|
||||
(mf/use-memo (mf/deps objects)
|
||||
#(bool-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))
|
||||
|
@ -151,6 +175,7 @@
|
|||
:image [:> image-wrapper opts]
|
||||
:circle [:> circle-wrapper opts]
|
||||
:group [:> group-container opts]
|
||||
:bool [:> bool-container opts]
|
||||
:svg-raw [:> svg-raw-container opts])))))))
|
||||
|
||||
(mf/defc render-frame-svg
|
||||
|
|
|
@ -13,14 +13,14 @@
|
|||
[app.main.ui.components.fullscreen :as fs]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.comments :refer [comments-menu]]
|
||||
[app.main.ui.viewer.interactions :refer [interactions-menu]]
|
||||
[app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]]
|
||||
[app.main.ui.workspace.header :refer [zoom-widget]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc header-options
|
||||
[{:keys [section zoom page file permissions]}]
|
||||
[{:keys [section zoom page file index permissions]}]
|
||||
(let [fullscreen (mf/use-ctx fs/fullscreen-context)
|
||||
|
||||
toggle-fullscreen
|
||||
|
@ -43,7 +43,10 @@
|
|||
|
||||
[:div.options-zone
|
||||
(case section
|
||||
:interactions [:& interactions-menu]
|
||||
:interactions [:*
|
||||
(when index
|
||||
[:& flows-menu {:page page :index index}])
|
||||
[:& interactions-menu]]
|
||||
:comments [:& comments-menu]
|
||||
|
||||
[:div.view-options])
|
||||
|
@ -64,10 +67,10 @@
|
|||
i/full-screen-off
|
||||
i/full-screen)]
|
||||
|
||||
(when (:edit permissions)
|
||||
(when (:is-admin permissions)
|
||||
[:span.btn-primary {:on-click open-share-dialog} (tr "labels.share-prototype")])
|
||||
|
||||
(when (:edit permissions)
|
||||
(when (:can-edit permissions)
|
||||
[:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")])]))
|
||||
|
||||
(mf/defc header-sitemap
|
||||
|
@ -84,15 +87,23 @@
|
|||
|
||||
show-dropdown? (mf/use-state false)
|
||||
|
||||
open-dropdown
|
||||
(fn []
|
||||
(reset! show-dropdown? true)
|
||||
(st/emit! dv/close-thumbnails-panel))
|
||||
|
||||
close-dropdown
|
||||
(fn []
|
||||
(reset! show-dropdown? false))
|
||||
|
||||
navigate-to
|
||||
(fn [page-id]
|
||||
(st/emit! (dv/go-to-page page-id))
|
||||
(reset! show-dropdown? false))
|
||||
]
|
||||
(reset! show-dropdown? false))]
|
||||
|
||||
[:div.sitemap-zone {:alt (tr "viewer.header.sitemap")}
|
||||
[:div.breadcrumb
|
||||
{:on-click #(swap! show-dropdown? not)}
|
||||
{:on-click open-dropdown}
|
||||
[:span.project-name project-name]
|
||||
[:span "/"]
|
||||
[:span.file-name file-name]
|
||||
|
@ -101,7 +112,7 @@
|
|||
[:span.icon i/arrow-down]
|
||||
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(swap! show-dropdown? not)}
|
||||
:on-close close-dropdown}
|
||||
[:ul.dropdown
|
||||
(for [id (get-in file [:data :pages])]
|
||||
[:li {:id (str id)
|
||||
|
@ -125,7 +136,6 @@
|
|||
(fn [section]
|
||||
(st/emit! (dv/go-to-section section)))]
|
||||
|
||||
|
||||
[:header.viewer-header
|
||||
[:div.main-icon
|
||||
[:a {:on-click go-to-dashboard
|
||||
|
@ -141,14 +151,16 @@
|
|||
:alt (tr "viewer.header.interactions-section")}
|
||||
i/play]
|
||||
|
||||
(when (:edit permissions)
|
||||
(when (:can-edit permissions)
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :comments)
|
||||
:class (dom/classnames :active (= section :comments))
|
||||
:alt (tr "viewer.header.comments-section")}
|
||||
i/chat])
|
||||
|
||||
(when (:read permissions)
|
||||
(when (or (= (:type permissions) :membership)
|
||||
(and (= (:type permissions) :share-link)
|
||||
(contains? (:flags permissions) :section-handoff)))
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :handoff)
|
||||
:class (dom/classnames :active (= section :handoff))
|
||||
|
@ -159,5 +171,6 @@
|
|||
:permissions permissions
|
||||
:page page
|
||||
:file file
|
||||
:index index
|
||||
:zoom zoom}]]))
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.types.page-options :as cto]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.refs :as refs]
|
||||
|
@ -41,25 +42,23 @@
|
|||
|
||||
(mf/defc viewport
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [local page frame size]}]
|
||||
(let [interactions? (:interactions-show? local)
|
||||
|
||||
objects (mf/use-memo
|
||||
[{:keys [page interactions-mode frame base-frame frame-offset size]}]
|
||||
(let [objects (mf/use-memo
|
||||
(mf/deps page frame)
|
||||
(prepare-objects page frame))
|
||||
|
||||
wrapper (mf/use-memo
|
||||
(mf/deps objects interactions?)
|
||||
#(shapes/frame-container-factory objects interactions?))
|
||||
(mf/deps objects)
|
||||
#(shapes/frame-container-factory objects))
|
||||
|
||||
;; Retrieve frame again with correct modifier
|
||||
;; Retrieve frames again with correct modifier
|
||||
frame (get objects (:id frame))
|
||||
base-frame (get objects (:id base-frame))
|
||||
|
||||
on-click
|
||||
(fn [_]
|
||||
(let [mode (:interactions-mode local)]
|
||||
(when (= mode :show-on-click)
|
||||
(st/emit! dv/flash-interactions))))
|
||||
(when (= interactions-mode :show-on-click)
|
||||
(st/emit! dv/flash-interactions)))
|
||||
|
||||
on-mouse-wheel
|
||||
(fn [event]
|
||||
|
@ -77,7 +76,7 @@
|
|||
(st/emit! (dcm/close-thread))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps local) ;; on-click event depends on local
|
||||
(mf/deps interactions-mode) ;; on-click event depends on interactions-mode
|
||||
(fn []
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
|
@ -89,15 +88,50 @@
|
|||
(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/provider shapes/base-frame-ctx) {:value base-frame}
|
||||
[:& (mf/provider shapes/frame-offset-ctx) {:value frame-offset}
|
||||
[: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
|
||||
:view-box (:vbox size)}]]]]))
|
||||
|
||||
|
||||
(mf/defc flows-menu
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [page index]}]
|
||||
(let [flows (get-in page [:options :flows])
|
||||
frames (:frames page)
|
||||
frame (get frames index)
|
||||
current-flow (mf/use-state
|
||||
(cto/get-frame-flow flows (:id frame)))
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
select-flow
|
||||
(mf/use-callback
|
||||
(fn [flow]
|
||||
(reset! current-flow flow)
|
||||
(st/emit! (dv/go-to-frame (:starting-frame flow)))))]
|
||||
|
||||
(when (seq flows)
|
||||
[:div.view-options {:on-click toggle-dropdown}
|
||||
[:span.icon i/play]
|
||||
[:span.label (:name @current-flow)]
|
||||
[:span.icon i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
(for [flow flows]
|
||||
[:li {:class (dom/classnames :selected (= (:id flow) (:id @current-flow)))
|
||||
:on-click #(select-flow flow)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (:name flow)]])]]])))
|
||||
|
||||
|
||||
(mf/defc interactions-menu
|
||||
|
|
|
@ -12,8 +12,11 @@
|
|||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.types.interactions :as cti]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.shapes.bool :as bool]
|
||||
[app.main.ui.shapes.circle :as circle]
|
||||
[app.main.ui.shapes.frame :as frame]
|
||||
[app.main.ui.shapes.group :as group]
|
||||
|
@ -25,21 +28,162 @@
|
|||
[app.main.ui.shapes.text :as text]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as tm]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn on-mouse-down
|
||||
[event interactions]
|
||||
(let [interaction (first (filter #(= (:event-type %) :click) interactions))]
|
||||
(case (:action-type interaction)
|
||||
:navigate
|
||||
(let [frame-id (:destination interaction)]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/go-to-frame frame-id)))
|
||||
(def base-frame-ctx (mf/create-context nil))
|
||||
(def frame-offset-ctx (mf/create-context nil))
|
||||
|
||||
nil)))
|
||||
(def viewer-interactions-show?
|
||||
(l/derived :interactions-show? refs/viewer-local))
|
||||
|
||||
(defn activate-interaction
|
||||
[interaction shape base-frame frame-offset objects]
|
||||
(case (:action-type interaction)
|
||||
:navigate
|
||||
(when-let [frame-id (:destination interaction)]
|
||||
(st/emit! (dv/go-to-frame frame-id)))
|
||||
|
||||
:open-overlay
|
||||
(let [dest-frame-id (:destination interaction)
|
||||
close-click-outside (:close-click-outside interaction)
|
||||
background-overlay (:background-overlay interaction)
|
||||
|
||||
dest-frame (get objects dest-frame-id)
|
||||
position (cti/calc-overlay-position interaction
|
||||
base-frame
|
||||
dest-frame
|
||||
frame-offset)]
|
||||
(when dest-frame-id
|
||||
(st/emit! (dv/open-overlay dest-frame-id
|
||||
position
|
||||
close-click-outside
|
||||
background-overlay))))
|
||||
|
||||
:toggle-overlay
|
||||
(let [frame-id (:destination interaction)
|
||||
position (:overlay-position interaction)
|
||||
close-click-outside (:close-click-outside interaction)
|
||||
background-overlay (:background-overlay interaction)]
|
||||
(when frame-id
|
||||
(st/emit! (dv/toggle-overlay frame-id
|
||||
position
|
||||
close-click-outside
|
||||
background-overlay))))
|
||||
|
||||
:close-overlay
|
||||
(let [frame-id (or (:destination interaction)
|
||||
(if (= (:type shape) :frame)
|
||||
(:id shape)
|
||||
(:frame-id shape)))]
|
||||
(st/emit! (dv/close-overlay frame-id)))
|
||||
|
||||
:prev-screen
|
||||
(st/emit! (rt/nav-back-local))
|
||||
|
||||
:open-url
|
||||
(st/emit! (dom/open-new-window (:url interaction)))
|
||||
|
||||
nil))
|
||||
|
||||
;; Perform the opposite action of an interaction, if possible
|
||||
(defn deactivate-interaction
|
||||
[interaction shape base-frame frame-offset objects]
|
||||
(case (:action-type interaction)
|
||||
:open-overlay
|
||||
(let [frame-id (or (:destination interaction)
|
||||
(if (= (:type shape) :frame)
|
||||
(:id shape)
|
||||
(:frame-id shape)))]
|
||||
(st/emit! (dv/close-overlay frame-id)))
|
||||
|
||||
:toggle-overlay
|
||||
(let [frame-id (:destination interaction)
|
||||
position (:overlay-position interaction)
|
||||
close-click-outside (:close-click-outside interaction)
|
||||
background-overlay (:background-overlay interaction)]
|
||||
(when frame-id
|
||||
(st/emit! (dv/toggle-overlay frame-id
|
||||
position
|
||||
close-click-outside
|
||||
background-overlay))))
|
||||
|
||||
:close-overlay
|
||||
(let [dest-frame-id (:destination interaction)
|
||||
close-click-outside (:close-click-outside interaction)
|
||||
background-overlay (:background-overlay interaction)
|
||||
|
||||
dest-frame (get objects dest-frame-id)
|
||||
position (cti/calc-overlay-position interaction
|
||||
base-frame
|
||||
dest-frame
|
||||
frame-offset)]
|
||||
(when dest-frame-id
|
||||
(st/emit! (dv/open-overlay dest-frame-id
|
||||
position
|
||||
close-click-outside
|
||||
background-overlay))))
|
||||
nil))
|
||||
|
||||
(defn on-mouse-down
|
||||
[event shape base-frame frame-offset objects]
|
||||
(let [interactions (->> (:interactions shape)
|
||||
(filter #(or (= (:event-type %) :click)
|
||||
(= (:event-type %) :mouse-press))))]
|
||||
(when (seq interactions)
|
||||
(dom/stop-propagation event)
|
||||
(doseq [interaction interactions]
|
||||
(activate-interaction interaction shape base-frame frame-offset objects)))))
|
||||
|
||||
(defn on-mouse-up
|
||||
[event shape base-frame frame-offset objects]
|
||||
(let [interactions (->> (:interactions shape)
|
||||
(filter #(= (:event-type %) :mouse-press)))]
|
||||
(when (seq interactions)
|
||||
(dom/stop-propagation event)
|
||||
(doseq [interaction interactions]
|
||||
(deactivate-interaction interaction shape base-frame frame-offset objects)))))
|
||||
|
||||
(defn on-mouse-enter
|
||||
[event shape base-frame frame-offset objects]
|
||||
(let [interactions (->> (:interactions shape)
|
||||
(filter #(or (= (:event-type %) :mouse-enter)
|
||||
(= (:event-type %) :mouse-over))))]
|
||||
(when (seq interactions)
|
||||
(dom/stop-propagation event)
|
||||
(doseq [interaction interactions]
|
||||
(activate-interaction interaction shape base-frame frame-offset objects)))))
|
||||
|
||||
(defn on-mouse-leave
|
||||
[event shape base-frame frame-offset objects]
|
||||
(let [interactions (->> (:interactions shape)
|
||||
(filter #(= (:event-type %) :mouse-leave)))
|
||||
interactions-inv (->> (:interactions shape)
|
||||
(filter #(= (:event-type %) :mouse-over)))]
|
||||
(when (or (seq interactions) (seq interactions-inv))
|
||||
(dom/stop-propagation event)
|
||||
(doseq [interaction interactions]
|
||||
(activate-interaction interaction shape base-frame frame-offset objects))
|
||||
(doseq [interaction interactions-inv]
|
||||
(deactivate-interaction interaction shape base-frame frame-offset objects)))))
|
||||
|
||||
(defn on-load
|
||||
[shape base-frame frame-offset objects]
|
||||
(let [interactions (->> (:interactions shape)
|
||||
(filter #(= (:event-type %) :after-delay)))]
|
||||
(loop [interactions (seq interactions)
|
||||
sems []]
|
||||
(if-let [interaction (first interactions)]
|
||||
(let [sem (tm/schedule (:delay interaction)
|
||||
#(activate-interaction interaction shape base-frame frame-offset objects))]
|
||||
(recur (next interactions)
|
||||
(conj sems sem)))
|
||||
sems))))
|
||||
|
||||
(mf/defc interaction
|
||||
[{:keys [shape interactions show-interactions?]}]
|
||||
[{:keys [shape interactions interactions-show?]}]
|
||||
(let [{:keys [x y width height]} (:selrect shape)
|
||||
frame? (= :frame (:type shape))]
|
||||
(when-not (empty? interactions)
|
||||
|
@ -49,37 +193,44 @@
|
|||
:height (+ height 2)
|
||||
:fill "#31EFB8"
|
||||
:stroke "#31EFB8"
|
||||
:stroke-width (if show-interactions? 1 0)
|
||||
:fill-opacity (if show-interactions? 0.2 0)
|
||||
:stroke-width (if interactions-show? 1 0)
|
||||
:fill-opacity (if interactions-show? 0.2 0)
|
||||
:style {:pointer-events (when frame? "none")}
|
||||
:transform (geom/transform-matrix shape)}])))
|
||||
|
||||
(defn generic-wrapper-factory
|
||||
"Wrap some svg shape and add interaction controls"
|
||||
[component show-interactions?]
|
||||
[component]
|
||||
(mf/fnc generic-wrapper
|
||||
{::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")
|
||||
objects (unchecked-get props "objects")
|
||||
|
||||
interactions (->> (:interactions shape)
|
||||
(filter #(contains? objects (:destination %))))
|
||||
base-frame (mf/use-ctx base-frame-ctx)
|
||||
frame-offset (mf/use-ctx frame-offset-ctx)
|
||||
|
||||
on-mouse-down (mf/use-callback
|
||||
(mf/deps interactions)
|
||||
(fn [event]
|
||||
(on-mouse-down event interactions)))
|
||||
interactions-show? (mf/deref viewer-interactions-show?)
|
||||
|
||||
interactions (:interactions shape)
|
||||
|
||||
svg-element? (and (= :svg-raw (:type shape))
|
||||
(not= :svg (get-in shape [:content :tag])))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [sems (on-load shape base-frame frame-offset objects)]
|
||||
#(run! tm/dispose! sems))))
|
||||
|
||||
(if-not svg-element?
|
||||
[:> shape-container {:shape shape
|
||||
:cursor (when-not (empty? interactions) "pointer")
|
||||
:on-mouse-down on-mouse-down}
|
||||
:cursor (when (cti/actionable? interactions) "pointer")
|
||||
:on-mouse-down #(on-mouse-down % shape base-frame frame-offset objects)
|
||||
:on-mouse-up #(on-mouse-up % shape base-frame frame-offset objects)
|
||||
:on-mouse-enter #(on-mouse-enter % shape base-frame frame-offset objects)
|
||||
:on-mouse-leave #(on-mouse-leave % shape base-frame frame-offset objects)}
|
||||
|
||||
[:& component {:shape shape
|
||||
:frame frame
|
||||
|
@ -88,7 +239,7 @@
|
|||
|
||||
[:& interaction {:shape shape
|
||||
:interactions interactions
|
||||
:show-interactions? show-interactions?}]]
|
||||
:interactions-show? interactions-show?}]]
|
||||
|
||||
;; Don't wrap svg elements inside a <g> otherwise some can break
|
||||
[:& component {:shape shape
|
||||
|
@ -96,60 +247,63 @@
|
|||
:childs childs}]))))
|
||||
|
||||
(defn frame-wrapper
|
||||
[shape-container show-interactions?]
|
||||
(generic-wrapper-factory (frame/frame-shape shape-container) show-interactions?))
|
||||
[shape-container]
|
||||
(generic-wrapper-factory (frame/frame-shape shape-container)))
|
||||
|
||||
(defn group-wrapper
|
||||
[shape-container show-interactions?]
|
||||
(generic-wrapper-factory (group/group-shape shape-container) show-interactions?))
|
||||
[shape-container]
|
||||
(generic-wrapper-factory (group/group-shape shape-container)))
|
||||
|
||||
(defn bool-wrapper
|
||||
[shape-container]
|
||||
(generic-wrapper-factory (bool/bool-shape shape-container)))
|
||||
|
||||
(defn svg-raw-wrapper
|
||||
[shape-container show-interactions?]
|
||||
(generic-wrapper-factory (svg-raw/svg-raw-shape shape-container) show-interactions?))
|
||||
[shape-container]
|
||||
(generic-wrapper-factory (svg-raw/svg-raw-shape shape-container)))
|
||||
|
||||
(defn rect-wrapper
|
||||
[show-interactions?]
|
||||
(generic-wrapper-factory rect/rect-shape show-interactions?))
|
||||
[]
|
||||
(generic-wrapper-factory rect/rect-shape))
|
||||
|
||||
(defn image-wrapper
|
||||
[show-interactions?]
|
||||
(generic-wrapper-factory image/image-shape show-interactions?))
|
||||
[]
|
||||
(generic-wrapper-factory image/image-shape))
|
||||
|
||||
(defn path-wrapper
|
||||
[show-interactions?]
|
||||
(generic-wrapper-factory path/path-shape show-interactions?))
|
||||
[]
|
||||
(generic-wrapper-factory path/path-shape))
|
||||
|
||||
(defn text-wrapper
|
||||
[show-interactions?]
|
||||
(generic-wrapper-factory text/text-shape show-interactions?))
|
||||
[]
|
||||
(generic-wrapper-factory text/text-shape))
|
||||
|
||||
(defn circle-wrapper
|
||||
[show-interactions?]
|
||||
(generic-wrapper-factory circle/circle-shape show-interactions?))
|
||||
[]
|
||||
(generic-wrapper-factory circle/circle-shape))
|
||||
|
||||
(declare shape-container-factory)
|
||||
|
||||
(defn frame-container-factory
|
||||
[objects show-interactions?]
|
||||
(let [shape-container (shape-container-factory objects show-interactions?)
|
||||
frame-wrapper (frame-wrapper shape-container show-interactions?)]
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
frame-wrapper (frame-wrapper shape-container)]
|
||||
(mf/fnc frame-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
childs (mapv #(get objects %) (:shapes shape))
|
||||
shape (geom/transform-shape shape)
|
||||
props (obj/merge! #js {} props
|
||||
#js {:shape shape
|
||||
:childs childs
|
||||
:objects objects
|
||||
:show-interactions? show-interactions?})]
|
||||
(let [shape (obj/get props "shape")
|
||||
childs (mapv #(get objects %) (:shapes shape))
|
||||
shape (geom/transform-shape shape)
|
||||
props (obj/merge! #js {} props
|
||||
#js {:shape shape
|
||||
:childs childs
|
||||
:objects objects})]
|
||||
[:> frame-wrapper props]))))
|
||||
|
||||
(defn group-container-factory
|
||||
[objects show-interactions?]
|
||||
(let [shape-container (shape-container-factory objects show-interactions?)
|
||||
group-wrapper (group-wrapper shape-container show-interactions?)]
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
group-wrapper (group-wrapper shape-container)]
|
||||
(mf/fnc group-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
@ -157,14 +311,27 @@
|
|||
childs (mapv #(get objects %) (:shapes shape))
|
||||
props (obj/merge! #js {} props
|
||||
#js {:childs childs
|
||||
:objects objects
|
||||
:show-interactions? show-interactions?})]
|
||||
:objects objects})]
|
||||
[:> group-wrapper props]))))
|
||||
|
||||
(defn bool-container-factory
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
bool-wrapper (bool-wrapper shape-container)]
|
||||
(mf/fnc bool-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (select-keys objects (cp/get-children (:id shape) objects))
|
||||
props (obj/merge! #js {} props
|
||||
#js {:childs childs
|
||||
:objects objects})]
|
||||
[:> bool-wrapper props]))))
|
||||
|
||||
(defn svg-raw-container-factory
|
||||
[objects show-interactions?]
|
||||
(let [shape-container (shape-container-factory objects show-interactions?)
|
||||
svg-raw-wrapper (svg-raw-wrapper shape-container show-interactions?)]
|
||||
[objects]
|
||||
(let [shape-container (shape-container-factory objects)
|
||||
svg-raw-wrapper (svg-raw-wrapper shape-container)]
|
||||
(mf/fnc svg-raw-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
@ -172,26 +339,30 @@
|
|||
childs (mapv #(get objects %) (:shapes shape))
|
||||
props (obj/merge! #js {} props
|
||||
#js {:childs childs
|
||||
:objects objects
|
||||
:show-interactions? show-interactions?})]
|
||||
:objects objects})]
|
||||
[:> svg-raw-wrapper props]))))
|
||||
|
||||
(defn shape-container-factory
|
||||
[objects show-interactions?]
|
||||
(let [path-wrapper (path-wrapper show-interactions?)
|
||||
text-wrapper (text-wrapper show-interactions?)
|
||||
rect-wrapper (rect-wrapper show-interactions?)
|
||||
image-wrapper (image-wrapper show-interactions?)
|
||||
circle-wrapper (circle-wrapper show-interactions?)]
|
||||
[objects]
|
||||
(let [path-wrapper (path-wrapper)
|
||||
text-wrapper (text-wrapper)
|
||||
rect-wrapper (rect-wrapper)
|
||||
image-wrapper (image-wrapper)
|
||||
circle-wrapper (circle-wrapper)]
|
||||
(mf/fnc shape-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [group-container (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(group-container-factory objects show-interactions?))
|
||||
svg-raw-container (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(svg-raw-container-factory objects show-interactions?))
|
||||
(let [group-container
|
||||
(mf/use-memo (mf/deps objects)
|
||||
#(group-container-factory objects))
|
||||
|
||||
bool-container
|
||||
(mf/use-memo (mf/deps objects)
|
||||
#(bool-container-factory objects))
|
||||
|
||||
svg-raw-container
|
||||
(mf/use-memo (mf/deps objects)
|
||||
#(svg-raw-container-factory objects))
|
||||
shape (unchecked-get props "shape")
|
||||
frame (unchecked-get props "frame")]
|
||||
(when (and shape (not (:hidden shape)))
|
||||
|
@ -207,11 +378,12 @@
|
|||
:image [:> image-wrapper opts]
|
||||
:circle [:> circle-wrapper opts]
|
||||
:group [:> group-container {:shape shape :frame frame :objects objects}]
|
||||
:bool [:> bool-container {:shape shape :frame frame :objects objects}]
|
||||
:svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}])))))))
|
||||
|
||||
(mf/defc frame-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects frame zoom show-interactions?] :or {zoom 1} :as props}]
|
||||
[{:keys [objects frame zoom] :or {zoom 1} :as props}]
|
||||
(let [modifier (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
@ -229,7 +401,7 @@
|
|||
" " (:height frame 0))
|
||||
wrapper (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(frame-container-factory objects show-interactions?))]
|
||||
#(frame-container-factory objects))]
|
||||
|
||||
[:svg {:view-box vbox
|
||||
:width width
|
||||
|
@ -238,6 +410,5 @@
|
|||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
[:& wrapper {:shape frame
|
||||
:show-interactions? show-interactions?
|
||||
:view-box vbox}]]))
|
||||
|
||||
|
|
|
@ -121,7 +121,6 @@
|
|||
(fn []
|
||||
(st/emit! (dw/setup-layout layout-name))))
|
||||
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps project-id file-id)
|
||||
(fn []
|
||||
|
|
|
@ -36,9 +36,6 @@
|
|||
(def picked-color-select
|
||||
(l/derived :picked-color-select refs/workspace-local))
|
||||
|
||||
(def picked-shift?
|
||||
(l/derived :picked-shift? refs/workspace-local))
|
||||
|
||||
(def viewport
|
||||
(l/derived (l/in [:workspace-local :vport]) st/state))
|
||||
|
||||
|
@ -202,10 +199,10 @@
|
|||
h
|
||||
(str (* s 100) "%")
|
||||
(str (* l 100) "%")))]
|
||||
(dom/set-css-property node "--color" (str/join ", " rgb))
|
||||
(dom/set-css-property node "--hue-rgb" (str/join ", " hue-rgb))
|
||||
(dom/set-css-property node "--saturation-grad-from" (format-hsl hsl-from))
|
||||
(dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to)))))
|
||||
(dom/set-css-property! node "--color" (str/join ", " rgb))
|
||||
(dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb))
|
||||
(dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from))
|
||||
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
|
||||
|
||||
;; When closing the modal we update the recent-color list
|
||||
(mf/use-effect
|
||||
|
|
|
@ -27,15 +27,27 @@
|
|||
:v (mf/use-ref nil)
|
||||
:alpha (mf/use-ref nil)}
|
||||
|
||||
setup-hex-color
|
||||
(fn [hex]
|
||||
(let [[r g b] (uc/hex->rgb hex)
|
||||
[h s v] (uc/hex->hsv hex)]
|
||||
(on-change {:hex hex
|
||||
:h h :s s :v v
|
||||
:r r :g g :b b})))
|
||||
on-change-hex
|
||||
(fn [e]
|
||||
(let [val (-> e dom/get-target-val parse-hex)]
|
||||
(when (uc/hex? val)
|
||||
(let [[r g b] (uc/hex->rgb val)
|
||||
[h s v] (uc/hex->hsv hex)]
|
||||
(on-change {:hex val
|
||||
:h h :s s :v v
|
||||
:r r :g g :b b})))))
|
||||
(setup-hex-color val))))
|
||||
|
||||
on-blur-hex
|
||||
(fn [e]
|
||||
(let [val (-> e dom/get-target-val)
|
||||
val (cond
|
||||
(uc/color? val) (uc/parse-color val)
|
||||
(uc/hex? (parse-hex val)) (parse-hex val))]
|
||||
(when (some? val)
|
||||
(setup-hex-color val))))
|
||||
|
||||
on-change-property
|
||||
(fn [property max-value]
|
||||
|
@ -81,9 +93,10 @@
|
|||
[:div.color-values
|
||||
{:class (when disable-opacity "disable-opacity")}
|
||||
[:input {:id "hex-value"
|
||||
:ref (:hex refs)
|
||||
:default-value hex
|
||||
:on-change on-change-hex}]
|
||||
:ref (:hex refs)
|
||||
:default-value hex
|
||||
:on-change on-change-hex
|
||||
:on-blur on-blur-hex}]
|
||||
|
||||
(if (= type :rgb)
|
||||
[:*
|
||||
|
|
|
@ -115,7 +115,8 @@
|
|||
:on-mouse-down #(reset! dragging? true)
|
||||
:on-mouse-up #(reset! dragging? false)
|
||||
:on-pointer-down (partial dom/capture-pointer)
|
||||
:on-pointer-up (partial dom/release-pointer)
|
||||
:on-lost-pointer-capture #(do (dom/release-pointer %)
|
||||
(reset! dragging? false))
|
||||
:on-click calculate-pos
|
||||
:on-mouse-move #(when @dragging? (calculate-pos %))}]
|
||||
[:div.handler {:style {:pointer-events "none"
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
{:on-mouse-down #(reset! dragging? true)
|
||||
:on-mouse-up #(reset! dragging? false)
|
||||
:on-pointer-down (partial dom/capture-pointer)
|
||||
:on-pointer-up (partial dom/release-pointer)
|
||||
:on-lost-pointer-capture #(do (dom/release-pointer %)
|
||||
(reset! dragging? false))
|
||||
:on-click calculate-pos
|
||||
:on-mouse-move #(when @dragging? (calculate-pos %))}
|
||||
[:div.handler {:style {:pointer-events "none"
|
||||
|
|
|
@ -35,7 +35,8 @@
|
|||
:on-mouse-down #(reset! dragging? true)
|
||||
:on-mouse-up #(reset! dragging? false)
|
||||
:on-pointer-down (partial dom/capture-pointer)
|
||||
:on-pointer-up (partial dom/release-pointer)
|
||||
:on-lost-pointer-capture #(do (dom/release-pointer %)
|
||||
(reset! dragging? false))
|
||||
:on-click calculate-pos
|
||||
:on-mouse-move #(when @dragging? (calculate-pos %))}
|
||||
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
(ns app.main.ui.workspace.context-menu
|
||||
"A workspace specific context menu (mouse right click)."
|
||||
(:require
|
||||
[app.common.types.page-options :as cto]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
|
@ -16,6 +18,7 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr] :as i18n]
|
||||
[app.util.timers :as timers]
|
||||
|
@ -31,10 +34,52 @@
|
|||
(dom/stop-propagation event))
|
||||
|
||||
(mf/defc menu-entry
|
||||
[{:keys [title shortcut on-click] :as props}]
|
||||
[:li {:on-click on-click}
|
||||
[:span.title title]
|
||||
[:span.shortcut (or shortcut "")]])
|
||||
[{:keys [title shortcut on-click children] :as props}]
|
||||
(let [submenu-ref (mf/use-ref nil)
|
||||
hovering? (mf/use-ref false)
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(mf/set-ref-val! hovering? true)
|
||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(when (some? submenu-node)
|
||||
(dom/set-css-property! submenu-node "display" "block")))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(mf/set-ref-val! hovering? false)
|
||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(when (some? submenu-node)
|
||||
(timers/schedule
|
||||
200
|
||||
#(when-not (mf/ref-val hovering?)
|
||||
(dom/set-css-property! submenu-node "display" "none")))))))
|
||||
|
||||
set-dom-node
|
||||
(mf/use-callback
|
||||
(fn [dom]
|
||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(when (and (some? dom) (some? submenu-node))
|
||||
(dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))]
|
||||
|
||||
[:li {:ref set-dom-node
|
||||
:on-click on-click
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave}
|
||||
[:span.title title]
|
||||
[:span.shortcut (or shortcut "")]
|
||||
|
||||
(when (> (count children) 1)
|
||||
[:span.submenu-icon i/arrow-slide])
|
||||
|
||||
(when (> (count children) 1)
|
||||
[:ul.workspace-context-menu
|
||||
{:ref submenu-ref
|
||||
:style {:display "none" :left 250}
|
||||
:on-context-menu prevent-default}
|
||||
children])]))
|
||||
|
||||
(mf/defc menu-separator
|
||||
[]
|
||||
|
@ -42,16 +87,36 @@
|
|||
|
||||
(mf/defc shape-context-menu
|
||||
[{:keys [mdata] :as props}]
|
||||
(let [{:keys [id] :as shape} (:shape mdata)
|
||||
selected (:selected mdata)
|
||||
(let [{:keys [shape selected disable-booleans? disable-flatten?]} mdata
|
||||
{:keys [id type]} shape
|
||||
|
||||
single? (= (count selected) 1)
|
||||
multiple? (> (count selected) 1)
|
||||
editable-shape? (#{:group :text :path} (:type shape))
|
||||
editable-shape? (#{:group :text :path} type)
|
||||
|
||||
is-group? (and (some? shape) (= :group type))
|
||||
is-bool? (and (some? shape) (= :bool type))
|
||||
|
||||
options (mf/deref refs/workspace-page-options)
|
||||
flows (:flows options)
|
||||
|
||||
options-mode (mf/deref refs/options-mode)
|
||||
|
||||
set-bool
|
||||
(fn [bool-type]
|
||||
#(cond
|
||||
(> (count selected) 1)
|
||||
(st/emit! (dw/create-bool bool-type))
|
||||
|
||||
(and (= (count selected) 1) is-group?)
|
||||
(st/emit! (dw/group-to-bool (:id shape) bool-type))
|
||||
|
||||
(and (= (count selected) 1) is-bool?)
|
||||
(st/emit! (dw/change-bool-type (:id shape) bool-type))))
|
||||
|
||||
current-file-id (mf/use-ctx ctx/current-file-id)
|
||||
|
||||
do-duplicate (st/emitf dw/duplicate-selected)
|
||||
do-duplicate (st/emitf (dw/duplicate-selected false))
|
||||
do-delete (st/emitf dw/delete-selected)
|
||||
do-copy (st/emitf (dw/copy-selected))
|
||||
do-cut (st/emitf (dw/copy-selected) dw/delete-selected)
|
||||
|
@ -64,6 +129,8 @@
|
|||
do-hide-shape (st/emitf (dw/update-shape-flags id {:hidden true}))
|
||||
do-lock-shape (st/emitf (dw/update-shape-flags id {:blocked true}))
|
||||
do-unlock-shape (st/emitf (dw/update-shape-flags id {:blocked false}))
|
||||
do-add-flow (st/emitf (dwi/add-flow-selected-frame))
|
||||
do-remove-flow #(st/emitf (dwi/remove-flow (:id %)))
|
||||
do-create-group (st/emitf dw/group-selected)
|
||||
do-remove-group (st/emitf dw/ungroup-selected)
|
||||
do-mask-group (st/emitf dw/mask-group)
|
||||
|
@ -98,7 +165,10 @@
|
|||
:on-accept confirm-update-remote-component}))
|
||||
do-show-component (st/emitf (dw/go-to-layout :assets))
|
||||
do-navigate-component-file (st/emitf (dwl/nav-to-component-file
|
||||
(:component-file shape)))]
|
||||
(:component-file shape)))
|
||||
|
||||
do-transform-to-path (st/emitf (dw/convert-selected-to-path))
|
||||
do-flatten (st/emitf (dw/convert-selected-to-path))]
|
||||
[:*
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.copy")
|
||||
:shortcut (sc/get-tooltip :copy)
|
||||
|
@ -147,7 +217,7 @@
|
|||
:on-click do-flip-horizontal}]
|
||||
[:& menu-separator]])
|
||||
|
||||
(when (and single? (= (:type shape) :group))
|
||||
(when (and single? (or is-bool? is-group?))
|
||||
[:*
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.ungroup")
|
||||
:shortcut (sc/get-tooltip :ungroup)
|
||||
|
@ -165,6 +235,32 @@
|
|||
:shortcut (sc/get-tooltip :start-editing)
|
||||
:on-click do-start-editing}])
|
||||
|
||||
(when-not disable-flatten?
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.transform-to-path")
|
||||
:on-click do-transform-to-path}])
|
||||
|
||||
(when (and (not disable-booleans?)
|
||||
(or multiple? (and single? (or is-group? is-bool?))))
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.path")}
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.union")
|
||||
:shortcut (sc/get-tooltip :boolean-union)
|
||||
:on-click (set-bool :union)}]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.difference")
|
||||
:shortcut (sc/get-tooltip :boolean-difference)
|
||||
:on-click (set-bool :difference)}]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.intersection")
|
||||
:shortcut (sc/get-tooltip :boolean-intersection)
|
||||
:on-click (set-bool :intersection)}]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.exclude")
|
||||
:shortcut (sc/get-tooltip :boolean-exclude)
|
||||
:on-click (set-bool :exclude)}]
|
||||
|
||||
(when (and single? is-bool? (not disable-flatten?))
|
||||
[:*
|
||||
[:& menu-separator]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.flatten")
|
||||
:on-click do-flatten}]])])
|
||||
|
||||
(if (:hidden shape)
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.show")
|
||||
:on-click do-show-shape}]
|
||||
|
@ -177,9 +273,15 @@
|
|||
[:& menu-entry {:title (tr "workspace.shape.menu.lock")
|
||||
:on-click do-lock-shape}])
|
||||
|
||||
(when (and (or (nil? (:shape-ref shape))
|
||||
(> (count selected) 1))
|
||||
(not= (:type shape) :frame))
|
||||
(when (and (= options-mode :prototype) (= (:type shape) :frame))
|
||||
(let [flow (cto/get-frame-flow flows (:id shape))]
|
||||
(if (nil? flow)
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.flow-start")
|
||||
:on-click do-add-flow}]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.delete-flow-start")
|
||||
:on-click (do-remove-flow flow)}])))
|
||||
|
||||
(when (not= (:type shape) :frame)
|
||||
[:*
|
||||
[:& menu-separator]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.create-component")
|
||||
|
@ -240,7 +342,7 @@
|
|||
(when dropdown
|
||||
(let [bounding-rect (dom/get-bounding-rect dropdown)
|
||||
window-size (dom/get-window-size)
|
||||
delta-x (max (- (:right bounding-rect) (:width window-size)) 0)
|
||||
delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0)
|
||||
delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0)
|
||||
new-style (str "top: " (- top delta-y) "px; "
|
||||
"left: " (- left delta-x) "px;")]
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
[app.main.ui.shapes.image :as image]
|
||||
[app.main.ui.shapes.rect :as rect]
|
||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||
[app.main.ui.workspace.shapes.bool :as bool]
|
||||
[app.main.ui.workspace.shapes.bounding-box :refer [bounding-box]]
|
||||
[app.main.ui.workspace.shapes.common :as common]
|
||||
[app.main.ui.workspace.shapes.frame :as frame]
|
||||
|
@ -35,6 +36,7 @@
|
|||
(declare shape-wrapper)
|
||||
(declare group-wrapper)
|
||||
(declare svg-raw-wrapper)
|
||||
(declare bool-wrapper)
|
||||
(declare frame-wrapper)
|
||||
|
||||
(def circle-wrapper (common/generic-wrapper-factory circle/circle-shape))
|
||||
|
@ -92,13 +94,14 @@
|
|||
[:*
|
||||
(if-not svg-element?
|
||||
(case (:type shape)
|
||||
:path [:> path/path-wrapper opts]
|
||||
:text [:> text/text-wrapper opts]
|
||||
:group [:> group-wrapper opts]
|
||||
:rect [:> rect-wrapper opts]
|
||||
:image [:> image-wrapper opts]
|
||||
:circle [:> circle-wrapper opts]
|
||||
:path [:> path/path-wrapper opts]
|
||||
:text [:> text/text-wrapper opts]
|
||||
:group [:> group-wrapper opts]
|
||||
:rect [:> rect-wrapper opts]
|
||||
:image [:> image-wrapper opts]
|
||||
:circle [:> circle-wrapper opts]
|
||||
:svg-raw [:> svg-raw-wrapper opts]
|
||||
:bool [:> bool-wrapper opts]
|
||||
|
||||
;; Only used when drawing a new frame.
|
||||
:frame [:> frame-wrapper {:shape shape}]
|
||||
|
@ -113,5 +116,6 @@
|
|||
|
||||
(def group-wrapper (group/group-wrapper-factory shape-wrapper))
|
||||
(def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper))
|
||||
(def bool-wrapper (bool/bool-wrapper-factory shape-wrapper))
|
||||
(def frame-wrapper (frame/frame-wrapper-factory shape-wrapper))
|
||||
|
||||
|
|
47
frontend/src/app/main/ui/workspace/shapes/bool.cljs
Normal file
47
frontend/src/app/main/ui/workspace/shapes/bool.cljs
Normal file
|
@ -0,0 +1,47 @@
|
|||
;; 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.workspace.shapes.bool
|
||||
(:require
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.shapes.bool :as bool]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.util.dom :as dom]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn use-double-click [{:keys [id]}]
|
||||
(mf/use-callback
|
||||
(mf/deps id)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (dw/select-inside-group id @ms/mouse-position)))))
|
||||
|
||||
(defn bool-wrapper-factory
|
||||
[shape-wrapper]
|
||||
(let [shape-component (bool/bool-shape shape-wrapper)]
|
||||
(mf/fnc bool-wrapper
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
frame (unchecked-get props "frame")
|
||||
|
||||
childs-ref (mf/use-memo
|
||||
(mf/deps (:id shape))
|
||||
#(refs/select-children (:id shape)))
|
||||
|
||||
childs (mf/deref childs-ref)]
|
||||
|
||||
[:> shape-container {:shape shape}
|
||||
[:& shape-component
|
||||
{:frame frame
|
||||
:shape shape
|
||||
:childs childs}]]))))
|
||||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
(ns app.main.ui.workspace.shapes.path
|
||||
(:require
|
||||
[app.common.path.commands :as upc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.shapes.path :as path]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.workspace.shapes.path.common :as pc]
|
||||
[app.util.path.commands :as upc]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc path-wrapper
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as gshp]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.path.commands :as upc]
|
||||
[app.common.path.shapes-to-path :as ups]
|
||||
[app.main.data.workspace.path :as drp]
|
||||
[app.main.snap :as snap]
|
||||
[app.main.store :as st]
|
||||
|
@ -18,10 +20,7 @@
|
|||
[app.main.ui.workspace.shapes.path.common :as pc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.path.commands :as upc]
|
||||
[app.util.path.format :as upf]
|
||||
[app.util.path.geom :as upg]
|
||||
[app.util.path.shapes-to-path :as ups]
|
||||
[clojure.set :refer [map-invert]]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
|
@ -217,16 +216,16 @@
|
|||
|
||||
shape (cond-> shape
|
||||
(not= :path (:type shape))
|
||||
ups/convert-to-path
|
||||
(ups/convert-to-path {})
|
||||
|
||||
:always
|
||||
hooks/use-equal-memo)
|
||||
|
||||
base-content (:content shape)
|
||||
base-points (mf/use-memo (mf/deps base-content) #(->> base-content upg/content->points))
|
||||
base-points (mf/use-memo (mf/deps base-content) #(->> base-content gsp/content->points))
|
||||
|
||||
content (upc/apply-content-modifiers base-content content-modifiers)
|
||||
content-points (mf/use-memo (mf/deps content) #(->> content upg/content->points))
|
||||
content-points (mf/use-memo (mf/deps content) #(->> content gsp/content->points))
|
||||
|
||||
point->base (->> (map hash-map content-points base-points) (reduce merge))
|
||||
base->point (map-invert point->base)
|
||||
|
@ -269,10 +268,14 @@
|
|||
ms/mouse-position
|
||||
(mf/deps shape zoom)
|
||||
(fn [position]
|
||||
(when-let [point (gshp/path-closest-point shape position)]
|
||||
(when-let [point (gsp/path-closest-point shape position)]
|
||||
(reset! hover-point (when (< (gpt/distance position point) (/ 10 zoom)) point)))))
|
||||
|
||||
[:g.path-editor {:ref editor-ref}
|
||||
[:path {:d (upf/format-path content)
|
||||
:style {:fill "none"
|
||||
:stroke pc/primary-color
|
||||
:strokeWidth (/ 1 zoom)}}]
|
||||
(when (and preview (not drag-handler))
|
||||
[:& path-preview {:command preview
|
||||
:from last-p
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.workspace.shapes.text
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
|
@ -13,7 +14,6 @@
|
|||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.logging :as log]
|
||||
[app.util.object :as obj]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.timers :as timers]
|
||||
|
|
|
@ -174,7 +174,7 @@
|
|||
handle-return
|
||||
(mf/use-callback
|
||||
(fn [_ state]
|
||||
(let [style (ted/get-editor-current-inline-styles state)
|
||||
(let [style (ted/get-editor-current-block-data state)
|
||||
state (-> (ted/insert-text state "\n" style)
|
||||
(handle-change))]
|
||||
(st/emit! (dwt/update-editor-state shape state)))
|
||||
|
|
|
@ -230,7 +230,7 @@
|
|||
|
||||
on-fold-group
|
||||
(mf/use-callback
|
||||
(mf/deps group-open?)
|
||||
(mf/deps file-id box path group-open?)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dwl/set-assets-group-open file-id
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as ts]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
@ -39,6 +40,11 @@
|
|||
(if (:masked-group? shape)
|
||||
i/mask
|
||||
i/folder))
|
||||
:bool (case (:bool-type shape)
|
||||
:difference i/boolean-difference
|
||||
:exclude i/boolean-exclude
|
||||
:intersection i/boolean-intersection
|
||||
#_:default i/boolean-union)
|
||||
:svg-raw i/file-svg
|
||||
nil))
|
||||
|
||||
|
@ -63,7 +69,8 @@
|
|||
(on-stop-edit)
|
||||
(swap! local assoc :edition false)
|
||||
(st/emit! (dw/end-rename-shape)
|
||||
(dw/update-shape (:id shape) {:name name}))))
|
||||
(when-not (str/empty? name)
|
||||
(dw/update-shape (:id shape) {:name name})))))
|
||||
|
||||
cancel-edit (fn []
|
||||
(on-stop-edit)
|
||||
|
@ -292,7 +299,8 @@
|
|||
:shape-ref
|
||||
:touched
|
||||
:metadata
|
||||
:masked-group?]))
|
||||
:masked-group?
|
||||
:bool-type]))
|
||||
|
||||
(defn- strip-objects
|
||||
[objects]
|
||||
|
|
|
@ -11,10 +11,12 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.workspace.sidebar.align :refer [align-options]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.booleans :refer [booleans-options]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.page :as page]
|
||||
[app.main.ui.workspace.sidebar.options.shapes.bool :as bool]
|
||||
[app.main.ui.workspace.sidebar.options.shapes.circle :as circle]
|
||||
[app.main.ui.workspace.sidebar.options.shapes.frame :as frame]
|
||||
[app.main.ui.workspace.sidebar.options.shapes.group :as group]
|
||||
|
@ -43,6 +45,7 @@
|
|||
:path [:& path/options {:shape shape}]
|
||||
:image [:& image/options {:shape shape}]
|
||||
:svg-raw [:& svg-raw/options {:shape shape}]
|
||||
:bool [:& bool/options {:shape shape}]
|
||||
nil)
|
||||
[:& exports-menu
|
||||
{:shape shape
|
||||
|
@ -60,6 +63,7 @@
|
|||
:title (tr "workspace.options.design")}
|
||||
[:div.element-options
|
||||
[:& align-options]
|
||||
[:& booleans-options]
|
||||
(case (count selected)
|
||||
0 [:& page/options]
|
||||
1 [:& shape-options {:shape (first shapes)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.sidebar.align
|
||||
(ns app.main.ui.workspace.sidebar.options.menus.align
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace :as dw]
|
|
@ -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.workspace.sidebar.options.menus.booleans
|
||||
(:require
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc booleans-options
|
||||
[]
|
||||
(let [selected (mf/deref refs/selected-objects)
|
||||
selected-with-children (mf/deref refs/selected-objects-with-children)
|
||||
|
||||
has-invalid-shapes? (->> selected-with-children
|
||||
(some (comp #{:frame :text} :type)))
|
||||
|
||||
first-not-group-like?
|
||||
(and (= (count selected) 1)
|
||||
(not (contains? #{:group :bool} (:type (first selected)))))
|
||||
|
||||
disabled-bool-btns (or (empty? selected) has-invalid-shapes? first-not-group-like?)
|
||||
disabled-flatten (or (empty? selected) has-invalid-shapes?)
|
||||
|
||||
head (first selected)
|
||||
is-group? (and (some? head) (= :group (:type head)))
|
||||
is-bool? (and (some? head) (= :bool (:type head)))
|
||||
head-bool-type (and (some? head) is-bool? (:bool-type head))
|
||||
|
||||
set-bool
|
||||
(fn [bool-type]
|
||||
#(cond
|
||||
(> (count selected) 1)
|
||||
(st/emit! (dw/create-bool bool-type))
|
||||
|
||||
(and (= (count selected) 1) is-group?)
|
||||
(st/emit! (dw/group-to-bool (:id head) bool-type))
|
||||
|
||||
(and (= (count selected) 1) is-bool?)
|
||||
(if (= head-bool-type bool-type)
|
||||
(st/emit! (dw/bool-to-group (:id head)))
|
||||
(st/emit! (dw/change-bool-type (:id head) bool-type)))))]
|
||||
|
||||
[:div.align-options
|
||||
[:div.align-group
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.union")
|
||||
:class (dom/classnames :disabled disabled-bool-btns
|
||||
:selected (= head-bool-type :union))
|
||||
:on-click (set-bool :union)}
|
||||
i/boolean-union]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.difference")
|
||||
:class (dom/classnames :disabled disabled-bool-btns
|
||||
:selected (= head-bool-type :difference))
|
||||
:on-click (set-bool :difference)}
|
||||
i/boolean-difference]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.intersection")
|
||||
:class (dom/classnames :disabled disabled-bool-btns
|
||||
:selected (= head-bool-type :intersection))
|
||||
:on-click (set-bool :intersection)}
|
||||
i/boolean-intersection]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.exclude")
|
||||
:class (dom/classnames :disabled disabled-bool-btns
|
||||
:selected (= head-bool-type :exclude))
|
||||
:on-click (set-bool :exclude)}
|
||||
i/boolean-exclude]]
|
||||
|
||||
[:div.align-group
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.flatten")
|
||||
:class (dom/classnames :disabled disabled-flatten)
|
||||
:on-click (st/emitf (dw/convert-selected-to-path))}
|
||||
i/boolean-flatten]]]))
|
||||
|
|
@ -8,70 +8,396 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.types.interactions :as cti]
|
||||
[app.common.types.page-options :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc interactions-menu
|
||||
[{:keys [shape] :as props}]
|
||||
(let [objects (deref refs/workspace-page-objects)
|
||||
interaction (first (:interactions shape)) ; TODO: in the
|
||||
; future we may
|
||||
; have several
|
||||
; interactions in
|
||||
; one shape
|
||||
(defn- event-type-names
|
||||
[]
|
||||
{:click (tr "workspace.options.interaction-on-click")
|
||||
; TODO: need more UX research
|
||||
;; :mouse-over (tr "workspace.options.interaction-while-hovering")
|
||||
;; :mouse-press (tr "workspace.options.interaction-while-pressing")
|
||||
:mouse-enter (tr "workspace.options.interaction-mouse-enter")
|
||||
:mouse-leave (tr "workspace.options.interaction-mouse-leave")
|
||||
:after-delay (tr "workspace.options.interaction-after-delay")})
|
||||
|
||||
(defn- event-type-name
|
||||
[interaction]
|
||||
(get (event-type-names) (:event-type interaction) "--"))
|
||||
|
||||
(defn- action-type-names
|
||||
[]
|
||||
{:navigate (tr "workspace.options.interaction-navigate-to")
|
||||
:open-overlay (tr "workspace.options.interaction-open-overlay")
|
||||
:toggle-overlay (tr "workspace.options.interaction-toggle-overlay")
|
||||
:close-overlay (tr "workspace.options.interaction-close-overlay")
|
||||
:prev-screen (tr "workspace.options.interaction-prev-screen")
|
||||
:open-url (tr "workspace.options.interaction-open-url")})
|
||||
|
||||
(defn- action-summary
|
||||
[interaction destination]
|
||||
(case (:action-type interaction)
|
||||
:navigate (tr "workspace.options.interaction-navigate-to-dest"
|
||||
(get destination :name (tr "workspace.options.interaction-none")))
|
||||
:open-overlay (tr "workspace.options.interaction-open-overlay-dest"
|
||||
(get destination :name (tr "workspace.options.interaction-none")))
|
||||
:toggle-overlay (tr "workspace.options.interaction-toggle-overlay-dest"
|
||||
(get destination :name (tr "workspace.options.interaction-none")))
|
||||
:close-overlay (tr "workspace.options.interaction-close-overlay-dest"
|
||||
(get destination :name (tr "workspace.options.interaction-self")))
|
||||
:prev-screen (tr "workspace.options.interaction-prev-screen")
|
||||
:open-url (tr "workspace.options.interaction-open-url")
|
||||
"--"))
|
||||
|
||||
(defn- overlay-pos-type-names
|
||||
[]
|
||||
{:manual (tr "workspace.options.interaction-pos-manual")
|
||||
:center (tr "workspace.options.interaction-pos-center")
|
||||
:top-left (tr "workspace.options.interaction-pos-top-left")
|
||||
:top-right (tr "workspace.options.interaction-pos-top-right")
|
||||
:top-center (tr "workspace.options.interaction-pos-top-center")
|
||||
:bottom-left (tr "workspace.options.interaction-pos-bottom-left")
|
||||
:bottom-right (tr "workspace.options.interaction-pos-bottom-right")
|
||||
:bottom-center (tr "workspace.options.interaction-pos-bottom-center")})
|
||||
|
||||
(def flow-for-rename-ref
|
||||
(l/derived (l/in [:workspace-local :flow-for-rename]) st/state))
|
||||
|
||||
(mf/defc flow-item
|
||||
[{:keys [flow]}]
|
||||
(let [editing? (mf/use-state false)
|
||||
flow-for-rename (mf/deref flow-for-rename-ref)
|
||||
name-ref (mf/use-ref)
|
||||
|
||||
start-edit (fn []
|
||||
(reset! editing? true))
|
||||
|
||||
accept-edit (fn []
|
||||
(let [name-input (mf/ref-val name-ref)
|
||||
name (dom/get-value name-input)]
|
||||
(reset! editing? false)
|
||||
(st/emit! (dwi/end-rename-flow)
|
||||
(when-not (str/empty? name)
|
||||
(dwi/rename-flow (:id flow) name)))))
|
||||
|
||||
cancel-edit (fn []
|
||||
(reset! editing? false)
|
||||
(st/emit! (dwi/end-rename-flow)))
|
||||
|
||||
on-key-down (fn [event]
|
||||
(when (kbd/enter? event) (accept-edit))
|
||||
(when (kbd/esc? event) (cancel-edit)))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
#(when editing?
|
||||
(cancel-edit))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps flow-for-rename)
|
||||
#(when (and (= flow-for-rename (:id flow))
|
||||
(not @editing?))
|
||||
(start-edit)))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @editing?)
|
||||
#(when @editing?
|
||||
(let [name-input (mf/ref-val name-ref)]
|
||||
(dom/select-text! name-input))
|
||||
nil))
|
||||
|
||||
[:div.flow-element
|
||||
[:div.flow-button {:on-click (st/emitf (dw/select-shape (:starting-frame flow)))}
|
||||
i/play]
|
||||
(if @editing?
|
||||
[:input.element-name
|
||||
{:type "text"
|
||||
:ref name-ref
|
||||
:on-blur accept-edit
|
||||
:on-key-down on-key-down
|
||||
:auto-focus true
|
||||
:default-value (:name flow "")}]
|
||||
[:span.element-label.flow-name
|
||||
{:on-double-click (st/emitf (dwi/start-rename-flow (:id flow)))}
|
||||
(:name flow)])
|
||||
[:div.add-page {:on-click (st/emitf (dwi/remove-flow (:id flow)))}
|
||||
i/minus]]))
|
||||
|
||||
(mf/defc page-flows
|
||||
[{:keys [flows]}]
|
||||
(when (seq flows)
|
||||
[:div.element-set.interactions-options
|
||||
[:div.element-set-title
|
||||
[:span (tr "workspace.options.flows.flow-starts")]]
|
||||
(for [flow flows]
|
||||
[:& flow-item {:flow flow :key (str (:id flow))}])]))
|
||||
|
||||
(mf/defc shape-flows
|
||||
[{:keys [flows shape]}]
|
||||
(when (= (:type shape) :frame)
|
||||
(let [flow (cto/get-frame-flow flows (:id shape))]
|
||||
[:div.element-set.interactions-options
|
||||
[:div.element-set-title
|
||||
[:span (tr "workspace.options.flows.flow-start")]]
|
||||
(if (nil? flow)
|
||||
[:div.flow-element
|
||||
[:span.element-label (tr "workspace.options.flows.add-flow-start")]
|
||||
[:div.add-page {:on-click (st/emitf (dwi/add-flow-selected-frame))}
|
||||
i/plus]]
|
||||
[:& flow-item {:flow flow :key (str (:id flow))}])])))
|
||||
|
||||
(mf/defc interaction-entry
|
||||
[{:keys [index shape interaction update-interaction remove-interaction]}]
|
||||
(let [objects (deref refs/workspace-page-objects)
|
||||
destination (get objects (:destination interaction))
|
||||
frames (mf/use-memo (mf/deps objects)
|
||||
#(cp/select-frames objects))
|
||||
|
||||
show-frames-dropdown? (mf/use-state false)
|
||||
overlay-pos-type (:overlay-pos-type interaction)
|
||||
close-click-outside? (:close-click-outside interaction false)
|
||||
background-overlay? (:background-overlay interaction false)
|
||||
|
||||
on-set-blur #(reset! show-frames-dropdown? false)
|
||||
on-navigate #(when destination
|
||||
(st/emit! (dw/select-shapes (d/ordered-set (:id destination)))))
|
||||
extended-open? (mf/use-state false)
|
||||
|
||||
on-select-destination
|
||||
(fn [dest]
|
||||
(if (nil? dest)
|
||||
(st/emit! (dw/update-shape (:id shape) {:interactions []}))
|
||||
(st/emit! (dw/update-shape (:id shape) {:interactions [{:event-type :click
|
||||
:action-type :navigate
|
||||
:destination dest}]}))))]
|
||||
ext-delay-ref (mf/use-ref nil)
|
||||
|
||||
(if (not shape)
|
||||
[:*
|
||||
[:div.interactions-help-icon i/interaction]
|
||||
[:div.interactions-help (tr "workspace.options.select-a-shape")]
|
||||
[:div.interactions-help-icon i/play]
|
||||
[:div.interactions-help (tr "workspace.options.use-play-button")]]
|
||||
select-text
|
||||
(fn [ref] (fn [_] (dom/select-text! (mf/ref-val ref))))
|
||||
|
||||
[:div.element-set {:on-blur on-set-blur}
|
||||
[:div.element-set-title
|
||||
[:span (tr "workspace.options.navigate-to")]]
|
||||
[:div.element-set-content
|
||||
[:div.row-flex
|
||||
[:div.custom-select.flex-grow {:on-click #(reset! show-frames-dropdown? true)}
|
||||
(if destination
|
||||
[:span (:name destination)]
|
||||
[:span (tr "workspace.options.select-artboard")])
|
||||
[:span.dropdown-button i/arrow-down]
|
||||
[:& dropdown {:show @show-frames-dropdown?
|
||||
:on-close #(reset! show-frames-dropdown? false)}
|
||||
[:ul.custom-select-dropdown
|
||||
[:li.dropdown-separator
|
||||
{:on-click #(on-select-destination nil)}
|
||||
(tr "workspace.options.none")]
|
||||
change-event-type
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value d/read-string)]
|
||||
(update-interaction index #(cti/set-event-type % value shape))))
|
||||
|
||||
change-action-type
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value d/read-string)]
|
||||
(update-interaction index #(cti/set-action-type % value))))
|
||||
|
||||
change-delay
|
||||
(fn [value]
|
||||
(update-interaction index #(cti/set-delay % value)))
|
||||
|
||||
change-destination
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value)
|
||||
value (when (not= value "") (uuid/uuid value))]
|
||||
(update-interaction index #(cti/set-destination % value))))
|
||||
|
||||
change-url
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value)]
|
||||
(update-interaction index #(cti/set-url % value))))
|
||||
|
||||
change-overlay-pos-type
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value d/read-string)]
|
||||
(update-interaction index #(cti/set-overlay-pos-type % value shape objects))))
|
||||
|
||||
toggle-overlay-pos-type
|
||||
(fn [pos-type]
|
||||
(update-interaction index #(cti/toggle-overlay-pos-type % pos-type shape objects)))
|
||||
|
||||
change-close-click-outside
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/checked?)]
|
||||
(update-interaction index #(cti/set-close-click-outside % value))))
|
||||
|
||||
change-background-overlay
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/checked?)]
|
||||
(update-interaction index #(cti/set-background-overlay % value))))]
|
||||
|
||||
[:*
|
||||
[:div.element-set-options-group {:class (dom/classnames
|
||||
:open @extended-open?)}
|
||||
|
||||
; Summary
|
||||
[:div.element-set-actions-button {:on-click #(swap! extended-open? not)}
|
||||
i/actions]
|
||||
[:div.interactions-summary {:on-click #(swap! extended-open? not)}
|
||||
[:div.trigger-name (event-type-name interaction)]
|
||||
[:div.action-summary (action-summary interaction destination)]]
|
||||
[:div.elemen-set-actions {:on-click #(remove-interaction index)}
|
||||
[:div.element-set-actions-button i/minus]]
|
||||
|
||||
(when @extended-open?
|
||||
[:div.element-set-content
|
||||
|
||||
; Trigger select
|
||||
[:div.interactions-element.separator
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-trigger")]
|
||||
[:select.input-select
|
||||
{:value (str (:event-type interaction))
|
||||
:on-change change-event-type}
|
||||
(for [[value name] (event-type-names)]
|
||||
(when-not (and (= value :after-delay)
|
||||
(not= (:type shape) :frame))
|
||||
[:option {:value (str value)} name]))]]
|
||||
|
||||
; Delay
|
||||
(when (cti/has-delay interaction)
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")]
|
||||
[:div.input-element
|
||||
[:> numeric-input {:ref ext-delay-ref
|
||||
:on-click (select-text ext-delay-ref)
|
||||
:on-change change-delay
|
||||
:value (:delay interaction)}]
|
||||
[:span.after (tr "workspace.options.interaction-ms")]]])
|
||||
|
||||
; Action select
|
||||
[:div.interactions-element.separator
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-action")]
|
||||
[:select.input-select
|
||||
{:value (str (:action-type interaction))
|
||||
:on-change change-action-type}
|
||||
(for [[value name] (action-type-names)]
|
||||
[:option {:value (str value)} name])]]
|
||||
|
||||
; Destination
|
||||
(when (cti/has-destination interaction)
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-destination")]
|
||||
[:select.input-select
|
||||
{:value (str (:destination interaction))
|
||||
:on-change change-destination}
|
||||
(if (= (:action-type interaction) :close-overlay)
|
||||
[:option {:value ""} (tr "workspace.options.interaction-self")]
|
||||
[:option {:value ""} (tr "workspace.options.interaction-none")])
|
||||
(for [frame frames]
|
||||
(when (and (not= (:id frame) (:id shape)) ; A frame cannot navigate to itself
|
||||
(not= (:id frame) (:frame-id shape))) ; nor a shape to its container frame
|
||||
[:li {:key (:id frame)
|
||||
:on-click #(on-select-destination (:id frame))}
|
||||
(:name frame)]))]]]
|
||||
[:span.navigate-icon {:style {:visibility (when (not destination) "hidden")}
|
||||
:on-click on-navigate} i/navigate]]]])))
|
||||
[:option {:value (str (:id frame))} (:name frame)]))]])
|
||||
|
||||
; URL
|
||||
(when (cti/has-url interaction)
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-url")]
|
||||
[:input.input-text {:default-value (str (:url interaction))
|
||||
:on-blur change-url}]])
|
||||
|
||||
(when (cti/has-overlay-opts interaction)
|
||||
[:*
|
||||
; Overlay position (select)
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-position")]
|
||||
[:select.input-select
|
||||
{:value (str (:overlay-pos-type interaction))
|
||||
:on-change change-overlay-pos-type}
|
||||
(for [[value name] (overlay-pos-type-names)]
|
||||
[:option {:value (str value)} name])]]
|
||||
|
||||
; Overlay position (buttons)
|
||||
[:div.interactions-element.interactions-pos-buttons
|
||||
[:div.element-set-actions-button
|
||||
{:class (dom/classnames :active (= overlay-pos-type :center))
|
||||
:on-click #(toggle-overlay-pos-type :center)}
|
||||
i/position-center]
|
||||
[:div.element-set-actions-button
|
||||
{:class (dom/classnames :active (= overlay-pos-type :top-left))
|
||||
:on-click #(toggle-overlay-pos-type :top-left)}
|
||||
i/position-top-left]
|
||||
[:div.element-set-actions-button
|
||||
{:class (dom/classnames :active (= overlay-pos-type :top-right))
|
||||
:on-click #(toggle-overlay-pos-type :top-right)}
|
||||
i/position-top-right]
|
||||
[:div.element-set-actions-button
|
||||
{:class (dom/classnames :active (= overlay-pos-type :top-center))
|
||||
:on-click #(toggle-overlay-pos-type :top-center)}
|
||||
i/position-top-center]
|
||||
[:div.element-set-actions-button
|
||||
{:class (dom/classnames :active (= overlay-pos-type :bottom-left))
|
||||
:on-click #(toggle-overlay-pos-type :bottom-left)}
|
||||
i/position-bottom-left]
|
||||
[:div.element-set-actions-button
|
||||
{:class (dom/classnames :active (= overlay-pos-type :bottom-right))
|
||||
:on-click #(toggle-overlay-pos-type :bottom-right)}
|
||||
i/position-bottom-right]
|
||||
[:div.element-set-actions-button
|
||||
{:class (dom/classnames :active (= overlay-pos-type :bottom-center))
|
||||
:on-click #(toggle-overlay-pos-type :bottom-center)}
|
||||
i/position-bottom-center]]
|
||||
|
||||
; Overlay click outside
|
||||
[:div.interactions-element
|
||||
[:div.input-checkbox
|
||||
[:input {:type "checkbox"
|
||||
:id (str "close-" index)
|
||||
:checked close-click-outside?
|
||||
:on-change change-close-click-outside}]
|
||||
[:label {:for (str "close-" index)}
|
||||
(tr "workspace.options.interaction-close-outside")]]]
|
||||
|
||||
; Overlay background
|
||||
[:div.interactions-element
|
||||
[:div.input-checkbox
|
||||
[:input {:type "checkbox"
|
||||
:id (str "background-" index)
|
||||
:checked background-overlay?
|
||||
:on-change change-background-overlay}]
|
||||
[:label {:for (str "background-" index)}
|
||||
(tr "workspace.options.interaction-background")]]]])])]]))
|
||||
|
||||
(mf/defc interactions-menu
|
||||
[{:keys [shape] :as props}]
|
||||
(let [interactions (get shape :interactions [])
|
||||
|
||||
options (mf/deref refs/workspace-page-options)
|
||||
flows (:flows options)
|
||||
|
||||
add-interaction
|
||||
(fn []
|
||||
(st/emit! (dwi/add-new-interaction shape)))
|
||||
|
||||
remove-interaction
|
||||
(fn [index]
|
||||
(st/emit! (dwi/remove-interaction shape index)))
|
||||
|
||||
update-interaction
|
||||
(fn [index update-fn]
|
||||
(st/emit! (dwi/update-interaction shape index update-fn)))]
|
||||
[:*
|
||||
(if shape
|
||||
[:& shape-flows {:flows flows
|
||||
:shape shape}]
|
||||
[:& page-flows {:flows flows}])
|
||||
|
||||
[:div.element-set.interactions-options
|
||||
(when (and shape (not (cp/unframed-shape? shape)))
|
||||
[:div.element-set-title
|
||||
[:span (tr "workspace.options.interactions")]
|
||||
[:div.add-page {:on-click add-interaction}
|
||||
i/plus]])
|
||||
[:div.element-set-content
|
||||
(when (= (count interactions) 0)
|
||||
[:*
|
||||
(when (and shape (not (cp/unframed-shape? shape)))
|
||||
[:*
|
||||
[:div.interactions-help-icon i/plus]
|
||||
[:div.interactions-help.separator (tr "workspace.options.add-interaction")]])
|
||||
[:div.interactions-help-icon i/interaction]
|
||||
[:div.interactions-help (tr "workspace.options.select-a-shape")]
|
||||
[:div.interactions-help-icon i/play]
|
||||
[:div.interactions-help (tr "workspace.options.use-play-button")]])]
|
||||
[:div.groups
|
||||
(for [[index interaction] (d/enumerate interactions)]
|
||||
[:& interaction-entry {:index index
|
||||
:shape shape
|
||||
:interaction interaction
|
||||
:update-interaction update-interaction
|
||||
:remove-interaction remove-interaction}])]]]))
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
:group (tr "workspace.options.group-stroke")
|
||||
(tr "workspace.options.stroke"))
|
||||
|
||||
show-options (not= (:stroke-style values :none) :none)
|
||||
show-options (not= (or (:stroke-style values) :none) :none)
|
||||
show-caps (and show-caps
|
||||
(not (#{:inner :outer} (:stroke-alignment values))))
|
||||
|
||||
|
@ -102,7 +102,10 @@
|
|||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn []
|
||||
(st/emit! (dc/change-stroke ids (dissoc current-stroke-color :id :file-id)))))
|
||||
(let [remove-multiple (fn [[_ value]] (not= value :multiple))
|
||||
current-stroke-color (-> (into {} (filter remove-multiple) current-stroke-color)
|
||||
(assoc :id nil :file-id nil))]
|
||||
(st/emit! (dc/change-stroke ids current-stroke-color)))))
|
||||
|
||||
on-stroke-style-change
|
||||
(fn [event]
|
||||
|
@ -141,7 +144,10 @@
|
|||
target (dom/get-current-target event)
|
||||
rect (dom/get-bounding-rect target)
|
||||
|
||||
top (+ (:bottom rect) 5)
|
||||
top (if (< (+ (:bottom rect) 320) (:height window-size))
|
||||
(+ (:bottom rect) 5)
|
||||
(- (:height window-size) 325))
|
||||
|
||||
left (if (< (+ (:left rect) 200) (:width window-size))
|
||||
(:left rect)
|
||||
(- (:width window-size) 205))]
|
||||
|
|
|
@ -74,7 +74,8 @@
|
|||
|
||||
(defn filter-fonts
|
||||
[{:keys [term backends]} fonts]
|
||||
(let [xform (cond-> (map identity)
|
||||
(let [term (str/lower term)
|
||||
xform (cond-> (map identity)
|
||||
(seq term)
|
||||
(comp (filter #(str/includes? (str/lower (:name %)) term)))
|
||||
|
||||
|
@ -175,7 +176,7 @@
|
|||
[:div.font-selector
|
||||
[:div.font-selector-dropdown
|
||||
[:header
|
||||
[:input {:placeholder "Search font"
|
||||
[:input {:placeholder (tr "workspace.options.search-font")
|
||||
:value (:term @state)
|
||||
:ref input
|
||||
:spell-check false
|
||||
|
|
|
@ -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.workspace.sidebar.options.shapes.bool
|
||||
(:require
|
||||
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc options
|
||||
[{:keys [shape] :as props}]
|
||||
(let [ids [(:id shape)]
|
||||
type (:type shape)
|
||||
measure-values (select-keys shape measure-attrs)
|
||||
stroke-values (select-keys shape stroke-attrs)
|
||||
layer-values (select-keys shape layer-attrs)
|
||||
constraint-values (select-keys shape constraint-attrs)]
|
||||
[:*
|
||||
[:& measures-menu {:ids ids
|
||||
:type type
|
||||
:values measure-values}]
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}]
|
||||
[:& layer-menu {:ids ids
|
||||
:type type
|
||||
:values layer-values}]
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
:values (select-keys shape fill-attrs)}]
|
||||
[:& stroke-menu {:ids ids
|
||||
:type type
|
||||
:show-caps true
|
||||
:values stroke-values}]
|
||||
[:& shadow-menu {:ids ids
|
||||
:values (select-keys shape [:shadow])}]
|
||||
[:& blur-menu {:ids ids
|
||||
:values (select-keys shape [:blur])}]]))
|
|
@ -93,6 +93,16 @@
|
|||
:text :ignore}
|
||||
|
||||
:svg-raw
|
||||
{:measure :shape
|
||||
:layer :shape
|
||||
:constraint :shape
|
||||
:fill :shape
|
||||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :shape
|
||||
:text :ignore}
|
||||
|
||||
:bool
|
||||
{:measure :shape
|
||||
:layer :shape
|
||||
:constraint :shape
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
cursor (mf/use-state (utils/get-cursor :pointer-inner))
|
||||
hover-ids (mf/use-state nil)
|
||||
hover (mf/use-state nil)
|
||||
hover-disabled? (mf/use-state false)
|
||||
frame-hover (mf/use-state nil)
|
||||
active-frames (mf/use-state {})
|
||||
|
||||
|
@ -153,13 +154,14 @@
|
|||
(hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? node-editing?)
|
||||
(hooks/setup-resize layout viewport-ref)
|
||||
(hooks/setup-keyboard alt? ctrl? space?)
|
||||
(hooks/setup-hover-shapes page-id move-stream objects transform selected ctrl? hover hover-ids zoom)
|
||||
(hooks/setup-hover-shapes page-id move-stream objects transform selected ctrl? hover hover-ids @hover-disabled? zoom)
|
||||
(hooks/setup-viewport-modifiers modifiers selected objects render-ref)
|
||||
(hooks/setup-shortcuts node-editing? drawing-path?)
|
||||
(hooks/setup-active-frames objects vbox hover active-frames)
|
||||
|
||||
[:div.viewport
|
||||
[:div.viewport-overlays
|
||||
|
||||
[:& wtr/frame-renderer {:objects objects
|
||||
:background background}]
|
||||
|
||||
|
@ -195,11 +197,12 @@
|
|||
|
||||
[:& use/export-page {:options options}]
|
||||
|
||||
[:& (mf/provider embed/context) {:value true}
|
||||
;; Render root shape
|
||||
[:& shapes/root-shape {:key page-id
|
||||
:objects objects
|
||||
:active-frames @active-frames}]]]
|
||||
[:& (mf/provider use/include-metadata-ctx) {:value false}
|
||||
[:& (mf/provider embed/context) {:value true}
|
||||
;; Render root shape
|
||||
[:& shapes/root-shape {:key page-id
|
||||
:objects objects
|
||||
:active-frames @active-frames}]]]]
|
||||
|
||||
[:svg.viewport-controls
|
||||
{:xmlns "http://www.w3.org/2000/svg"
|
||||
|
@ -228,7 +231,6 @@
|
|||
:on-pointer-up on-pointer-up}
|
||||
|
||||
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
||||
|
||||
(when show-outlines?
|
||||
[:& outline/shape-outlines
|
||||
{:objects objects
|
||||
|
@ -267,6 +269,17 @@
|
|||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select}]
|
||||
|
||||
(when show-prototypes?
|
||||
[:& widgets/frame-flows
|
||||
{:flows (:flows options)
|
||||
:objects objects
|
||||
:selected selected
|
||||
:zoom zoom
|
||||
:modifiers modifiers
|
||||
:on-frame-enter on-frame-enter
|
||||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select}])
|
||||
|
||||
(when show-gradient-handlers?
|
||||
[:& gradients/gradient-handlers
|
||||
{:id (first selected)
|
||||
|
@ -320,7 +333,8 @@
|
|||
|
||||
(when show-prototypes?
|
||||
[:& interactions/interactions
|
||||
{:selected selected}])
|
||||
{:selected selected
|
||||
:hover-disabled? hover-disabled?}])
|
||||
|
||||
(when show-selrect?
|
||||
[:& widgets/selection-rect {:data selrect
|
||||
|
|
|
@ -363,12 +363,14 @@
|
|||
delta-y (-> (.-deltaY ^js event)
|
||||
(* unit)
|
||||
(/ zoom))
|
||||
|
||||
delta-x (-> (.-deltaX ^js event)
|
||||
(* unit)
|
||||
(/ zoom))]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(if (kbd/shift? event)
|
||||
(if (and (not (cfg/check-platform? :macos)) ;; macos sends delta-x automaticaly, don't need to do it
|
||||
(kbd/shift? event))
|
||||
(st/emit! (dw/update-viewport-position {:x #(+ % delta-y)}))
|
||||
(st/emit! (dw/update-viewport-position {:x #(+ % delta-x)
|
||||
:y #(+ % delta-y)})))))))))
|
||||
|
|
|
@ -89,36 +89,46 @@
|
|||
(hooks/use-stream ms/keyboard-ctrl #(reset! ctrl? %))
|
||||
(hooks/use-stream ms/keyboard-space #(reset! space? %)))
|
||||
|
||||
(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids zoom]
|
||||
(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? zoom]
|
||||
(let [;; We use ref so we don't recreate the stream on a change
|
||||
zoom-ref (mf/use-ref zoom)
|
||||
ctrl-ref (mf/use-ref @ctrl?)
|
||||
transform-ref (mf/use-ref nil)
|
||||
selected-ref (mf/use-ref selected)
|
||||
hover-disabled-ref (mf/use-ref hover-disabled?)
|
||||
|
||||
query-point
|
||||
(mf/use-callback
|
||||
(mf/deps page-id)
|
||||
(fn [point]
|
||||
(let [zoom (mf/ref-val zoom-ref)
|
||||
ctrl? (mf/ref-val ctrl-ref)
|
||||
rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))]
|
||||
(uw/ask-buffered!
|
||||
{:cmd :selection/query
|
||||
:page-id page-id
|
||||
:rect rect
|
||||
:include-frames? true
|
||||
:reverse? true})))) ;; we want the topmost shape to be selected first
|
||||
(if (mf/ref-val hover-disabled-ref)
|
||||
(rx/of nil)
|
||||
(uw/ask-buffered!
|
||||
{:cmd :selection/query
|
||||
:page-id page-id
|
||||
:rect rect
|
||||
:include-frames? true
|
||||
:clip-children? (not ctrl?)
|
||||
:reverse? true}))))) ;; we want the topmost shape to be selected first
|
||||
|
||||
over-shapes-stream
|
||||
(mf/use-memo
|
||||
(fn []
|
||||
(->> move-stream
|
||||
;; When transforming shapes we stop querying the worker
|
||||
(rx/filter #(not (some? (mf/ref-val transform-ref))))
|
||||
(rx/switch-map query-point))))
|
||||
]
|
||||
(fn []
|
||||
(rx/merge
|
||||
(->> move-stream
|
||||
;; When transforming shapes we stop querying the worker
|
||||
(rx/filter #(not (some? (mf/ref-val transform-ref))))
|
||||
(rx/switch-map query-point))
|
||||
|
||||
(->> move-stream
|
||||
;; When transforming shapes we stop querying the worker
|
||||
(rx/filter #(some? (mf/ref-val transform-ref)))
|
||||
(rx/map (constantly nil))))))]
|
||||
|
||||
;; Refresh the refs on a value change
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps transform)
|
||||
#(mf/set-ref-val! transform-ref transform))
|
||||
|
@ -127,23 +137,38 @@
|
|||
(mf/deps zoom)
|
||||
#(mf/set-ref-val! zoom-ref zoom))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @ctrl?)
|
||||
#(mf/set-ref-val! ctrl-ref @ctrl?))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps selected)
|
||||
#(mf/set-ref-val! selected-ref selected))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps hover-disabled?)
|
||||
#(mf/set-ref-val! hover-disabled-ref hover-disabled?))
|
||||
|
||||
(hooks/use-stream
|
||||
over-shapes-stream
|
||||
(mf/deps page-id objects @ctrl?)
|
||||
(fn [ids]
|
||||
(let [selected (mf/ref-val selected-ref)
|
||||
remove-id? (into #{} (mapcat #(cp/get-parents % objects)) selected)
|
||||
remove-id? (if @ctrl?
|
||||
(d/concat remove-id?
|
||||
(->> ids
|
||||
(filterv #(= :group (get-in objects [% :type])))))
|
||||
remove-id?)
|
||||
ids (->> ids (filterv (comp not remove-id?)))]
|
||||
(reset! hover (get objects (first ids)))
|
||||
(let [is-group?
|
||||
(fn [id]
|
||||
(contains? #{:group :bool} (get-in objects [id :type])))
|
||||
|
||||
selected (mf/ref-val selected-ref)
|
||||
|
||||
remove-xfm (mapcat #(cp/get-parents % objects))
|
||||
remove-id? (cond-> (into #{} remove-xfm selected)
|
||||
@ctrl?
|
||||
(d/concat (filterv is-group? ids)))
|
||||
|
||||
ids (->> ids (filterv (comp not remove-id?)))
|
||||
|
||||
hover-shape (get objects (first ids))]
|
||||
|
||||
(reset! hover hover-shape)
|
||||
(reset! hover-ids ids))))))
|
||||
|
||||
(defn setup-viewport-modifiers [modifiers selected objects render-ref]
|
||||
|
|
|
@ -7,24 +7,22 @@
|
|||
(ns app.main.ui.workspace.viewport.interactions
|
||||
"Visually show shape interactions in workspace"
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.types.interactions :as cti]
|
||||
[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 +82,62 @@
|
|||
|
||||
|
||||
(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 -6.5 0 l 12 0 l -6 -6 m 6 6 l -6 6"
|
||||
:left "M 6.5 0 l -12 0 l 6 -6 m -6 6 l 6 6"
|
||||
nil)
|
||||
|
||||
:open-overlay "M-5 -5 h7 v7 h-7 z M2 -2 h3.5 v7 h-7 v-2.5"
|
||||
|
||||
:toggle-overlay "M-5 -5 h7 v7 h-7 z M2 -2 h3.5 v7 h-7 v-2.5"
|
||||
|
||||
:close-overlay "M -5 -5 L 5 5 M -5 5 L 5 -5"
|
||||
|
||||
:prev-screen (case arrow-dir
|
||||
:left "M -6.5 0 l 12 0 l -6 -6 m 6 6 l -6 6"
|
||||
:right "M 6.5 0 l -12 0 l 6 -6 m -6 6 l 6 6"
|
||||
nil)
|
||||
|
||||
:open-url (str "M1 -5 L 3 -7 L 7 -3 L 1 3 L -1 1"
|
||||
"M-1 5 L -3 7 L -7 3 L -1 -3 L 1 -1")
|
||||
|
||||
nil)
|
||||
inv-zoom (/ 1 zoom)]
|
||||
[:*
|
||||
[:circle {:cx 0
|
||||
:cy 0
|
||||
:r 8
|
||||
:stroke "#31EFB8"
|
||||
:stroke-width 2
|
||||
:fill "#FFFFFF"
|
||||
:r (if (some? action-type) 11 4)
|
||||
:fill stroke
|
||||
:transform (str
|
||||
"scale(" inv-zoom ", " inv-zoom ") "
|
||||
"translate(" (* zoom x) ", " (* zoom y) ")")}]
|
||||
(when arrow-dir
|
||||
[:path {:stroke "#31EFB8"
|
||||
:fill "none"
|
||||
(when icon-pdata
|
||||
[:path {:fill stroke
|
||||
:stroke-width 2
|
||||
:d arrow-pdata
|
||||
:stroke "#FFFFFF"
|
||||
: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 level 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)
|
||||
(/ (* level 32) zoom))}))
|
||||
|
||||
orig-dx (if (= orig-pos :right) 100 -100)
|
||||
dest-dx (if (= dest-pos :right) 100 -100)
|
||||
|
@ -126,93 +148,183 @@
|
|||
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 (not dest-shape)
|
||||
[:& 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
|
||||
:y orig-y
|
||||
:arrow-dir nil
|
||||
:zoom zoom}]
|
||||
[:& interaction-marker {:x dest-x
|
||||
:y dest-y
|
||||
:arrow-dir arrow-dir
|
||||
:zoom zoom}]
|
||||
|
||||
(when dest-shape
|
||||
[:& outline {:shape dest-shape
|
||||
:color "#31EFB8"}])])))
|
||||
:color "#31EFB8"}])
|
||||
|
||||
[:& interaction-marker {:index index
|
||||
:x orig-x
|
||||
:y orig-y
|
||||
:stroke "#31EFB8"
|
||||
:zoom zoom}]
|
||||
[:& interaction-marker {:index index
|
||||
:x dest-x
|
||||
:y dest-y
|
||||
:stroke "#31EFB8"
|
||||
:action-type action-type
|
||||
:arrow-dir arrow-dir
|
||||
:zoom zoom}]])))
|
||||
|
||||
|
||||
(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}]]))
|
||||
|
||||
|
||||
(mf/defc overlay-marker
|
||||
[{:keys [index orig-shape dest-shape position objects hover-disabled?] :as props}]
|
||||
(let [start-move-position
|
||||
(fn [_]
|
||||
(st/emit! (dw/start-move-overlay-pos index)))]
|
||||
|
||||
(when dest-shape
|
||||
(let [orig-frame (cp/get-frame orig-shape objects)
|
||||
marker-x (+ (:x orig-frame) (:x position))
|
||||
marker-y (+ (:y orig-frame) (:y position))
|
||||
width (:width dest-shape)
|
||||
height (:height dest-shape)]
|
||||
[:g {:on-mouse-down start-move-position
|
||||
:on-mouse-enter #(reset! hover-disabled? true)
|
||||
:on-mouse-leave #(reset! hover-disabled? false)}
|
||||
[:path {:stroke "#31EFB8"
|
||||
:fill "#000000"
|
||||
:fill-opacity 0.3
|
||||
:stroke-width 1
|
||||
:d (str "M" marker-x " " marker-y " "
|
||||
"h " width " "
|
||||
"v " height " "
|
||||
"h -" width " z"
|
||||
"M" marker-x " " marker-y " "
|
||||
"l " width " " height " "
|
||||
"M" marker-x " " (+ marker-y height) " "
|
||||
"l " width " -" height " ")}]
|
||||
[:circle {:cx (+ marker-x (/ width 2))
|
||||
:cy (+ marker-y (/ height 2))
|
||||
:r 8
|
||||
:fill "#31EFB8"}]]))))
|
||||
|
||||
(mf/defc interactions
|
||||
[{:keys [selected] :as props}]
|
||||
[{:keys [selected hover-disabled?] :as props}]
|
||||
(let [local (mf/deref refs/workspace-local)
|
||||
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)]
|
||||
move-overlay-to (:move-overlay-to local)
|
||||
move-overlay-index (:move-overlay-index local)
|
||||
first-selected (first selected-shapes)
|
||||
|
||||
calc-level (fn [index interactions]
|
||||
(->> (subvec interactions 0 index)
|
||||
(filter #(nil? (:destination %)))
|
||||
(count)))]
|
||||
|
||||
[: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 (when (cti/destination? interaction)
|
||||
(get objects (:destination interaction)))
|
||||
selected? (contains? selected (:id shape))
|
||||
level (calc-level index (:interactions shape))]
|
||||
(when-not selected?
|
||||
[:& interaction-path {:key (str (:id shape) "-" index)
|
||||
:index index
|
||||
:level level
|
||||
: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
|
||||
:selected selected
|
||||
:selected? true
|
||||
:zoom zoom}]
|
||||
(when (not (#{:move :rotate} current-transform))
|
||||
[:& interaction-handle {:key (:id shape)
|
||||
:shape 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 (when (cti/destination? interaction)
|
||||
(get objects (:destination interaction)))
|
||||
level (calc-level index (:interactions shape))]
|
||||
[:*
|
||||
[:& interaction-path {:key (str (:id shape) "-" index)
|
||||
:index index
|
||||
:level level
|
||||
:orig-shape shape
|
||||
:dest-shape dest-shape
|
||||
:selected selected
|
||||
:zoom zoom}])))))]]))
|
||||
:selected? true
|
||||
:action-type (:action-type interaction)
|
||||
:zoom zoom}]
|
||||
(when (and (or (= (:action-type interaction) :open-overlay)
|
||||
(= (:action-type interaction) :toggle-overlay))
|
||||
(= (:overlay-pos-type interaction) :manual))
|
||||
(if (and (some? move-overlay-to)
|
||||
(= move-overlay-index index))
|
||||
[:& overlay-marker {:key (str "pos" (:id shape) "-" index)
|
||||
:index index
|
||||
:orig-shape shape
|
||||
:dest-shape dest-shape
|
||||
:position move-overlay-to
|
||||
:objects objects
|
||||
:hover-disabled? hover-disabled?}]
|
||||
[:& overlay-marker {:key (str "pos" (:id shape) "-" index)
|
||||
:index index
|
||||
:orig-shape shape
|
||||
:dest-shape dest-shape
|
||||
:position (:overlay-position interaction)
|
||||
:objects objects
|
||||
:hover-disabled? hover-disabled?}]))])))
|
||||
(when (and shape
|
||||
(not (cp/unframed-shape? shape))
|
||||
(not (#{:move :rotate} current-transform)))
|
||||
[:& interaction-handle {:key (:id shape)
|
||||
:index nil
|
||||
:shape shape
|
||||
:selected selected
|
||||
:zoom zoom}])))]]))
|
||||
|
||||
|
|
|
@ -229,7 +229,12 @@
|
|||
current-transform (mf/deref refs/current-transform)
|
||||
|
||||
selrect (:selrect shape)
|
||||
transform (geom/transform-matrix shape {:no-flip true})]
|
||||
transform (geom/transform-matrix shape {:no-flip true})
|
||||
|
||||
rotation (-> (gpt/point 1 0)
|
||||
(gpt/transform (:transform shape))
|
||||
(gpt/angle)
|
||||
(mod 360))]
|
||||
|
||||
(when (not (#{:move :rotate} current-transform))
|
||||
[:g.controls {:pointer-events (if disable-handlers "none" "visible")}
|
||||
|
@ -249,7 +254,7 @@
|
|||
:on-rotate on-rotate
|
||||
:on-resize (partial on-resize position)
|
||||
:transform transform
|
||||
:rotation (:rotation shape)
|
||||
:rotation rotation
|
||||
:color color
|
||||
:overflow-text overflow-text}
|
||||
props (map->obj (merge common-props props))]
|
||||
|
|
|
@ -10,10 +10,12 @@
|
|||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.viewport.path-actions :refer [path-actions]]
|
||||
[app.util.dom :as dom]
|
||||
[rumext.alpha :as mf]))
|
||||
|
@ -90,7 +92,7 @@
|
|||
(mf/defc frame-title
|
||||
[{:keys [frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}]
|
||||
(let [{:keys [width x y]} (gsh/transform-shape frame)
|
||||
label-pos (gpt/point x (- y (/ 10 zoom)))
|
||||
label-pos (gpt/point x (- y (/ 10 zoom)))
|
||||
|
||||
on-mouse-down
|
||||
(mf/use-callback
|
||||
|
@ -156,3 +158,75 @@
|
|||
:on-frame-enter on-frame-enter
|
||||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select}])]))
|
||||
|
||||
(mf/defc frame-flow
|
||||
[{:keys [flow frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}]
|
||||
(let [{:keys [x y]} (gsh/transform-shape frame)
|
||||
flow-pos (gpt/point x (- y (/ 35 zoom)))
|
||||
|
||||
on-mouse-down
|
||||
(mf/use-callback
|
||||
(mf/deps (:id frame) on-frame-select)
|
||||
(fn [bevent]
|
||||
(let [event (.-nativeEvent bevent)]
|
||||
(when (= 1 (.-which event))
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(on-frame-select event (:id frame))))))
|
||||
|
||||
on-double-click
|
||||
(mf/use-callback
|
||||
(mf/deps (:id frame))
|
||||
(st/emitf (dwi/start-rename-flow (:id flow))))
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-callback
|
||||
(mf/deps (:id frame) on-frame-enter)
|
||||
(fn [_]
|
||||
(on-frame-enter (:id frame))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-callback
|
||||
(mf/deps (:id frame) on-frame-leave)
|
||||
(fn [_]
|
||||
(on-frame-leave (:id frame))))]
|
||||
|
||||
[:foreignObject {:x 0
|
||||
:y -15
|
||||
:width 100000
|
||||
:height 24
|
||||
:transform (str (when (and selected? modifiers)
|
||||
(str (:displacement modifiers) " " ))
|
||||
(text-transform flow-pos zoom))}
|
||||
[:div.flow-badge {:class (dom/classnames :selected selected?)}
|
||||
[:div.content {:on-mouse-down on-mouse-down
|
||||
:on-double-click on-double-click
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave}
|
||||
i/play
|
||||
[:span (:name flow)]]]]))
|
||||
|
||||
(mf/defc frame-flows
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [flows (unchecked-get props "flows")
|
||||
objects (unchecked-get props "objects")
|
||||
zoom (unchecked-get props "zoom")
|
||||
modifiers (unchecked-get props "modifiers")
|
||||
selected (or (unchecked-get props "selected") #{})
|
||||
|
||||
on-frame-enter (unchecked-get props "on-frame-enter")
|
||||
on-frame-leave (unchecked-get props "on-frame-leave")
|
||||
on-frame-select (unchecked-get props "on-frame-select")]
|
||||
[:g.frame-flows
|
||||
(for [flow flows]
|
||||
(let [frame (get objects (:starting-frame flow))]
|
||||
[:& frame-flow {:flow flow
|
||||
:frame frame
|
||||
:selected? (contains? selected (:id frame))
|
||||
:zoom zoom
|
||||
:modifiers modifiers
|
||||
:on-frame-enter on-frame-enter
|
||||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select}]))]))
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
goog.provide("app.util.browser_history");
|
||||
goog.require("goog.history.Html5History");
|
||||
|
||||
|
||||
goog.scope(function() {
|
||||
const self = app.util.browser_history;
|
||||
const Html5History = goog.history.Html5History;
|
||||
|
|
|
@ -281,7 +281,7 @@
|
|||
(defn set-text! [node text]
|
||||
(set! (.-textContent node) text))
|
||||
|
||||
(defn set-css-property [node property value]
|
||||
(defn set-css-property! [node property value]
|
||||
(.setProperty (.-style ^js node) property value))
|
||||
|
||||
(defn capture-pointer [event]
|
||||
|
@ -394,3 +394,16 @@
|
|||
(defn left-mouse? [bevent]
|
||||
(let [event (.-nativeEvent ^js bevent)]
|
||||
(= 1 (.-which event))))
|
||||
|
||||
(defn open-new-window
|
||||
([uri]
|
||||
(open-new-window uri "_blank"))
|
||||
([uri name]
|
||||
;; Warning: need to protect against reverse tabnabbing attack
|
||||
;; https://www.comparitech.com/blog/information-security/reverse-tabnabbing/
|
||||
(.open js/window (str uri) name "noopener,noreferrer")))
|
||||
|
||||
(defn browser-back
|
||||
[]
|
||||
(.back (.-history js/window)))
|
||||
|
||||
|
|
|
@ -54,8 +54,10 @@
|
|||
{"x-frontend-version" (:full @cfg/version)})
|
||||
|
||||
(defn fetch
|
||||
[{:keys [method uri query headers body mode omit-default-headers]
|
||||
:or {mode :cors headers {}}}]
|
||||
[{:keys [method uri query headers body mode omit-default-headers credentials]
|
||||
:or {mode :cors
|
||||
headers {}
|
||||
credentials "same-origin"}}]
|
||||
(rx/Observable.create
|
||||
(fn [subscriber]
|
||||
(let [controller (js/AbortController.)
|
||||
|
@ -83,7 +85,7 @@
|
|||
:body body
|
||||
:mode (d/name mode)
|
||||
:redirect "follow"
|
||||
:credentials "same-origin"
|
||||
:credentials credentials
|
||||
:referrerPolicy "no-referrer"
|
||||
:signal signal}]
|
||||
(-> (js/fetch (str uri) params)
|
||||
|
@ -165,7 +167,6 @@
|
|||
:uri uri
|
||||
:response-type :blob
|
||||
:omit-default-headers true})
|
||||
|
||||
(rx/filter #(= 200 (:status %)))
|
||||
(rx/map :body)
|
||||
(rx/mapcat wapi/read-file-as-data-url)
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
{:label "Rumanian (communit)" :value "ro"}
|
||||
{:label "Portuguese (Brazil, community)" :value "pt_br"}
|
||||
{:label "Ελληνική γλώσσα (community)" :value "el"}
|
||||
{:label "עִבְרִית (community)" :value "he"}
|
||||
{:label "عربي/عربى (community)" :value "ar"}
|
||||
{:label "简体中文 (community)" :value "zh_cn"}])
|
||||
|
||||
(defn- parse-locale
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue