mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge remote-tracking branch 'origin/staging-render' into develop
This commit is contained in:
commit
c99fac000a
@ -73,6 +73,9 @@
|
|||||||
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
|
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
|
||||||
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
|
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
|
||||||
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
|
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
|
||||||
|
- Fix import a file with shadow tokens [Taiga #13229](https://tree.taiga.io/project/penpot/issue/13229)
|
||||||
|
- Fix allow spaces on token description [Taiga #13184](https://tree.taiga.io/project/penpot/issue/13184)
|
||||||
|
- Fix error when creating a token with an invalid name [Taiga #13219](https://tree.taiga.io/project/penpot/issue/13219)
|
||||||
|
|
||||||
## 2.12.1
|
## 2.12.1
|
||||||
|
|
||||||
|
|||||||
@ -2016,7 +2016,9 @@
|
|||||||
(let [;; We need to sync only the position relative to the origin of the component.
|
(let [;; We need to sync only the position relative to the origin of the component.
|
||||||
;; (see update-attrs for a full explanation)
|
;; (see update-attrs for a full explanation)
|
||||||
previous-shape (reposition-shape previous-shape prev-root current-root)
|
previous-shape (reposition-shape previous-shape prev-root current-root)
|
||||||
touched (get previous-shape :touched #{})]
|
touched (get previous-shape :touched #{})
|
||||||
|
text-auto? (and (cfh/text-shape? current-shape)
|
||||||
|
(contains? #{:auto-height :auto-width} (:grow-type current-shape)))]
|
||||||
|
|
||||||
(loop [attrs updatable-attrs
|
(loop [attrs updatable-attrs
|
||||||
roperations [{:type :set-touched :touched (:touched previous-shape)}]
|
roperations [{:type :set-touched :touched (:touched previous-shape)}]
|
||||||
@ -2025,6 +2027,10 @@
|
|||||||
(let [attr-group (get ctk/sync-attrs attr)
|
(let [attr-group (get ctk/sync-attrs attr)
|
||||||
skip-operations?
|
skip-operations?
|
||||||
(or
|
(or
|
||||||
|
;; For auto text, avoid copying geometry-driven attrs on switch.
|
||||||
|
(and text-auto?
|
||||||
|
(contains? #{:points :selrect :width :height :position-data} attr))
|
||||||
|
|
||||||
;; If the attribute is not valid for the destiny, don't copy it
|
;; If the attribute is not valid for the destiny, don't copy it
|
||||||
(not (cts/is-allowed-switch-keep-attr? attr (:type current-shape)))
|
(not (cts/is-allowed-switch-keep-attr? attr (:type current-shape)))
|
||||||
|
|
||||||
|
|||||||
@ -109,9 +109,12 @@
|
|||||||
(def token-types
|
(def token-types
|
||||||
(into #{} (keys token-type->dtcg-token-type)))
|
(into #{} (keys token-type->dtcg-token-type)))
|
||||||
|
|
||||||
|
(def token-name-validation-regex
|
||||||
|
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
||||||
|
|
||||||
(def token-name-ref
|
(def token-name-ref
|
||||||
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
||||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$"])
|
token-name-validation-regex])
|
||||||
|
|
||||||
(def ^:private schema:color
|
(def ^:private schema:color
|
||||||
[:map
|
[:map
|
||||||
|
|||||||
@ -1467,11 +1467,12 @@ Will return a value that matches this schema:
|
|||||||
(def ^:private schema:dtcg-node
|
(def ^:private schema:dtcg-node
|
||||||
[:schema {:registry
|
[:schema {:registry
|
||||||
{::simple-value
|
{::simple-value
|
||||||
[:or :string :int :double]
|
[:or :string :int :double ::sm/boolean]
|
||||||
::value
|
::value
|
||||||
[:or
|
[:or
|
||||||
[:ref ::simple-value]
|
[:ref ::simple-value]
|
||||||
[:vector ::simple-value]
|
[:vector ::simple-value]
|
||||||
|
[:vector [:map-of :string ::simple-value]]
|
||||||
[:map-of :string [:or
|
[:map-of :string [:or
|
||||||
[:ref ::simple-value]
|
[:ref ::simple-value]
|
||||||
[:vector ::simple-value]]]]}}
|
[:vector ::simple-value]]]]}}
|
||||||
|
|||||||
@ -110,7 +110,7 @@ test("Update an already created text shape by prepending text", async ({
|
|||||||
await workspace.textEditor.stopEditing();
|
await workspace.textEditor.stopEditing();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update an already created text shape by inserting text in between", async ({
|
test.skip("Update an already created text shape by inserting text in between", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
@ -151,7 +151,7 @@ test("Update a new text shape appending text by pasting text", async ({
|
|||||||
await workspace.textEditor.stopEditing();
|
await workspace.textEditor.stopEditing();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update a new text shape prepending text by pasting text", async ({
|
test.skip("Update a new text shape prepending text by pasting text", async ({
|
||||||
page,
|
page,
|
||||||
context,
|
context,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@ -46,7 +46,9 @@
|
|||||||
[app.main.data.workspace.thumbnails :as dwt]
|
[app.main.data.workspace.thumbnails :as dwt]
|
||||||
[app.main.data.workspace.transforms :as dwtr]
|
[app.main.data.workspace.transforms :as dwtr]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
|
[app.main.data.workspace.wasm-text :as dwwt]
|
||||||
[app.main.data.workspace.zoom :as dwz]
|
[app.main.data.workspace.zoom :as dwz]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.features.pointer-map :as fpmap]
|
[app.main.features.pointer-map :as fpmap]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
@ -1012,6 +1014,13 @@
|
|||||||
|
|
||||||
updated-objects (pcb/get-objects changes)
|
updated-objects (pcb/get-objects changes)
|
||||||
new-children-ids (cfh/get-children-ids-with-self updated-objects (:id new-shape))
|
new-children-ids (cfh/get-children-ids-with-self updated-objects (:id new-shape))
|
||||||
|
new-text-ids (->> new-children-ids
|
||||||
|
(keep (fn [id]
|
||||||
|
(when-let [child (get updated-objects id)]
|
||||||
|
(when (and (cfh/text-shape? child)
|
||||||
|
(not= :fixed (:grow-type child)))
|
||||||
|
id))))
|
||||||
|
(vec))
|
||||||
|
|
||||||
[changes parents-of-swapped]
|
[changes parents-of-swapped]
|
||||||
(if keep-touched?
|
(if keep-touched?
|
||||||
@ -1021,6 +1030,9 @@
|
|||||||
(rx/of
|
(rx/of
|
||||||
(dwu/start-undo-transaction undo-id)
|
(dwu/start-undo-transaction undo-id)
|
||||||
(dch/commit-changes changes)
|
(dch/commit-changes changes)
|
||||||
|
(when (and (features/active-feature? state "render-wasm/v1")
|
||||||
|
(seq new-text-ids))
|
||||||
|
(dwwt/resize-wasm-text-all new-text-ids))
|
||||||
(ptk/data-event :layout/update {:ids update-layout-ids :undo-group undo-group})
|
(ptk/data-event :layout/update {:ids update-layout-ids :undo-group undo-group})
|
||||||
(dwu/commit-undo-transaction undo-id)
|
(dwu/commit-undo-transaction undo-id)
|
||||||
(dws/select-shape (:id new-shape) false))))))
|
(dws/select-shape (:id new-shape) false))))))
|
||||||
|
|||||||
@ -712,8 +712,7 @@
|
|||||||
(ctm/rotation-modifiers shape center angle))
|
(ctm/rotation-modifiers shape center angle))
|
||||||
|
|
||||||
modif-tree
|
modif-tree
|
||||||
(-> (build-modif-tree ids objects get-modifier)
|
(build-modif-tree ids objects get-modifier)
|
||||||
(gm/set-objects-modifiers objects))
|
|
||||||
|
|
||||||
modifiers
|
modifiers
|
||||||
(mapv (fn [[id {:keys [modifiers]}]]
|
(mapv (fn [[id {:keys [modifiers]}]]
|
||||||
|
|||||||
@ -11,7 +11,6 @@
|
|||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.matrix :as gmt]
|
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
@ -29,10 +28,10 @@
|
|||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
[app.main.data.workspace.transforms :as dwt]
|
[app.main.data.workspace.transforms :as dwt]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
|
[app.main.data.workspace.wasm-text :as dwwt]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
[app.render-wasm.api :as wasm.api]
|
|
||||||
[app.util.text-editor :as ted]
|
[app.util.text-editor :as ted]
|
||||||
[app.util.text.content.styles :as styles]
|
[app.util.text.content.styles :as styles]
|
||||||
[app.util.timers :as ts]
|
[app.util.timers :as ts]
|
||||||
@ -52,50 +51,6 @@
|
|||||||
(declare v2-update-text-shape-content)
|
(declare v2-update-text-shape-content)
|
||||||
(declare v2-update-text-editor-styles)
|
(declare v2-update-text-editor-styles)
|
||||||
|
|
||||||
(defn resize-wasm-text-modifiers
|
|
||||||
([shape]
|
|
||||||
(resize-wasm-text-modifiers shape (:content shape)))
|
|
||||||
|
|
||||||
([{:keys [id points selrect grow-type] :as shape} content]
|
|
||||||
(wasm.api/use-shape id)
|
|
||||||
(wasm.api/set-shape-text-content id content)
|
|
||||||
(wasm.api/set-shape-text-images id content)
|
|
||||||
|
|
||||||
(let [dimension (wasm.api/get-text-dimensions)
|
|
||||||
width-scale (if (#{:fixed :auto-height} grow-type)
|
|
||||||
1.0
|
|
||||||
(/ (:width dimension) (:width selrect)))
|
|
||||||
height-scale (if (= :fixed grow-type)
|
|
||||||
1.0
|
|
||||||
(/ (:height dimension) (:height selrect)))
|
|
||||||
resize-v (gpt/point width-scale height-scale)
|
|
||||||
origin (first points)]
|
|
||||||
|
|
||||||
{id
|
|
||||||
{:modifiers
|
|
||||||
(ctm/resize-modifiers
|
|
||||||
resize-v
|
|
||||||
origin
|
|
||||||
(:transform shape (gmt/matrix))
|
|
||||||
(:transform-inverse shape (gmt/matrix)))}})))
|
|
||||||
|
|
||||||
(defn resize-wasm-text
|
|
||||||
[id]
|
|
||||||
(ptk/reify ::resize-wasm-text
|
|
||||||
ptk/WatchEvent
|
|
||||||
(watch [_ state _]
|
|
||||||
(let [objects (dsh/lookup-page-objects state)
|
|
||||||
shape (get objects id)]
|
|
||||||
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))))))
|
|
||||||
|
|
||||||
(defn resize-wasm-text-all
|
|
||||||
[ids]
|
|
||||||
(ptk/reify ::resize-wasm-text-all
|
|
||||||
ptk/WatchEvent
|
|
||||||
(watch [_ _ _]
|
|
||||||
(->> (rx/from ids)
|
|
||||||
(rx/map resize-wasm-text)))))
|
|
||||||
|
|
||||||
;; -- Content helpers
|
;; -- Content helpers
|
||||||
|
|
||||||
(defn- v2-content-has-text?
|
(defn- v2-content-has-text?
|
||||||
@ -178,7 +133,7 @@
|
|||||||
{:undo-group (when new-shape? id)})
|
{:undo-group (when new-shape? id)})
|
||||||
|
|
||||||
(dwm/apply-wasm-modifiers
|
(dwm/apply-wasm-modifiers
|
||||||
(resize-wasm-text-modifiers shape content)
|
(dwwt/resize-wasm-text-modifiers shape content)
|
||||||
{:undo-group (when new-shape? id)})))))
|
{:undo-group (when new-shape? id)})))))
|
||||||
|
|
||||||
(let [content (d/merge (ted/export-content content)
|
(let [content (d/merge (ted/export-content content)
|
||||||
@ -823,7 +778,7 @@
|
|||||||
(when (features/active-feature? state "render-wasm/v1")
|
(when (features/active-feature? state "render-wasm/v1")
|
||||||
;; This delay is to give time for the font to be correctly rendered
|
;; This delay is to give time for the font to be correctly rendered
|
||||||
;; in wasm.
|
;; in wasm.
|
||||||
(cond->> (rx/of (resize-wasm-text id))
|
(cond->> (rx/of (dwwt/resize-wasm-text id))
|
||||||
(contains? attrs :font-id)
|
(contains? attrs :font-id)
|
||||||
(rx/delay 200)))))))
|
(rx/delay 200)))))))
|
||||||
|
|
||||||
@ -973,11 +928,11 @@
|
|||||||
|
|
||||||
(if (and (not= :fixed (:grow-type shape)) finalize?)
|
(if (and (not= :fixed (:grow-type shape)) finalize?)
|
||||||
(dwm/apply-wasm-modifiers
|
(dwm/apply-wasm-modifiers
|
||||||
(resize-wasm-text-modifiers shape content)
|
(dwwt/resize-wasm-text-modifiers shape content)
|
||||||
{:undo-group (when new-shape? id)})
|
{:undo-group (when new-shape? id)})
|
||||||
|
|
||||||
(dwm/set-wasm-modifiers
|
(dwm/set-wasm-modifiers
|
||||||
(resize-wasm-text-modifiers shape content)
|
(dwwt/resize-wasm-text-modifiers shape content)
|
||||||
{:undo-group (when new-shape? id)})))
|
{:undo-group (when new-shape? id)})))
|
||||||
|
|
||||||
(when finalize?
|
(when finalize?
|
||||||
|
|||||||
@ -27,9 +27,9 @@
|
|||||||
[app.main.data.workspace.colors :as wdc]
|
[app.main.data.workspace.colors :as wdc]
|
||||||
[app.main.data.workspace.shape-layout :as dwsl]
|
[app.main.data.workspace.shape-layout :as dwsl]
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
|
||||||
[app.main.data.workspace.transforms :as dwtr]
|
[app.main.data.workspace.transforms :as dwtr]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
|
[app.main.data.workspace.wasm-text :as dwwt]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
@ -304,7 +304,7 @@
|
|||||||
(and affects-layout?
|
(and affects-layout?
|
||||||
(features/active-feature? state "render-wasm/v1"))
|
(features/active-feature? state "render-wasm/v1"))
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
|
||||||
|
|
||||||
(defn update-line-height
|
(defn update-line-height
|
||||||
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
|
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
|
||||||
@ -363,7 +363,7 @@
|
|||||||
:page-id page-id}))
|
:page-id page-id}))
|
||||||
(features/active-feature? state "render-wasm/v1")
|
(features/active-feature? state "render-wasm/v1")
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
|
||||||
|
|
||||||
(defn- create-font-family-text-attrs
|
(defn- create-font-family-text-attrs
|
||||||
[value]
|
[value]
|
||||||
@ -440,7 +440,7 @@
|
|||||||
:page-id page-id}))
|
:page-id page-id}))
|
||||||
(features/active-feature? state "render-wasm/v1")
|
(features/active-feature? state "render-wasm/v1")
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
|
||||||
|
|
||||||
(defn update-font-weight
|
(defn update-font-weight
|
||||||
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
|
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
|
||||||
|
|||||||
@ -406,13 +406,13 @@
|
|||||||
(ctm/change-property :grow-type new-grow-type)))
|
(ctm/change-property :grow-type new-grow-type)))
|
||||||
modifiers)))
|
modifiers)))
|
||||||
|
|
||||||
modif-tree
|
modif-tree (dwm/build-modif-tree ids objects get-modifier)]
|
||||||
(-> (dwm/build-modif-tree ids objects get-modifier)
|
|
||||||
(gm/set-objects-modifiers objects))]
|
|
||||||
|
|
||||||
(if (features/active-feature? state "render-wasm/v1")
|
(if (features/active-feature? state "render-wasm/v1")
|
||||||
(rx/of (dwm/apply-wasm-modifiers modif-tree {:ignore-snap-pixel true}))
|
(rx/of (dwm/apply-wasm-modifiers modif-tree {:ignore-snap-pixel true}))
|
||||||
(rx/of (dwm/apply-modifiers* objects modif-tree nil options))))))))
|
|
||||||
|
(let [modif-tree (gm/set-objects-modifiers modif-tree objects)]
|
||||||
|
(rx/of (dwm/apply-modifiers* objects modif-tree nil options)))))))))
|
||||||
|
|
||||||
(defn change-orientation
|
(defn change-orientation
|
||||||
"Change orientation of shapes, from the sidebar options form.
|
"Change orientation of shapes, from the sidebar options form.
|
||||||
|
|||||||
72
frontend/src/app/main/data/workspace/wasm_text.cljs
Normal file
72
frontend/src/app/main/data/workspace/wasm_text.cljs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
;; 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 app.main.data.workspace.wasm-text
|
||||||
|
"Helpers/events to resize wasm text shapes without depending on workspace.texts.
|
||||||
|
|
||||||
|
This exists to avoid circular deps:
|
||||||
|
workspace.texts -> workspace.libraries -> workspace.texts"
|
||||||
|
(:require
|
||||||
|
[app.common.files.helpers :as cfh]
|
||||||
|
[app.common.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.common.types.modifiers :as ctm]
|
||||||
|
[app.main.data.helpers :as dsh]
|
||||||
|
[app.main.data.workspace.modifiers :as dwm]
|
||||||
|
[app.render-wasm.api :as wasm.api]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
|
(defn resize-wasm-text-modifiers
|
||||||
|
([shape]
|
||||||
|
(resize-wasm-text-modifiers shape (:content shape)))
|
||||||
|
|
||||||
|
([{:keys [id points selrect grow-type] :as shape} content]
|
||||||
|
(wasm.api/use-shape id)
|
||||||
|
(wasm.api/set-shape-text-content id content)
|
||||||
|
(wasm.api/set-shape-text-images id content)
|
||||||
|
|
||||||
|
(let [dimension (wasm.api/get-text-dimensions)
|
||||||
|
width-scale (if (#{:fixed :auto-height} grow-type)
|
||||||
|
1.0
|
||||||
|
(/ (:width dimension) (:width selrect)))
|
||||||
|
height-scale (if (= :fixed grow-type)
|
||||||
|
1.0
|
||||||
|
(/ (:height dimension) (:height selrect)))
|
||||||
|
resize-v (gpt/point width-scale height-scale)
|
||||||
|
origin (first points)]
|
||||||
|
|
||||||
|
{id
|
||||||
|
{:modifiers
|
||||||
|
(ctm/resize-modifiers
|
||||||
|
resize-v
|
||||||
|
origin
|
||||||
|
(:transform shape (gmt/matrix))
|
||||||
|
(:transform-inverse shape (gmt/matrix)))}})))
|
||||||
|
|
||||||
|
(defn resize-wasm-text
|
||||||
|
"Resize a single text shape (auto-width/auto-height) by id.
|
||||||
|
No-op if the id is not a text shape or is :fixed."
|
||||||
|
[id]
|
||||||
|
(ptk/reify ::resize-wasm-text
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [objects (dsh/lookup-page-objects state)
|
||||||
|
shape (get objects id)]
|
||||||
|
(if (and (some? shape)
|
||||||
|
(cfh/text-shape? shape)
|
||||||
|
(not= :fixed (:grow-type shape)))
|
||||||
|
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))
|
||||||
|
(rx/empty))))))
|
||||||
|
|
||||||
|
(defn resize-wasm-text-all
|
||||||
|
"Resize all text shapes (auto-width/auto-height) from a collection of ids."
|
||||||
|
[ids]
|
||||||
|
(ptk/reify ::resize-wasm-text-all
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(->> (rx/from ids)
|
||||||
|
(rx/map resize-wasm-text)))))
|
||||||
@ -36,10 +36,12 @@
|
|||||||
|
|
||||||
(defn- hide-popover
|
(defn- hide-popover
|
||||||
[node]
|
[node]
|
||||||
(dom/unset-css-property! node "block-size")
|
(when (and (some? node)
|
||||||
(dom/unset-css-property! node "inset-block-start")
|
(fn? (.-hidePopover node)))
|
||||||
(dom/unset-css-property! node "inset-inline-start")
|
(dom/unset-css-property! node "block-size")
|
||||||
(.hidePopover ^js node))
|
(dom/unset-css-property! node "inset-block-start")
|
||||||
|
(dom/unset-css-property! node "inset-inline-start")
|
||||||
|
(.hidePopover ^js node)))
|
||||||
|
|
||||||
(defn- calculate-placement-bounding-rect
|
(defn- calculate-placement-bounding-rect
|
||||||
"Given a placement, calcultates the bounding rect for it taking in
|
"Given a placement, calcultates the bounding rect for it taking in
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
(def context (mf/create-context nil))
|
(def context (mf/create-context nil))
|
||||||
|
|
||||||
(mf/defc form-input*
|
(mf/defc form-input*
|
||||||
[{:keys [name] :rest props}]
|
[{:keys [name trim] :rest props}]
|
||||||
|
|
||||||
(let [form (mf/use-ctx context)
|
(let [form (mf/use-ctx context)
|
||||||
input-name name
|
input-name name
|
||||||
@ -33,7 +33,7 @@
|
|||||||
(mf/deps input-name)
|
(mf/deps input-name)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [value (-> event dom/get-target dom/get-input-value)]
|
(let [value (-> event dom/get-target dom/get-input-value)]
|
||||||
(fm/on-input-change form input-name value true))))
|
(fm/on-input-change form input-name value trim))))
|
||||||
|
|
||||||
props
|
props
|
||||||
(mf/spread-props props {:on-change on-change
|
(mf/spread-props props {:on-change on-change
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
[app.main.data.workspace.shortcuts :as sc]
|
[app.main.data.workspace.shortcuts :as sc]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
[app.main.data.workspace.texts :as dwt]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
|
[app.main.data.workspace.wasm-text :as dwwt]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
@ -141,7 +142,7 @@
|
|||||||
(dwsh/update-shapes ids #(assoc % :grow-type grow-type)))
|
(dwsh/update-shapes ids #(assoc % :grow-type grow-type)))
|
||||||
|
|
||||||
(when (features/active-feature? @st/state "render-wasm/v1")
|
(when (features/active-feature? @st/state "render-wasm/v1")
|
||||||
(st/emit! (dwt/resize-wasm-text-all ids)))
|
(st/emit! (dwwt/resize-wasm-text-all ids)))
|
||||||
;; We asynchronously commit so every sychronous event is resolved first and inside the transaction
|
;; We asynchronously commit so every sychronous event is resolved first and inside the transaction
|
||||||
(ts/schedule #(st/emit! (dwu/commit-undo-transaction uid))))
|
(ts/schedule #(st/emit! (dwu/commit-undo-transaction uid))))
|
||||||
(when (some? on-blur)
|
(when (some? on-blur)
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.types.color :as cl]
|
[app.common.types.color :as cl]
|
||||||
|
[app.common.types.token :as cto]
|
||||||
[app.common.types.tokens-lib :as ctob]
|
[app.common.types.tokens-lib :as ctob]
|
||||||
[app.main.data.style-dictionary :as sd]
|
[app.main.data.style-dictionary :as sd]
|
||||||
[app.main.data.tinycolor :as tinycolor]
|
[app.main.data.tinycolor :as tinycolor]
|
||||||
@ -51,12 +52,15 @@
|
|||||||
;; Both variants provide identical color-picker and text-input behavior, but
|
;; Both variants provide identical color-picker and text-input behavior, but
|
||||||
;; differ in how they persist the value within the form’s nested structure.
|
;; differ in how they persist the value within the form’s nested structure.
|
||||||
|
|
||||||
|
|
||||||
(defn- resolve-value
|
(defn- resolve-value
|
||||||
[tokens prev-token token-name value]
|
[tokens prev-token token-name value]
|
||||||
(let [token
|
(let [valid-token-name?
|
||||||
|
(and (string? token-name)
|
||||||
|
(re-matches cto/token-name-validation-regex token-name))
|
||||||
|
|
||||||
|
token
|
||||||
{:value value
|
{:value value
|
||||||
:name (if (str/blank? token-name)
|
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||||
token-name)}
|
token-name)}
|
||||||
|
|
||||||
|
|||||||
@ -50,9 +50,13 @@
|
|||||||
|
|
||||||
(defn- resolve-value
|
(defn- resolve-value
|
||||||
[tokens prev-token token-name value]
|
[tokens prev-token token-name value]
|
||||||
(let [token
|
(let [valid-token-name?
|
||||||
|
(and (string? token-name)
|
||||||
|
(re-matches cto/token-name-validation-regex token-name))
|
||||||
|
|
||||||
|
token
|
||||||
{:value (cto/split-font-family value)
|
{:value (cto/split-font-family value)
|
||||||
:name (if (str/blank? token-name)
|
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||||
token-name)}
|
token-name)}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.files.tokens :as cft]
|
[app.common.files.tokens :as cft]
|
||||||
|
[app.common.types.token :as cto]
|
||||||
[app.common.types.tokens-lib :as ctob]
|
[app.common.types.tokens-lib :as ctob]
|
||||||
[app.main.data.style-dictionary :as sd]
|
[app.main.data.style-dictionary :as sd]
|
||||||
[app.main.data.workspace.tokens.format :as dwtf]
|
[app.main.data.workspace.tokens.format :as dwtf]
|
||||||
@ -140,9 +141,13 @@
|
|||||||
|
|
||||||
(defn- resolve-value
|
(defn- resolve-value
|
||||||
[tokens prev-token token-name value]
|
[tokens prev-token token-name value]
|
||||||
(let [token
|
(let [valid-token-name?
|
||||||
|
(and (string? token-name)
|
||||||
|
(re-matches cto/token-name-validation-regex token-name))
|
||||||
|
|
||||||
|
token
|
||||||
{:value value
|
{:value value
|
||||||
:name (if (str/blank? token-name)
|
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||||
token-name)}
|
token-name)}
|
||||||
tokens
|
tokens
|
||||||
|
|||||||
@ -270,7 +270,13 @@
|
|||||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||||
:max-length max-input-length
|
:max-length max-input-length
|
||||||
:variant "comfortable"
|
:variant "comfortable"
|
||||||
:auto-focus true}]]
|
:trim true
|
||||||
|
:auto-focus true}]
|
||||||
|
|
||||||
|
(when (and warning-name-change? (= action "edit"))
|
||||||
|
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
|
||||||
|
[:> context-notification*
|
||||||
|
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||||
|
|
||||||
[:div {:class (stl/css :input-row)}
|
[:div {:class (stl/css :input-row)}
|
||||||
(case type
|
(case type
|
||||||
|
|||||||
@ -106,17 +106,20 @@
|
|||||||
|
|
||||||
(defn stop-propagation
|
(defn stop-propagation
|
||||||
[^js event]
|
[^js event]
|
||||||
(when event
|
(when (and (some? event)
|
||||||
|
(fn? (.-stopPropagation event)))
|
||||||
(.stopPropagation event)))
|
(.stopPropagation event)))
|
||||||
|
|
||||||
(defn stop-immediate-propagation
|
(defn stop-immediate-propagation
|
||||||
[^js event]
|
[^js event]
|
||||||
(when event
|
(when (and (some? event)
|
||||||
|
(fn? (.-stopImmediatePropagation event)))
|
||||||
(.stopImmediatePropagation event)))
|
(.stopImmediatePropagation event)))
|
||||||
|
|
||||||
(defn prevent-default
|
(defn prevent-default
|
||||||
[^js event]
|
[^js event]
|
||||||
(when event
|
(when (and (some? event)
|
||||||
|
(fn? (.-preventDefault event)))
|
||||||
(.preventDefault event)))
|
(.preventDefault event)))
|
||||||
|
|
||||||
(defn get-target
|
(defn get-target
|
||||||
|
|||||||
@ -7,15 +7,16 @@
|
|||||||
(ns app.util.keyboard
|
(ns app.util.keyboard
|
||||||
(:require
|
(:require
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
|
[app.util.dom :as dom]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(defrecord KeyboardEvent [type key shift ctrl alt meta mod editing native-event]
|
(defrecord KeyboardEvent [type key shift ctrl alt meta mod editing native-event]
|
||||||
Object
|
Object
|
||||||
(preventDefault [_]
|
(preventDefault [_]
|
||||||
(.preventDefault native-event))
|
(dom/prevent-default native-event))
|
||||||
|
|
||||||
(stopPropagation [_]
|
(stopPropagation [_]
|
||||||
(.stopPropagation native-event)))
|
(dom/stop-propagation native-event)))
|
||||||
|
|
||||||
(defn keyboard-event?
|
(defn keyboard-event?
|
||||||
[o]
|
[o]
|
||||||
|
|||||||
@ -405,12 +405,8 @@ export class TextEditor extends EventTarget {
|
|||||||
|
|
||||||
if (e.inputType in commands) {
|
if (e.inputType in commands) {
|
||||||
const command = commands[e.inputType];
|
const command = commands[e.inputType];
|
||||||
if (!this.#selectionController.startMutation()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
command(e, this, this.#selectionController);
|
command(e, this, this.#selectionController);
|
||||||
const mutations = this.#selectionController.endMutation();
|
this.#notifyLayout(LayoutType.FULL);
|
||||||
this.#notifyLayout(LayoutType.FULL, mutations);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -456,19 +452,12 @@ export class TextEditor extends EventTarget {
|
|||||||
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "Backspace") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "Backspace") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.#selectionController.startMutation()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#selectionController.isCollapsed) {
|
if (this.#selectionController.isCollapsed) {
|
||||||
this.#selectionController.removeWordBackward();
|
this.#selectionController.removeWordBackward();
|
||||||
} else {
|
} else {
|
||||||
this.#selectionController.removeSelected();
|
this.#selectionController.removeSelected();
|
||||||
}
|
}
|
||||||
|
this.#notifyLayout(LayoutType.FULL);
|
||||||
const mutations = this.#selectionController.endMutation();
|
|
||||||
this.#notifyLayout(LayoutType.FULL, mutations);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -476,14 +465,12 @@ export class TextEditor extends EventTarget {
|
|||||||
* Notifies that the edited texts needs layout.
|
* Notifies that the edited texts needs layout.
|
||||||
*
|
*
|
||||||
* @param {'full'|'partial'} type
|
* @param {'full'|'partial'} type
|
||||||
* @param {CommandMutations} mutations
|
|
||||||
*/
|
*/
|
||||||
#notifyLayout(type = LayoutType.FULL, mutations) {
|
#notifyLayout(type = LayoutType.FULL) {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("needslayout", {
|
new CustomEvent("needslayout", {
|
||||||
detail: {
|
detail: {
|
||||||
type: type,
|
type: type,
|
||||||
mutations: mutations,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -630,10 +617,8 @@ export class TextEditor extends EventTarget {
|
|||||||
* @returns {TextEditor}
|
* @returns {TextEditor}
|
||||||
*/
|
*/
|
||||||
applyStylesToSelection(styles) {
|
applyStylesToSelection(styles) {
|
||||||
this.#selectionController.startMutation();
|
|
||||||
this.#selectionController.applyStyles(styles);
|
this.#selectionController.applyStyles(styles);
|
||||||
const mutations = this.#selectionController.endMutation();
|
this.#notifyLayout(LayoutType.FULL);
|
||||||
this.#notifyLayout(LayoutType.FULL, mutations);
|
|
||||||
this.#changeController.notifyImmediately();
|
this.#changeController.notifyImmediately();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Command mutations
|
|
||||||
*/
|
|
||||||
export class CommandMutations {
|
|
||||||
#added = new Set();
|
|
||||||
#removed = new Set();
|
|
||||||
#updated = new Set();
|
|
||||||
|
|
||||||
constructor(added, updated, removed) {
|
|
||||||
if (added && Array.isArray(added)) this.#added = new Set(added);
|
|
||||||
if (updated && Array.isArray(updated)) this.#updated = new Set(updated);
|
|
||||||
if (removed && Array.isArray(removed)) this.#removed = new Set(removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
get added() {
|
|
||||||
return this.#added;
|
|
||||||
}
|
|
||||||
|
|
||||||
get removed() {
|
|
||||||
return this.#removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
get updated() {
|
|
||||||
return this.#updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.#added.clear();
|
|
||||||
this.#removed.clear();
|
|
||||||
this.#updated.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.#added.clear();
|
|
||||||
this.#added = null;
|
|
||||||
this.#removed.clear();
|
|
||||||
this.#removed = null;
|
|
||||||
this.#updated.clear();
|
|
||||||
this.#updated = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
add(node) {
|
|
||||||
this.#added.add(node);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(node) {
|
|
||||||
this.#removed.add(node);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
update(node) {
|
|
||||||
this.#updated.add(node);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommandMutations;
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
import { describe, test, expect } from "vitest";
|
|
||||||
import CommandMutations from "./CommandMutations.js";
|
|
||||||
|
|
||||||
describe("CommandMutations", () => {
|
|
||||||
test("should create a new CommandMutations", () => {
|
|
||||||
const mutations = new CommandMutations();
|
|
||||||
expect(mutations).toHaveProperty("added");
|
|
||||||
expect(mutations).toHaveProperty("updated");
|
|
||||||
expect(mutations).toHaveProperty("removed");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should create an initialized new CommandMutations", () => {
|
|
||||||
const mutations = new CommandMutations([1], [2], [3]);
|
|
||||||
expect(mutations.added.size).toBe(1);
|
|
||||||
expect(mutations.updated.size).toBe(1);
|
|
||||||
expect(mutations.removed.size).toBe(1);
|
|
||||||
expect(mutations.added.has(1)).toBe(true);
|
|
||||||
expect(mutations.updated.has(2)).toBe(true);
|
|
||||||
expect(mutations.removed.has(3)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should add an added node to a CommandMutations", () => {
|
|
||||||
const mutations = new CommandMutations();
|
|
||||||
mutations.add(1);
|
|
||||||
expect(mutations.added.has(1)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should add an updated node to a CommandMutations", () => {
|
|
||||||
const mutations = new CommandMutations();
|
|
||||||
mutations.update(1);
|
|
||||||
expect(mutations.updated.has(1)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should add an removed node to a CommandMutations", () => {
|
|
||||||
const mutations = new CommandMutations();
|
|
||||||
mutations.remove(1);
|
|
||||||
expect(mutations.removed.has(1)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should clear a CommandMutations", () => {
|
|
||||||
const mutations = new CommandMutations();
|
|
||||||
mutations.add(1);
|
|
||||||
mutations.update(2);
|
|
||||||
mutations.remove(3);
|
|
||||||
expect(mutations.added.has(1)).toBe(true);
|
|
||||||
expect(mutations.added.size).toBe(1);
|
|
||||||
expect(mutations.updated.has(2)).toBe(true);
|
|
||||||
expect(mutations.updated.size).toBe(1);
|
|
||||||
expect(mutations.removed.has(3)).toBe(true);
|
|
||||||
expect(mutations.removed.size).toBe(1);
|
|
||||||
|
|
||||||
mutations.clear();
|
|
||||||
expect(mutations.added.size).toBe(0);
|
|
||||||
expect(mutations.added.has(1)).toBe(false);
|
|
||||||
expect(mutations.updated.size).toBe(0);
|
|
||||||
expect(mutations.updated.has(1)).toBe(false);
|
|
||||||
expect(mutations.removed.size).toBe(0);
|
|
||||||
expect(mutations.removed.has(1)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should dispose a CommandMutations", () => {
|
|
||||||
const mutations = new CommandMutations();
|
|
||||||
mutations.add(1);
|
|
||||||
mutations.update(2);
|
|
||||||
mutations.remove(3);
|
|
||||||
mutations.dispose();
|
|
||||||
expect(mutations.added).toBe(null);
|
|
||||||
expect(mutations.updated).toBe(null);
|
|
||||||
expect(mutations.removed).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from "vitest";
|
import { describe, test, expect } from "vitest";
|
||||||
import { insertInto, removeBackward, removeForward, replaceWith } from "./Text";
|
import { insertInto, removeSlice, removeBackward, removeForward, removeWordBackward, replaceWith, findPreviousWordBoundary } from "./Text";
|
||||||
|
|
||||||
describe("Text", () => {
|
describe("Text", () => {
|
||||||
test("* should throw when passed wrong parameters", () => {
|
test("* should throw when passed wrong parameters", () => {
|
||||||
@ -51,4 +51,23 @@ describe("Text", () => {
|
|||||||
test("`removeForward` should remove string forward from offset 6", () => {
|
test("`removeForward` should remove string forward from offset 6", () => {
|
||||||
expect(removeForward("Hello, World!", 6)).toBe("Hello,World!");
|
expect(removeForward("Hello, World!", 6)).toBe("Hello,World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("`removeSlice` should remove a part of a text", () => {
|
||||||
|
expect(removeSlice("Hello, World!", 7, 12)).toBe("Hello, !");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("`findPreviousWordBoundary` edge cases", () => {
|
||||||
|
expect(findPreviousWordBoundary(null)).toBe(0);
|
||||||
|
expect(findPreviousWordBoundary("Hello, World!", 0)).toBe(0);
|
||||||
|
expect(findPreviousWordBoundary(" Hello, World!", 3)).toBe(0);
|
||||||
|
})
|
||||||
|
|
||||||
|
test("`removeWordBackward` with no text should return an empty string", () => {
|
||||||
|
expect(removeWordBackward(null, 0)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("`removeWordBackward` should remove a word backward", () => {
|
||||||
|
expect(removeWordBackward("Hello, World!", 13)).toBe("Hello, World");
|
||||||
|
expect(removeWordBackward("Hello, World", 12)).toBe("Hello, ");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest";
|
|||||||
import { getFills } from "./Color.js";
|
import { getFills } from "./Color.js";
|
||||||
|
|
||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
describe("Color", () => {
|
describe.skip("Color", () => {
|
||||||
test("getFills", () => {
|
test("getFills", () => {
|
||||||
expect(getFills("#aa0000")).toBe(
|
expect(getFills("#aa0000")).toBe(
|
||||||
'[["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]',
|
'[["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]',
|
||||||
|
|||||||
@ -49,7 +49,6 @@ import {
|
|||||||
} from "../content/dom/TextNode.js";
|
} from "../content/dom/TextNode.js";
|
||||||
import TextNodeIterator from "../content/dom/TextNodeIterator.js";
|
import TextNodeIterator from "../content/dom/TextNodeIterator.js";
|
||||||
import TextEditor from "../TextEditor.js";
|
import TextEditor from "../TextEditor.js";
|
||||||
import CommandMutations from "../commands/CommandMutations.js";
|
|
||||||
import { isRoot, setRootStyles } from "../content/dom/Root.js";
|
import { isRoot, setRootStyles } from "../content/dom/Root.js";
|
||||||
import { SelectionDirection } from "./SelectionDirection.js";
|
import { SelectionDirection } from "./SelectionDirection.js";
|
||||||
import { SafeGuard } from "./SafeGuard.js";
|
import { SafeGuard } from "./SafeGuard.js";
|
||||||
@ -145,13 +144,6 @@ export class SelectionController extends EventTarget {
|
|||||||
*/
|
*/
|
||||||
#debug = null;
|
#debug = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Command Mutations.
|
|
||||||
*
|
|
||||||
* @type {CommandMutations}
|
|
||||||
*/
|
|
||||||
#mutations = new CommandMutations();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Style defaults.
|
* Style defaults.
|
||||||
*
|
*
|
||||||
@ -449,14 +441,14 @@ export class SelectionController extends EventTarget {
|
|||||||
dispose() {
|
dispose() {
|
||||||
document.removeEventListener("selectionchange", this.#onSelectionChange);
|
document.removeEventListener("selectionchange", this.#onSelectionChange);
|
||||||
this.#textEditor = null;
|
this.#textEditor = null;
|
||||||
|
this.#currentStyle = null;
|
||||||
|
this.#options = null;
|
||||||
this.#ranges.clear();
|
this.#ranges.clear();
|
||||||
this.#ranges = null;
|
this.#ranges = null;
|
||||||
this.#range = null;
|
this.#range = null;
|
||||||
this.#selection = null;
|
this.#selection = null;
|
||||||
this.#focusNode = null;
|
this.#focusNode = null;
|
||||||
this.#anchorNode = null;
|
this.#anchorNode = null;
|
||||||
this.#mutations.dispose();
|
|
||||||
this.#mutations = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -522,28 +514,6 @@ export class SelectionController extends EventTarget {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the start of a mutation.
|
|
||||||
*
|
|
||||||
* Clears all the mutations kept in CommandMutations.
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
startMutation() {
|
|
||||||
this.#mutations.clear();
|
|
||||||
if (!this.#focusNode) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the end of a mutation.
|
|
||||||
*
|
|
||||||
* @returns {CommandMutations}
|
|
||||||
*/
|
|
||||||
endMutation() {
|
|
||||||
return this.#mutations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects all content.
|
* Selects all content.
|
||||||
*
|
*
|
||||||
@ -597,11 +567,18 @@ export class SelectionController extends EventTarget {
|
|||||||
* @returns {SelectionController}
|
* @returns {SelectionController}
|
||||||
*/
|
*/
|
||||||
cursorToEnd() {
|
cursorToEnd() {
|
||||||
|
const root = this.#textEditor.root;
|
||||||
|
|
||||||
const range = document.createRange(); //Create a range (a range is a like the selection but invisible)
|
const range = document.createRange(); //Create a range (a range is a like the selection but invisible)
|
||||||
range.selectNodeContents(this.#textEditor.element);
|
range.setStart(root.lastChild.firstChild.firstChild, root.lastChild.firstChild.firstChild?.nodeValue?.length ?? 0);
|
||||||
|
range.setEnd(root.lastChild.firstChild.firstChild, root.lastChild.firstChild.firstChild?.nodeValue?.length ?? 0);
|
||||||
range.collapse(false);
|
range.collapse(false);
|
||||||
|
|
||||||
this.#selection.removeAllRanges();
|
this.#selection.removeAllRanges();
|
||||||
this.#selection.addRange(range);
|
this.#selection.addRange(range);
|
||||||
|
|
||||||
|
this.#updateState();
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1340,7 +1317,6 @@ export class SelectionController extends EventTarget {
|
|||||||
|
|
||||||
if (this.focusNode.nodeValue !== removedData) {
|
if (this.focusNode.nodeValue !== removedData) {
|
||||||
this.focusNode.nodeValue = removedData;
|
this.focusNode.nodeValue = removedData;
|
||||||
this.#mutations.update(this.focusTextSpan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const paragraph = this.focusParagraph;
|
const paragraph = this.focusParagraph;
|
||||||
@ -1383,7 +1359,6 @@ export class SelectionController extends EventTarget {
|
|||||||
this.focusOffset,
|
this.focusOffset,
|
||||||
newText,
|
newText,
|
||||||
);
|
);
|
||||||
this.#mutations.update(this.focusTextSpan);
|
|
||||||
return this.collapse(this.focusNode, this.focusOffset + newText.length);
|
return this.collapse(this.focusNode, this.focusOffset + newText.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1447,7 +1422,6 @@ export class SelectionController extends EventTarget {
|
|||||||
this.#textEditor.root.replaceChildren(newParagraph);
|
this.#textEditor.root.replaceChildren(newParagraph);
|
||||||
return this.collapse(newTextNode, newText.length + 1);
|
return this.collapse(newTextNode, newText.length + 1);
|
||||||
}
|
}
|
||||||
this.#mutations.update(this.focusTextSpan);
|
|
||||||
return this.collapse(this.focusNode, startOffset + newText.length);
|
return this.collapse(this.focusNode, startOffset + newText.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1525,8 +1499,6 @@ export class SelectionController extends EventTarget {
|
|||||||
const currentParagraph = this.focusParagraph;
|
const currentParagraph = this.focusParagraph;
|
||||||
const newParagraph = createEmptyParagraph(this.#currentStyle);
|
const newParagraph = createEmptyParagraph(this.#currentStyle);
|
||||||
currentParagraph.after(newParagraph);
|
currentParagraph.after(newParagraph);
|
||||||
this.#mutations.update(currentParagraph);
|
|
||||||
this.#mutations.add(newParagraph);
|
|
||||||
return this.collapse(newParagraph.firstChild.firstChild, 0);
|
return this.collapse(newParagraph.firstChild.firstChild, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1537,8 +1509,6 @@ export class SelectionController extends EventTarget {
|
|||||||
const currentParagraph = this.focusParagraph;
|
const currentParagraph = this.focusParagraph;
|
||||||
const newParagraph = createEmptyParagraph(this.#currentStyle);
|
const newParagraph = createEmptyParagraph(this.#currentStyle);
|
||||||
currentParagraph.before(newParagraph);
|
currentParagraph.before(newParagraph);
|
||||||
this.#mutations.update(currentParagraph);
|
|
||||||
this.#mutations.add(newParagraph);
|
|
||||||
return this.collapse(currentParagraph.firstChild.firstChild, 0);
|
return this.collapse(currentParagraph.firstChild.firstChild, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1553,8 +1523,6 @@ export class SelectionController extends EventTarget {
|
|||||||
this.#focusOffset,
|
this.#focusOffset,
|
||||||
);
|
);
|
||||||
this.focusParagraph.after(newParagraph);
|
this.focusParagraph.after(newParagraph);
|
||||||
this.#mutations.update(currentParagraph);
|
|
||||||
this.#mutations.add(newParagraph);
|
|
||||||
return this.collapse(newParagraph.firstChild.firstChild, 0);
|
return this.collapse(newParagraph.firstChild.firstChild, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1586,10 +1554,6 @@ export class SelectionController extends EventTarget {
|
|||||||
this.focusOffset,
|
this.focusOffset,
|
||||||
);
|
);
|
||||||
currentParagraph.after(newParagraph);
|
currentParagraph.after(newParagraph);
|
||||||
|
|
||||||
this.#mutations.update(currentParagraph);
|
|
||||||
this.#mutations.add(newParagraph);
|
|
||||||
|
|
||||||
// FIXME: Missing collapse?
|
// FIXME: Missing collapse?
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1610,7 +1574,6 @@ export class SelectionController extends EventTarget {
|
|||||||
const previousOffset = isLineBreak(previousTextSpan.firstChild)
|
const previousOffset = isLineBreak(previousTextSpan.firstChild)
|
||||||
? 0
|
? 0
|
||||||
: previousTextSpan.firstChild.nodeValue?.length || 0;
|
: previousTextSpan.firstChild.nodeValue?.length || 0;
|
||||||
this.#mutations.remove(paragraphToBeRemoved);
|
|
||||||
return this.collapse(previousTextSpan.firstChild, previousOffset);
|
return this.collapse(previousTextSpan.firstChild, previousOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1632,8 +1595,6 @@ export class SelectionController extends EventTarget {
|
|||||||
} else {
|
} else {
|
||||||
mergeParagraphs(previousParagraph, currentParagraph);
|
mergeParagraphs(previousParagraph, currentParagraph);
|
||||||
}
|
}
|
||||||
this.#mutations.remove(currentParagraph);
|
|
||||||
this.#mutations.update(previousParagraph);
|
|
||||||
return this.collapse(previousTextSpan.firstChild, previousOffset);
|
return this.collapse(previousTextSpan.firstChild, previousOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1647,8 +1608,6 @@ export class SelectionController extends EventTarget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mergeParagraphs(this.focusParagraph, nextParagraph);
|
mergeParagraphs(this.focusParagraph, nextParagraph);
|
||||||
this.#mutations.update(currentParagraph);
|
|
||||||
this.#mutations.remove(nextParagraph);
|
|
||||||
|
|
||||||
// FIXME: Missing collapse?
|
// FIXME: Missing collapse?
|
||||||
}
|
}
|
||||||
@ -1665,7 +1624,6 @@ export class SelectionController extends EventTarget {
|
|||||||
paragraphToBeRemoved.remove();
|
paragraphToBeRemoved.remove();
|
||||||
const nextTextSpan = nextParagraph.firstChild;
|
const nextTextSpan = nextParagraph.firstChild;
|
||||||
const nextOffset = this.focusOffset;
|
const nextOffset = this.focusOffset;
|
||||||
this.#mutations.remove(paragraphToBeRemoved);
|
|
||||||
return this.collapse(nextTextSpan.firstChild, nextOffset);
|
return this.collapse(nextTextSpan.firstChild, nextOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1680,7 +1638,6 @@ export class SelectionController extends EventTarget {
|
|||||||
for (const textSpan of affectedTextSpans) {
|
for (const textSpan of affectedTextSpans) {
|
||||||
if (textSpan.textContent === "") {
|
if (textSpan.textContent === "") {
|
||||||
textSpan.remove();
|
textSpan.remove();
|
||||||
this.#mutations.remove(textSpan);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1688,7 +1645,6 @@ export class SelectionController extends EventTarget {
|
|||||||
for (const paragraph of affectedParagraphs) {
|
for (const paragraph of affectedParagraphs) {
|
||||||
if (paragraph.children.length === 0) {
|
if (paragraph.children.length === 0) {
|
||||||
paragraph.remove();
|
paragraph.remove();
|
||||||
this.#mutations.remove(paragraph);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -581,6 +581,136 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.textContent).toBe("");
|
expect(textEditorMock.root.textContent).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("`insertParagraph` should insert a new paragraph in an empty editor", () => {
|
||||||
|
const textEditorMock = TextEditorMock.createTextEditorMockEmpty();
|
||||||
|
const root = textEditorMock.root;
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const selectionController = new SelectionController(textEditorMock, selection);
|
||||||
|
focus(
|
||||||
|
selection,
|
||||||
|
textEditorMock,
|
||||||
|
root.firstChild.firstChild.firstChild,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
selectionController.insertParagraph();
|
||||||
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||||
|
expect(textEditorMock.root.children.length).toBe(2);
|
||||||
|
expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.children.item(0).dataset.itype).toBe("paragraph");
|
||||||
|
expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf(
|
||||||
|
HTMLSpanElement,
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe("span");
|
||||||
|
expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.children.item(1).dataset.itype).toBe("paragraph");
|
||||||
|
expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf(
|
||||||
|
HTMLSpanElement,
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe(
|
||||||
|
"span",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.textContent).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("`insertParagraph` should insert a new paragraph after a text", () => {
|
||||||
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
|
["Hello, World!"]
|
||||||
|
]);
|
||||||
|
const root = textEditorMock.root;
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const selectionController = new SelectionController(
|
||||||
|
textEditorMock,
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
focus(
|
||||||
|
selection,
|
||||||
|
textEditorMock,
|
||||||
|
root.firstChild.firstChild.firstChild,
|
||||||
|
"Hello, World!".length
|
||||||
|
);
|
||||||
|
selectionController.insertParagraph();
|
||||||
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||||
|
expect(textEditorMock.root.children.length).toBe(2);
|
||||||
|
expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.children.item(0).dataset.itype).toBe(
|
||||||
|
"paragraph",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf(
|
||||||
|
HTMLSpanElement,
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe(
|
||||||
|
"span",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(0).firstChild.textContent).toBe(
|
||||||
|
"Hello, World!",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.children.item(1).dataset.itype).toBe(
|
||||||
|
"paragraph",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf(
|
||||||
|
HTMLSpanElement,
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe(
|
||||||
|
"span",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1).firstChild.firstChild).toBeInstanceOf(
|
||||||
|
HTMLBRElement,
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("`insertParagraph` should insert a new paragraph before a text", () => {
|
||||||
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
|
["Hello, World!"],
|
||||||
|
]);
|
||||||
|
const root = textEditorMock.root;
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const selectionController = new SelectionController(
|
||||||
|
textEditorMock,
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
focus(
|
||||||
|
selection,
|
||||||
|
textEditorMock,
|
||||||
|
root.firstChild.firstChild.firstChild,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
selectionController.insertParagraph();
|
||||||
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||||
|
expect(textEditorMock.root.children.length).toBe(2);
|
||||||
|
expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.children.item(0).dataset.itype).toBe(
|
||||||
|
"paragraph",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf(
|
||||||
|
HTMLSpanElement,
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe(
|
||||||
|
"span",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(0).firstChild.firstChild).toBeInstanceOf(
|
||||||
|
HTMLBRElement,
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(textEditorMock.root.children.item(1).dataset.itype).toBe(
|
||||||
|
"paragraph",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf(
|
||||||
|
HTMLSpanElement,
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe(
|
||||||
|
"span",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.children.item(1).firstChild.textContent).toBe(
|
||||||
|
"Hello, World!",
|
||||||
|
);
|
||||||
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
|
});
|
||||||
|
|
||||||
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
["Hello, "],
|
["Hello, "],
|
||||||
@ -1027,7 +1157,7 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("`removeSelected` multiple paragraphs", () => {
|
test("`removeSelected` multiple paragraphs", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
["Hello, "],
|
["Hello, "],
|
||||||
["\n"],
|
["\n"],
|
||||||
@ -1392,7 +1522,10 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.lastChild.firstChild.nodeValue.length - 3,
|
root.firstChild.lastChild.firstChild.nodeValue.length - 3,
|
||||||
);
|
);
|
||||||
selectionController.applyStyles({
|
selectionController.applyStyles({
|
||||||
|
"font-family": "Montserrat, sans-serif",
|
||||||
"font-weight": "bold",
|
"font-weight": "bold",
|
||||||
|
"--fills":
|
||||||
|
'[["^ ","~:fill-color","#000000","~:fill-opacity",1],["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]',
|
||||||
});
|
});
|
||||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(textEditorMock.root.children.length).toBe(1);
|
expect(textEditorMock.root.children.length).toBe(1);
|
||||||
@ -1492,4 +1625,68 @@ describe("SelectionController", () => {
|
|||||||
"ld!",
|
"ld!",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("`selectAll` should select everything", () => {
|
||||||
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
|
createParagraphWith(["Hello, "], {
|
||||||
|
"font-style": "italic",
|
||||||
|
}),
|
||||||
|
createParagraphWith(["World!"], {
|
||||||
|
"font-style": "oblique",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const root = textEditorMock.root;
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const selectionController = new SelectionController(textEditorMock, selection);
|
||||||
|
textEditorMock.element.focus();
|
||||||
|
selectionController.selectAll();
|
||||||
|
expect(selectionController.anchorNode).toBe(
|
||||||
|
root.firstChild.firstChild.firstChild
|
||||||
|
);
|
||||||
|
expect(selectionController.focusNode).toBe(
|
||||||
|
root.lastChild.firstChild.firstChild,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("`cursorToEnd` should move cursor to the end", () => {
|
||||||
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
|
createParagraphWith(["Hello, "], {
|
||||||
|
"font-style": "italic",
|
||||||
|
}),
|
||||||
|
createParagraphWith(["World!"], {
|
||||||
|
"font-style": "oblique",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const root = textEditorMock.root;
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const selectionController = new SelectionController(textEditorMock, selection);
|
||||||
|
textEditorMock.element.focus();
|
||||||
|
selectionController.cursorToEnd();
|
||||||
|
expect(selectionController.focusNode).toBe(root.lastChild.firstChild.firstChild);
|
||||||
|
expect(selectionController.focusAtEnd).toBeTruthy();
|
||||||
|
})
|
||||||
|
|
||||||
|
test("`dispose` should release every held reference", () => {
|
||||||
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
|
createParagraphWith(["Hello, "], {
|
||||||
|
"font-style": "italic",
|
||||||
|
}),
|
||||||
|
createParagraphWith(["World!"], {
|
||||||
|
"font-style": "oblique",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const root = textEditorMock.root;
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const selectionController = new SelectionController(textEditorMock, selection);
|
||||||
|
focus(
|
||||||
|
selection,
|
||||||
|
textEditorMock,
|
||||||
|
root.firstChild.firstChild.firstChild,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
selectionController.dispose();
|
||||||
|
expect(selectionController.selection).toBe(null);
|
||||||
|
expect(selectionController.currentStyle).toBe(null);
|
||||||
|
expect(selectionController.options).toBe(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,8 +27,8 @@ fn draw_stroke_on_rect(
|
|||||||
// - The same rect if it's a center stroke
|
// - The same rect if it's a center stroke
|
||||||
// - A bigger rect if it's an outer stroke
|
// - A bigger rect if it's an outer stroke
|
||||||
// - A smaller rect if it's an outer stroke
|
// - A smaller rect if it's an outer stroke
|
||||||
let stroke_rect = stroke.outer_rect(rect);
|
let stroke_rect = stroke.aligned_rect(rect, scale);
|
||||||
let mut paint = stroke.to_paint(selrect, svg_attrs, scale, antialias);
|
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
|
||||||
|
|
||||||
// Apply both blur and shadow filters if present, composing them if necessary.
|
// Apply both blur and shadow filters if present, composing them if necessary.
|
||||||
let filter = compose_filters(blur, shadow);
|
let filter = compose_filters(blur, shadow);
|
||||||
@ -63,8 +63,8 @@ fn draw_stroke_on_circle(
|
|||||||
// - The same oval if it's a center stroke
|
// - The same oval if it's a center stroke
|
||||||
// - A bigger oval if it's an outer stroke
|
// - A bigger oval if it's an outer stroke
|
||||||
// - A smaller oval if it's an outer stroke
|
// - A smaller oval if it's an outer stroke
|
||||||
let stroke_rect = stroke.outer_rect(rect);
|
let stroke_rect = stroke.aligned_rect(rect, scale);
|
||||||
let mut paint = stroke.to_paint(selrect, svg_attrs, scale, antialias);
|
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
|
||||||
|
|
||||||
// Apply both blur and shadow filters if present, composing them if necessary.
|
// Apply both blur and shadow filters if present, composing them if necessary.
|
||||||
let filter = compose_filters(blur, shadow);
|
let filter = compose_filters(blur, shadow);
|
||||||
@ -131,7 +131,6 @@ pub fn draw_stroke_on_path(
|
|||||||
selrect: &Rect,
|
selrect: &Rect,
|
||||||
path_transform: Option<&Matrix>,
|
path_transform: Option<&Matrix>,
|
||||||
svg_attrs: Option<&SvgAttrs>,
|
svg_attrs: Option<&SvgAttrs>,
|
||||||
scale: f32,
|
|
||||||
shadow: Option<&ImageFilter>,
|
shadow: Option<&ImageFilter>,
|
||||||
blur: Option<&ImageFilter>,
|
blur: Option<&ImageFilter>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
@ -142,7 +141,7 @@ pub fn draw_stroke_on_path(
|
|||||||
let is_open = path.is_open();
|
let is_open = path.is_open();
|
||||||
|
|
||||||
let mut paint: skia_safe::Handle<_> =
|
let mut paint: skia_safe::Handle<_> =
|
||||||
stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale, antialias);
|
stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias);
|
||||||
|
|
||||||
let filter = compose_filters(blur, shadow);
|
let filter = compose_filters(blur, shadow);
|
||||||
paint.set_image_filter(filter);
|
paint.set_image_filter(filter);
|
||||||
@ -166,7 +165,6 @@ pub fn draw_stroke_on_path(
|
|||||||
canvas,
|
canvas,
|
||||||
is_open,
|
is_open,
|
||||||
svg_attrs,
|
svg_attrs,
|
||||||
scale,
|
|
||||||
blur,
|
blur,
|
||||||
antialias,
|
antialias,
|
||||||
);
|
);
|
||||||
@ -218,7 +216,6 @@ fn handle_stroke_caps(
|
|||||||
canvas: &skia::Canvas,
|
canvas: &skia::Canvas,
|
||||||
is_open: bool,
|
is_open: bool,
|
||||||
svg_attrs: Option<&SvgAttrs>,
|
svg_attrs: Option<&SvgAttrs>,
|
||||||
scale: f32,
|
|
||||||
blur: Option<&ImageFilter>,
|
blur: Option<&ImageFilter>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) {
|
) {
|
||||||
@ -233,8 +230,7 @@ fn handle_stroke_caps(
|
|||||||
let first_point = points.first().unwrap();
|
let first_point = points.first().unwrap();
|
||||||
let last_point = points.last().unwrap();
|
let last_point = points.last().unwrap();
|
||||||
|
|
||||||
let mut paint_stroke =
|
let mut paint_stroke = stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias);
|
||||||
stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale, antialias);
|
|
||||||
|
|
||||||
if let Some(filter) = blur {
|
if let Some(filter) = blur {
|
||||||
paint_stroke.set_image_filter(filter.clone());
|
paint_stroke.set_image_filter(filter.clone());
|
||||||
@ -405,7 +401,7 @@ fn draw_image_stroke_in_container(
|
|||||||
|
|
||||||
// Draw the stroke based on the shape type, we are using this stroke as
|
// Draw the stroke based on the shape type, we are using this stroke as
|
||||||
// a "selector" of the area of the image we want to show.
|
// a "selector" of the area of the image we want to show.
|
||||||
let outer_rect = stroke.outer_rect(container);
|
let outer_rect = stroke.aligned_rect(container, scale);
|
||||||
|
|
||||||
match &shape.shape_type {
|
match &shape.shape_type {
|
||||||
shape_type @ (Type::Rect(_) | Type::Frame(_)) => {
|
shape_type @ (Type::Rect(_) | Type::Frame(_)) => {
|
||||||
@ -450,8 +446,7 @@ fn draw_image_stroke_in_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let is_open = p.is_open();
|
let is_open = p.is_open();
|
||||||
let mut paint =
|
let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, antialias);
|
||||||
stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, scale, antialias);
|
|
||||||
canvas.draw_path(&path, &paint);
|
canvas.draw_path(&path, &paint);
|
||||||
if stroke.render_kind(is_open) == StrokeKind::Outer {
|
if stroke.render_kind(is_open) == StrokeKind::Outer {
|
||||||
// Small extra inner stroke to overlap with the fill
|
// Small extra inner stroke to overlap with the fill
|
||||||
@ -466,7 +461,6 @@ fn draw_image_stroke_in_container(
|
|||||||
canvas,
|
canvas,
|
||||||
is_open,
|
is_open,
|
||||||
svg_attrs,
|
svg_attrs,
|
||||||
scale,
|
|
||||||
shape.image_filter(1.).as_ref(),
|
shape.image_filter(1.).as_ref(),
|
||||||
antialias,
|
antialias,
|
||||||
);
|
);
|
||||||
@ -662,7 +656,6 @@ fn render_internal(
|
|||||||
&selrect,
|
&selrect,
|
||||||
path_transform.as_ref(),
|
path_transform.as_ref(),
|
||||||
svg_attrs,
|
svg_attrs,
|
||||||
scale,
|
|
||||||
shadow,
|
shadow,
|
||||||
shape.image_filter(1.).as_ref(),
|
shape.image_filter(1.).as_ref(),
|
||||||
antialias,
|
antialias,
|
||||||
@ -685,14 +678,13 @@ pub fn render_text_paths(
|
|||||||
shadow: Option<&ImageFilter>,
|
shadow: Option<&ImageFilter>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) {
|
) {
|
||||||
let scale = render_state.get_scale();
|
|
||||||
let canvas = render_state
|
let canvas = render_state
|
||||||
.surfaces
|
.surfaces
|
||||||
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes));
|
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes));
|
||||||
let selrect = &shape.selrect;
|
let selrect = &shape.selrect;
|
||||||
let svg_attrs = shape.svg_attrs.as_ref();
|
let svg_attrs = shape.svg_attrs.as_ref();
|
||||||
let mut paint: skia_safe::Handle<_> =
|
let mut paint: skia_safe::Handle<_> =
|
||||||
stroke.to_text_stroked_paint(false, selrect, svg_attrs, scale, antialias);
|
stroke.to_text_stroked_paint(false, selrect, svg_attrs, antialias);
|
||||||
|
|
||||||
if let Some(filter) = shadow {
|
if let Some(filter) = shadow {
|
||||||
paint.set_image_filter(filter.clone());
|
paint.set_image_filter(filter.clone());
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::math::is_close_to;
|
||||||
use crate::shapes::fills::{Fill, SolidColor};
|
use crate::shapes::fills::{Fill, SolidColor};
|
||||||
use skia_safe::{self as skia, Rect};
|
use skia_safe::{self as skia, Rect};
|
||||||
|
|
||||||
@ -144,6 +145,15 @@ impl Stroke {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn aligned_rect(&self, rect: &Rect, scale: f32) -> Rect {
|
||||||
|
let stroke_rect = self.outer_rect(rect);
|
||||||
|
if self.kind != StrokeKind::Center {
|
||||||
|
return stroke_rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
align_rect_to_half_pixel(&stroke_rect, self.width, scale)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn outer_corners(&self, corners: &Corners) -> Corners {
|
pub fn outer_corners(&self, corners: &Corners) -> Corners {
|
||||||
let offset = match self.kind {
|
let offset = match self.kind {
|
||||||
StrokeKind::Center => 0.0,
|
StrokeKind::Center => 0.0,
|
||||||
@ -162,7 +172,6 @@ impl Stroke {
|
|||||||
&self,
|
&self,
|
||||||
rect: &Rect,
|
rect: &Rect,
|
||||||
svg_attrs: Option<&SvgAttrs>,
|
svg_attrs: Option<&SvgAttrs>,
|
||||||
scale: f32,
|
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) -> skia::Paint {
|
) -> skia::Paint {
|
||||||
let mut paint = self.fill.to_paint(rect, antialias);
|
let mut paint = self.fill.to_paint(rect, antialias);
|
||||||
@ -171,7 +180,7 @@ impl Stroke {
|
|||||||
let width = match self.kind {
|
let width = match self.kind {
|
||||||
StrokeKind::Inner => self.width,
|
StrokeKind::Inner => self.width,
|
||||||
StrokeKind::Center => self.width,
|
StrokeKind::Center => self.width,
|
||||||
StrokeKind::Outer => self.width + (1. / scale),
|
StrokeKind::Outer => self.width,
|
||||||
};
|
};
|
||||||
|
|
||||||
paint.set_stroke_width(width);
|
paint.set_stroke_width(width);
|
||||||
@ -230,10 +239,9 @@ impl Stroke {
|
|||||||
is_open: bool,
|
is_open: bool,
|
||||||
rect: &Rect,
|
rect: &Rect,
|
||||||
svg_attrs: Option<&SvgAttrs>,
|
svg_attrs: Option<&SvgAttrs>,
|
||||||
scale: f32,
|
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) -> skia::Paint {
|
) -> skia::Paint {
|
||||||
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
|
let mut paint = self.to_paint(rect, svg_attrs, antialias);
|
||||||
match self.render_kind(is_open) {
|
match self.render_kind(is_open) {
|
||||||
StrokeKind::Inner => {
|
StrokeKind::Inner => {
|
||||||
paint.set_stroke_width(2. * paint.stroke_width());
|
paint.set_stroke_width(2. * paint.stroke_width());
|
||||||
@ -254,10 +262,9 @@ impl Stroke {
|
|||||||
is_open: bool,
|
is_open: bool,
|
||||||
rect: &Rect,
|
rect: &Rect,
|
||||||
svg_attrs: Option<&SvgAttrs>,
|
svg_attrs: Option<&SvgAttrs>,
|
||||||
scale: f32,
|
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) -> skia::Paint {
|
) -> skia::Paint {
|
||||||
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
|
let mut paint = self.to_paint(rect, svg_attrs, antialias);
|
||||||
match self.render_kind(is_open) {
|
match self.render_kind(is_open) {
|
||||||
StrokeKind::Inner => {
|
StrokeKind::Inner => {
|
||||||
paint.set_stroke_width(2. * paint.stroke_width());
|
paint.set_stroke_width(2. * paint.stroke_width());
|
||||||
@ -284,6 +291,38 @@ impl Stroke {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn align_rect_to_half_pixel(rect: &Rect, stroke_width: f32, scale: f32) -> Rect {
|
||||||
|
if scale <= 0.0 {
|
||||||
|
return *rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stroke_pixels = stroke_width * scale;
|
||||||
|
let stroke_pixels_rounded = stroke_pixels.round();
|
||||||
|
if !is_close_to(stroke_pixels, stroke_pixels_rounded) {
|
||||||
|
return *rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stroke_pixels_rounded as i32) % 2 == 0 {
|
||||||
|
return *rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
let left_px = rect.left * scale;
|
||||||
|
let top_px = rect.top * scale;
|
||||||
|
let target_frac = 0.5;
|
||||||
|
let dx_px = target_frac - (left_px - left_px.floor());
|
||||||
|
let dy_px = target_frac - (top_px - top_px.floor());
|
||||||
|
|
||||||
|
if is_close_to(dx_px, 0.0) && is_close_to(dy_px, 0.0) {
|
||||||
|
return *rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect::from_xywh(
|
||||||
|
rect.left + (dx_px / scale),
|
||||||
|
rect.top + (dy_px / scale),
|
||||||
|
rect.width(),
|
||||||
|
rect.height(),
|
||||||
|
)
|
||||||
|
}
|
||||||
fn cap_margin_for_cap(cap: Option<StrokeCap>, width: f32) -> f32 {
|
fn cap_margin_for_cap(cap: Option<StrokeCap>, width: f32) -> f32 {
|
||||||
match cap {
|
match cap {
|
||||||
Some(StrokeCap::LineArrow)
|
Some(StrokeCap::LineArrow)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user