diff --git a/CHANGES.md b/CHANGES.md index b38886f843..f7d6975823 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,16 +15,28 @@ ### :sparkles: New features +- Add many performance related improvements to indexes handling on workspace. - Add option to interactively scale text [Taiga #1527](https://tree.taiga.io/project/penpot/us/1527) +- Add the ability to upload/use custom fonts (and automatically generate all needed webfonts). +- Refactor dashboard state management (improves considerably the performance when you have a dashboard with a big collection of projects and files). - Translate automatic names of new files and projects. ### :bug: Bugs fixed - Remove interactions when the destination artboard is deleted [Taiga #1656](https://tree.taiga.io/project/penpot/issue/1656) -- Fix snap index problem [Taiga #1661](https://tree.taiga.io/project/penpot/issue/1661) ### :arrow_up: Deps updates + +- Update exporter dependencies (puppetteer), that fixes some unexpected exceptions. +- Update string manipulation library. + + ### :boom: Breaking changes + +- The OIDC setting `PENPOT_OIDC_SCOPES` has changed the default semantics. Before this + configuration added scopes to the default set. Now it replaces it, so use with care, because + penpot requires at least `name` and `email` props found on the user info object. + ### :heart: Community contributions by (Thank you!) diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj index 55c7a8a508..63b9f481d1 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/http/oauth.clj @@ -232,8 +232,7 @@ :token-uri (cf/get :oidc-token-uri) :auth-uri (cf/get :oidc-auth-uri) :user-uri (cf/get :oidc-user-uri) - :scopes (into #{"openid" "profile" "email" "name"} - (cf/get :oidc-scopes #{})) + :scopes (cf/get :oidc-scopes #{"openid" "profile"}) :roles-attr (cf/get :oidc-roles-attr) :roles (cf/get :oidc-roles) :name "oidc"}] diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index a1253dc65b..2ca686f500 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -13,6 +13,8 @@ [app.common.spec :as us] [app.rlimits :as rlm] [app.rpc.queries.svg :as svg] + [buddy.core.bytes :as bb] + [buddy.core.codecs :as bc] [clojure.java.io :as io] [clojure.java.shell :as sh] [clojure.spec.alpha :as s] @@ -64,7 +66,8 @@ (defmethod process-error :default [error] - (ex/raise :type :internal :cause error)) + (ex/raise :type :internal + :cause error)) (defn run [{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}] @@ -232,6 +235,19 @@ (fs/slurp-bytes output-file)))) + (otf->ttf [data] + (let [input-file (fs/create-tempfile :prefix "penpot") + output-file (fs/path (str input-file ".ttf")) + _ (with-open [out (io/output-stream input-file)] + (IOUtils/writeChunked ^bytes data ^OutputStream out) + (.flush ^OutputStream out)) + res (sh/sh "fontforge" "-lang=ff" "-c" + (str/fmt "Open('%s'); Generate('%s')" + (str input-file) + (str output-file)))] + (when (zero? (:exit res)) + (fs/slurp-bytes output-file)))) + (ttf-or-otf->woff [data] (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "") output-file (fs/path (str input-file ".woff")) @@ -250,17 +266,68 @@ (.flush ^OutputStream out)) res (sh/sh "woff2_compress" (str input-file))] (when (zero? (:exit res)) - (fs/slurp-bytes output-file))))] + (fs/slurp-bytes output-file)))) + + (woff->sfnt [data] + (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "") + _ (with-open [out (io/output-stream input-file)] + (IOUtils/writeChunked ^bytes data ^OutputStream out) + (.flush ^OutputStream out)) + res (sh/sh "woff2sfnt" (str input-file) + :out-enc :bytes)] + (when (zero? (:exit res)) + (:out res)))) + + ;; Documented here: + ;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory + (get-sfnt-type [data] + (let [buff (bb/slice data 0 4) + type (bc/bytes->hex buff)] + (case type + "4f54544f" :otf + "00010000" :ttf + (ex/raise :type :internal + :code :unexpected-data + :hint "unexpected font data")))) + + (gen-if-nil [val factory] + (if (nil? val) + (factory) + val))] (let [current (into #{} (keys input))] - (if (contains? current "font/ttf") - (-> input - (assoc "font/otf" (ttf->otf (get input "font/ttf"))) - (assoc "font/woff" (ttf-or-otf->woff (get input "font/ttf"))) - (assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/ttf")))) + (cond + (contains? current "font/ttf") + (let [data (get input "font/ttf")] + (-> input + (update "font/otf" gen-if-nil #(ttf->otf data)) + (update "font/woff" gen-if-nil #(ttf-or-otf->woff data)) + (assoc "font/woff2" (ttf-or-otf->woff2 data)))) - (-> input - ;; TODO: pending to implement - ;; (assoc "font/ttf" (otf->ttf (get input "font/ttf"))) - (assoc "font/woff" (ttf-or-otf->woff (get input "font/otf"))) - (assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/otf")))))))) + (contains? current "font/otf") + (let [data (get input "font/otf")] + (-> input + (update "font/woff" gen-if-nil #(ttf-or-otf->woff data)) + (assoc "font/ttf" (otf->ttf data)) + (assoc "font/woff2" (ttf-or-otf->woff2 data)))) + + (contains? current "font/woff") + (let [data (get input "font/woff") + sfnt (woff->sfnt data)] + (when-not sfnt + (ex/raise :type :validation + :code :invalid-woff-file + :hint "invalid woff file")) + (let [stype (get-sfnt-type sfnt)] + (cond-> input + true + (-> (assoc "font/woff" data) + (assoc "font/woff2" (ttf-or-otf->woff2 sfnt))) + + (= stype :otf) + (-> (assoc "font/otf" sfnt) + (assoc "font/ttf" (otf->ttf sfnt))) + + (= stype :ttf) + (-> (assoc "font/otf" (ttf->otf sfnt)) + (assoc "font/ttf" sfnt))))))))) diff --git a/backend/tests/app/tests/_files/font-1.otf b/backend/tests/app/tests/_files/font-1.otf new file mode 100644 index 0000000000..9326ec7844 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.otf differ diff --git a/backend/tests/app/tests/_files/font-1.ttf b/backend/tests/app/tests/_files/font-1.ttf new file mode 100644 index 0000000000..cb2f335971 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.ttf differ diff --git a/backend/tests/app/tests/_files/font-1.woff b/backend/tests/app/tests/_files/font-1.woff new file mode 100644 index 0000000000..9607e1e194 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.woff differ diff --git a/backend/tests/app/tests/_files/font-2.otf b/backend/tests/app/tests/_files/font-2.otf new file mode 100644 index 0000000000..2fbddd00e7 Binary files /dev/null and b/backend/tests/app/tests/_files/font-2.otf differ diff --git a/backend/tests/app/tests/_files/font-2.woff b/backend/tests/app/tests/_files/font-2.woff new file mode 100644 index 0000000000..4edf9e60ee Binary files /dev/null and b/backend/tests/app/tests/_files/font-2.woff differ diff --git a/backend/tests/app/tests/test_services_fonts.clj b/backend/tests/app/tests/test_services_fonts.clj new file mode 100644 index 0000000000..50ee24abf0 --- /dev/null +++ b/backend/tests/app/tests/test_services_fonts.clj @@ -0,0 +1,92 @@ +;; 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.tests.test-services-fonts + (:require + [app.common.uuid :as uuid] + [app.db :as db] + [app.http :as http] + [app.storage :as sto] + [app.tests.helpers :as th] + [clojure.java.io :as io] + [clojure.test :as t] + [datoteka.core :as fs])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest ttf-font-upload-1 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + + ttfdata (-> (io/resource "app/tests/_files/font-1.ttf") + (fs/slurp-bytes)) + + params {::th/type :create-font-variant + :profile-id (:id prof) + :team-id team-id + :font-id "custom-somefont" + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/ttf" ttfdata}} + out (th/mutation! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (uuid? (:id result))) + (t/is (uuid? (:ttf-file-id result))) + (t/is (uuid? (:otf-file-id result))) + (t/is (uuid? (:woff1-file-id result))) + (t/is (uuid? (:woff2-file-id result))) + (t/are [k] (= (get params k) + (get result k)) + :team-id + :font-id + :font-family + :font-weight + :font-style)))) + +(t/deftest ttf-font-upload-2 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + + data (-> (io/resource "app/tests/_files/font-1.woff") + (fs/slurp-bytes)) + + params {::th/type :create-font-variant + :profile-id (:id prof) + :team-id team-id + :font-id "custom-somefont" + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/woff" data}} + out (th/mutation! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (uuid? (:id result))) + (t/is (uuid? (:ttf-file-id result))) + (t/is (uuid? (:otf-file-id result))) + (t/is (uuid? (:woff1-file-id result))) + (t/is (uuid? (:woff2-file-id result))) + (t/are [k] (= (get params k) + (get result k)) + :team-id + :font-id + :font-family + :font-weight + :font-style)))) + + + + + diff --git a/frontend/package.json b/frontend/package.json index e011a8043f..21a8f90694 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,14 +29,14 @@ "map-stream": "0.0.7", "marked": "^2.0.3", "mkdirp": "^1.0.4", - "postcss": "^8.2.7", + "postcss": "^8.2.15", "postcss-clean": "^1.2.2", "rimraf": "^3.0.0", "sass": "^1.32.8", - "shadow-cljs": "2.12.5" + "shadow-cljs": "2.12.6" }, "dependencies": { - "date-fns": "^2.21.1", + "date-fns": "^2.21.3", "draft-js": "^0.11.7", "highlight.js": "^10.6.0", "js-beautify": "^1.13.5", @@ -46,7 +46,7 @@ "randomcolor": "^0.6.2", "react": "~17.0.1", "react-dom": "~17.0.1", - "rxjs": "~7.0.0-beta.12", + "rxjs": "~7.0.1", "source-map-support": "^0.5.16", "tdigest": "^0.1.1", "ua-parser-js": "^0.7.28", diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index a845f442b2..06cf32402c 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -3,7 +3,7 @@ :jvm-opts ["-Xmx600m" "-Xms50m" "-XX:+UseSerialGC"] :dev-http {8888 "classpath:public"} - :source-paths ["src", "vendor", "resources", "../common", "tests"] + :source-paths ["src", "vendor", "resources", "../common", "tests", "dev"] :dependencies [[binaryage/devtools "RELEASE"] @@ -19,7 +19,7 @@ [funcool/okulary "2020.04.14-0"] [funcool/potok "4.0.0"] [funcool/promesa "6.0.0"] - [funcool/rumext "2021.01.26-0"] + [funcool/rumext "2021.05.12-1"] [lambdaisland/uri "1.4.54" :exclusions [org.clojure/data.json]] diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index 850380517c..e3eb7810d0 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -5,14 +5,22 @@ ;; Copyright (c) UXBOX Labs SL (ns app.main.data.shortcuts + (:refer-clojure :exclude [meta reset!]) (:require ["mousetrap" :as mousetrap] + [app.common.data :as d] + [app.common.spec :as us] [app.config :as cfg] - [app.util.logging :as log]) - (:refer-clojure :exclude [meta])) + [app.util.logging :as log] + [cljs.spec.alpha :as s] + [potok.core :as ptk])) (log/set-level! :warn) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (def mac-command "\u2318") (def mac-option "\u2325") (def mac-delete "\u232B") @@ -44,30 +52,8 @@ [shortcut] (c-mod (a-mod shortcut))) -(defn bind-shortcuts - ([shortcuts-config] - (bind-shortcuts - shortcuts-config - mousetrap/bind - (fn [key cb] - (fn [event] - (log/debug :msg (str "Shortcut" key)) - (.preventDefault event) - (cb event))))) - - ([shortcuts-config bind-fn cb-fn] - (doseq [[key {:keys [command disabled fn type]}] shortcuts-config] - (when-not disabled - (if (vector? command) - (doseq [cmd (seq command)] - (bind-fn cmd (cb-fn key fn) type)) - (bind-fn command (cb-fn key fn) type)))))) - -(defn remove-shortcuts - [] - (mousetrap/reset)) - -(defn meta [key] +(defn meta + [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)) @@ -80,37 +66,120 @@ "Ctrl+") key))) -(defn shift [key] +(defn shift + [key] (str (if (cfg/check-platform? :macos) mac-shift "Shift+") key)) -(defn alt [key] +(defn alt + [key] (str (if (cfg/check-platform? :macos) mac-option "Alt+") key)) -(defn meta-shift [key] +(defn meta-shift + [key] (-> key meta shift)) -(defn meta-alt [key] +(defn meta-alt + [key] (-> key meta alt)) -(defn supr [] +(defn supr + [] (if (cfg/check-platform? :macos) mac-delete "Supr")) -(defn esc [] +(defn esc + [] (if (cfg/check-platform? :macos) mac-esc "Escape")) -(defn enter [] +(defn enter + [] (if (cfg/check-platform? :macos) mac-enter "Enter")) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Events +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- EVENT: push + +(s/def ::tooltip ::us/string) +(s/def ::fn fn?) + +(s/def ::command + (s/or :str ::us/string + :vec vector?)) + +(s/def ::shortcut + (s/keys :req-un [::command] + :opt-un [::fn + ::tooltip])) + +(s/def ::shortcuts + (s/map-of ::us/keyword + ::shortcut)) + +(defn- wrap-cb + [key cb] + (fn [event] + (log/debug :msg (str "Shortcut" key)) + (.preventDefault event) + (cb event))) + +(defn- bind! + [shortcuts] + (->> shortcuts + (remove #(:disabled (second %))) + (run! (fn [[key {:keys [command fn type]}]] + (if (vector? command) + (run! #(mousetrap/bind % (wrap-cb key fn) type) command) + (mousetrap/bind command (wrap-cb key fn) type)))))) + +(defn- reset! + ([] + (mousetrap/reset)) + ([shortcuts] + (mousetrap/reset) + (bind! shortcuts))) + +(defn push-shortcuts + [key shortcuts] + (us/assert ::us/keyword key) + (us/assert ::shortcuts shortcuts) + (ptk/reify ::push-shortcuts + ptk/UpdateEvent + (update [_ state] + (-> state + (update :shortcuts (fnil conj '()) [key shortcuts]))) + + ptk/EffectEvent + (effect [_ state stream] + (let [[key shortcuts] (peek (:shortcuts state))] + (reset! shortcuts))))) + +(defn pop-shortcuts + [key] + (ptk/reify ::pop-shortcuts + ptk/UpdateEvent + (update [_ state] + (update state :shortcuts (fn [shortcuts] + (let [current-key (first (peek shortcuts))] + (if (= key current-key) + (pop shortcuts) + shortcuts))))) + ptk/EffectEvent + (effect [_ state stream] + (let [[key* shortcuts] (peek (:shortcuts state))] + (when (not= key key*) + (reset! shortcuts)))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 085d3c4274..6a3ce495f4 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -161,40 +161,40 @@ (->> stream (rx/filter (ptk/type? ::dwp/bundle-fetched)) (rx/take 1) - (rx/map deref) - (rx/mapcat (fn [bundle] - (rx/of (dwn/initialize file-id) - (dwp/initialize-file-persistence file-id) - (dwc/initialize-indices bundle))))) + (rx/mapcat (fn [{:keys [project] :as bundle}] + (rx/merge + (rx/of (dwn/initialize file-id) + (dwp/initialize-file-persistence file-id) + (dwc/initialize-indices bundle)) - ;; Mark file initialized when indexes are ready - (->> stream - (rx/filter #(= ::dwc/index-initialized %)) - (rx/first) - (rx/map (fn [] - (file-initialized project-id file-id)))) - - )))) + (->> stream + (rx/filter #(= ::dwc/index-initialized %)) + (rx/first) + (rx/map #(file-initialized bundle))))))))))) (defn- file-initialized - [project-id file-id] + [{:keys [file users project libraries] :as bundle}] (ptk/reify ::file-initialized ptk/UpdateEvent (update [_ state] - (update state :workspace-file - (fn [file] - (if (= (:id file) file-id) - (assoc file :initialized true) - file)))) + (assoc state + :current-team-id (:team-id project) + :users (d/index-by :id users) + :workspace-undo {} + :workspace-project project + :workspace-file (assoc file :initialized true) + :workspace-data (:data file) + :workspace-libraries (d/index-by :id libraries))) ptk/WatchEvent (watch [it state stream] - (let [ignore-until (get-in state [:workspace-file :ignore-sync-until]) + (let [file-id (:id file) + ignore-until (:ignore-sync-until file) needs-update? (some #(and (> (:modified-at %) (:synced-at %)) (or (not ignore-until) (> (:modified-at %) ignore-until))) - (vals (get state :workspace-libraries)))] + libraries)] (when needs-update? (rx/of (dwl/notify-sync-file file-id))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 2c8f0d7fcf..7a6d3fa592 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -263,29 +263,14 @@ (rp/query :team-users {:file-id file-id}) (rp/query :project {:id project-id}) (rp/query :file-libraries {:file-id file-id})) - (rx/first) - (rx/map (fn [bundle] (apply bundle-fetched bundle))))))) - -(defn- bundle-fetched - [file users project libraries] - (ptk/reify ::bundle-fetched - IDeref - (-deref [_] - {:file file - :users users - :project project - :libraries libraries}) - - ptk/UpdateEvent - (update [_ state] - (assoc state - :users (d/index-by :id users) - :workspace-undo {} - :workspace-project project - :workspace-file file - :workspace-data (:data file) - :workspace-libraries (d/index-by :id libraries))))) - + (rx/take 1) + (rx/map (fn [[file users project libraries]] + {:file file + :users users + :project project + :libraries libraries})) + (rx/mapcat (fn [{:keys [project] :as bundle}] + (rx/of (ptk/data-event ::bundle-fetched bundle)))))))) ;; --- Set File shared diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 64b41594f5..7d2fe9bf4f 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -33,7 +33,7 @@ :toggle-assets {:tooltip (ds/alt "I") :command (ds/a-mod "i") :fn #(st/emit! (dw/go-to-layout :assets))} - + :toggle-history {:tooltip (ds/alt "H") :command (ds/a-mod "h") :fn #(st/emit! (dw/go-to-layout :document-history))} @@ -45,7 +45,7 @@ :toggle-rules {:tooltip (ds/meta-shift "R") :command (ds/c-mod "shift+r") :fn #(st/emit! (dw/toggle-layout-flags :rules))} - + :select-all {:tooltip (ds/meta "A") :command (ds/c-mod "a") :fn #(st/emit! (dw/select-all))} @@ -73,7 +73,7 @@ :decrease-zoom {:tooltip "-" :command "-" :fn #(st/emit! (dw/decrease-zoom nil))} - + :group {:tooltip (ds/meta "G") :command (ds/c-mod "g") :fn #(st/emit! dw/group-selected)} @@ -173,7 +173,8 @@ :paste {:tooltip (ds/meta "V") :disabled true - :command (ds/c-mod "v")} + :command (ds/c-mod "v") + :fn (constantly nil)} :delete {:tooltip (ds/supr) :command ["del" "backspace"] diff --git a/frontend/src/app/main/ui/components/editable_select.cljs b/frontend/src/app/main/ui/components/editable_select.cljs index e9de807d72..27fe4d897e 100644 --- a/frontend/src/app/main/ui/components/editable_select.cljs +++ b/frontend/src/app/main/ui/components/editable_select.cljs @@ -44,17 +44,18 @@ (fn [node] ;; There is a problem when changing the state in this callback that ;; produces the dropdown to close in the same event - (timers/schedule - #(when-let [bounds (when node (dom/get-bounding-rect node))] - (let [{window-height :height} (dom/get-window-size) - {:keys [left top height]} bounds - bottom (when (< (- window-height top) 300) (- window-height top)) - top (when (>= (- window-height top) 300) (+ top height))] - (swap! state - assoc - :left left - :top top - :bottom bottom)))))] + (when node + (timers/schedule + #(when-let [bounds (when node (dom/get-bounding-rect node))] + (let [{window-height :height} (dom/get-window-size) + {:keys [left top height]} bounds + bottom (when (< (- window-height top) 300) (- window-height top)) + top (when (>= (- window-height top) 300) (+ top height))] + (swap! state + assoc + :left left + :top top + :bottom bottom))))))] (mf/use-effect (mf/deps value) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index df35fe7cf1..b6a8b10d25 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -26,6 +26,7 @@ [app.util.router :as rt] [app.util.time :as dt] [app.util.timers :as ts] + [app.util.webapi :as wapi] [beicon.core :as rx] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -255,17 +256,19 @@ (mf/use-effect (fn [] - (let [node (mf/ref-val rowref) - obs (new js/ResizeObserver - (fn [entries x] - (ts/raf #(let [row (first entries) - row-rect (.-contentRect ^js row) - row-width (.-width ^js row-rect)] - (reset! width row-width)))))] - - (.observe ^js obs node) + (let [node (mf/ref-val rowref) + mnt? (volatile! true) + sub (->> (wapi/observe-resize node) + (rx/observe-on :af) + (rx/subs (fn [entries] + (let [row (first entries) + row-rect (.-contentRect ^js row) + row-width (.-width ^js row-rect)] + (when @mnt? + (reset! width row-width))))))] (fn [] - (.disconnect ^js obs))))) + (vreset! mnt? false) + (rx/dispose! sub))))) [:div.grid-row.no-wrap {:ref rowref} (when dragging? diff --git a/frontend/src/app/main/ui/handoff.cljs b/frontend/src/app/main/ui/handoff.cljs index 0fd4817711..ee82788f08 100644 --- a/frontend/src/app/main/ui/handoff.cljs +++ b/frontend/src/app/main/ui/handoff.cljs @@ -91,7 +91,7 @@ (events/unlistenByKey key1))))] (mf/use-effect on-mount) - (hooks/use-shortcuts sc/shortcuts) + (hooks/use-shortcuts ::handoff sc/shortcuts) [:div.handoff-layout {:class (dom/classnames :force-visible (:show-thumbnails state))} diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index bc2a8fd2df..5b0b0645e8 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -9,10 +9,11 @@ (:require [app.common.spec :as us] [app.main.data.shortcuts :as dsc] + [app.main.store :as st] [app.util.dom :as dom] - [app.util.object :as obj] [app.util.dom.dnd :as dnd] [app.util.logging :as log] + [app.util.object :as obj] [app.util.timers :as ts] [app.util.transit :as t] [app.util.webapi :as wapi] @@ -35,11 +36,13 @@ state)) (defn use-shortcuts - [shortcuts] + [key shortcuts] (mf/use-effect + #js [(str key) shortcuts] (fn [] - (dsc/bind-shortcuts shortcuts) - (fn [] (dsc/remove-shortcuts))))) + (st/emit! (dsc/push-shortcuts key shortcuts)) + (fn [] + (st/emit! (dsc/pop-shortcuts key)))))) (defn invisible-image [] diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index bbb9404134..930968526a 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -237,7 +237,7 @@ (events/unlistenByKey key3))))] (mf/use-effect on-mount) - (hooks/use-shortcuts sc/shortcuts) + (hooks/use-shortcuts ::viewer sc/shortcuts) [:div.viewer-layout {:class (dom/classnames :force-visible (:show-thumbnails state))} diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index bb7f69c929..6fc58ebd2c 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -138,7 +138,7 @@ (mf/use-effect (fn [] ;; Close any non-modal dialog that may be still open - (st/emitf dm/hide))) + (st/emit! dm/hide))) (mf/use-effect (mf/deps file) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 084da1e56a..9e4a3efbec 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -21,6 +21,7 @@ [app.util.logging :as log] [app.util.object :as obj] [app.util.timers :as timers] + [app.util.webapi :as wapi] [app.util.text-editor :as ted] [okulary.core :as l] [beicon.core :as rx] @@ -62,6 +63,7 @@ (true? (obj/get props "edition?")) (update-with-current-editor-state)) + mnt (mf/use-ref true) paragraph-ref (mf/use-state nil) handle-resize-text @@ -83,20 +85,24 @@ (mf/deps handle-resize-text) (fn [node] (when node - (let [obs-ref (atom nil)] - (timers/schedule - (fn [] - (when-let [ps-node (dom/query node ".paragraph-set")] - (reset! paragraph-ref ps-node))))))))] + (timers/schedule + #(when (mf/ref-val mnt) + (when-let [ps-node (dom/query node ".paragraph-set")] + (reset! paragraph-ref ps-node)))))))] (mf/use-effect (mf/deps @paragraph-ref handle-resize-text grow-type) (fn [] (when-let [paragraph-node @paragraph-ref] - (let [observer (js/ResizeObserver. handle-resize-text)] + (let [sub (->> (wapi/observe-resize paragraph-node) + (rx/observe-on :af) + (rx/subs handle-resize-text))] (log/debug :msg "Attach resize observer" :shape-id id :shape-name name) - (.observe observer paragraph-node) - #(.disconnect observer))))) + (fn [] + (rx/dispose! sub)))))) + + (mf/use-effect + (fn [] #(mf/set-ref-val! mnt false))) [:& text/text-shape {:ref text-ref-cb :shape shape :grow-type (:grow-type shape)}])) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index d5837c2e45..c79651342e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -153,18 +153,6 @@ (utils/update-transform render-node roots modifiers) (utils/remove-transform render-node roots)))))) -(defn setup-shortcuts [path-editing? drawing-path?] - (mf/use-effect - (mf/deps path-editing? drawing-path?) - (fn [] - (cond - (or drawing-path? path-editing?) - (dsc/bind-shortcuts psc/shortcuts) - - :else - (dsc/bind-shortcuts wsc/shortcuts)) - dsc/remove-shortcuts))) - (defn inside-vbox [vbox objects frame-id] (let [frame (get objects frame-id)] @@ -195,3 +183,17 @@ (:frame-id @hover))] (when (not (contains? @active-frames frame-id)) (swap! active-frames assoc frame-id true)))))) + +;; NOTE: this is executed on each page change, maybe we need to move +;; this shortcuts outside the viewport? + +(defn setup-shortcuts + [path-editing? drawing-path?] + (hooks/use-shortcuts ::workspace wsc/shortcuts) + + (mf/use-effect + (mf/deps path-editing? drawing-path?) + (fn [] + (when (or drawing-path? path-editing?) + (st/emit! (dsc/push-shortcuts ::path psc/shortcuts)) + (st/emitf (dsc/pop-shortcuts ::path)))))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 95679b7c80..7601435ca6 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -293,3 +293,21 @@ (defn remove-attribute [^js node ^string attr] (.removeAttribute node attr)) + +(defn scroll-into-view! + ([element] + (.scrollIntoView ^js element false)) + ([element scroll-top] + (.scrollIntoView ^js element scroll-top))) + +(defn is-in-viewport? + [element] + (let [rect (.getBoundingClientRect element) + height (or (.-innerHeight js/window) + (.. js/document -documentElement -clientHeight)) + width (or (.-innerWidth js/window) + (.. js/document -documentElement -clientWidth))] + (and (>= (.-top rect) 0) + (>= (.-left rect) 0) + (<= (.-bottom rect) height) + (<= (.-right rect) width)))) diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 9664de3a8a..0da4d753a4 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -131,3 +131,14 @@ :else (ex/raise :type :not-supported :hint "seems like the current browset does not support fullscreen api."))) + +(defn observe-resize + [node] + (rx/create + (fn [subs] + (let [obs (js/ResizeObserver. + (fn [entries x] + (rx/push! subs entries)))] + (.observe ^js obs node) + (fn [] + (.disconnect ^js obs)))))) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 0390385c18..9ab017eaed 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1169,10 +1169,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.21.1: - version "2.21.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0" - integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA== +date-fns@^2.21.3: + version "2.21.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.3.tgz#8f5f6889d7a96bbcc1f0ea50239b397a83357f9b" + integrity sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw== dateformat@^3.0.3: version "3.0.3" @@ -3343,10 +3343,10 @@ nan@^2.12.1, nan@^2.13.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nanoid@^3.1.22: - version "3.1.22" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" - integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== +nanoid@^3.1.23: + version "3.1.23" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" + integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== nanomatch@^1.2.9: version "1.2.13" @@ -3962,13 +3962,13 @@ postcss@^7.0.16: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^8.2.7: - version "8.2.13" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.13.tgz#dbe043e26e3c068e45113b1ed6375d2d37e2129f" - integrity sha512-FCE5xLH+hjbzRdpbRb1IMCvPv9yZx2QnDarBEYSN0N0HYk+TcXsEhwdFcFb+SRWOKzKGErhIEbBK2ogyLdTtfQ== +postcss@^8.2.15: + version "8.2.15" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65" + integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q== dependencies: colorette "^1.2.2" - nanoid "^3.1.22" + nanoid "^3.1.23" source-map "^0.6.1" pretty-hrtime@^1.0.0: @@ -4376,10 +4376,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rxjs@~7.0.0-beta.12: - version "7.0.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0.tgz#c55d67c52aee8804d32ab60965e335bd41e2dc2d" - integrity sha512-I1V/ArAtGJg4kmCfms8fULm0SwYgEsAf2d5WPCBGzTYm2qTjO3Tx4EDFaGjbOox8CeEsC69jQK22mnmfyA26sw== +rxjs@~7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.1.tgz#5f41c4f991cea550471fc5d215727390103702c7" + integrity sha512-wViQ4Vgps1xJwqWIBooMNN44usCSthL7wCUl4qWqrVjhGfWyVyXcxlYzfDKkJKACQvZMTOft/jJ3RkbwK1j9QQ== dependencies: tslib "~2.1.0" @@ -4493,10 +4493,10 @@ shadow-cljs-jar@1.3.2: resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== -shadow-cljs@2.12.5: - version "2.12.5" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4" - integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ== +shadow-cljs@2.12.6: + version "2.12.6" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.6.tgz#039539fdc35a19c2f2cd15792ae17e7928f97428" + integrity sha512-dNw989EFQki/59kD8Cd8b6HIpBTqPj9ksWIvSg6hI1bgezZHT0oHfJH5UIbXPD+dnVLvbOnDnfOMWYH6ozalcA== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7"