mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
cf94b56154
@ -64,6 +64,11 @@
|
||||
- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
|
||||
- Fix incorrect query for file versions [Github #8463](https://github.com/penpot/penpot/pull/8463)
|
||||
- Fix warning when clicking on number token pills [Taiga #13661](https://tree.taiga.io/project/penpot/issue/13661)
|
||||
- Fix 'not ISeqable' error when entering float values in layout item and opacity inputs [Github #8569](https://github.com/penpot/penpot/pull/8569)
|
||||
- Fix crash in select component when options vector is empty [Github #8578](https://github.com/penpot/penpot/pull/8578)
|
||||
- Fix scroll on colorpicker [Taiga #13623](https://tree.taiga.io/project/penpot/issue/13623)
|
||||
- Fix crash when pasting non-map transit clipboard data [Github #8580](https://github.com/penpot/penpot/pull/8580)
|
||||
- Fix `penpot.openPage()` plugin API not navigating in the same tab; change default to same-tab navigation and allow passing a UUID string instead of a Page object [Github #8520](https://github.com/penpot/penpot/issues/8520)
|
||||
|
||||
## 2.13.3
|
||||
|
||||
|
||||
@ -205,40 +205,41 @@
|
||||
(defn format-inst
|
||||
([v] (format-inst v :iso))
|
||||
([v fmt]
|
||||
(case fmt
|
||||
(:iso :iso8601)
|
||||
#?(:clj (.format DateTimeFormatter/ISO_INSTANT ^Instant v)
|
||||
:cljs (dfn-format-iso v))
|
||||
(when (some? v)
|
||||
(case fmt
|
||||
(:iso :iso8601)
|
||||
#?(:clj (.format DateTimeFormatter/ISO_INSTANT ^Instant v)
|
||||
:cljs (dfn-format-iso v))
|
||||
|
||||
:iso-date
|
||||
#?(:clj (.format DateTimeFormatter/ISO_LOCAL_DATE
|
||||
^ZonedDateTime (ZonedDateTime/ofInstant v (ZoneId/of "UTC")))
|
||||
:cljs (dfn-format-iso v #js {:representation "date"}))
|
||||
:iso-date
|
||||
#?(:clj (.format DateTimeFormatter/ISO_LOCAL_DATE
|
||||
^ZonedDateTime (ZonedDateTime/ofInstant v (ZoneId/of "UTC")))
|
||||
:cljs (dfn-format-iso v #js {:representation "date"}))
|
||||
|
||||
(:rfc1123 :http)
|
||||
#?(:clj (.format DateTimeFormatter/RFC_1123_DATE_TIME
|
||||
^ZonedDateTime (ZonedDateTime/ofInstant v (ZoneId/of "UTC")))
|
||||
:cljs (dfn-format v "EEE, dd LLL yyyy HH:mm:ss 'GMT'"))
|
||||
(:rfc1123 :http)
|
||||
#?(:clj (.format DateTimeFormatter/RFC_1123_DATE_TIME
|
||||
^ZonedDateTime (ZonedDateTime/ofInstant v (ZoneId/of "UTC")))
|
||||
:cljs (dfn-format v "EEE, dd LLL yyyy HH:mm:ss 'GMT'"))
|
||||
|
||||
#?@(:cljs [:time-24-simple
|
||||
(dfn-format v "HH:mm")
|
||||
#?@(:cljs [:time-24-simple
|
||||
(dfn-format v "HH:mm")
|
||||
|
||||
;; DEPRECATED
|
||||
:date-full
|
||||
(dfn-format v "PPP")
|
||||
;; DEPRECATED
|
||||
:date-full
|
||||
(dfn-format v "PPP")
|
||||
|
||||
:localized-date
|
||||
(dfn-format v "PPP")
|
||||
:localized-date
|
||||
(dfn-format v "PPP")
|
||||
|
||||
:localized-time
|
||||
(dfn-format v "p")
|
||||
:localized-time
|
||||
(dfn-format v "p")
|
||||
|
||||
:localized-date-time
|
||||
(dfn-format v "PPP . p")
|
||||
:localized-date-time
|
||||
(dfn-format v "PPP . p")
|
||||
|
||||
(if (string? fmt)
|
||||
(dfn-format v fmt)
|
||||
(throw (js/Error. "unpexted format")))]))))
|
||||
(if (string? fmt)
|
||||
(dfn-format v fmt)
|
||||
(throw (js/Error. "unpexted format")))])))))
|
||||
|
||||
#?(:cljs
|
||||
(def locales
|
||||
|
||||
@ -212,25 +212,39 @@
|
||||
(get-instance-root objects (get objects (:parent-id shape)))))
|
||||
|
||||
(defn find-component-main
|
||||
"If the shape is a component main instance or is inside one, return that instance"
|
||||
"If the shape is a component main instance or is inside one, return that instance.
|
||||
Uses an iterative loop with cycle detection to prevent stack overflow on circular
|
||||
parent references or malformed data structures."
|
||||
([objects shape]
|
||||
(find-component-main objects shape true))
|
||||
([objects shape only-direct-child?]
|
||||
(cond
|
||||
(or (nil? shape) (cfh/root? shape))
|
||||
nil
|
||||
(nil? (:parent-id shape)) ; This occurs in the root of components v1
|
||||
shape
|
||||
(ctk/main-instance? shape)
|
||||
shape
|
||||
(and only-direct-child? ;; If we are asking only for direct childs of a component-main,
|
||||
(ctk/instance-head? shape)) ;; stop when we found a instance-head that isn't main-instance
|
||||
nil
|
||||
(and (not only-direct-child?)
|
||||
(ctk/instance-root? shape))
|
||||
nil
|
||||
:else
|
||||
(find-component-main objects (get objects (:parent-id shape))))))
|
||||
(loop [shape shape
|
||||
visited #{}]
|
||||
(cond
|
||||
(or (nil? shape) (cfh/root? shape))
|
||||
nil
|
||||
|
||||
;; Cycle detected: we have already visited this shape id
|
||||
(contains? visited (:id shape))
|
||||
nil
|
||||
|
||||
(nil? (:parent-id shape)) ; This occurs in the root of components v1
|
||||
shape
|
||||
|
||||
(ctk/main-instance? shape)
|
||||
shape
|
||||
|
||||
(and only-direct-child? ;; If we are asking only for direct childs of a component-main,
|
||||
(ctk/instance-head? shape)) ;; stop when we found a instance-head that isn't main-instance
|
||||
nil
|
||||
|
||||
(and (not only-direct-child?)
|
||||
(ctk/instance-root? shape))
|
||||
nil
|
||||
|
||||
:else
|
||||
(recur (get objects (:parent-id shape))
|
||||
(conj visited (:id shape)))))))
|
||||
|
||||
(defn inside-component-main?
|
||||
"Check if the shape is a component main instance or is inside one."
|
||||
|
||||
@ -777,7 +777,7 @@
|
||||
content as PathData instance."
|
||||
[content transform]
|
||||
(if (some? transform)
|
||||
(impl/-transform content transform)
|
||||
(impl/-transform (impl/path-data content) transform)
|
||||
content))
|
||||
|
||||
(defn move-content
|
||||
|
||||
@ -168,7 +168,7 @@
|
||||
child-as-paths)]
|
||||
(-> group
|
||||
(assoc :type :path)
|
||||
(assoc :content content)
|
||||
(assoc :content (path.impl/path-data content))
|
||||
(merge head-data)
|
||||
(d/without-keys dissoc-attrs))))
|
||||
|
||||
@ -184,7 +184,8 @@
|
||||
(:bool-type shape)
|
||||
|
||||
content
|
||||
(bool/calculate-content bool-type (map :content children))]
|
||||
(-> (bool/calculate-content bool-type (map :content children))
|
||||
(path.impl/path-data))]
|
||||
|
||||
(-> shape
|
||||
(assoc :type :path)
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
[common-tests.time-test]
|
||||
[common-tests.types.absorb-assets-test]
|
||||
[common-tests.types.components-test]
|
||||
[common-tests.types.container-test]
|
||||
[common-tests.types.fill-test]
|
||||
[common-tests.types.modifiers-test]
|
||||
[common-tests.types.objects-map-test]
|
||||
@ -92,6 +93,7 @@
|
||||
'common-tests.time-test
|
||||
'common-tests.types.absorb-assets-test
|
||||
'common-tests.types.components-test
|
||||
'common-tests.types.container-test
|
||||
'common-tests.types.fill-test
|
||||
'common-tests.types.modifiers-test
|
||||
'common-tests.types.objects-map-test
|
||||
|
||||
156
common/test/common_tests/types/container_test.cljc
Normal file
156
common/test/common_tests/types/container_test.cljc
Normal file
@ -0,0 +1,156 @@
|
||||
;; 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) KALEIDOS INC
|
||||
|
||||
(ns common-tests.types.container-test
|
||||
(:require
|
||||
[app.common.types.container :as ctc]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn- make-shape
|
||||
"Build a realistic shape using setup-shape, so it has proper geometric
|
||||
data (selrect, points, transform, …) and follows project data standards."
|
||||
[id & {:as attrs}]
|
||||
(cts/setup-shape (merge {:type :rect
|
||||
:x 0
|
||||
:y 0
|
||||
:width 100
|
||||
:height 100}
|
||||
attrs
|
||||
{:id id})))
|
||||
|
||||
(defn- objects-map
|
||||
"Build an objects map from a seq of shapes."
|
||||
[& shapes]
|
||||
(into {} (map (juxt :id identity) shapes)))
|
||||
|
||||
;; The sentinel root shape (uuid/zero) recognised by cfh/root?
|
||||
(def root-id uuid/zero)
|
||||
|
||||
(defn- root-shape
|
||||
"Create the page-root frame shape (id = uuid/zero, type :frame)."
|
||||
[]
|
||||
(cts/setup-shape {:id root-id
|
||||
:type :frame
|
||||
:x 0
|
||||
:y 0
|
||||
:width 100
|
||||
:height 100}))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests – base cases
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest find-component-main-nil-shape
|
||||
(t/testing "returns nil when shape is nil"
|
||||
(t/is (nil? (ctc/find-component-main {} nil)))))
|
||||
|
||||
(t/deftest find-component-main-root-shape
|
||||
(t/testing "returns nil when shape is the page root (uuid/zero)"
|
||||
(let [root (root-shape)
|
||||
objects (objects-map root)]
|
||||
(t/is (nil? (ctc/find-component-main objects root))))))
|
||||
|
||||
(t/deftest find-component-main-no-parent-id
|
||||
(t/testing "returns the shape itself when parent-id is nil (v1 component root)"
|
||||
(let [id (uuid/next)
|
||||
;; Simulate a v1 component root: setup-shape produces a full shape,
|
||||
;; then we explicitly clear :parent-id to nil, which is how legacy
|
||||
;; component roots appear in deserialized data.
|
||||
shape (assoc (make-shape id) :parent-id nil)
|
||||
objects (objects-map shape)]
|
||||
(t/is (= shape (ctc/find-component-main objects shape))))))
|
||||
|
||||
(t/deftest find-component-main-main-instance
|
||||
(t/testing "returns the shape when it is a main-instance"
|
||||
(let [parent-id (uuid/next)
|
||||
id (uuid/next)
|
||||
parent (make-shape parent-id)
|
||||
shape (make-shape id :parent-id parent-id :main-instance true)
|
||||
objects (objects-map parent shape)]
|
||||
(t/is (= shape (ctc/find-component-main objects shape))))))
|
||||
|
||||
(t/deftest find-component-main-instance-head-stops-when-only-direct-child
|
||||
(t/testing "returns nil when hitting an instance-head that is not main (only-direct-child? true)"
|
||||
(let [parent-id (uuid/next)
|
||||
id (uuid/next)
|
||||
;; instance-head? ← has :component-id but NOT :main-instance
|
||||
shape (make-shape id
|
||||
:parent-id parent-id
|
||||
:component-id (uuid/next))
|
||||
parent (make-shape parent-id)
|
||||
objects (objects-map parent shape)]
|
||||
(t/is (nil? (ctc/find-component-main objects shape true))))))
|
||||
|
||||
(t/deftest find-component-main-instance-root-stops-when-not-only-direct-child
|
||||
(t/testing "returns nil when hitting an instance-root and only-direct-child? is false"
|
||||
(let [parent-id (uuid/next)
|
||||
id (uuid/next)
|
||||
;; instance-root? ← has :component-root true
|
||||
shape (make-shape id
|
||||
:parent-id parent-id
|
||||
:component-id (uuid/next)
|
||||
:component-root true)
|
||||
parent (make-shape parent-id)
|
||||
objects (objects-map parent shape)]
|
||||
(t/is (nil? (ctc/find-component-main objects shape false))))))
|
||||
|
||||
(t/deftest find-component-main-walks-to-main-ancestor
|
||||
(t/testing "traverses ancestors and returns the first main-instance found"
|
||||
(let [gp-id (uuid/next)
|
||||
p-id (uuid/next)
|
||||
child-id (uuid/next)
|
||||
grandparent (make-shape gp-id :parent-id nil :main-instance true)
|
||||
parent (make-shape p-id :parent-id gp-id)
|
||||
child (make-shape child-id :parent-id p-id)
|
||||
objects (objects-map grandparent parent child)]
|
||||
(t/is (= grandparent (ctc/find-component-main objects child))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests – cycle detection (the bug fix)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest find-component-main-direct-self-loop
|
||||
(t/testing "returns nil (no crash) when a shape's parent-id points to itself"
|
||||
(let [id (uuid/next)
|
||||
;; deliberately malformed: parent-id == id (self-loop)
|
||||
shape (make-shape id :parent-id id)
|
||||
objects (objects-map shape)]
|
||||
(t/is (nil? (ctc/find-component-main objects shape))))))
|
||||
|
||||
(t/deftest find-component-main-two-node-cycle
|
||||
(t/testing "returns nil (no crash) for a two-node circular reference A→B→A"
|
||||
(let [id-a (uuid/next)
|
||||
id-b (uuid/next)
|
||||
shape-a (make-shape id-a :parent-id id-b)
|
||||
shape-b (make-shape id-b :parent-id id-a)
|
||||
objects (objects-map shape-a shape-b)]
|
||||
(t/is (nil? (ctc/find-component-main objects shape-a))))))
|
||||
|
||||
(t/deftest find-component-main-multi-node-cycle
|
||||
(t/testing "returns nil (no crash) for a longer cycle A→B→C→A"
|
||||
(let [id-a (uuid/next)
|
||||
id-b (uuid/next)
|
||||
id-c (uuid/next)
|
||||
shape-a (make-shape id-a :parent-id id-b)
|
||||
shape-b (make-shape id-b :parent-id id-c)
|
||||
shape-c (make-shape id-c :parent-id id-a)
|
||||
objects (objects-map shape-a shape-b shape-c)]
|
||||
(t/is (nil? (ctc/find-component-main objects shape-a))))))
|
||||
|
||||
(t/deftest find-component-main-only-direct-child-with-cycle
|
||||
(t/testing "cycle detection works correctly with only-direct-child? false as well"
|
||||
(let [id-a (uuid/next)
|
||||
id-b (uuid/next)
|
||||
shape-a (make-shape id-a :parent-id id-b)
|
||||
shape-b (make-shape id-b :parent-id id-a)
|
||||
objects (objects-map shape-a shape-b)]
|
||||
(t/is (nil? (ctc/find-component-main objects shape-a false))))))
|
||||
@ -270,6 +270,15 @@
|
||||
(t/is (= result1 result2))
|
||||
(t/is (= result2 result3))))
|
||||
|
||||
(t/deftest path-get-points-nil-safe
|
||||
(t/testing "path/get-points returns nil for nil content without throwing"
|
||||
(t/is (nil? (path/get-points nil))))
|
||||
(t/testing "path/get-points returns correct points for valid content"
|
||||
(let [content (path/content sample-content)
|
||||
points (path/get-points content)]
|
||||
(t/is (some? points))
|
||||
(t/is (= 3 (count points))))))
|
||||
|
||||
(defn calculate-extremities
|
||||
"Calculate extremities for the provided content.
|
||||
A legacy implementation used mainly as reference for testing"
|
||||
|
||||
@ -258,33 +258,39 @@
|
||||
#js {:decodeTransit t/decode-str
|
||||
:allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")})
|
||||
|
||||
(defn create-paste-from-blob
|
||||
(defn- create-paste-from-blob
|
||||
[in-viewport?]
|
||||
(fn [blob]
|
||||
(let [type (.-type blob)
|
||||
result (cond
|
||||
(= type "image/svg+xml")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-svg-text))
|
||||
(let [type (.-type blob)]
|
||||
(cond
|
||||
(= type "image/svg+xml")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-svg-text))
|
||||
|
||||
(some #(= type %) clipboard/image-types)
|
||||
(rx/of (paste-image blob))
|
||||
(some #(= type %) clipboard/image-types)
|
||||
(rx/of (paste-image blob))
|
||||
|
||||
(= type "text/html")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-html-text))
|
||||
(= type "text/html")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-html-text))
|
||||
|
||||
(= type "application/transit+json")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map (fn [text]
|
||||
(let [transit-data (t/decode-str text)]
|
||||
(assoc transit-data :in-viewport in-viewport?))))
|
||||
(rx/map paste-transit-shapes))
|
||||
(= type "application/transit+json")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map t/decode-str)
|
||||
(rx/filter map?)
|
||||
(rx/map
|
||||
(fn [pdata]
|
||||
(assoc pdata :in-viewport in-viewport?)))
|
||||
(rx/mapcat
|
||||
(fn [pdata]
|
||||
(case (:type pdata)
|
||||
:copied-props (rx/of (paste-transit-props pdata))
|
||||
:copied-shapes (rx/of (paste-transit-shapes pdata))
|
||||
(rx/empty)))))
|
||||
|
||||
:else
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-text)))]
|
||||
result)))
|
||||
:else
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-text))))))
|
||||
|
||||
(def default-paste-from-blob (create-paste-from-blob false))
|
||||
|
||||
|
||||
@ -59,8 +59,8 @@
|
||||
content (get shape :content)
|
||||
new-content (path/apply-content-modifiers content content-modifiers)
|
||||
|
||||
old-points (path.segment/get-points content)
|
||||
new-points (path.segment/get-points new-content)
|
||||
old-points (path/get-points content)
|
||||
new-points (path/get-points new-content)
|
||||
point-change (->> (map hash-map old-points new-points) (reduce merge))]
|
||||
|
||||
(when (and (some? new-content) (some? shape))
|
||||
@ -162,7 +162,7 @@
|
||||
start-position (apply min-key #(gpt/distance start-position %) selected-points)
|
||||
|
||||
content (st/get-path state :content)
|
||||
points (path.segment/get-points content)]
|
||||
points (path/get-points content)]
|
||||
|
||||
(rx/concat
|
||||
;; This stream checks the consecutive mouse positions to do the dragging
|
||||
@ -255,7 +255,7 @@
|
||||
start-delta-y (dm/get-in modifiers [index cy] 0)
|
||||
|
||||
content (st/get-path state :content)
|
||||
points (path.segment/get-points content)
|
||||
points (path/get-points content)
|
||||
|
||||
point (-> content (nth (if (= prefix :c1) (dec index) index)) (path.helpers/segment->point))
|
||||
handler (-> content (nth index) (path.segment/get-handler prefix))
|
||||
|
||||
@ -137,16 +137,16 @@
|
||||
(ptk/reify ::pin-version
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [version (->> (dm/get-in state [:workspace-versions :data])
|
||||
(d/seek #(= (:id %) id)))
|
||||
params {:id id
|
||||
:label (ct/format-inst (:created-at version) :localized-date)}]
|
||||
(when-let [version (->> (dm/get-in state [:workspace-versions :data])
|
||||
(d/seek #(= (:id %) id)))]
|
||||
(let [params {:id id
|
||||
:label (ct/format-inst (:created-at version) :localized-date)}]
|
||||
|
||||
(->> (rp/cmd! :update-file-snapshot params)
|
||||
(rx/mapcat (fn [_]
|
||||
(rx/of (update-versions-state {:editing id})
|
||||
(fetch-versions)
|
||||
(ptk/event ::ev/event {::ev/name "pin-version"})))))))))
|
||||
(->> (rp/cmd! :update-file-snapshot params)
|
||||
(rx/mapcat (fn [_]
|
||||
(rx/of (update-versions-state {:editing id})
|
||||
(fetch-versions)
|
||||
(ptk/event ::ev/event {::ev/name "pin-version"}))))))))))
|
||||
|
||||
(defn lock-version
|
||||
[id]
|
||||
|
||||
@ -350,20 +350,32 @@
|
||||
(st/async-emit! (rt/assign-exception error)))
|
||||
|
||||
(defonce uncaught-error-handler
|
||||
(letfn [(is-ignorable-exception? [cause]
|
||||
(letfn [(from-extension? [cause]
|
||||
(let [stack (.-stack cause)]
|
||||
(and (string? stack)
|
||||
(or (str/includes? stack "chrome-extension://")
|
||||
(str/includes? stack "moz-extension://")))))
|
||||
|
||||
(is-ignorable-exception? [cause]
|
||||
(let [message (ex-message cause)]
|
||||
(or (= message "Possible side-effect in debug-evaluate")
|
||||
(or (from-extension? cause)
|
||||
(= message "Possible side-effect in debug-evaluate")
|
||||
(= message "Unexpected end of input")
|
||||
(str/starts-with? message "invalid props on component")
|
||||
(str/starts-with? message "Unexpected token "))))
|
||||
(str/starts-with? message "Unexpected token ")
|
||||
;; Abort errors are expected when an in-flight HTTP request is
|
||||
;; cancelled (e.g. via RxJS unsubscription / take-until). They
|
||||
;; are handled gracefully inside app.util.http/fetch and must
|
||||
;; NOT be surfaced as application errors.
|
||||
(= (.-name ^js cause) "AbortError"))))
|
||||
|
||||
(on-unhandled-error [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "error")]
|
||||
(set! last-exception cause)
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(let [data (ex-data cause)
|
||||
type (get data :type)]
|
||||
(set! last-exception cause)
|
||||
(if (#{:wasm-critical :wasm-non-blocking :wasm-exception} type)
|
||||
(on-error cause)
|
||||
(do
|
||||
@ -373,9 +385,10 @@
|
||||
(on-unhandled-rejection [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "reason")]
|
||||
(set! last-exception cause)
|
||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||
(ts/schedule #(flash :cause cause :type :unhandled))))]
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(set! last-exception cause)
|
||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||
(ts/schedule #(flash :cause cause :type :unhandled)))))]
|
||||
|
||||
(.addEventListener g/window "error" on-unhandled-error)
|
||||
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
||||
|
||||
@ -11,6 +11,11 @@
|
||||
|
||||
(mf/defc portal-on-document*
|
||||
[{:keys [children]}]
|
||||
(mf/portal
|
||||
(mf/html [:* children])
|
||||
(dom/get-body)))
|
||||
(let [container (mf/use-memo #(dom/create-element "div"))]
|
||||
(mf/with-effect []
|
||||
(let [body (dom/get-body)]
|
||||
(dom/append-child! body container)
|
||||
#(dom/remove-child! body container)))
|
||||
(mf/portal
|
||||
(mf/html [:* children])
|
||||
container)))
|
||||
|
||||
@ -328,7 +328,11 @@
|
||||
;; it right afterwards, in the next render cycle.
|
||||
(dom/append-child! item-el counter-el)
|
||||
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
|
||||
(ts/raf #(dom/remove-child! item-el counter-el))))))
|
||||
;; Guard against race condition: if the user navigates away
|
||||
;; before the RAF fires, item-el may have been unmounted and
|
||||
;; counter-el is no longer a child — removeChild would throw.
|
||||
(ts/raf #(when (dom/child? counter-el item-el)
|
||||
(dom/remove-child! item-el counter-el)))))))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-fn
|
||||
|
||||
@ -116,13 +116,13 @@
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(st/emit!
|
||||
(dcm/go-to-dashboard-files :project-id project-id)
|
||||
(ts/schedule-on-idle
|
||||
(fn []
|
||||
(when-let [title (dom/get-element (str project-id))]
|
||||
(dom/set-attribute! title "tabindex" "0")
|
||||
(dom/focus! title)
|
||||
(dom/set-attribute! title "tabindex" "-1"))))))))
|
||||
(dcm/go-to-dashboard-files :project-id project-id))
|
||||
(ts/schedule
|
||||
(fn []
|
||||
(when-let [title (dom/get-element (str project-id))]
|
||||
(dom/set-attribute! title "tabindex" "0")
|
||||
(dom/focus! title)
|
||||
(dom/set-attribute! title "tabindex" "-1")))))))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-fn
|
||||
@ -246,7 +246,7 @@
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when (kbd/enter? e)
|
||||
(ts/schedule-on-idle
|
||||
(ts/schedule
|
||||
(fn []
|
||||
(let [search-title (dom/get-element (str "dashboard-search-title"))]
|
||||
(when search-title
|
||||
@ -820,13 +820,13 @@
|
||||
(mf/deps team-id)
|
||||
(fn []
|
||||
(st/emit!
|
||||
(dcm/go-to-dashboard-recent :team-id team-id)
|
||||
(ts/schedule-on-idle
|
||||
(fn []
|
||||
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
|
||||
(dom/set-attribute! projects-title "tabindex" "0")
|
||||
(dom/focus! projects-title)
|
||||
(dom/set-attribute! projects-title "tabindex" "-1")))))))
|
||||
(dcm/go-to-dashboard-recent :team-id team-id))
|
||||
(ts/schedule
|
||||
(fn []
|
||||
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
|
||||
(dom/set-attribute! projects-title "tabindex" "0")
|
||||
(dom/focus! projects-title)
|
||||
(dom/set-attribute! projects-title "tabindex" "-1"))))))
|
||||
|
||||
go-fonts
|
||||
(mf/use-fn
|
||||
@ -838,14 +838,14 @@
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(st/emit!
|
||||
(dcm/go-to-dashboard-fonts :team-id team-id)
|
||||
(ts/schedule-on-idle
|
||||
(fn []
|
||||
(let [font-title (dom/get-element "dashboard-fonts-title")]
|
||||
(when font-title
|
||||
(dom/set-attribute! font-title "tabindex" "0")
|
||||
(dom/focus! font-title)
|
||||
(dom/set-attribute! font-title "tabindex" "-1"))))))))
|
||||
(dcm/go-to-dashboard-fonts :team-id team-id))
|
||||
(ts/schedule
|
||||
(fn []
|
||||
(let [font-title (dom/get-element "dashboard-fonts-title")]
|
||||
(when font-title
|
||||
(dom/set-attribute! font-title "tabindex" "0")
|
||||
(dom/focus! font-title)
|
||||
(dom/set-attribute! font-title "tabindex" "-1")))))))
|
||||
|
||||
go-drafts
|
||||
(mf/use-fn
|
||||
@ -858,7 +858,7 @@
|
||||
(mf/deps team-id default-project-id)
|
||||
(fn []
|
||||
(st/emit! (dcm/go-to-dashboard-files :team-id team-id :project-id default-project-id))
|
||||
(ts/schedule-on-idle
|
||||
(ts/schedule
|
||||
(fn []
|
||||
(when-let [title (dom/get-element "dashboard-drafts-title")]
|
||||
(dom/set-attribute! title "tabindex" "0")
|
||||
@ -875,14 +875,14 @@
|
||||
(mf/deps team-id)
|
||||
(fn []
|
||||
(st/emit!
|
||||
(dcm/go-to-dashboard-libraries :team-id team-id)
|
||||
(ts/schedule-on-idle
|
||||
(fn []
|
||||
(let [libs-title (dom/get-element "dashboard-libraries-title")]
|
||||
(when libs-title
|
||||
(dom/set-attribute! libs-title "tabindex" "0")
|
||||
(dom/focus! libs-title)
|
||||
(dom/set-attribute! libs-title "tabindex" "-1"))))))))
|
||||
(dcm/go-to-dashboard-libraries :team-id team-id))
|
||||
(ts/schedule
|
||||
(fn []
|
||||
(let [libs-title (dom/get-element "dashboard-libraries-title")]
|
||||
(when libs-title
|
||||
(dom/set-attribute! libs-title "tabindex" "0")
|
||||
(dom/focus! libs-title)
|
||||
(dom/set-attribute! libs-title "tabindex" "-1")))))))
|
||||
|
||||
pinned-projects
|
||||
(mf/with-memo [projects]
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
[options id]
|
||||
(let [options (if (delay? options) @options options)]
|
||||
(or (d/seek #(= id (get % :id)) options)
|
||||
(nth options 0))))
|
||||
(when (seq options)
|
||||
(nth options 0)))))
|
||||
|
||||
(defn- get-selected-option-id
|
||||
[options default]
|
||||
@ -178,7 +179,8 @@
|
||||
|
||||
selected-option
|
||||
(mf/with-memo [options selected-id]
|
||||
(get-option options selected-id))
|
||||
(when (d/not-empty? options)
|
||||
(get-option options selected-id)))
|
||||
|
||||
label
|
||||
(get selected-option :label)
|
||||
|
||||
@ -103,6 +103,7 @@
|
||||
[:map
|
||||
[:tabs [:vector {:min 1} schema:tab]]
|
||||
[:class {:optional true} :string]
|
||||
[:scrollable-panel {:optional true} :boolean]
|
||||
[:on-change fn?]
|
||||
[:selected :string]
|
||||
[:action-button {:optional true} some?]
|
||||
@ -111,15 +112,15 @@
|
||||
|
||||
(mf/defc tab-switcher*
|
||||
{::mf/schema schema:tab-switcher}
|
||||
[{:keys [tabs class on-change selected action-button-position action-button children] :rest props}]
|
||||
[{:keys [tabs class on-change selected action-button-position action-button children scrollable-panel] :rest props}]
|
||||
(let [nodes-ref (mf/use-ref nil)
|
||||
scrollable-panel (d/nilv scrollable-panel false)
|
||||
|
||||
tabs
|
||||
(if (array? tabs)
|
||||
(mfu/bean tabs)
|
||||
tabs)
|
||||
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps on-change)
|
||||
@ -186,7 +187,8 @@
|
||||
:on-key-down on-key-down
|
||||
:on-click on-click}]]
|
||||
|
||||
[:section {:class (stl/css :tab-panel)
|
||||
[:section {:class (stl/css-case :tab-panel true
|
||||
:scrollable-panel scrollable-panel)
|
||||
:tab-index 0
|
||||
:role "tabpanel"
|
||||
:aria-labelledby selected}
|
||||
|
||||
@ -114,5 +114,8 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: $b-1 solid var(--tab-panel-outline-color);
|
||||
}
|
||||
|
||||
.scrollable-panel {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change handle-opacity-change)
|
||||
(fn [value]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(handle-opacity-change value)
|
||||
(do
|
||||
(st/emit! (dwta/toggle-token {:token (first value)
|
||||
|
||||
@ -117,7 +117,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-change :simple attr value)
|
||||
(do
|
||||
(st/emit!
|
||||
@ -247,7 +247,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-change :multiple attr value)
|
||||
(do
|
||||
(st/emit!
|
||||
@ -577,7 +577,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(st/emit! (dwsl/update-layout-child ids {attr value}))
|
||||
(do
|
||||
(st/emit!
|
||||
|
||||
@ -25,60 +25,68 @@
|
||||
[app.util.dom :as dom]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def guide-width 1)
|
||||
(def guide-opacity 0.7)
|
||||
(def guide-opacity-hover 1)
|
||||
(def guide-color colors/new-danger)
|
||||
(def guide-pill-width 34)
|
||||
(def guide-pill-height 20)
|
||||
(def guide-pill-corner-radius 4)
|
||||
(def guide-active-area 16)
|
||||
(def ^:const guide-width 1)
|
||||
(def ^:const guide-opacity 0.7)
|
||||
(def ^:const guide-opacity-hover 1)
|
||||
(def ^:const guide-color colors/new-danger)
|
||||
(def ^:const guide-pill-width 34)
|
||||
(def ^:const guide-pill-height 20)
|
||||
(def ^:const guide-pill-corner-radius 4)
|
||||
(def ^:const guide-active-area 16)
|
||||
|
||||
(def guide-creation-margin-left 8)
|
||||
(def guide-creation-margin-top 28)
|
||||
(def guide-creation-width 16)
|
||||
(def guide-creation-height 24)
|
||||
(def ^:const guide-creation-margin-left 8)
|
||||
(def ^:const guide-creation-margin-top 28)
|
||||
(def ^:const guide-creation-width 16)
|
||||
(def ^:const guide-creation-height 24)
|
||||
|
||||
(defn use-guide
|
||||
"Hooks to support drag/drop for existing guides and new guides"
|
||||
[on-guide-change get-hover-frame zoom {:keys [id position axis frame-id]}]
|
||||
(let [dragging-ref (mf/use-ref false)
|
||||
start-ref (mf/use-ref nil)
|
||||
(let [dragging-ref (mf/use-ref false)
|
||||
start-ref (mf/use-ref nil)
|
||||
start-pos-ref (mf/use-ref nil)
|
||||
state (mf/use-state {:hover false
|
||||
state (mf/use-state
|
||||
#(do {:hover false
|
||||
:new-position nil
|
||||
:new-frame-id frame-id})
|
||||
:new-frame-id frame-id}))
|
||||
|
||||
frame-id (:new-frame-id @state)
|
||||
frame-id
|
||||
(:new-frame-id @state)
|
||||
|
||||
frame-ref (mf/use-memo (mf/deps frame-id) #(refs/object-by-id frame-id))
|
||||
frame (mf/deref frame-ref)
|
||||
frame-ref
|
||||
(mf/with-memo [frame-id]
|
||||
(refs/object-by-id frame-id))
|
||||
|
||||
snap-pixel? (mf/deref refs/snap-pixel?)
|
||||
frame
|
||||
(mf/deref frame-ref)
|
||||
|
||||
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
snap-pixel?
|
||||
(mf/deref refs/snap-pixel?)
|
||||
|
||||
read-only?
|
||||
(mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-callback
|
||||
(mf/deps workspace-read-only?)
|
||||
(mf/use-fn
|
||||
(mf/deps read-only?)
|
||||
(fn []
|
||||
(when-not workspace-read-only?
|
||||
(when-not read-only?
|
||||
(st/emit! (dw/set-hover-guide id true))
|
||||
(swap! state assoc :hover true))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-callback
|
||||
(mf/deps workspace-read-only?)
|
||||
(mf/use-fn
|
||||
(mf/deps read-only?)
|
||||
(fn []
|
||||
(when-not workspace-read-only?
|
||||
(when-not read-only?
|
||||
(st/emit! (dw/set-hover-guide id false))
|
||||
(swap! state assoc :hover false))))
|
||||
|
||||
on-pointer-down
|
||||
(mf/use-callback
|
||||
(mf/deps workspace-read-only?)
|
||||
(mf/use-fn
|
||||
(mf/deps read-only?)
|
||||
(fn [event]
|
||||
(when-not workspace-read-only?
|
||||
(when-not read-only?
|
||||
(when (= 0 (.-button event))
|
||||
(dom/capture-pointer event)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
@ -86,20 +94,20 @@
|
||||
(mf/set-ref-val! start-pos-ref (get @ms/mouse-position axis))))))
|
||||
|
||||
on-pointer-up
|
||||
(mf/use-callback
|
||||
(mf/deps (select-keys @state [:new-position :new-frame-id]) on-guide-change workspace-read-only?)
|
||||
(mf/use-fn
|
||||
(mf/deps (select-keys @state [:new-position :new-frame-id]) on-guide-change read-only?)
|
||||
(fn []
|
||||
(when-not workspace-read-only?
|
||||
(when-not read-only?
|
||||
(when (some? on-guide-change)
|
||||
(when (some? (:new-position @state))
|
||||
(on-guide-change {:position (:new-position @state)
|
||||
:frame-id (:new-frame-id @state)}))))))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-callback
|
||||
(mf/deps workspace-read-only?)
|
||||
(mf/use-fn
|
||||
(mf/deps read-only?)
|
||||
(fn [event]
|
||||
(when-not workspace-read-only?
|
||||
(when-not read-only?
|
||||
(dom/release-pointer event)
|
||||
(mf/set-ref-val! dragging-ref false)
|
||||
(mf/set-ref-val! start-ref nil)
|
||||
@ -107,27 +115,29 @@
|
||||
(swap! state assoc :new-position nil))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-callback
|
||||
(mf/deps position zoom snap-pixel? workspace-read-only?)
|
||||
(mf/use-fn
|
||||
(mf/deps position zoom snap-pixel? read-only? get-hover-frame)
|
||||
(fn [event]
|
||||
(when-not workspace-read-only?
|
||||
(when-let [_ (mf/ref-val dragging-ref)]
|
||||
(let [start-pt (mf/ref-val start-ref)
|
||||
start-pos (mf/ref-val start-pos-ref)
|
||||
current-pt (dom/get-client-position event)
|
||||
delta (/ (- (get current-pt axis) (get start-pt axis)) zoom)
|
||||
(when-not read-only?
|
||||
(when (mf/ref-val dragging-ref)
|
||||
(let [start-pt (mf/ref-val start-ref)
|
||||
start-pos (mf/ref-val start-pos-ref)
|
||||
current-pt (dom/get-client-position event)
|
||||
delta (/ (- (get current-pt axis) (get start-pt axis)) zoom)
|
||||
new-position (if (some? position)
|
||||
(+ position delta)
|
||||
(+ start-pos delta))
|
||||
|
||||
new-position (if snap-pixel?
|
||||
(mth/round new-position)
|
||||
new-position)
|
||||
|
||||
new-frame-id (:id (get-hover-frame))]
|
||||
new-frame-id (-> (get-hover-frame)
|
||||
(get :id))]
|
||||
|
||||
(swap! state assoc
|
||||
:new-position new-position
|
||||
:new-frame-id new-frame-id))))))]
|
||||
|
||||
{:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-pointer-down on-pointer-down
|
||||
@ -137,8 +147,8 @@
|
||||
:state state
|
||||
:frame frame}))
|
||||
|
||||
;; This functions are auxiliary to get the coords of components depending on the axis
|
||||
;; we're handling
|
||||
;; This functions are auxiliary to get the coords of components
|
||||
;; depending on the axis we're handling
|
||||
|
||||
(defn guide-area-axis
|
||||
[pos vbox zoom frame axis]
|
||||
@ -270,11 +280,11 @@
|
||||
(<= (:position guide) (+ (:y frame) (:height frame))))))
|
||||
|
||||
(mf/defc guide*
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/props :obj}
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [guide is-hover on-guide-change get-hover-frame vbox zoom
|
||||
hover-frame disabled-guides frame-modifier frame-transform]}]
|
||||
(let [axis (:axis guide)
|
||||
(let [axis
|
||||
(get guide :axis)
|
||||
|
||||
handle-change-position
|
||||
(mf/use-fn
|
||||
@ -290,9 +300,11 @@
|
||||
on-lost-pointer-capture
|
||||
on-pointer-move
|
||||
state
|
||||
frame]} (use-guide handle-change-position get-hover-frame zoom guide)
|
||||
frame]}
|
||||
(use-guide handle-change-position get-hover-frame zoom guide)
|
||||
|
||||
base-frame (or frame hover-frame)
|
||||
base-frame
|
||||
(or frame hover-frame)
|
||||
|
||||
frame
|
||||
(cond-> base-frame
|
||||
@ -302,12 +314,18 @@
|
||||
(some? frame-transform)
|
||||
(gsh/apply-transform frame-transform))
|
||||
|
||||
move-vec (gpt/to-vec (gpt/point (:x base-frame) (:y base-frame))
|
||||
(gpt/point (:x frame) (:y frame)))
|
||||
move-vec
|
||||
(gpt/to-vec (gpt/point (:x base-frame) (:y base-frame))
|
||||
(gpt/point (:x frame) (:y frame)))
|
||||
|
||||
pos (+ (or (:new-position @state) (:position guide)) (get move-vec axis))
|
||||
guide-width (/ guide-width zoom)
|
||||
guide-pill-corner-radius (/ guide-pill-corner-radius zoom)
|
||||
pos
|
||||
(+ (or (:new-position @state) (:position guide)) (get move-vec axis))
|
||||
|
||||
guide-width
|
||||
(/ guide-width zoom)
|
||||
|
||||
guide-pill-corner-radius
|
||||
(/ guide-pill-corner-radius zoom)
|
||||
|
||||
frame-guide-outside?
|
||||
(and (some? frame)
|
||||
@ -404,9 +422,7 @@
|
||||
(fmt/format-number (- pos (if (= axis :x) (:x frame) (:y frame))))]]))])))
|
||||
|
||||
(mf/defc new-guide-area*
|
||||
{::mf/props :obj}
|
||||
[{:keys [vbox zoom axis get-hover-frame disabled-guides]}]
|
||||
|
||||
(let [on-guide-change
|
||||
(mf/use-fn
|
||||
(mf/deps vbox)
|
||||
@ -426,7 +442,9 @@
|
||||
state
|
||||
frame]}
|
||||
(use-guide on-guide-change get-hover-frame zoom {:axis axis})
|
||||
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)]
|
||||
|
||||
read-only?
|
||||
(mf/use-ctx ctx/workspace-read-only?)]
|
||||
|
||||
[:g.new-guides
|
||||
(when-not disabled-guides
|
||||
@ -441,13 +459,15 @@
|
||||
:on-pointer-up on-pointer-up
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:on-pointer-move on-pointer-move
|
||||
:class (when-not workspace-read-only?
|
||||
(if (= axis :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ns" 0)))
|
||||
:class (when-not read-only?
|
||||
(if (= axis :x)
|
||||
(cur/get-dynamic "resize-ew" 0)
|
||||
(cur/get-dynamic "resize-ns" 0)))
|
||||
:style {:fill "none"
|
||||
:pointer-events "fill"}}]))
|
||||
|
||||
(when (:new-position @state)
|
||||
[:& guide* {:guide {:axis axis :position (:new-position @state)}
|
||||
[:> guide* {:guide {:axis axis :position (:new-position @state)}
|
||||
:get-hover-frame get-hover-frame
|
||||
:vbox vbox
|
||||
:zoom zoom
|
||||
@ -455,17 +475,18 @@
|
||||
:hover-frame frame}])]))
|
||||
|
||||
(mf/defc viewport-guides*
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/props :obj}
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [zoom vbox hover-frame disabled-guides modifiers guides]}]
|
||||
(let [guides
|
||||
(mf/with-memo [guides vbox]
|
||||
(->> (vals guides)
|
||||
(filter (partial guide-inside-vbox? zoom vbox))))
|
||||
|
||||
focus (mf/deref refs/workspace-focus-selected)
|
||||
focus
|
||||
(mf/deref refs/workspace-focus-selected)
|
||||
|
||||
hover-frame-ref (mf/use-ref nil)
|
||||
hover-frame-ref
|
||||
(mf/use-ref nil)
|
||||
|
||||
;; We use the ref to not redraw every guide everytime the hovering frame change
|
||||
;; we're only interested to get the frame in the guide we're moving
|
||||
|
||||
@ -30,6 +30,15 @@ const exclusiveTypes = [
|
||||
* @property {boolean} [allowHTMLPaste]
|
||||
*/
|
||||
|
||||
const looksLikeJSON = (str) => {
|
||||
if (typeof str !== 'string') return false;
|
||||
const trimmed = str.trim();
|
||||
return (
|
||||
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text
|
||||
@ -39,13 +48,14 @@ const exclusiveTypes = [
|
||||
*/
|
||||
function parseText(text, options) {
|
||||
options = options || {};
|
||||
|
||||
const decodeTransit = options["decodeTransit"];
|
||||
if (decodeTransit) {
|
||||
if (decodeTransit && looksLikeJSON(text)) {
|
||||
try {
|
||||
decodeTransit(text);
|
||||
return new Blob([text], { type: "application/transit+json" });
|
||||
} catch (_error) {
|
||||
// NOOP
|
||||
return new Blob([text], { type: "text/plain" });
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +145,7 @@ function sortItems(a, b) {
|
||||
export async function fromNavigator(options) {
|
||||
options = options || {};
|
||||
const items = await navigator.clipboard.read();
|
||||
return Promise.all(
|
||||
const result = await Promise.all(
|
||||
Array.from(items).map(async (item) => {
|
||||
const itemAllowedTypes = Array.from(item.types)
|
||||
.filter(filterAllowedTypes(options))
|
||||
@ -155,9 +165,15 @@ export async function fromNavigator(options) {
|
||||
}
|
||||
|
||||
const type = itemAllowedTypes.at(0);
|
||||
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.getType(type);
|
||||
}),
|
||||
);
|
||||
return result.filter((item) => !!item);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -104,10 +104,17 @@
|
||||
(.next ^js subscriber response)
|
||||
(.complete ^js subscriber)))
|
||||
(p/catch
|
||||
(fn [err]
|
||||
(fn [cause]
|
||||
(vreset! abortable? false)
|
||||
(when-not @unsubscribed?
|
||||
(.error ^js subscriber err))))
|
||||
(let [error (ex-info (ex-message cause)
|
||||
{:type :internal
|
||||
:code :unable-to-fetch
|
||||
:hint "unable to perform fetch operation"
|
||||
:uri uri
|
||||
:headers headers}
|
||||
cause)]
|
||||
(.error ^js subscriber error)))))
|
||||
(p/finally
|
||||
(fn []
|
||||
(let [{:keys [count average] :or {count 0 average 0}} (get @network-averages (:path uri))
|
||||
@ -116,10 +123,15 @@
|
||||
(/ current-time (inc count)))
|
||||
count (inc count)]
|
||||
(swap! network-averages assoc (:path uri) {:count count :average average})))))
|
||||
|
||||
(fn []
|
||||
(vreset! unsubscribed? true)
|
||||
(when @abortable?
|
||||
(.abort ^js controller)))))))
|
||||
;; Provide an explicit reason so that the resulting AbortError carries
|
||||
;; a meaningful message instead of the browser default
|
||||
;; "signal is aborted without reason".
|
||||
(.abort ^js controller (ex-info (str "fetch to '" uri "' is aborted")
|
||||
{:uri uri}))))))))
|
||||
|
||||
(defn response->map
|
||||
[response]
|
||||
|
||||
@ -68,8 +68,21 @@ export function createApi(
|
||||
},
|
||||
|
||||
sendMessage(message: unknown) {
|
||||
let cloneableMessage: unknown;
|
||||
|
||||
try {
|
||||
cloneableMessage = structuredClone(message);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'plugin sendMessage: the message could not be cloned. ' +
|
||||
'Ensure the message does not contain functions, DOM nodes, or other non-serializable values.',
|
||||
err,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const event = new CustomEvent('message', {
|
||||
detail: message,
|
||||
detail: cloneableMessage,
|
||||
});
|
||||
|
||||
plugin.getModal()?.dispatchEvent(event);
|
||||
|
||||
@ -129,7 +129,14 @@ export class PluginModalElement extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
iframe.contentWindow.postMessage((e as CustomEvent).detail, '*');
|
||||
try {
|
||||
iframe.contentWindow.postMessage((e as CustomEvent).detail, '*');
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'plugin modal: failed to send message to iframe via postMessage.',
|
||||
err,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.shadowRoot.appendChild(this.wrapper);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user