mirror of
https://github.com/penpot/penpot.git
synced 2026-09-13 15:38:38 +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 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 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 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
|
## 2.13.3
|
||||||
|
|
||||||
|
|||||||
@ -205,40 +205,41 @@
|
|||||||
(defn format-inst
|
(defn format-inst
|
||||||
([v] (format-inst v :iso))
|
([v] (format-inst v :iso))
|
||||||
([v fmt]
|
([v fmt]
|
||||||
(case fmt
|
(when (some? v)
|
||||||
(:iso :iso8601)
|
(case fmt
|
||||||
#?(:clj (.format DateTimeFormatter/ISO_INSTANT ^Instant v)
|
(:iso :iso8601)
|
||||||
:cljs (dfn-format-iso v))
|
#?(:clj (.format DateTimeFormatter/ISO_INSTANT ^Instant v)
|
||||||
|
:cljs (dfn-format-iso v))
|
||||||
|
|
||||||
:iso-date
|
:iso-date
|
||||||
#?(:clj (.format DateTimeFormatter/ISO_LOCAL_DATE
|
#?(:clj (.format DateTimeFormatter/ISO_LOCAL_DATE
|
||||||
^ZonedDateTime (ZonedDateTime/ofInstant v (ZoneId/of "UTC")))
|
^ZonedDateTime (ZonedDateTime/ofInstant v (ZoneId/of "UTC")))
|
||||||
:cljs (dfn-format-iso v #js {:representation "date"}))
|
:cljs (dfn-format-iso v #js {:representation "date"}))
|
||||||
|
|
||||||
(:rfc1123 :http)
|
(:rfc1123 :http)
|
||||||
#?(:clj (.format DateTimeFormatter/RFC_1123_DATE_TIME
|
#?(:clj (.format DateTimeFormatter/RFC_1123_DATE_TIME
|
||||||
^ZonedDateTime (ZonedDateTime/ofInstant v (ZoneId/of "UTC")))
|
^ZonedDateTime (ZonedDateTime/ofInstant v (ZoneId/of "UTC")))
|
||||||
:cljs (dfn-format v "EEE, dd LLL yyyy HH:mm:ss 'GMT'"))
|
:cljs (dfn-format v "EEE, dd LLL yyyy HH:mm:ss 'GMT'"))
|
||||||
|
|
||||||
#?@(:cljs [:time-24-simple
|
#?@(:cljs [:time-24-simple
|
||||||
(dfn-format v "HH:mm")
|
(dfn-format v "HH:mm")
|
||||||
|
|
||||||
;; DEPRECATED
|
;; DEPRECATED
|
||||||
:date-full
|
:date-full
|
||||||
(dfn-format v "PPP")
|
(dfn-format v "PPP")
|
||||||
|
|
||||||
:localized-date
|
:localized-date
|
||||||
(dfn-format v "PPP")
|
(dfn-format v "PPP")
|
||||||
|
|
||||||
:localized-time
|
:localized-time
|
||||||
(dfn-format v "p")
|
(dfn-format v "p")
|
||||||
|
|
||||||
:localized-date-time
|
:localized-date-time
|
||||||
(dfn-format v "PPP . p")
|
(dfn-format v "PPP . p")
|
||||||
|
|
||||||
(if (string? fmt)
|
(if (string? fmt)
|
||||||
(dfn-format v fmt)
|
(dfn-format v fmt)
|
||||||
(throw (js/Error. "unpexted format")))]))))
|
(throw (js/Error. "unpexted format")))])))))
|
||||||
|
|
||||||
#?(:cljs
|
#?(:cljs
|
||||||
(def locales
|
(def locales
|
||||||
|
|||||||
@ -212,25 +212,39 @@
|
|||||||
(get-instance-root objects (get objects (:parent-id shape)))))
|
(get-instance-root objects (get objects (:parent-id shape)))))
|
||||||
|
|
||||||
(defn find-component-main
|
(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]
|
([objects shape]
|
||||||
(find-component-main objects shape true))
|
(find-component-main objects shape true))
|
||||||
([objects shape only-direct-child?]
|
([objects shape only-direct-child?]
|
||||||
(cond
|
(loop [shape shape
|
||||||
(or (nil? shape) (cfh/root? shape))
|
visited #{}]
|
||||||
nil
|
(cond
|
||||||
(nil? (:parent-id shape)) ; This occurs in the root of components v1
|
(or (nil? shape) (cfh/root? shape))
|
||||||
shape
|
nil
|
||||||
(ctk/main-instance? shape)
|
|
||||||
shape
|
;; Cycle detected: we have already visited this shape id
|
||||||
(and only-direct-child? ;; If we are asking only for direct childs of a component-main,
|
(contains? visited (:id shape))
|
||||||
(ctk/instance-head? shape)) ;; stop when we found a instance-head that isn't main-instance
|
nil
|
||||||
nil
|
|
||||||
(and (not only-direct-child?)
|
(nil? (:parent-id shape)) ; This occurs in the root of components v1
|
||||||
(ctk/instance-root? shape))
|
shape
|
||||||
nil
|
|
||||||
:else
|
(ctk/main-instance? shape)
|
||||||
(find-component-main objects (get objects (:parent-id 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?
|
(defn inside-component-main?
|
||||||
"Check if the shape is a component main instance or is inside one."
|
"Check if the shape is a component main instance or is inside one."
|
||||||
|
|||||||
@ -777,7 +777,7 @@
|
|||||||
content as PathData instance."
|
content as PathData instance."
|
||||||
[content transform]
|
[content transform]
|
||||||
(if (some? transform)
|
(if (some? transform)
|
||||||
(impl/-transform content transform)
|
(impl/-transform (impl/path-data content) transform)
|
||||||
content))
|
content))
|
||||||
|
|
||||||
(defn move-content
|
(defn move-content
|
||||||
|
|||||||
@ -168,7 +168,7 @@
|
|||||||
child-as-paths)]
|
child-as-paths)]
|
||||||
(-> group
|
(-> group
|
||||||
(assoc :type :path)
|
(assoc :type :path)
|
||||||
(assoc :content content)
|
(assoc :content (path.impl/path-data content))
|
||||||
(merge head-data)
|
(merge head-data)
|
||||||
(d/without-keys dissoc-attrs))))
|
(d/without-keys dissoc-attrs))))
|
||||||
|
|
||||||
@ -184,7 +184,8 @@
|
|||||||
(:bool-type shape)
|
(:bool-type shape)
|
||||||
|
|
||||||
content
|
content
|
||||||
(bool/calculate-content bool-type (map :content children))]
|
(-> (bool/calculate-content bool-type (map :content children))
|
||||||
|
(path.impl/path-data))]
|
||||||
|
|
||||||
(-> shape
|
(-> shape
|
||||||
(assoc :type :path)
|
(assoc :type :path)
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
[common-tests.time-test]
|
[common-tests.time-test]
|
||||||
[common-tests.types.absorb-assets-test]
|
[common-tests.types.absorb-assets-test]
|
||||||
[common-tests.types.components-test]
|
[common-tests.types.components-test]
|
||||||
|
[common-tests.types.container-test]
|
||||||
[common-tests.types.fill-test]
|
[common-tests.types.fill-test]
|
||||||
[common-tests.types.modifiers-test]
|
[common-tests.types.modifiers-test]
|
||||||
[common-tests.types.objects-map-test]
|
[common-tests.types.objects-map-test]
|
||||||
@ -92,6 +93,7 @@
|
|||||||
'common-tests.time-test
|
'common-tests.time-test
|
||||||
'common-tests.types.absorb-assets-test
|
'common-tests.types.absorb-assets-test
|
||||||
'common-tests.types.components-test
|
'common-tests.types.components-test
|
||||||
|
'common-tests.types.container-test
|
||||||
'common-tests.types.fill-test
|
'common-tests.types.fill-test
|
||||||
'common-tests.types.modifiers-test
|
'common-tests.types.modifiers-test
|
||||||
'common-tests.types.objects-map-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 (= result1 result2))
|
||||||
(t/is (= result2 result3))))
|
(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
|
(defn calculate-extremities
|
||||||
"Calculate extremities for the provided content.
|
"Calculate extremities for the provided content.
|
||||||
A legacy implementation used mainly as reference for testing"
|
A legacy implementation used mainly as reference for testing"
|
||||||
|
|||||||
@ -258,33 +258,39 @@
|
|||||||
#js {:decodeTransit t/decode-str
|
#js {:decodeTransit t/decode-str
|
||||||
:allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")})
|
:allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")})
|
||||||
|
|
||||||
(defn create-paste-from-blob
|
(defn- create-paste-from-blob
|
||||||
[in-viewport?]
|
[in-viewport?]
|
||||||
(fn [blob]
|
(fn [blob]
|
||||||
(let [type (.-type blob)
|
(let [type (.-type blob)]
|
||||||
result (cond
|
(cond
|
||||||
(= type "image/svg+xml")
|
(= type "image/svg+xml")
|
||||||
(->> (rx/from (.text blob))
|
(->> (rx/from (.text blob))
|
||||||
(rx/map paste-svg-text))
|
(rx/map paste-svg-text))
|
||||||
|
|
||||||
(some #(= type %) clipboard/image-types)
|
(some #(= type %) clipboard/image-types)
|
||||||
(rx/of (paste-image blob))
|
(rx/of (paste-image blob))
|
||||||
|
|
||||||
(= type "text/html")
|
(= type "text/html")
|
||||||
(->> (rx/from (.text blob))
|
(->> (rx/from (.text blob))
|
||||||
(rx/map paste-html-text))
|
(rx/map paste-html-text))
|
||||||
|
|
||||||
(= type "application/transit+json")
|
(= type "application/transit+json")
|
||||||
(->> (rx/from (.text blob))
|
(->> (rx/from (.text blob))
|
||||||
(rx/map (fn [text]
|
(rx/map t/decode-str)
|
||||||
(let [transit-data (t/decode-str text)]
|
(rx/filter map?)
|
||||||
(assoc transit-data :in-viewport in-viewport?))))
|
(rx/map
|
||||||
(rx/map paste-transit-shapes))
|
(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
|
:else
|
||||||
(->> (rx/from (.text blob))
|
(->> (rx/from (.text blob))
|
||||||
(rx/map paste-text)))]
|
(rx/map paste-text))))))
|
||||||
result)))
|
|
||||||
|
|
||||||
(def default-paste-from-blob (create-paste-from-blob false))
|
(def default-paste-from-blob (create-paste-from-blob false))
|
||||||
|
|
||||||
|
|||||||
@ -59,8 +59,8 @@
|
|||||||
content (get shape :content)
|
content (get shape :content)
|
||||||
new-content (path/apply-content-modifiers content content-modifiers)
|
new-content (path/apply-content-modifiers content content-modifiers)
|
||||||
|
|
||||||
old-points (path.segment/get-points content)
|
old-points (path/get-points content)
|
||||||
new-points (path.segment/get-points new-content)
|
new-points (path/get-points new-content)
|
||||||
point-change (->> (map hash-map old-points new-points) (reduce merge))]
|
point-change (->> (map hash-map old-points new-points) (reduce merge))]
|
||||||
|
|
||||||
(when (and (some? new-content) (some? shape))
|
(when (and (some? new-content) (some? shape))
|
||||||
@ -162,7 +162,7 @@
|
|||||||
start-position (apply min-key #(gpt/distance start-position %) selected-points)
|
start-position (apply min-key #(gpt/distance start-position %) selected-points)
|
||||||
|
|
||||||
content (st/get-path state :content)
|
content (st/get-path state :content)
|
||||||
points (path.segment/get-points content)]
|
points (path/get-points content)]
|
||||||
|
|
||||||
(rx/concat
|
(rx/concat
|
||||||
;; This stream checks the consecutive mouse positions to do the dragging
|
;; 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)
|
start-delta-y (dm/get-in modifiers [index cy] 0)
|
||||||
|
|
||||||
content (st/get-path state :content)
|
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))
|
point (-> content (nth (if (= prefix :c1) (dec index) index)) (path.helpers/segment->point))
|
||||||
handler (-> content (nth index) (path.segment/get-handler prefix))
|
handler (-> content (nth index) (path.segment/get-handler prefix))
|
||||||
|
|||||||
@ -137,16 +137,16 @@
|
|||||||
(ptk/reify ::pin-version
|
(ptk/reify ::pin-version
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [version (->> (dm/get-in state [:workspace-versions :data])
|
(when-let [version (->> (dm/get-in state [:workspace-versions :data])
|
||||||
(d/seek #(= (:id %) id)))
|
(d/seek #(= (:id %) id)))]
|
||||||
params {:id id
|
(let [params {:id id
|
||||||
:label (ct/format-inst (:created-at version) :localized-date)}]
|
:label (ct/format-inst (:created-at version) :localized-date)}]
|
||||||
|
|
||||||
(->> (rp/cmd! :update-file-snapshot params)
|
(->> (rp/cmd! :update-file-snapshot params)
|
||||||
(rx/mapcat (fn [_]
|
(rx/mapcat (fn [_]
|
||||||
(rx/of (update-versions-state {:editing id})
|
(rx/of (update-versions-state {:editing id})
|
||||||
(fetch-versions)
|
(fetch-versions)
|
||||||
(ptk/event ::ev/event {::ev/name "pin-version"})))))))))
|
(ptk/event ::ev/event {::ev/name "pin-version"}))))))))))
|
||||||
|
|
||||||
(defn lock-version
|
(defn lock-version
|
||||||
[id]
|
[id]
|
||||||
|
|||||||
@ -350,20 +350,32 @@
|
|||||||
(st/async-emit! (rt/assign-exception error)))
|
(st/async-emit! (rt/assign-exception error)))
|
||||||
|
|
||||||
(defonce uncaught-error-handler
|
(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)]
|
(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")
|
(= message "Unexpected end of input")
|
||||||
(str/starts-with? message "invalid props on component")
|
(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]
|
(on-unhandled-error [event]
|
||||||
(.preventDefault ^js event)
|
(.preventDefault ^js event)
|
||||||
(when-let [cause (unchecked-get event "error")]
|
(when-let [cause (unchecked-get event "error")]
|
||||||
(set! last-exception cause)
|
|
||||||
(when-not (is-ignorable-exception? cause)
|
(when-not (is-ignorable-exception? cause)
|
||||||
(let [data (ex-data cause)
|
(let [data (ex-data cause)
|
||||||
type (get data :type)]
|
type (get data :type)]
|
||||||
|
(set! last-exception cause)
|
||||||
(if (#{:wasm-critical :wasm-non-blocking :wasm-exception} type)
|
(if (#{:wasm-critical :wasm-non-blocking :wasm-exception} type)
|
||||||
(on-error cause)
|
(on-error cause)
|
||||||
(do
|
(do
|
||||||
@ -373,9 +385,10 @@
|
|||||||
(on-unhandled-rejection [event]
|
(on-unhandled-rejection [event]
|
||||||
(.preventDefault ^js event)
|
(.preventDefault ^js event)
|
||||||
(when-let [cause (unchecked-get event "reason")]
|
(when-let [cause (unchecked-get event "reason")]
|
||||||
(set! last-exception cause)
|
(when-not (is-ignorable-exception? cause)
|
||||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
(set! last-exception cause)
|
||||||
(ts/schedule #(flash :cause cause :type :unhandled))))]
|
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||||
|
(ts/schedule #(flash :cause cause :type :unhandled)))))]
|
||||||
|
|
||||||
(.addEventListener g/window "error" on-unhandled-error)
|
(.addEventListener g/window "error" on-unhandled-error)
|
||||||
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
||||||
|
|||||||
@ -11,6 +11,11 @@
|
|||||||
|
|
||||||
(mf/defc portal-on-document*
|
(mf/defc portal-on-document*
|
||||||
[{:keys [children]}]
|
[{:keys [children]}]
|
||||||
(mf/portal
|
(let [container (mf/use-memo #(dom/create-element "div"))]
|
||||||
(mf/html [:* children])
|
(mf/with-effect []
|
||||||
(dom/get-body)))
|
(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.
|
;; it right afterwards, in the next render cycle.
|
||||||
(dom/append-child! item-el counter-el)
|
(dom/append-child! item-el counter-el)
|
||||||
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
|
(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
|
on-menu-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|||||||
@ -116,13 +116,13 @@
|
|||||||
(fn [event]
|
(fn [event]
|
||||||
(when (kbd/enter? event)
|
(when (kbd/enter? event)
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(dcm/go-to-dashboard-files :project-id project-id)
|
(dcm/go-to-dashboard-files :project-id project-id))
|
||||||
(ts/schedule-on-idle
|
(ts/schedule
|
||||||
(fn []
|
(fn []
|
||||||
(when-let [title (dom/get-element (str project-id))]
|
(when-let [title (dom/get-element (str project-id))]
|
||||||
(dom/set-attribute! title "tabindex" "0")
|
(dom/set-attribute! title "tabindex" "0")
|
||||||
(dom/focus! title)
|
(dom/focus! title)
|
||||||
(dom/set-attribute! title "tabindex" "-1"))))))))
|
(dom/set-attribute! title "tabindex" "-1")))))))
|
||||||
|
|
||||||
on-menu-click
|
on-menu-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -246,7 +246,7 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(when (kbd/enter? e)
|
(when (kbd/enter? e)
|
||||||
(ts/schedule-on-idle
|
(ts/schedule
|
||||||
(fn []
|
(fn []
|
||||||
(let [search-title (dom/get-element (str "dashboard-search-title"))]
|
(let [search-title (dom/get-element (str "dashboard-search-title"))]
|
||||||
(when search-title
|
(when search-title
|
||||||
@ -820,13 +820,13 @@
|
|||||||
(mf/deps team-id)
|
(mf/deps team-id)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(dcm/go-to-dashboard-recent :team-id team-id)
|
(dcm/go-to-dashboard-recent :team-id team-id))
|
||||||
(ts/schedule-on-idle
|
(ts/schedule
|
||||||
(fn []
|
(fn []
|
||||||
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
|
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
|
||||||
(dom/set-attribute! projects-title "tabindex" "0")
|
(dom/set-attribute! projects-title "tabindex" "0")
|
||||||
(dom/focus! projects-title)
|
(dom/focus! projects-title)
|
||||||
(dom/set-attribute! projects-title "tabindex" "-1")))))))
|
(dom/set-attribute! projects-title "tabindex" "-1"))))))
|
||||||
|
|
||||||
go-fonts
|
go-fonts
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -838,14 +838,14 @@
|
|||||||
(mf/deps team)
|
(mf/deps team)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(dcm/go-to-dashboard-fonts :team-id team-id)
|
(dcm/go-to-dashboard-fonts :team-id team-id))
|
||||||
(ts/schedule-on-idle
|
(ts/schedule
|
||||||
(fn []
|
(fn []
|
||||||
(let [font-title (dom/get-element "dashboard-fonts-title")]
|
(let [font-title (dom/get-element "dashboard-fonts-title")]
|
||||||
(when font-title
|
(when font-title
|
||||||
(dom/set-attribute! font-title "tabindex" "0")
|
(dom/set-attribute! font-title "tabindex" "0")
|
||||||
(dom/focus! font-title)
|
(dom/focus! font-title)
|
||||||
(dom/set-attribute! font-title "tabindex" "-1"))))))))
|
(dom/set-attribute! font-title "tabindex" "-1")))))))
|
||||||
|
|
||||||
go-drafts
|
go-drafts
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -858,7 +858,7 @@
|
|||||||
(mf/deps team-id default-project-id)
|
(mf/deps team-id default-project-id)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (dcm/go-to-dashboard-files :team-id team-id :project-id default-project-id))
|
(st/emit! (dcm/go-to-dashboard-files :team-id team-id :project-id default-project-id))
|
||||||
(ts/schedule-on-idle
|
(ts/schedule
|
||||||
(fn []
|
(fn []
|
||||||
(when-let [title (dom/get-element "dashboard-drafts-title")]
|
(when-let [title (dom/get-element "dashboard-drafts-title")]
|
||||||
(dom/set-attribute! title "tabindex" "0")
|
(dom/set-attribute! title "tabindex" "0")
|
||||||
@ -875,14 +875,14 @@
|
|||||||
(mf/deps team-id)
|
(mf/deps team-id)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(dcm/go-to-dashboard-libraries :team-id team-id)
|
(dcm/go-to-dashboard-libraries :team-id team-id))
|
||||||
(ts/schedule-on-idle
|
(ts/schedule
|
||||||
(fn []
|
(fn []
|
||||||
(let [libs-title (dom/get-element "dashboard-libraries-title")]
|
(let [libs-title (dom/get-element "dashboard-libraries-title")]
|
||||||
(when libs-title
|
(when libs-title
|
||||||
(dom/set-attribute! libs-title "tabindex" "0")
|
(dom/set-attribute! libs-title "tabindex" "0")
|
||||||
(dom/focus! libs-title)
|
(dom/focus! libs-title)
|
||||||
(dom/set-attribute! libs-title "tabindex" "-1"))))))))
|
(dom/set-attribute! libs-title "tabindex" "-1")))))))
|
||||||
|
|
||||||
pinned-projects
|
pinned-projects
|
||||||
(mf/with-memo [projects]
|
(mf/with-memo [projects]
|
||||||
|
|||||||
@ -22,7 +22,8 @@
|
|||||||
[options id]
|
[options id]
|
||||||
(let [options (if (delay? options) @options options)]
|
(let [options (if (delay? options) @options options)]
|
||||||
(or (d/seek #(= id (get % :id)) options)
|
(or (d/seek #(= id (get % :id)) options)
|
||||||
(nth options 0))))
|
(when (seq options)
|
||||||
|
(nth options 0)))))
|
||||||
|
|
||||||
(defn- get-selected-option-id
|
(defn- get-selected-option-id
|
||||||
[options default]
|
[options default]
|
||||||
@ -178,7 +179,8 @@
|
|||||||
|
|
||||||
selected-option
|
selected-option
|
||||||
(mf/with-memo [options selected-id]
|
(mf/with-memo [options selected-id]
|
||||||
(get-option options selected-id))
|
(when (d/not-empty? options)
|
||||||
|
(get-option options selected-id)))
|
||||||
|
|
||||||
label
|
label
|
||||||
(get selected-option :label)
|
(get selected-option :label)
|
||||||
|
|||||||
@ -103,6 +103,7 @@
|
|||||||
[:map
|
[:map
|
||||||
[:tabs [:vector {:min 1} schema:tab]]
|
[:tabs [:vector {:min 1} schema:tab]]
|
||||||
[:class {:optional true} :string]
|
[:class {:optional true} :string]
|
||||||
|
[:scrollable-panel {:optional true} :boolean]
|
||||||
[:on-change fn?]
|
[:on-change fn?]
|
||||||
[:selected :string]
|
[:selected :string]
|
||||||
[:action-button {:optional true} some?]
|
[:action-button {:optional true} some?]
|
||||||
@ -111,15 +112,15 @@
|
|||||||
|
|
||||||
(mf/defc tab-switcher*
|
(mf/defc tab-switcher*
|
||||||
{::mf/schema schema: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)
|
(let [nodes-ref (mf/use-ref nil)
|
||||||
|
scrollable-panel (d/nilv scrollable-panel false)
|
||||||
|
|
||||||
tabs
|
tabs
|
||||||
(if (array? tabs)
|
(if (array? tabs)
|
||||||
(mfu/bean tabs)
|
(mfu/bean tabs)
|
||||||
tabs)
|
tabs)
|
||||||
|
|
||||||
|
|
||||||
on-click
|
on-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-change)
|
(mf/deps on-change)
|
||||||
@ -186,7 +187,8 @@
|
|||||||
:on-key-down on-key-down
|
:on-key-down on-key-down
|
||||||
:on-click on-click}]]
|
: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
|
:tab-index 0
|
||||||
:role "tabpanel"
|
:role "tabpanel"
|
||||||
:aria-labelledby selected}
|
:aria-labelledby selected}
|
||||||
|
|||||||
@ -114,5 +114,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
outline: $b-1 solid var(--tab-panel-outline-color);
|
outline: $b-1 solid var(--tab-panel-outline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-panel {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,7 +140,7 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-change handle-opacity-change)
|
(mf/deps on-change handle-opacity-change)
|
||||||
(fn [value]
|
(fn [value]
|
||||||
(if (or (string? value) (int? value))
|
(if (or (string? value) (number? value))
|
||||||
(handle-opacity-change value)
|
(handle-opacity-change value)
|
||||||
(do
|
(do
|
||||||
(st/emit! (dwta/toggle-token {:token (first value)
|
(st/emit! (dwta/toggle-token {:token (first value)
|
||||||
|
|||||||
@ -117,7 +117,7 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-change ids)
|
(mf/deps on-change ids)
|
||||||
(fn [value attr]
|
(fn [value attr]
|
||||||
(if (or (string? value) (int? value))
|
(if (or (string? value) (number? value))
|
||||||
(on-change :simple attr value)
|
(on-change :simple attr value)
|
||||||
(do
|
(do
|
||||||
(st/emit!
|
(st/emit!
|
||||||
@ -247,7 +247,7 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-change ids)
|
(mf/deps on-change ids)
|
||||||
(fn [value attr]
|
(fn [value attr]
|
||||||
(if (or (string? value) (int? value))
|
(if (or (string? value) (number? value))
|
||||||
(on-change :multiple attr value)
|
(on-change :multiple attr value)
|
||||||
(do
|
(do
|
||||||
(st/emit!
|
(st/emit!
|
||||||
@ -577,7 +577,7 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps ids)
|
(mf/deps ids)
|
||||||
(fn [value attr]
|
(fn [value attr]
|
||||||
(if (or (string? value) (int? value))
|
(if (or (string? value) (number? value))
|
||||||
(st/emit! (dwsl/update-layout-child ids {attr value}))
|
(st/emit! (dwsl/update-layout-child ids {attr value}))
|
||||||
(do
|
(do
|
||||||
(st/emit!
|
(st/emit!
|
||||||
|
|||||||
@ -25,60 +25,68 @@
|
|||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def guide-width 1)
|
(def ^:const guide-width 1)
|
||||||
(def guide-opacity 0.7)
|
(def ^:const guide-opacity 0.7)
|
||||||
(def guide-opacity-hover 1)
|
(def ^:const guide-opacity-hover 1)
|
||||||
(def guide-color colors/new-danger)
|
(def ^:const guide-color colors/new-danger)
|
||||||
(def guide-pill-width 34)
|
(def ^:const guide-pill-width 34)
|
||||||
(def guide-pill-height 20)
|
(def ^:const guide-pill-height 20)
|
||||||
(def guide-pill-corner-radius 4)
|
(def ^:const guide-pill-corner-radius 4)
|
||||||
(def guide-active-area 16)
|
(def ^:const guide-active-area 16)
|
||||||
|
|
||||||
(def guide-creation-margin-left 8)
|
(def ^:const guide-creation-margin-left 8)
|
||||||
(def guide-creation-margin-top 28)
|
(def ^:const guide-creation-margin-top 28)
|
||||||
(def guide-creation-width 16)
|
(def ^:const guide-creation-width 16)
|
||||||
(def guide-creation-height 24)
|
(def ^:const guide-creation-height 24)
|
||||||
|
|
||||||
(defn use-guide
|
(defn use-guide
|
||||||
"Hooks to support drag/drop for existing guides and new guides"
|
"Hooks to support drag/drop for existing guides and new guides"
|
||||||
[on-guide-change get-hover-frame zoom {:keys [id position axis frame-id]}]
|
[on-guide-change get-hover-frame zoom {:keys [id position axis frame-id]}]
|
||||||
(let [dragging-ref (mf/use-ref false)
|
(let [dragging-ref (mf/use-ref false)
|
||||||
start-ref (mf/use-ref nil)
|
start-ref (mf/use-ref nil)
|
||||||
start-pos-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-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-ref
|
||||||
frame (mf/deref 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
|
on-pointer-enter
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps workspace-read-only?)
|
(mf/deps read-only?)
|
||||||
(fn []
|
(fn []
|
||||||
(when-not workspace-read-only?
|
(when-not read-only?
|
||||||
(st/emit! (dw/set-hover-guide id true))
|
(st/emit! (dw/set-hover-guide id true))
|
||||||
(swap! state assoc :hover true))))
|
(swap! state assoc :hover true))))
|
||||||
|
|
||||||
on-pointer-leave
|
on-pointer-leave
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps workspace-read-only?)
|
(mf/deps read-only?)
|
||||||
(fn []
|
(fn []
|
||||||
(when-not workspace-read-only?
|
(when-not read-only?
|
||||||
(st/emit! (dw/set-hover-guide id false))
|
(st/emit! (dw/set-hover-guide id false))
|
||||||
(swap! state assoc :hover false))))
|
(swap! state assoc :hover false))))
|
||||||
|
|
||||||
on-pointer-down
|
on-pointer-down
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps workspace-read-only?)
|
(mf/deps read-only?)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(when-not workspace-read-only?
|
(when-not read-only?
|
||||||
(when (= 0 (.-button event))
|
(when (= 0 (.-button event))
|
||||||
(dom/capture-pointer event)
|
(dom/capture-pointer event)
|
||||||
(mf/set-ref-val! dragging-ref true)
|
(mf/set-ref-val! dragging-ref true)
|
||||||
@ -86,20 +94,20 @@
|
|||||||
(mf/set-ref-val! start-pos-ref (get @ms/mouse-position axis))))))
|
(mf/set-ref-val! start-pos-ref (get @ms/mouse-position axis))))))
|
||||||
|
|
||||||
on-pointer-up
|
on-pointer-up
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps (select-keys @state [:new-position :new-frame-id]) on-guide-change workspace-read-only?)
|
(mf/deps (select-keys @state [:new-position :new-frame-id]) on-guide-change read-only?)
|
||||||
(fn []
|
(fn []
|
||||||
(when-not workspace-read-only?
|
(when-not read-only?
|
||||||
(when (some? on-guide-change)
|
(when (some? on-guide-change)
|
||||||
(when (some? (:new-position @state))
|
(when (some? (:new-position @state))
|
||||||
(on-guide-change {:position (:new-position @state)
|
(on-guide-change {:position (:new-position @state)
|
||||||
:frame-id (:new-frame-id @state)}))))))
|
:frame-id (:new-frame-id @state)}))))))
|
||||||
|
|
||||||
on-lost-pointer-capture
|
on-lost-pointer-capture
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps workspace-read-only?)
|
(mf/deps read-only?)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(when-not workspace-read-only?
|
(when-not read-only?
|
||||||
(dom/release-pointer event)
|
(dom/release-pointer event)
|
||||||
(mf/set-ref-val! dragging-ref false)
|
(mf/set-ref-val! dragging-ref false)
|
||||||
(mf/set-ref-val! start-ref nil)
|
(mf/set-ref-val! start-ref nil)
|
||||||
@ -107,27 +115,29 @@
|
|||||||
(swap! state assoc :new-position nil))))
|
(swap! state assoc :new-position nil))))
|
||||||
|
|
||||||
on-pointer-move
|
on-pointer-move
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps position zoom snap-pixel? workspace-read-only?)
|
(mf/deps position zoom snap-pixel? read-only? get-hover-frame)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(when-not workspace-read-only?
|
(when-not read-only?
|
||||||
(when-let [_ (mf/ref-val dragging-ref)]
|
(when (mf/ref-val dragging-ref)
|
||||||
(let [start-pt (mf/ref-val start-ref)
|
(let [start-pt (mf/ref-val start-ref)
|
||||||
start-pos (mf/ref-val start-pos-ref)
|
start-pos (mf/ref-val start-pos-ref)
|
||||||
current-pt (dom/get-client-position event)
|
current-pt (dom/get-client-position event)
|
||||||
delta (/ (- (get current-pt axis) (get start-pt axis)) zoom)
|
delta (/ (- (get current-pt axis) (get start-pt axis)) zoom)
|
||||||
new-position (if (some? position)
|
new-position (if (some? position)
|
||||||
(+ position delta)
|
(+ position delta)
|
||||||
(+ start-pos delta))
|
(+ start-pos delta))
|
||||||
|
|
||||||
new-position (if snap-pixel?
|
new-position (if snap-pixel?
|
||||||
(mth/round new-position)
|
(mth/round new-position)
|
||||||
new-position)
|
new-position)
|
||||||
|
|
||||||
new-frame-id (:id (get-hover-frame))]
|
new-frame-id (-> (get-hover-frame)
|
||||||
|
(get :id))]
|
||||||
|
|
||||||
(swap! state assoc
|
(swap! state assoc
|
||||||
:new-position new-position
|
:new-position new-position
|
||||||
:new-frame-id new-frame-id))))))]
|
:new-frame-id new-frame-id))))))]
|
||||||
|
|
||||||
{:on-pointer-enter on-pointer-enter
|
{:on-pointer-enter on-pointer-enter
|
||||||
:on-pointer-leave on-pointer-leave
|
:on-pointer-leave on-pointer-leave
|
||||||
:on-pointer-down on-pointer-down
|
:on-pointer-down on-pointer-down
|
||||||
@ -137,8 +147,8 @@
|
|||||||
:state state
|
:state state
|
||||||
:frame frame}))
|
:frame frame}))
|
||||||
|
|
||||||
;; This functions are auxiliary to get the coords of components depending on the axis
|
;; This functions are auxiliary to get the coords of components
|
||||||
;; we're handling
|
;; depending on the axis we're handling
|
||||||
|
|
||||||
(defn guide-area-axis
|
(defn guide-area-axis
|
||||||
[pos vbox zoom frame axis]
|
[pos vbox zoom frame axis]
|
||||||
@ -270,11 +280,11 @@
|
|||||||
(<= (:position guide) (+ (:y frame) (:height frame))))))
|
(<= (:position guide) (+ (:y frame) (:height frame))))))
|
||||||
|
|
||||||
(mf/defc guide*
|
(mf/defc guide*
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]}
|
||||||
::mf/props :obj}
|
|
||||||
[{:keys [guide is-hover on-guide-change get-hover-frame vbox zoom
|
[{:keys [guide is-hover on-guide-change get-hover-frame vbox zoom
|
||||||
hover-frame disabled-guides frame-modifier frame-transform]}]
|
hover-frame disabled-guides frame-modifier frame-transform]}]
|
||||||
(let [axis (:axis guide)
|
(let [axis
|
||||||
|
(get guide :axis)
|
||||||
|
|
||||||
handle-change-position
|
handle-change-position
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -290,9 +300,11 @@
|
|||||||
on-lost-pointer-capture
|
on-lost-pointer-capture
|
||||||
on-pointer-move
|
on-pointer-move
|
||||||
state
|
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
|
frame
|
||||||
(cond-> base-frame
|
(cond-> base-frame
|
||||||
@ -302,12 +314,18 @@
|
|||||||
(some? frame-transform)
|
(some? frame-transform)
|
||||||
(gsh/apply-transform frame-transform))
|
(gsh/apply-transform frame-transform))
|
||||||
|
|
||||||
move-vec (gpt/to-vec (gpt/point (:x base-frame) (:y base-frame))
|
move-vec
|
||||||
(gpt/point (:x frame) (:y frame)))
|
(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))
|
pos
|
||||||
guide-width (/ guide-width zoom)
|
(+ (or (:new-position @state) (:position guide)) (get move-vec axis))
|
||||||
guide-pill-corner-radius (/ guide-pill-corner-radius zoom)
|
|
||||||
|
guide-width
|
||||||
|
(/ guide-width zoom)
|
||||||
|
|
||||||
|
guide-pill-corner-radius
|
||||||
|
(/ guide-pill-corner-radius zoom)
|
||||||
|
|
||||||
frame-guide-outside?
|
frame-guide-outside?
|
||||||
(and (some? frame)
|
(and (some? frame)
|
||||||
@ -404,9 +422,7 @@
|
|||||||
(fmt/format-number (- pos (if (= axis :x) (:x frame) (:y frame))))]]))])))
|
(fmt/format-number (- pos (if (= axis :x) (:x frame) (:y frame))))]]))])))
|
||||||
|
|
||||||
(mf/defc new-guide-area*
|
(mf/defc new-guide-area*
|
||||||
{::mf/props :obj}
|
|
||||||
[{:keys [vbox zoom axis get-hover-frame disabled-guides]}]
|
[{:keys [vbox zoom axis get-hover-frame disabled-guides]}]
|
||||||
|
|
||||||
(let [on-guide-change
|
(let [on-guide-change
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps vbox)
|
(mf/deps vbox)
|
||||||
@ -426,7 +442,9 @@
|
|||||||
state
|
state
|
||||||
frame]}
|
frame]}
|
||||||
(use-guide on-guide-change get-hover-frame zoom {:axis axis})
|
(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
|
[:g.new-guides
|
||||||
(when-not disabled-guides
|
(when-not disabled-guides
|
||||||
@ -441,13 +459,15 @@
|
|||||||
:on-pointer-up on-pointer-up
|
:on-pointer-up on-pointer-up
|
||||||
:on-lost-pointer-capture on-lost-pointer-capture
|
:on-lost-pointer-capture on-lost-pointer-capture
|
||||||
:on-pointer-move on-pointer-move
|
:on-pointer-move on-pointer-move
|
||||||
:class (when-not workspace-read-only?
|
:class (when-not read-only?
|
||||||
(if (= axis :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ns" 0)))
|
(if (= axis :x)
|
||||||
|
(cur/get-dynamic "resize-ew" 0)
|
||||||
|
(cur/get-dynamic "resize-ns" 0)))
|
||||||
:style {:fill "none"
|
:style {:fill "none"
|
||||||
:pointer-events "fill"}}]))
|
:pointer-events "fill"}}]))
|
||||||
|
|
||||||
(when (:new-position @state)
|
(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
|
:get-hover-frame get-hover-frame
|
||||||
:vbox vbox
|
:vbox vbox
|
||||||
:zoom zoom
|
:zoom zoom
|
||||||
@ -455,17 +475,18 @@
|
|||||||
:hover-frame frame}])]))
|
:hover-frame frame}])]))
|
||||||
|
|
||||||
(mf/defc viewport-guides*
|
(mf/defc viewport-guides*
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]}
|
||||||
::mf/props :obj}
|
|
||||||
[{:keys [zoom vbox hover-frame disabled-guides modifiers guides]}]
|
[{:keys [zoom vbox hover-frame disabled-guides modifiers guides]}]
|
||||||
(let [guides
|
(let [guides
|
||||||
(mf/with-memo [guides vbox]
|
(mf/with-memo [guides vbox]
|
||||||
(->> (vals guides)
|
(->> (vals guides)
|
||||||
(filter (partial guide-inside-vbox? zoom vbox))))
|
(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 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
|
;; we're only interested to get the frame in the guide we're moving
|
||||||
|
|||||||
@ -30,6 +30,15 @@ const exclusiveTypes = [
|
|||||||
* @property {boolean} [allowHTMLPaste]
|
* @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
|
* @param {string} text
|
||||||
@ -39,13 +48,14 @@ const exclusiveTypes = [
|
|||||||
*/
|
*/
|
||||||
function parseText(text, options) {
|
function parseText(text, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
const decodeTransit = options["decodeTransit"];
|
const decodeTransit = options["decodeTransit"];
|
||||||
if (decodeTransit) {
|
if (decodeTransit && looksLikeJSON(text)) {
|
||||||
try {
|
try {
|
||||||
decodeTransit(text);
|
decodeTransit(text);
|
||||||
return new Blob([text], { type: "application/transit+json" });
|
return new Blob([text], { type: "application/transit+json" });
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// NOOP
|
return new Blob([text], { type: "text/plain" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +145,7 @@ function sortItems(a, b) {
|
|||||||
export async function fromNavigator(options) {
|
export async function fromNavigator(options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
const items = await navigator.clipboard.read();
|
const items = await navigator.clipboard.read();
|
||||||
return Promise.all(
|
const result = await Promise.all(
|
||||||
Array.from(items).map(async (item) => {
|
Array.from(items).map(async (item) => {
|
||||||
const itemAllowedTypes = Array.from(item.types)
|
const itemAllowedTypes = Array.from(item.types)
|
||||||
.filter(filterAllowedTypes(options))
|
.filter(filterAllowedTypes(options))
|
||||||
@ -155,9 +165,15 @@ export async function fromNavigator(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const type = itemAllowedTypes.at(0);
|
const type = itemAllowedTypes.at(0);
|
||||||
|
|
||||||
|
if (type == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return item.getType(type);
|
return item.getType(type);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
return result.filter((item) => !!item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -104,10 +104,17 @@
|
|||||||
(.next ^js subscriber response)
|
(.next ^js subscriber response)
|
||||||
(.complete ^js subscriber)))
|
(.complete ^js subscriber)))
|
||||||
(p/catch
|
(p/catch
|
||||||
(fn [err]
|
(fn [cause]
|
||||||
(vreset! abortable? false)
|
(vreset! abortable? false)
|
||||||
(when-not @unsubscribed?
|
(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
|
(p/finally
|
||||||
(fn []
|
(fn []
|
||||||
(let [{:keys [count average] :or {count 0 average 0}} (get @network-averages (:path uri))
|
(let [{:keys [count average] :or {count 0 average 0}} (get @network-averages (:path uri))
|
||||||
@ -116,10 +123,15 @@
|
|||||||
(/ current-time (inc count)))
|
(/ current-time (inc count)))
|
||||||
count (inc count)]
|
count (inc count)]
|
||||||
(swap! network-averages assoc (:path uri) {:count count :average average})))))
|
(swap! network-averages assoc (:path uri) {:count count :average average})))))
|
||||||
|
|
||||||
(fn []
|
(fn []
|
||||||
(vreset! unsubscribed? true)
|
(vreset! unsubscribed? true)
|
||||||
(when @abortable?
|
(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
|
(defn response->map
|
||||||
[response]
|
[response]
|
||||||
|
|||||||
@ -68,8 +68,21 @@ export function createApi(
|
|||||||
},
|
},
|
||||||
|
|
||||||
sendMessage(message: unknown) {
|
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', {
|
const event = new CustomEvent('message', {
|
||||||
detail: message,
|
detail: cloneableMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
plugin.getModal()?.dispatchEvent(event);
|
plugin.getModal()?.dispatchEvent(event);
|
||||||
|
|||||||
@ -129,7 +129,14 @@ export class PluginModalElement extends HTMLElement {
|
|||||||
return;
|
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);
|
this.shadowRoot.appendChild(this.wrapper);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user