Merge branch 'staging'

This commit is contained in:
Andrey Antukh 2021-10-27 12:45:53 +02:00
commit 78d1c57b7c
306 changed files with 14686 additions and 4386 deletions

View file

@ -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

View file

@ -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))))

View file

@ -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})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -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

View file

@ -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]

View file

@ -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)))))

View file

@ -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"))

View file

@ -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)

View file

@ -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)

View file

@ -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)

View 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}))))))

View file

@ -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]))

View file

@ -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)

View file

@ -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

View 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})))))))

View file

@ -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]

View file

@ -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]))

View file

@ -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]))

View file

@ -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]))

View file

@ -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]

View file

@ -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))))))

View file

@ -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

View file

@ -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]))

View file

@ -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]))

View file

@ -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]

View file

@ -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]

View file

@ -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

View file

@ -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]

View file

@ -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))))))

View 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))))

View file

@ -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

View file

@ -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"))

View file

@ -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 %)

View file

@ -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)))

View 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)

View file

@ -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 [])

View file

@ -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))))

View file

@ -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

View file

@ -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]}

View file

@ -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)

View file

@ -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"}

View file

@ -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 [_]

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)))

View file

@ -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)))))

View file

@ -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

View file

@ -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")))

View 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 youre 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 "Weve created a new space on Penpot where you can share your libraries and templates and download the ones you like. Material Design, Cocomaterial or Penpots 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}]]]]]])))

View file

@ -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

View 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)))))

View file

@ -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

View 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}]])))

View file

@ -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]

View file

@ -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]}]

View file

@ -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)]
[:*

View file

@ -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)}])]]))))

View file

@ -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}]

View file

@ -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

View file

@ -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]}]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -13,14 +13,14 @@
[app.main.ui.components.fullscreen :as fs]
[app.main.ui.icons :as i]
[app.main.ui.viewer.comments :refer [comments-menu]]
[app.main.ui.viewer.interactions :refer [interactions-menu]]
[app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]]
[app.main.ui.workspace.header :refer [zoom-widget]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
(mf/defc header-options
[{:keys [section zoom page file permissions]}]
[{:keys [section zoom page file index permissions]}]
(let [fullscreen (mf/use-ctx fs/fullscreen-context)
toggle-fullscreen
@ -43,7 +43,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}]]))

View file

@ -10,6 +10,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.pages :as cp]
[app.common.types.page-options :as cto]
[app.main.data.comments :as dcm]
[app.main.data.viewer :as dv]
[app.main.refs :as refs]
@ -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

View file

@ -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}]]))

View file

@ -121,7 +121,6 @@
(fn []
(st/emit! (dw/setup-layout layout-name))))
(mf/use-effect
(mf/deps project-id file-id)
(fn []

View file

@ -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

View file

@ -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)
[:*

View file

@ -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"

View file

@ -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"

View file

@ -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 %))}

View file

@ -7,8 +7,10 @@
(ns app.main.ui.workspace.context-menu
"A workspace specific context menu (mouse right click)."
(:require
[app.common.types.page-options :as cto]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.shortcuts :as sc]
[app.main.data.workspace.undo :as dwu]
@ -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;")]

View file

@ -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))

View 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}]]))))

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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)))

View file

@ -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

View file

@ -19,6 +19,7 @@
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.timers :as ts]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]))
@ -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]

View file

@ -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)

View file

@ -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]

View file

@ -0,0 +1,87 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.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]]]))

View file

@ -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}])]]]))

View file

@ -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))]

View file

@ -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

View file

@ -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])}]]))

View file

@ -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

View file

@ -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

View file

@ -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)})))))))))

View file

@ -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]

View file

@ -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}])))]]))

View file

@ -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))]

View file

@ -10,10 +10,12 @@
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.streams :as ms]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.main.ui.workspace.viewport.path-actions :refer [path-actions]]
[app.util.dom :as dom]
[rumext.alpha :as mf]))
@ -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}]))]))

View file

@ -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;

View file

@ -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)))

View file

@ -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)

View file

@ -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