mirror of
https://github.com/penpot/penpot.git
synced 2025-05-11 14:46:37 +02:00
Merge pull request #5029 from penpot/azazeln28-refactor-text-editor
♻️ Refactor text editor
This commit is contained in:
commit
dcc49dafd3
26 changed files with 3846 additions and 49 deletions
|
@ -10,6 +10,14 @@
|
||||||
|
|
||||||
### :sparkles: New features
|
### :sparkles: New features
|
||||||
|
|
||||||
|
- **Replace Draft.js completely with a custom editor** [Taiga #7706](https://tree.taiga.io/project/penpot/us/7706)
|
||||||
|
|
||||||
|
This refactor adds better IME support, more performant text editing
|
||||||
|
experience and a better clipboard support while keeping full
|
||||||
|
retrocompatibility with previous editor.
|
||||||
|
|
||||||
|
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
## 2.2.0
|
## 2.2.0
|
||||||
|
|
|
@ -49,7 +49,8 @@
|
||||||
"components/v2"
|
"components/v2"
|
||||||
"styles/v2"
|
"styles/v2"
|
||||||
"layout/grid"
|
"layout/grid"
|
||||||
"plugins/runtime"})
|
"plugins/runtime"
|
||||||
|
"text-editor/v2"})
|
||||||
|
|
||||||
;; A set of features enabled by default
|
;; A set of features enabled by default
|
||||||
(def default-features
|
(def default-features
|
||||||
|
@ -64,7 +65,8 @@
|
||||||
;; team feature field
|
;; team feature field
|
||||||
(def frontend-only-features
|
(def frontend-only-features
|
||||||
#{"styles/v2"
|
#{"styles/v2"
|
||||||
"plugins/runtime"})
|
"plugins/runtime"
|
||||||
|
"text-editor/v2"})
|
||||||
|
|
||||||
;; Features that are mainly backend only or there are a proper
|
;; Features that are mainly backend only or there are a proper
|
||||||
;; fallback when frontend reports no support for it
|
;; fallback when frontend reports no support for it
|
||||||
|
@ -81,7 +83,8 @@
|
||||||
"fdata/pointer-map"
|
"fdata/pointer-map"
|
||||||
"layout/grid"
|
"layout/grid"
|
||||||
"fdata/shape-data-type"
|
"fdata/shape-data-type"
|
||||||
"plugins/runtime"}
|
"plugins/runtime"
|
||||||
|
"text-editor/v2"}
|
||||||
(into frontend-only-features)))
|
(into frontend-only-features)))
|
||||||
|
|
||||||
(sm/register! ::features
|
(sm/register! ::features
|
||||||
|
@ -101,6 +104,7 @@
|
||||||
:feature-fdata-objects-map "fdata/objects-map"
|
:feature-fdata-objects-map "fdata/objects-map"
|
||||||
:feature-fdata-pointer-map "fdata/pointer-map"
|
:feature-fdata-pointer-map "fdata/pointer-map"
|
||||||
:feature-plugins "plugins/runtime"
|
:feature-plugins "plugins/runtime"
|
||||||
|
:feature-text-editor-v2 "text-editor/v2"
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
(defn migrate-legacy-features
|
(defn migrate-legacy-features
|
||||||
|
|
|
@ -78,6 +78,12 @@
|
||||||
|
|
||||||
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
|
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
|
||||||
|
|
||||||
|
(def text-style-attrs
|
||||||
|
(d/concat-vec root-attrs paragraph-attrs text-node-attrs))
|
||||||
|
|
||||||
|
(def default-root-attrs
|
||||||
|
{:vertical-align "top"})
|
||||||
|
|
||||||
(def default-text-attrs
|
(def default-text-attrs
|
||||||
{:typography-ref-file nil
|
{:typography-ref-file nil
|
||||||
:typography-ref-id nil
|
:typography-ref-id nil
|
||||||
|
@ -92,9 +98,13 @@
|
||||||
:text-transform "none"
|
:text-transform "none"
|
||||||
:text-align "left"
|
:text-align "left"
|
||||||
:text-decoration "none"
|
:text-decoration "none"
|
||||||
|
:text-direction "ltr"
|
||||||
:fills [{:fill-color clr/black
|
:fills [{:fill-color clr/black
|
||||||
:fill-opacity 1}]})
|
:fill-opacity 1}]})
|
||||||
|
|
||||||
|
(def default-attrs
|
||||||
|
(merge default-root-attrs default-text-attrs))
|
||||||
|
|
||||||
(def typography-fields
|
(def typography-fields
|
||||||
[:font-id
|
[:font-id
|
||||||
:font-family
|
:font-family
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
[app.util.webapi :as wapi]
|
[app.util.webapi :as wapi]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
|
[clojure.set :as set]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[potok.v2.core :as ptk]
|
[potok.v2.core :as ptk]
|
||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
@ -1545,7 +1546,8 @@
|
||||||
(let [objects (wsh/lookup-page-objects state)
|
(let [objects (wsh/lookup-page-objects state)
|
||||||
selected (->> (wsh/lookup-selected state)
|
selected (->> (wsh/lookup-selected state)
|
||||||
(cfh/clean-loops objects))
|
(cfh/clean-loops objects))
|
||||||
features (features/get-team-enabled-features state)
|
features (-> (features/get-team-enabled-features state)
|
||||||
|
(set/difference cfeat/frontend-only-features))
|
||||||
|
|
||||||
file-id (:current-file-id state)
|
file-id (:current-file-id state)
|
||||||
frame-id (cfh/common-parent-frame objects selected)
|
frame-id (cfh/common-parent-frame objects selected)
|
||||||
|
|
|
@ -24,14 +24,21 @@
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
[app.main.data.workspace.state-helpers :as wsh]
|
[app.main.data.workspace.state-helpers :as wsh]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
[app.util.text-editor :as ted]
|
[app.util.text-editor :as ted]
|
||||||
|
[app.util.text.content.styles :as styles]
|
||||||
[app.util.timers :as ts]
|
[app.util.timers :as ts]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
|
;; -- V2 Editor
|
||||||
|
|
||||||
|
(declare v2-update-text-shape-content)
|
||||||
|
(declare v2-update-text-editor-styles)
|
||||||
|
|
||||||
;; -- Editor
|
;; -- Editor
|
||||||
|
|
||||||
(defn update-editor
|
(defn update-editor
|
||||||
|
@ -186,22 +193,41 @@
|
||||||
[{:keys [attrs shape]}]
|
[{:keys [attrs shape]}]
|
||||||
(shape-current-values shape txt/is-root-node? attrs))
|
(shape-current-values shape txt/is-root-node? attrs))
|
||||||
|
|
||||||
(defn current-paragraph-values
|
(defn v2-current-text-values
|
||||||
|
[{:keys [editor-instance attrs]}]
|
||||||
|
(let [result (-> (.-currentStyle editor-instance)
|
||||||
|
(styles/get-styles-from-style-declaration)
|
||||||
|
(select-keys attrs))
|
||||||
|
result (if (empty? result) txt/default-text-attrs result)]
|
||||||
|
result))
|
||||||
|
|
||||||
|
(defn v1-current-paragraph-values
|
||||||
[{:keys [editor-state attrs shape]}]
|
[{:keys [editor-state attrs shape]}]
|
||||||
(if editor-state
|
(if editor-state
|
||||||
(-> (ted/get-editor-current-block-data editor-state)
|
(-> (ted/get-editor-current-block-data editor-state)
|
||||||
(select-keys attrs))
|
(select-keys attrs))
|
||||||
(shape-current-values shape txt/is-paragraph-node? attrs)))
|
(shape-current-values shape txt/is-paragraph-node? attrs)))
|
||||||
|
|
||||||
(defn current-text-values
|
(defn current-paragraph-values
|
||||||
[{:keys [editor-state attrs shape]}]
|
[{:keys [editor-state editor-instance attrs shape] :as options}]
|
||||||
(if editor-state
|
(cond
|
||||||
(let [result (-> (ted/get-editor-current-inline-styles editor-state)
|
(some? editor-instance) (v2-current-text-values options)
|
||||||
(select-keys attrs))
|
(some? editor-state) (v1-current-paragraph-values options)
|
||||||
result (if (empty? result) txt/default-text-attrs result)]
|
:else (shape-current-values shape txt/is-paragraph-node? attrs)))
|
||||||
result)
|
|
||||||
(shape-current-values shape txt/is-text-node? attrs)))
|
|
||||||
|
|
||||||
|
(defn v1-current-text-values
|
||||||
|
[{:keys [editor-state attrs]}]
|
||||||
|
(let [result (-> (ted/get-editor-current-inline-styles editor-state)
|
||||||
|
(select-keys attrs))
|
||||||
|
result (if (empty? result) txt/default-text-attrs result)]
|
||||||
|
result))
|
||||||
|
|
||||||
|
(defn current-text-values
|
||||||
|
[{:keys [editor-state editor-instance attrs shape] :as options}]
|
||||||
|
(cond
|
||||||
|
(some? editor-instance) (v2-current-text-values options)
|
||||||
|
(some? editor-state) (v1-current-text-values options)
|
||||||
|
:else (shape-current-values shape txt/is-text-node? attrs)))
|
||||||
|
|
||||||
;; --- TEXT EDITION IMPL
|
;; --- TEXT EDITION IMPL
|
||||||
|
|
||||||
|
@ -408,7 +434,9 @@
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(when (nil? (get-in state [:workspace-editor-state id]))
|
(when (or
|
||||||
|
(and (features/active-feature? state "text-editor/v2") (nil? (:workspace-editor state)))
|
||||||
|
(and (not (features/active-feature? state "text-editor/v2")) (nil? (get-in state [:workspace-editor-state id]))))
|
||||||
(let [objects (wsh/lookup-page-objects state)
|
(let [objects (wsh/lookup-page-objects state)
|
||||||
shape (get objects id)
|
shape (get objects id)
|
||||||
|
|
||||||
|
@ -430,8 +458,18 @@
|
||||||
(-> shape
|
(-> shape
|
||||||
(dissoc :fills)
|
(dissoc :fills)
|
||||||
(d/update-when :content update-content)))]
|
(d/update-when :content update-content)))]
|
||||||
|
(rx/of (dwsh/update-shapes shape-ids update-shape)))))
|
||||||
|
|
||||||
(rx/of (dwsh/update-shapes shape-ids update-shape)))))))
|
ptk/EffectEvent
|
||||||
|
(effect [_ state _]
|
||||||
|
(when (features/active-feature? state "text-editor/v2")
|
||||||
|
(let [text-editor-instance (:workspace-editor state)]
|
||||||
|
(when (some? text-editor-instance)
|
||||||
|
(let [attrs (-> (.-currentStyle text-editor-instance)
|
||||||
|
(styles/get-styles-from-style-declaration)
|
||||||
|
((comp update-node-fn migrate-node)))
|
||||||
|
styles (styles/attrs->styles attrs)]
|
||||||
|
(.applyStylesToSelection text-editor-instance styles))))))))
|
||||||
|
|
||||||
;; --- RESIZE UTILS
|
;; --- RESIZE UTILS
|
||||||
|
|
||||||
|
@ -664,22 +702,37 @@
|
||||||
[id attrs]
|
[id attrs]
|
||||||
(ptk/reify ::update-attrs
|
(ptk/reify ::update-attrs
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ state _]
|
||||||
(rx/concat
|
(let [text-editor-instance (:workspace-editor state)]
|
||||||
(let [attrs (select-keys attrs txt/root-attrs)]
|
(if (and (features/active-feature? state "text-editor/v2")
|
||||||
(if-not (empty? attrs)
|
(some? text-editor-instance))
|
||||||
(rx/of (update-root-attrs {:id id :attrs attrs}))
|
(rx/empty)
|
||||||
(rx/empty)))
|
(rx/concat
|
||||||
|
(let [attrs (select-keys attrs txt/root-attrs)]
|
||||||
|
(if-not (empty? attrs)
|
||||||
|
(rx/of (update-root-attrs {:id id :attrs attrs}))
|
||||||
|
(rx/empty)))
|
||||||
|
|
||||||
(let [attrs (select-keys attrs txt/paragraph-attrs)]
|
(let [attrs (select-keys attrs txt/paragraph-attrs)]
|
||||||
(if-not (empty? attrs)
|
(if-not (empty? attrs)
|
||||||
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))
|
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))
|
||||||
(rx/empty)))
|
(rx/empty)))
|
||||||
|
|
||||||
(let [attrs (select-keys attrs txt/text-node-attrs)]
|
(let [attrs (select-keys attrs txt/text-node-attrs)]
|
||||||
(if-not (empty? attrs)
|
(if-not (empty? attrs)
|
||||||
(rx/of (update-text-attrs {:id id :attrs attrs}))
|
(rx/of (update-text-attrs {:id id :attrs attrs}))
|
||||||
(rx/empty)))))))
|
(rx/empty)))
|
||||||
|
|
||||||
|
(when (features/active-feature? state "text-editor/v2")
|
||||||
|
(rx/of (v2-update-text-editor-styles id attrs)))))))
|
||||||
|
|
||||||
|
ptk/EffectEvent
|
||||||
|
(effect [_ state _]
|
||||||
|
(when (features/active-feature? state "text-editor/v2")
|
||||||
|
(let [text-editor-instance (:workspace-editor state)
|
||||||
|
styles (styles/attrs->styles attrs)]
|
||||||
|
(when (some? text-editor-instance)
|
||||||
|
(.applyStylesToSelection text-editor-instance styles)))))))
|
||||||
|
|
||||||
(defn update-all-attrs
|
(defn update-all-attrs
|
||||||
[ids attrs]
|
[ids attrs]
|
||||||
|
@ -773,3 +826,52 @@
|
||||||
(rx/of (update-attrs (:id shape)
|
(rx/of (update-attrs (:id shape)
|
||||||
{:typography-ref-id typ-id
|
{:typography-ref-id typ-id
|
||||||
:typography-ref-file file-id}))))))))
|
:typography-ref-file file-id}))))))))
|
||||||
|
|
||||||
|
;; -- New Editor
|
||||||
|
|
||||||
|
(defn v2-update-text-editor-styles
|
||||||
|
[id new-styles]
|
||||||
|
(ptk/reify ::v2-update-text-editor-styles
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [merged-styles (d/merge txt/default-text-attrs
|
||||||
|
(get-in state [:workspace-global :default-font])
|
||||||
|
new-styles)]
|
||||||
|
(update-in state [:workspace-v2-editor-state id] (fnil merge {}) merged-styles)))))
|
||||||
|
|
||||||
|
(defn v2-update-text-shape-position-data
|
||||||
|
[shape-id position-data]
|
||||||
|
(ptk/reify ::v2-update-text-shape-position-data
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let []
|
||||||
|
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data})))))
|
||||||
|
|
||||||
|
(defn v2-update-text-shape-content
|
||||||
|
([id content]
|
||||||
|
(v2-update-text-shape-content id content false nil))
|
||||||
|
([id content update-name?]
|
||||||
|
(v2-update-text-shape-content id content update-name? nil))
|
||||||
|
([id content update-name? name]
|
||||||
|
(ptk/reify ::v2-update-text-shape-content
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [objects (wsh/lookup-page-objects state)
|
||||||
|
shape (get objects id)
|
||||||
|
modifiers (get-in state [:workspace-text-modifier id])
|
||||||
|
new-shape? (nil? (:content shape))]
|
||||||
|
(rx/of
|
||||||
|
(dwsh/update-shapes
|
||||||
|
[id]
|
||||||
|
(fn [shape]
|
||||||
|
(let [{:keys [width height position-data]} modifiers]
|
||||||
|
(let [new-shape (-> shape
|
||||||
|
(assoc :content content)
|
||||||
|
(cond-> position-data
|
||||||
|
(assoc :position-data position-data))
|
||||||
|
(cond-> (and update-name? (some? name))
|
||||||
|
(assoc :name name))
|
||||||
|
(cond-> (or (some? width) (some? height))
|
||||||
|
(gsh/transform-shape (ctm/change-size shape width height))))]
|
||||||
|
new-shape)))
|
||||||
|
{:undo-group (when new-shape? id)})))))))
|
||||||
|
|
|
@ -109,7 +109,8 @@
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
(when *assert*
|
(when *assert*
|
||||||
(->> (rx/from cfeat/no-migration-features)
|
(->> (rx/from cfeat/no-migration-features)
|
||||||
(rx/filter #(not (contains? cfeat/backend-only-features %)))
|
;; text editor v2 isn't enabled by default even in devenv
|
||||||
|
(rx/filter #(not (or (contains? cfeat/backend-only-features %) (= "text-editor/v2" %))))
|
||||||
(rx/observe-on :async)
|
(rx/observe-on :async)
|
||||||
(rx/map enable-feature))))
|
(rx/map enable-feature))))
|
||||||
|
|
||||||
|
|
|
@ -184,6 +184,9 @@
|
||||||
(def options-mode-global
|
(def options-mode-global
|
||||||
(l/derived :options-mode workspace-global))
|
(l/derived :options-mode workspace-global))
|
||||||
|
|
||||||
|
(def default-font
|
||||||
|
(l/derived :default-font workspace-global))
|
||||||
|
|
||||||
(def inspect-expanded
|
(def inspect-expanded
|
||||||
(l/derived :inspect-expanded workspace-local))
|
(l/derived :inspect-expanded workspace-local))
|
||||||
|
|
||||||
|
@ -355,6 +358,9 @@
|
||||||
(def workspace-editor-state
|
(def workspace-editor-state
|
||||||
(l/derived :workspace-editor-state st/state))
|
(l/derived :workspace-editor-state st/state))
|
||||||
|
|
||||||
|
(def workspace-v2-editor-state
|
||||||
|
(l/derived :workspace-v2-editor-state st/state))
|
||||||
|
|
||||||
(def workspace-modifiers
|
(def workspace-modifiers
|
||||||
(l/derived :workspace-modifiers st/state =))
|
(l/derived :workspace-modifiers st/state =))
|
||||||
|
|
||||||
|
|
|
@ -30,4 +30,4 @@
|
||||||
|
|
||||||
(def workspace-read-only? (mf/create-context nil))
|
(def workspace-read-only? (mf/create-context nil))
|
||||||
(def is-component? (mf/create-context false))
|
(def is-component? (mf/create-context false))
|
||||||
(def sidebar (mf/create-context nil))
|
(def sidebar (mf/create-context nil))
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
:fontSize 0 ;;(str (:font-size data (:font-size txt/default-text-attrs)) "px")
|
:fontSize 0 ;;(str (:font-size data (:font-size txt/default-text-attrs)) "px")
|
||||||
:lineHeight (:line-height data (:line-height txt/default-text-attrs))
|
:lineHeight (:line-height data (:line-height txt/default-text-attrs))
|
||||||
:margin 0}]
|
:margin 0}]
|
||||||
|
|
||||||
(cond-> base
|
(cond-> base
|
||||||
(some? line-height) (obj/set! "lineHeight" line-height)
|
(some? line-height) (obj/set! "lineHeight" line-height)
|
||||||
(some? text-align) (obj/set! "textAlign" text-align))))
|
(some? text-align) (obj/set! "textAlign" text-align))))
|
||||||
|
@ -74,6 +75,7 @@
|
||||||
font-variant-id (:font-variant-id data)
|
font-variant-id (:font-variant-id data)
|
||||||
|
|
||||||
font-size (:font-size data)
|
font-size (:font-size data)
|
||||||
|
|
||||||
fill-color (or (-> data :fills first :fill-color) (:fill-color data))
|
fill-color (or (-> data :fills first :fill-color) (:fill-color data))
|
||||||
fill-opacity (or (-> data :fills first :fill-opacity) (:fill-opacity data))
|
fill-opacity (or (-> data :fills first :fill-opacity) (:fill-opacity data))
|
||||||
fill-gradient (or (-> data :fills first :fill-color-gradient) (:fill-color-gradient data))
|
fill-gradient (or (-> data :fills first :fill-color-gradient) (:fill-color-gradient data))
|
||||||
|
@ -92,6 +94,7 @@
|
||||||
|
|
||||||
base #js {:textDecoration text-decoration
|
base #js {:textDecoration text-decoration
|
||||||
:textTransform text-transform
|
:textTransform text-transform
|
||||||
|
:fontSize font-size
|
||||||
:color (if (and show-text? (not gradient?)) text-color "transparent")
|
:color (if (and show-text? (not gradient?)) text-color "transparent")
|
||||||
:background (when (and show-text? gradient?) text-color)
|
:background (when (and show-text? gradient?) text-color)
|
||||||
:caretColor (if (and (not gradient?) text-color) text-color "black")
|
:caretColor (if (and (not gradient?) text-color) text-color "black")
|
||||||
|
|
|
@ -200,7 +200,7 @@
|
||||||
(fn [editor]
|
(fn [editor]
|
||||||
(st/emit! (dwt/update-editor editor))
|
(st/emit! (dwt/update-editor editor))
|
||||||
(when editor
|
(when editor
|
||||||
(dom/add-class! (dom/get-element-by-class "public-DraftEditor-content") "mousetrap")
|
(dom/add-class! (dom/get-element-by-class "public-DraftEditor-content") "mousetrap")
|
||||||
(.focus ^js editor))))
|
(.focus ^js editor))))
|
||||||
|
|
||||||
handle-return
|
handle-return
|
||||||
|
|
File diff suppressed because it is too large
Load diff
259
frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs
Normal file
259
frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
;; 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.ui.workspace.shapes.text.v2-editor
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
["./v2_editor_impl.js" :as impl]
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.geom.shapes.text :as gst]
|
||||||
|
[app.common.math :as mth]
|
||||||
|
[app.common.text :as txt]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.main.data.workspace :as dw]
|
||||||
|
[app.main.data.workspace.texts :as dwt]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.css-cursors :as cur]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.keyboard :as kbd]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[app.util.text.content :as content]
|
||||||
|
[app.util.text.content.styles :as styles]
|
||||||
|
[goog.events :as events]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(mf/defc text-editor-html
|
||||||
|
"Text editor (HTML)"
|
||||||
|
{::mf/wrap [mf/memo]
|
||||||
|
::mf/wrap-props false}
|
||||||
|
[{:keys [shape] :as props}]
|
||||||
|
(let [content (:content shape)
|
||||||
|
shape-id (:id shape)
|
||||||
|
|
||||||
|
;; Gets the default font from the workspace refs.
|
||||||
|
default-font (deref refs/default-font)
|
||||||
|
|
||||||
|
;; This is a reference to the dom element that
|
||||||
|
;; should contain the TextEditor.
|
||||||
|
text-editor-ref (mf/use-ref nil)
|
||||||
|
|
||||||
|
;; This reference is to the container
|
||||||
|
text-editor-container-ref (mf/use-ref nil)
|
||||||
|
text-editor-instance-ref (mf/use-ref nil)
|
||||||
|
text-editor-selection-ref (mf/use-ref nil)
|
||||||
|
|
||||||
|
on-blur
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(let [text-editor-instance (mf/ref-val text-editor-instance-ref)
|
||||||
|
container (mf/ref-val text-editor-container-ref)
|
||||||
|
new-content (content/dom->cljs (impl/getRoot text-editor-instance))]
|
||||||
|
(when (some? new-content)
|
||||||
|
(st/emit! (dwt/v2-update-text-shape-content shape-id new-content true)))
|
||||||
|
(dom/set-style! container "opacity" 0))))
|
||||||
|
|
||||||
|
on-focus
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(let [container (mf/ref-val text-editor-container-ref)]
|
||||||
|
(dom/set-style! container "opacity" 1))))
|
||||||
|
|
||||||
|
on-stylechange
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [e]
|
||||||
|
(let [new-styles (styles/get-styles-from-event e)]
|
||||||
|
(st/emit! (dwt/v2-update-text-editor-styles shape-id new-styles)))))
|
||||||
|
|
||||||
|
on-needslayout
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(let [text-editor-instance (mf/ref-val text-editor-instance-ref)
|
||||||
|
new-content (content/dom->cljs (impl/getRoot text-editor-instance))]
|
||||||
|
(when (some? new-content)
|
||||||
|
(st/emit! (dwt/v2-update-text-shape-content shape-id new-content true)))
|
||||||
|
;; FIXME: We need to find a better way to trigger layout changes.
|
||||||
|
#_(st/emit!
|
||||||
|
(dwt/v2-update-text-shape-position-data shape-id [])))))
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(let [text-editor-instance (mf/ref-val text-editor-instance-ref)
|
||||||
|
new-content (content/dom->cljs (impl/getRoot text-editor-instance))]
|
||||||
|
(when (some? new-content)
|
||||||
|
(st/emit! (dwt/v2-update-text-shape-content shape-id new-content true))))))
|
||||||
|
|
||||||
|
on-key-up
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [e]
|
||||||
|
(dom/stop-propagation e)
|
||||||
|
(when (kbd/esc? e)
|
||||||
|
(st/emit! :interrupt (dw/clear-edition-mode)))))]
|
||||||
|
|
||||||
|
;; Initialize text editor content.
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps text-editor-ref)
|
||||||
|
(fn []
|
||||||
|
(let [keys [(events/listen js/document "keyup" on-key-up)]
|
||||||
|
text-editor (mf/ref-val text-editor-ref)
|
||||||
|
style-defaults (styles/get-style-defaults (d/merge txt/default-attrs default-font))
|
||||||
|
text-editor-options #js {:styleDefaults style-defaults
|
||||||
|
:selectionImposterElement (mf/ref-val text-editor-selection-ref)}
|
||||||
|
text-editor-instance (impl/createTextEditor text-editor text-editor-options)]
|
||||||
|
(mf/set-ref-val! text-editor-instance-ref text-editor-instance)
|
||||||
|
(.addEventListener text-editor-instance "blur" on-blur)
|
||||||
|
(.addEventListener text-editor-instance "focus" on-focus)
|
||||||
|
(.addEventListener text-editor-instance "needslayout" on-needslayout)
|
||||||
|
(.addEventListener text-editor-instance "stylechange" on-stylechange)
|
||||||
|
(.addEventListener text-editor-instance "change" on-change)
|
||||||
|
(st/emit! (dwt/update-editor text-editor-instance))
|
||||||
|
(when (some? content)
|
||||||
|
(impl/setRoot text-editor-instance (content/cljs->dom content)))
|
||||||
|
(st/emit! (dwt/focus-editor))
|
||||||
|
|
||||||
|
;; This function is called when the component is unmount.
|
||||||
|
(fn []
|
||||||
|
(.removeEventListener text-editor-instance "blur" on-blur)
|
||||||
|
(.removeEventListener text-editor-instance "focus" on-focus)
|
||||||
|
(.removeEventListener text-editor-instance "needslayout" on-needslayout)
|
||||||
|
(.removeEventListener text-editor-instance "stylechange" on-stylechange)
|
||||||
|
(.removeEventListener text-editor-instance "change" on-change)
|
||||||
|
(.dispose text-editor-instance)
|
||||||
|
(st/emit! (dwt/update-editor nil))
|
||||||
|
(doseq [key keys]
|
||||||
|
(events/unlistenByKey key))))))
|
||||||
|
|
||||||
|
[:div
|
||||||
|
{:class (dm/str (cur/get-dynamic "text" (:rotation shape))
|
||||||
|
" "
|
||||||
|
(stl/css :text-editor-container))
|
||||||
|
:ref text-editor-container-ref
|
||||||
|
:data-testid "text-editor-container"
|
||||||
|
:style {:width (:width shape)
|
||||||
|
:height (:height shape)}
|
||||||
|
;; We hide the editor when is blurred because otherwise the selection won't let us see
|
||||||
|
;; the underlying text. Use opacity because display or visibility won't allow to recover
|
||||||
|
;; focus afterwards.
|
||||||
|
;; IMPORTANT! This is now done through DOM mutations (see on-blur and on-focus)
|
||||||
|
;; but I keep this for future references.
|
||||||
|
;; :opacity (when @blurred 0)}}
|
||||||
|
}
|
||||||
|
[:div
|
||||||
|
{:class (stl/css :text-editor-selection-imposter)
|
||||||
|
:ref text-editor-selection-ref}]
|
||||||
|
[:div
|
||||||
|
{:class (dm/str
|
||||||
|
"mousetrap "
|
||||||
|
(stl/css-case
|
||||||
|
:text-editor-content true
|
||||||
|
:grow-type-fixed (= (:grow-type shape) :fixed)
|
||||||
|
:grow-type-auto-width (= (:grow-type shape) :auto-width)
|
||||||
|
:grow-type-auto-height (= (:grow-type shape) :auto-height)
|
||||||
|
:align-top (= (:vertical-align content "top") "top")
|
||||||
|
:align-center (= (:vertical-align content) "center")
|
||||||
|
:align-bottom (= (:vertical-align content) "bottom")))
|
||||||
|
:ref text-editor-ref
|
||||||
|
:data-testid "text-editor-content"
|
||||||
|
:data-x (dm/get-prop shape :x)
|
||||||
|
:data-y (dm/get-prop shape :y)
|
||||||
|
:content-editable true
|
||||||
|
:role "textbox"
|
||||||
|
:aria-multiline true
|
||||||
|
:aria-autocomplete "none"}]]))
|
||||||
|
|
||||||
|
(defn- shape->justify
|
||||||
|
[{:keys [content]}]
|
||||||
|
(case (d/nilv (:vertical-align content) "top")
|
||||||
|
"center" "center"
|
||||||
|
"top" "flex-start"
|
||||||
|
"bottom" "flex-end"
|
||||||
|
nil))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Text Editor Wrapper
|
||||||
|
;; This is an SVG element that wraps the HTML editor.
|
||||||
|
;;
|
||||||
|
(mf/defc text-editor
|
||||||
|
"Text editor wrapper component"
|
||||||
|
{::mf/wrap [mf/memo]
|
||||||
|
::mf/wrap-props false
|
||||||
|
::mf/forward-ref true}
|
||||||
|
[{:keys [shape modifiers] :as props} _]
|
||||||
|
(let [shape-id (dm/get-prop shape :id)
|
||||||
|
modifiers (dm/get-in modifiers [shape-id :modifiers])
|
||||||
|
|
||||||
|
clip-id (dm/str "text-edition-clip" shape-id)
|
||||||
|
|
||||||
|
text-modifier-ref
|
||||||
|
(mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape)))
|
||||||
|
|
||||||
|
text-modifier
|
||||||
|
(mf/deref text-modifier-ref)
|
||||||
|
|
||||||
|
;; For Safari It's necesary to scale the editor with the zoom
|
||||||
|
;; level to fix a problem with foreignObjects not scaling
|
||||||
|
;; correctly with the viewbox
|
||||||
|
;;
|
||||||
|
;; NOTE: this teoretically breaks hooks rules, but in practice
|
||||||
|
;; it is imposible to really break it
|
||||||
|
maybe-zoom
|
||||||
|
(when (cf/check-browser? :safari-16)
|
||||||
|
(mf/deref refs/selected-zoom))
|
||||||
|
|
||||||
|
shape (cond-> shape
|
||||||
|
(some? text-modifier)
|
||||||
|
(dwt/apply-text-modifier text-modifier)
|
||||||
|
|
||||||
|
(some? modifiers)
|
||||||
|
(gsh/transform-shape modifiers))
|
||||||
|
|
||||||
|
bounds (gst/shape->rect shape)
|
||||||
|
|
||||||
|
x (mth/min (dm/get-prop bounds :x)
|
||||||
|
(dm/get-prop shape :x))
|
||||||
|
y (mth/min (dm/get-prop bounds :y)
|
||||||
|
(dm/get-prop shape :y))
|
||||||
|
width (mth/max (dm/get-prop bounds :width)
|
||||||
|
(dm/get-prop shape :width))
|
||||||
|
height (mth/max (dm/get-prop bounds :height)
|
||||||
|
(dm/get-prop shape :height))
|
||||||
|
style
|
||||||
|
(cond-> #js {:pointerEvents "all"}
|
||||||
|
|
||||||
|
(not (cf/check-browser? :safari))
|
||||||
|
(obj/merge!
|
||||||
|
#js {:transform (dm/fmt "translate(%px, %px)" (- (dm/get-prop shape :x) x) (- (dm/get-prop shape :y) y))})
|
||||||
|
|
||||||
|
(cf/check-browser? :safari-17)
|
||||||
|
(obj/merge!
|
||||||
|
#js {:height "100%"
|
||||||
|
:display "flex"
|
||||||
|
:flexDirection "column"
|
||||||
|
:justifyContent (shape->justify shape)})
|
||||||
|
|
||||||
|
(cf/check-browser? :safari-16)
|
||||||
|
(obj/merge!
|
||||||
|
#js {:position "fixed"
|
||||||
|
:left 0
|
||||||
|
:top (- (dm/get-prop shape :y) y)
|
||||||
|
:transform-origin "top left"
|
||||||
|
:transform (when (some? maybe-zoom)
|
||||||
|
(dm/fmt "scale(%)" maybe-zoom))}))]
|
||||||
|
|
||||||
|
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
|
||||||
|
:transform (dm/str (gsh/transform-matrix shape))}
|
||||||
|
[:defs
|
||||||
|
[:clipPath {:id clip-id}
|
||||||
|
[:rect {:x x :y y :width width :height height}]]]
|
||||||
|
|
||||||
|
[:foreignObject {:x x :y y :width width :height height}
|
||||||
|
[:div {:style style}
|
||||||
|
[:& text-editor-html {:shape shape
|
||||||
|
:key (dm/str shape-id)}]]]]))
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
:global {
|
||||||
|
.selection-imposter-rect {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--text-editor-selection-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-editor-container {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-editor-selection-imposter {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-editor-content {
|
||||||
|
height: 100%;
|
||||||
|
font-family: sourcesanspro;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
user-select: text;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
caret-color: black;
|
||||||
|
|
||||||
|
color: transparent;
|
||||||
|
|
||||||
|
[data-itype="paragraph"] {
|
||||||
|
line-height: inherit;
|
||||||
|
user-select: text;
|
||||||
|
margin: 0px;
|
||||||
|
font-size: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-itype="inline"] {
|
||||||
|
line-break: auto;
|
||||||
|
line-height: inherit;
|
||||||
|
overflow-wrap: initial;
|
||||||
|
caret-color: rgb(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-itype="root"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow type
|
||||||
|
.grow-type-fixed,
|
||||||
|
.grow-type-auto-height {
|
||||||
|
[data-itype="inline"],
|
||||||
|
[data-itype="paragraph"] {
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow-type-auto-width {
|
||||||
|
[data-itype="inline"],
|
||||||
|
[data-itype="paragraph"] {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical align.
|
||||||
|
.align-top {
|
||||||
|
[data-itype="root"] {
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
[data-itype="root"] {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-bottom {
|
||||||
|
[data-itype="root"] {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TextEditor from "./new_editor/TextEditor.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies styles to the current selection or the
|
||||||
|
* saved selection.
|
||||||
|
*
|
||||||
|
* @param {TextEditor} editor
|
||||||
|
* @param {*} styles
|
||||||
|
*/
|
||||||
|
export function applyStylesToSelection(editor, styles) {
|
||||||
|
return editor.applyStylesToSelection(styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the editor root.
|
||||||
|
*
|
||||||
|
* @param {TextEditor} editor
|
||||||
|
* @returns {HTMLDivElement}
|
||||||
|
*/
|
||||||
|
export function getRoot(editor) {
|
||||||
|
return editor.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the editor root.
|
||||||
|
*
|
||||||
|
* @param {TextEditor} editor
|
||||||
|
* @param {HTMLDivElement} root
|
||||||
|
* @returns {TextEditor}
|
||||||
|
*/
|
||||||
|
export function setRoot(editor, root) {
|
||||||
|
editor.root = root;
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Text Editor instance.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
* @param {object} options
|
||||||
|
* @returns {TextEditor}
|
||||||
|
*/
|
||||||
|
export function createTextEditor(element, options) {
|
||||||
|
return new TextEditor(element, {
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createTextEditor,
|
||||||
|
setRoot,
|
||||||
|
getRoot
|
||||||
|
};
|
|
@ -26,6 +26,7 @@
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.util.text-editor :as ted]
|
[app.util.text-editor :as ted]
|
||||||
[app.util.text-svg-position :as tsp]
|
[app.util.text-svg-position :as tsp]
|
||||||
|
[app.util.text.content :as content]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
@ -46,6 +47,12 @@
|
||||||
(dissoc :modifiers)))
|
(dissoc :modifiers)))
|
||||||
shape))
|
shape))
|
||||||
|
|
||||||
|
(defn- update-shape-with-content
|
||||||
|
[shape content editor-content]
|
||||||
|
(cond-> shape
|
||||||
|
(and (some? shape) (some? editor-content))
|
||||||
|
(assoc :content (d/txt-merge content editor-content))))
|
||||||
|
|
||||||
(defn- update-with-editor-state
|
(defn- update-with-editor-state
|
||||||
"Updates the shape with the current state in the editor"
|
"Updates the shape with the current state in the editor"
|
||||||
[shape editor-state]
|
[shape editor-state]
|
||||||
|
@ -56,9 +63,15 @@
|
||||||
(ted/get-editor-current-content)
|
(ted/get-editor-current-content)
|
||||||
(ted/export-content)))]
|
(ted/export-content)))]
|
||||||
|
|
||||||
(cond-> shape
|
(update-shape-with-content shape content editor-content)))
|
||||||
(and (some? shape) (some? editor-content))
|
|
||||||
(assoc :content (d/txt-merge content editor-content)))))
|
(defn- update-with-editor-v2
|
||||||
|
"Updates the shape with the current editor"
|
||||||
|
[shape editor]
|
||||||
|
(let [content (:content shape)
|
||||||
|
editor-content (content/dom->cljs (.-root editor))]
|
||||||
|
|
||||||
|
(update-shape-with-content shape content editor-content)))
|
||||||
|
|
||||||
(defn- update-text-shape
|
(defn- update-text-shape
|
||||||
[{:keys [grow-type id migrate] :as shape} node]
|
[{:keys [grow-type id migrate] :as shape} node]
|
||||||
|
@ -219,22 +232,28 @@
|
||||||
{::mf/wrap-props false
|
{::mf/wrap-props false
|
||||||
::mf/wrap [mf/memo]}
|
::mf/wrap [mf/memo]}
|
||||||
[props]
|
[props]
|
||||||
|
|
||||||
(let [shape (obj/get props "shape")
|
(let [shape (obj/get props "shape")
|
||||||
|
shape-id (:id shape)
|
||||||
|
|
||||||
workspace-editor-state (mf/deref refs/workspace-editor-state)
|
workspace-editor-state (mf/deref refs/workspace-editor-state)
|
||||||
|
workspace-v2-editor-state (mf/deref refs/workspace-v2-editor-state)
|
||||||
|
workspace-editor (mf/deref refs/workspace-editor)
|
||||||
|
|
||||||
editor-state (get workspace-editor-state (:id shape))
|
editor-state (get workspace-editor-state shape-id)
|
||||||
|
v2-editor-state (get workspace-v2-editor-state shape-id)
|
||||||
|
|
||||||
text-modifier-ref
|
text-modifier-ref
|
||||||
(mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape)))
|
(mf/use-memo (mf/deps shape-id) #(refs/workspace-text-modifier-by-id shape-id))
|
||||||
|
|
||||||
text-modifier
|
text-modifier
|
||||||
(mf/deref text-modifier-ref)
|
(mf/deref text-modifier-ref)
|
||||||
|
|
||||||
shape (cond-> shape
|
shape (cond-> shape
|
||||||
(some? editor-state)
|
(some? editor-state)
|
||||||
(update-with-editor-state editor-state))
|
(update-with-editor-state editor-state)
|
||||||
|
|
||||||
|
(and (some? v2-editor-state) (some? workspace-editor))
|
||||||
|
(update-with-editor-v2 workspace-editor))
|
||||||
|
|
||||||
;; When we have a text with grow-type :auto-height or :auto-height we need to check the correct height
|
;; When we have a text with grow-type :auto-height or :auto-height we need to check the correct height
|
||||||
;; otherwise the center alignment will break
|
;; otherwise the center alignment will break
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry text-options]]
|
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry text-options]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[app.util.text.ui :as txu]
|
||||||
[app.util.timers :as ts]
|
[app.util.timers :as ts]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
@ -278,7 +279,7 @@
|
||||||
100
|
100
|
||||||
(fn []
|
(fn []
|
||||||
(when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name)))
|
(when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name)))
|
||||||
(let [node (dom/get-element-by-class "public-DraftEditor-content")]
|
(let [node (txu/get-text-editor-content)]
|
||||||
(dom/focus! node))))))}]
|
(dom/focus! node))))))}]
|
||||||
|
|
||||||
[:div {:class (stl/css :element-set)}
|
[:div {:class (stl/css :element-set)}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
[app.main.data.fonts :as fts]
|
[app.main.data.fonts :as fts]
|
||||||
[app.main.data.shortcuts :as dsc]
|
[app.main.data.shortcuts :as dsc]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
@ -399,10 +400,11 @@
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[{:keys [values on-change on-blur]}]
|
[{:keys [values on-change on-blur]}]
|
||||||
(let [text-transform (or (:text-transform values) "none")
|
(let [text-transform (or (:text-transform values) "none")
|
||||||
|
unset-value (if (features/active-feature? @st/state "text-editor/v2") "none" "unset")
|
||||||
handle-change
|
handle-change
|
||||||
(fn [type]
|
(fn [type]
|
||||||
(if (= text-transform type)
|
(if (= text-transform type)
|
||||||
(on-change {:text-transform "unset"})
|
(on-change {:text-transform unset-value})
|
||||||
(on-change {:text-transform type}))
|
(on-change {:text-transform type}))
|
||||||
(when (some? on-blur) (on-blur)))]
|
(when (some? on-blur) (on-blur)))]
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,9 @@
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
[app.main.data.workspace.texts :as dwt]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]]
|
||||||
|
@ -47,15 +49,22 @@
|
||||||
parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
|
parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
|
||||||
parents (mf/deref parents-by-ids-ref)
|
parents (mf/deref parents-by-ids-ref)
|
||||||
|
|
||||||
state-map (mf/deref refs/workspace-editor-state)
|
state-map (if (features/active-feature? @st/state "text-editor/v2")
|
||||||
|
(mf/deref refs/workspace-v2-editor-state)
|
||||||
|
(mf/deref refs/workspace-editor-state))
|
||||||
|
|
||||||
shared-libs (mf/deref refs/workspace-libraries)
|
shared-libs (mf/deref refs/workspace-libraries)
|
||||||
|
|
||||||
editor-state (get state-map (:id shape))
|
editor-state (when (not (features/active-feature? @st/state "text-editor/v2"))
|
||||||
|
(get state-map (:id shape)))
|
||||||
|
|
||||||
layer-values (select-keys shape layer-attrs)
|
layer-values (select-keys shape layer-attrs)
|
||||||
|
editor-instance (when (features/active-feature? @st/state "text-editor/v2")
|
||||||
|
(mf/deref refs/workspace-editor))
|
||||||
|
|
||||||
fill-values (-> (dwt/current-text-values
|
fill-values (-> (dwt/current-text-values
|
||||||
{:editor-state editor-state
|
{:editor-state editor-state
|
||||||
|
:editor-instance editor-instance
|
||||||
:shape shape
|
:shape shape
|
||||||
:attrs (conj txt/text-fill-attrs :fills)})
|
:attrs (conj txt/text-fill-attrs :fills)})
|
||||||
(d/update-in-when [:fill-color-gradient :type] keyword))
|
(d/update-in-when [:fill-color-gradient :type] keyword))
|
||||||
|
@ -75,10 +84,12 @@
|
||||||
:attrs txt/root-attrs})
|
:attrs txt/root-attrs})
|
||||||
(dwt/current-paragraph-values
|
(dwt/current-paragraph-values
|
||||||
{:editor-state editor-state
|
{:editor-state editor-state
|
||||||
|
:editor-instance editor-instance
|
||||||
:shape shape
|
:shape shape
|
||||||
:attrs txt/paragraph-attrs})
|
:attrs txt/paragraph-attrs})
|
||||||
(dwt/current-text-values
|
(dwt/current-text-values
|
||||||
{:editor-state editor-state
|
{:editor-state editor-state
|
||||||
|
:editor-instance editor-instance
|
||||||
:shape shape
|
:shape shape
|
||||||
:attrs txt/text-node-attrs}))
|
:attrs txt/text-node-attrs}))
|
||||||
layout-item-values (select-keys shape layout-item-attrs)]
|
layout-item-values (select-keys shape layout-item-attrs)]
|
||||||
|
|
|
@ -15,15 +15,18 @@
|
||||||
[app.common.types.shape-tree :as ctt]
|
[app.common.types.shape-tree :as ctt]
|
||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.main.data.workspace.modifiers :as dwm]
|
[app.main.data.workspace.modifiers :as dwm]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
[app.main.ui.context :as ctx]
|
[app.main.ui.context :as ctx]
|
||||||
[app.main.ui.flex-controls :as mfc]
|
[app.main.ui.flex-controls :as mfc]
|
||||||
[app.main.ui.hooks :as ui-hooks]
|
[app.main.ui.hooks :as ui-hooks]
|
||||||
[app.main.ui.measurements :as msr]
|
[app.main.ui.measurements :as msr]
|
||||||
[app.main.ui.shapes.export :as use]
|
[app.main.ui.shapes.export :as use]
|
||||||
[app.main.ui.workspace.shapes :as shapes]
|
[app.main.ui.workspace.shapes :as shapes]
|
||||||
[app.main.ui.workspace.shapes.text.editor :as editor]
|
[app.main.ui.workspace.shapes.text.editor :as editor-v1]
|
||||||
[app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline]]
|
[app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline]]
|
||||||
|
[app.main.ui.workspace.shapes.text.v2-editor :as editor-v2]
|
||||||
[app.main.ui.workspace.shapes.text.viewport-texts-html :as stvh]
|
[app.main.ui.workspace.shapes.text.viewport-texts-html :as stvh]
|
||||||
[app.main.ui.workspace.viewport.actions :as actions]
|
[app.main.ui.workspace.viewport.actions :as actions]
|
||||||
[app.main.ui.workspace.viewport.comments :as comments]
|
[app.main.ui.workspace.viewport.comments :as comments]
|
||||||
|
@ -383,8 +386,11 @@
|
||||||
|
|
||||||
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
||||||
(when show-text-editor?
|
(when show-text-editor?
|
||||||
[:& editor/text-editor-svg {:shape editing-shape
|
(if (features/active-feature? @st/state "text-editor/v2")
|
||||||
:modifiers modifiers}])
|
[:& editor-v2/text-editor {:shape editing-shape
|
||||||
|
:modifiers modifiers}]
|
||||||
|
[:& editor-v1/text-editor-svg {:shape editing-shape
|
||||||
|
:modifiers modifiers}]))
|
||||||
|
|
||||||
(when show-frame-outline?
|
(when show-frame-outline?
|
||||||
(let [outlined-frame-id
|
(let [outlined-frame-id
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
[app.util.mouse :as mse]
|
[app.util.mouse :as mse]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.util.rxops :refer [throttle-fn]]
|
[app.util.rxops :refer [throttle-fn]]
|
||||||
|
[app.util.text.ui :as txu]
|
||||||
[app.util.timers :as ts]
|
[app.util.timers :as ts]
|
||||||
[app.util.webapi :as wapi]
|
[app.util.webapi :as wapi]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
|
@ -49,7 +50,7 @@
|
||||||
;; We need to handle editor related stuff here because
|
;; We need to handle editor related stuff here because
|
||||||
;; handling on editor dom node does not works properly.
|
;; handling on editor dom node does not works properly.
|
||||||
(let [target (dom/get-target bevent)
|
(let [target (dom/get-target bevent)
|
||||||
editor (.closest ^js target ".public-DraftEditor-content")]
|
editor (txu/closest-text-editor-content target)]
|
||||||
;; Capture mouse pointer to detect the movements even if cursor
|
;; Capture mouse pointer to detect the movements even if cursor
|
||||||
;; leaves the viewport or the browser itself
|
;; leaves the viewport or the browser itself
|
||||||
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
|
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
|
||||||
|
@ -319,7 +320,7 @@
|
||||||
mod? (kbd/mod? event)
|
mod? (kbd/mod? event)
|
||||||
target (dom/get-target event)
|
target (dom/get-target event)
|
||||||
|
|
||||||
editing? (or (some? (.closest ^js target ".public-DraftEditor-content"))
|
editing? (or (txu/some-text-editor-content? target)
|
||||||
(= "rich-text" (obj/get target "className"))
|
(= "rich-text" (obj/get target "className"))
|
||||||
(= "INPUT" (obj/get target "tagName"))
|
(= "INPUT" (obj/get target "tagName"))
|
||||||
(= "TEXTAREA" (obj/get target "tagName")))]
|
(= "TEXTAREA" (obj/get target "tagName")))]
|
||||||
|
@ -338,7 +339,7 @@
|
||||||
mod? (kbd/mod? event)
|
mod? (kbd/mod? event)
|
||||||
target (dom/get-target event)
|
target (dom/get-target event)
|
||||||
|
|
||||||
editing? (or (some? (.closest ^js target ".public-DraftEditor-content"))
|
editing? (or (txu/some-text-editor-content? target)
|
||||||
(= "rich-text" (obj/get target "className"))
|
(= "rich-text" (obj/get target "className"))
|
||||||
(= "INPUT" (obj/get target "tagName"))
|
(= "INPUT" (obj/get target "tagName"))
|
||||||
(= "TEXTAREA" (obj/get target "tagName")))]
|
(= "TEXTAREA" (obj/get target "tagName")))]
|
||||||
|
|
|
@ -632,6 +632,11 @@
|
||||||
(when (some? node)
|
(when (some? node)
|
||||||
(.setAttribute node attr value)))
|
(.setAttribute node attr value)))
|
||||||
|
|
||||||
|
(defn set-style!
|
||||||
|
[^js node ^string style value]
|
||||||
|
(when (some? node)
|
||||||
|
(.setProperty (.-style node) style value)))
|
||||||
|
|
||||||
(defn remove-attribute! [^js node ^string attr]
|
(defn remove-attribute! [^js node ^string attr]
|
||||||
(when (some? node)
|
(when (some? node)
|
||||||
(.removeAttribute node attr)))
|
(.removeAttribute node attr)))
|
||||||
|
|
20
frontend/src/app/util/text/content.cljs
Normal file
20
frontend/src/app/util/text/content.cljs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
;; 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.util.text.content
|
||||||
|
(:require
|
||||||
|
[app.util.text.content.from-dom :as fd]
|
||||||
|
[app.util.text.content.to-dom :as td]))
|
||||||
|
|
||||||
|
(defn dom->cljs
|
||||||
|
"Gets the editor content from a DOM structure"
|
||||||
|
[root]
|
||||||
|
(fd/create-root root))
|
||||||
|
|
||||||
|
(defn cljs->dom
|
||||||
|
"Sets the editor content from a CLJS structure"
|
||||||
|
[root]
|
||||||
|
(td/create-root root))
|
83
frontend/src/app/util/text/content/from_dom.cljs
Normal file
83
frontend/src/app/util/text/content/from_dom.cljs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
;; 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.util.text.content.from-dom
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.text :as txt]
|
||||||
|
[app.util.text.content.styles :as styles]))
|
||||||
|
|
||||||
|
(defn is-text-node
|
||||||
|
[node]
|
||||||
|
(= (.-nodeType node) js/Node.TEXT_NODE))
|
||||||
|
|
||||||
|
(defn is-element
|
||||||
|
[node tag]
|
||||||
|
(and (= (.-nodeType node) js/Node.ELEMENT_NODE)
|
||||||
|
(= (.-nodeName node) (.toUpperCase tag))))
|
||||||
|
|
||||||
|
(defn is-line-break
|
||||||
|
[node]
|
||||||
|
(is-element node "br"))
|
||||||
|
|
||||||
|
(defn is-inline-child
|
||||||
|
[node]
|
||||||
|
(or (is-line-break node)
|
||||||
|
(is-text-node node)))
|
||||||
|
|
||||||
|
(defn get-inline-text
|
||||||
|
[element]
|
||||||
|
(when-not (is-inline-child (.-firstChild element))
|
||||||
|
(throw (js/TypeError. "Invalid inline child")))
|
||||||
|
(if (is-line-break (.-firstChild element))
|
||||||
|
""
|
||||||
|
(.-textContent element)))
|
||||||
|
|
||||||
|
(defn get-attrs-from-styles
|
||||||
|
[element attrs]
|
||||||
|
(reduce (fn [acc key]
|
||||||
|
(let [style (.-style element)]
|
||||||
|
(if (contains? styles/mapping key)
|
||||||
|
(let [style-name (styles/get-style-name-as-css-variable key)
|
||||||
|
[_ style-decode] (get styles/mapping key)
|
||||||
|
value (style-decode (.getPropertyValue style style-name))]
|
||||||
|
(assoc acc key value))
|
||||||
|
(let [style-name (styles/get-style-name key)]
|
||||||
|
(assoc acc key (styles/normalize-attr-value key (.getPropertyValue style style-name))))))) {} attrs))
|
||||||
|
|
||||||
|
(defn get-inline-styles
|
||||||
|
[element]
|
||||||
|
(get-attrs-from-styles element txt/text-node-attrs))
|
||||||
|
|
||||||
|
(defn get-paragraph-styles
|
||||||
|
[element]
|
||||||
|
(get-attrs-from-styles element (d/concat-set txt/paragraph-attrs txt/text-node-attrs)))
|
||||||
|
|
||||||
|
(defn get-root-styles
|
||||||
|
[element]
|
||||||
|
(get-attrs-from-styles element txt/root-attrs))
|
||||||
|
|
||||||
|
(defn create-inline
|
||||||
|
[element]
|
||||||
|
(d/merge {:text (get-inline-text element)
|
||||||
|
:key (.-id element)}
|
||||||
|
(get-inline-styles element)))
|
||||||
|
|
||||||
|
(defn create-paragraph
|
||||||
|
[element]
|
||||||
|
(d/merge {:type "paragraph"
|
||||||
|
:key (.-id element)
|
||||||
|
:children (mapv create-inline (.-children element))}
|
||||||
|
(get-paragraph-styles element)))
|
||||||
|
|
||||||
|
(defn create-root
|
||||||
|
[element]
|
||||||
|
(let [root-styles (get-root-styles element)]
|
||||||
|
(d/merge {:type "root",
|
||||||
|
:key (.-id element)
|
||||||
|
:children [{:type "paragraph-set"
|
||||||
|
:children (mapv create-paragraph (.-children element))}]}
|
||||||
|
root-styles)))
|
198
frontend/src/app/util/text/content/styles.cljs
Normal file
198
frontend/src/app/util/text/content/styles.cljs
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
;; 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.util.text.content.styles
|
||||||
|
(:require
|
||||||
|
[app.common.text :as txt]
|
||||||
|
[app.common.transit :as transit]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
(defn encode
|
||||||
|
[value]
|
||||||
|
(transit/encode-str value))
|
||||||
|
|
||||||
|
(defn decode
|
||||||
|
[value]
|
||||||
|
(if (= value "")
|
||||||
|
nil
|
||||||
|
(transit/decode-str value)))
|
||||||
|
|
||||||
|
(def mapping
|
||||||
|
{:fills [encode decode]
|
||||||
|
:typography-ref-id [encode decode]
|
||||||
|
:typography-ref-file [encode decode]
|
||||||
|
:font-id [identity identity]
|
||||||
|
:font-variant-id [identity identity]
|
||||||
|
:vertical-align [identity identity]})
|
||||||
|
|
||||||
|
(defn normalize-style-value
|
||||||
|
"This function adds units to style values"
|
||||||
|
[k v]
|
||||||
|
(cond
|
||||||
|
(and (or (= k :font-size)
|
||||||
|
(= k :letter-spacing))
|
||||||
|
(not= (str/slice v -2) "px"))
|
||||||
|
(str v "px")
|
||||||
|
|
||||||
|
:else
|
||||||
|
v))
|
||||||
|
|
||||||
|
(defn normalize-attr-value
|
||||||
|
"This function strips units from attr values"
|
||||||
|
[k v]
|
||||||
|
(cond
|
||||||
|
(and (or (= k :font-size)
|
||||||
|
(= k :letter-spacing))
|
||||||
|
(= (str/slice v -2) "px"))
|
||||||
|
(str/slice v 0 -2)
|
||||||
|
|
||||||
|
:else
|
||||||
|
v))
|
||||||
|
|
||||||
|
(defn get-style-name-as-css-variable
|
||||||
|
[key]
|
||||||
|
(str/concat "--" (name key)))
|
||||||
|
|
||||||
|
(defn get-style-name
|
||||||
|
[key]
|
||||||
|
(cond
|
||||||
|
(= key :text-direction)
|
||||||
|
"direction"
|
||||||
|
|
||||||
|
:else
|
||||||
|
(name key)))
|
||||||
|
|
||||||
|
(defn get-style-keyword
|
||||||
|
[key]
|
||||||
|
(keyword (get-style-name-as-css-variable key)))
|
||||||
|
|
||||||
|
(defn get-attr-keyword-from-css-variable
|
||||||
|
[style-name]
|
||||||
|
(keyword (str/slice style-name 2)))
|
||||||
|
|
||||||
|
(defn get-attr-keyword
|
||||||
|
[style-name]
|
||||||
|
(cond
|
||||||
|
(= style-name "direction")
|
||||||
|
:text-direction
|
||||||
|
|
||||||
|
:else
|
||||||
|
(keyword style-name)))
|
||||||
|
|
||||||
|
(defn attr-needs-mapping?
|
||||||
|
[key]
|
||||||
|
(let [contained? (contains? mapping key)]
|
||||||
|
contained?))
|
||||||
|
|
||||||
|
(defn attr->style-key
|
||||||
|
[key]
|
||||||
|
(if (attr-needs-mapping? key)
|
||||||
|
(let [name (get-style-name-as-css-variable key)]
|
||||||
|
(keyword name))
|
||||||
|
(cond
|
||||||
|
(= key :text-direction)
|
||||||
|
(keyword "direction")
|
||||||
|
|
||||||
|
:else
|
||||||
|
key)))
|
||||||
|
|
||||||
|
(defn attr->style-value
|
||||||
|
([key value]
|
||||||
|
(attr->style-value key value false))
|
||||||
|
([key value normalize?]
|
||||||
|
(if (attr-needs-mapping? key)
|
||||||
|
(let [[encoder] (get mapping key)]
|
||||||
|
(if normalize?
|
||||||
|
(normalize-style-value key (encoder value))
|
||||||
|
(encoder value)))
|
||||||
|
(if normalize?
|
||||||
|
(normalize-style-value key value)
|
||||||
|
value))))
|
||||||
|
|
||||||
|
(defn attr->style
|
||||||
|
[[key value]]
|
||||||
|
[(attr->style-key key)
|
||||||
|
(attr->style-value key value)])
|
||||||
|
|
||||||
|
(defn attrs->styles
|
||||||
|
"Maps attrs to styles"
|
||||||
|
[styles]
|
||||||
|
(let [mapped-styles
|
||||||
|
(into {} (map attr->style styles))]
|
||||||
|
(clj->js mapped-styles)))
|
||||||
|
|
||||||
|
(defn style-needs-mapping?
|
||||||
|
[name]
|
||||||
|
(str/starts-with? name "--"))
|
||||||
|
|
||||||
|
(defn style->attr-key
|
||||||
|
[key]
|
||||||
|
(if (style-needs-mapping? key)
|
||||||
|
(keyword (str/slice key 2))
|
||||||
|
(keyword key)))
|
||||||
|
|
||||||
|
(defn style->attr-value
|
||||||
|
([name value]
|
||||||
|
(style->attr-value name value false))
|
||||||
|
([name value normalize?]
|
||||||
|
(if (style-needs-mapping? name)
|
||||||
|
(let [key (get-attr-keyword-from-css-variable name)
|
||||||
|
[_ decoder] (get mapping key)]
|
||||||
|
(if normalize?
|
||||||
|
(normalize-attr-value key (decoder value))
|
||||||
|
(decoder value)))
|
||||||
|
(let [key (get-attr-keyword name)]
|
||||||
|
(if normalize?
|
||||||
|
(normalize-attr-value key value)
|
||||||
|
value)))))
|
||||||
|
|
||||||
|
(defn style->attr
|
||||||
|
"Maps style to attr"
|
||||||
|
[[key value]]
|
||||||
|
[(style->attr-key key)
|
||||||
|
(style->attr-value key value)])
|
||||||
|
|
||||||
|
(defn styles->attrs
|
||||||
|
"Maps styles to attrs"
|
||||||
|
[styles]
|
||||||
|
(let [mapped-attrs
|
||||||
|
(into {} (map style->attr styles))]
|
||||||
|
mapped-attrs))
|
||||||
|
|
||||||
|
(defn get-style-defaults
|
||||||
|
"Returns a Javascript object compatible with the TextEditor default styles"
|
||||||
|
[style-defaults]
|
||||||
|
(clj->js
|
||||||
|
(reduce
|
||||||
|
(fn [acc [k v]]
|
||||||
|
(if (contains? mapping k)
|
||||||
|
(let [[style-encode] (get mapping k)
|
||||||
|
style-name (get-style-name-as-css-variable k)
|
||||||
|
style-value (normalize-style-value style-name (style-encode v))]
|
||||||
|
(assoc acc style-name style-value))
|
||||||
|
(let [style-name (get-style-name k)
|
||||||
|
style-value (normalize-style-value style-name v)]
|
||||||
|
(assoc acc style-name style-value)))) {} style-defaults)))
|
||||||
|
|
||||||
|
(defn get-styles-from-style-declaration
|
||||||
|
"Returns a ClojureScript object compatible with text nodes"
|
||||||
|
[style-declaration]
|
||||||
|
(reduce
|
||||||
|
(fn [acc k]
|
||||||
|
(if (contains? mapping k)
|
||||||
|
(let [style-name (get-style-name-as-css-variable k)
|
||||||
|
[_ style-decode] (get mapping k)
|
||||||
|
style-value (.getPropertyValue style-declaration style-name)]
|
||||||
|
(assoc acc k (style-decode style-value)))
|
||||||
|
(let [style-name (get-style-name k)
|
||||||
|
style-value (normalize-attr-value k (.getPropertyValue style-declaration style-name))]
|
||||||
|
(assoc acc k style-value)))) {} txt/text-style-attrs))
|
||||||
|
|
||||||
|
(defn get-styles-from-event
|
||||||
|
"Returns a ClojureScript object compatible with text nodes"
|
||||||
|
[e]
|
||||||
|
(let [style-declaration (.-detail e)]
|
||||||
|
(get-styles-from-style-declaration style-declaration)))
|
124
frontend/src/app/util/text/content/to_dom.cljs
Normal file
124
frontend/src/app/util/text/content/to_dom.cljs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
;; 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.util.text.content.to-dom
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.text :as txt]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.text.content.styles :as styles]))
|
||||||
|
|
||||||
|
(defn set-dataset
|
||||||
|
[element data]
|
||||||
|
(doseq [[data-name data-value] data]
|
||||||
|
(dom/set-data! element (name data-name) data-value)))
|
||||||
|
|
||||||
|
(defn set-styles
|
||||||
|
[element styles]
|
||||||
|
(doseq [[style-name style-value] styles]
|
||||||
|
(if (contains? styles/mapping style-name)
|
||||||
|
(let [[style-encode] (get styles/mapping style-name)
|
||||||
|
style-encoded-value (style-encode style-value)]
|
||||||
|
(dom/set-style! element (styles/get-style-name-as-css-variable style-name) style-encoded-value))
|
||||||
|
(dom/set-style! element (styles/get-style-name style-name) (styles/normalize-style-value style-name style-value)))))
|
||||||
|
|
||||||
|
(defn create-element
|
||||||
|
([tag]
|
||||||
|
(create-element tag nil nil))
|
||||||
|
([tag attrs]
|
||||||
|
(create-element tag attrs nil))
|
||||||
|
([tag attrs children]
|
||||||
|
(let [element (dom/create-element tag)]
|
||||||
|
;; set attributes to the element if necessary.
|
||||||
|
(doseq [[attr-name attr-value] attrs]
|
||||||
|
(case attr-name
|
||||||
|
:data (set-dataset element attr-value)
|
||||||
|
:style (set-styles element attr-value)
|
||||||
|
(dom/set-attribute! element (name attr-name) attr-value)))
|
||||||
|
|
||||||
|
;; add childs to the element if necessary.
|
||||||
|
(doseq [child children]
|
||||||
|
(dom/append-child! element child))
|
||||||
|
|
||||||
|
;; we need to return the DOM element
|
||||||
|
element)))
|
||||||
|
|
||||||
|
(defn get-styles-from-attrs
|
||||||
|
[node attrs]
|
||||||
|
(let [styles (reduce (fn [acc key] (assoc acc key (get node key))) {} attrs)
|
||||||
|
fills
|
||||||
|
(cond
|
||||||
|
;; DEPRECATED: still here for backward compatibility with
|
||||||
|
;; old penpot files that still has a single color.
|
||||||
|
(or (some? (:fill-color node))
|
||||||
|
(some? (:fill-opacity node))
|
||||||
|
(some? (:fill-color-gradient node)))
|
||||||
|
[(d/without-nils (select-keys node [:fill-color :fill-opacity :fill-color-gradient
|
||||||
|
:fill-color-ref-id :fill-color-ref-file]))]
|
||||||
|
|
||||||
|
(nil? (:fills node))
|
||||||
|
[{:fill-color "#000000" :fill-opacity 1}]
|
||||||
|
|
||||||
|
:else
|
||||||
|
(:fills node))]
|
||||||
|
(assoc styles :fills fills)))
|
||||||
|
|
||||||
|
(defn get-paragraph-styles
|
||||||
|
[paragraph]
|
||||||
|
(let [styles (get-styles-from-attrs paragraph (d/concat-set txt/paragraph-attrs txt/text-node-attrs))
|
||||||
|
;; If the text is not empty we must the paragraph font size to 0,
|
||||||
|
;; it affects to the height calculation the browser does
|
||||||
|
font-size (if (some #(not= "" (:text %)) (:children paragraph))
|
||||||
|
"0"
|
||||||
|
(:font-size styles (:font-size txt/default-text-attrs)))]
|
||||||
|
(cond-> styles
|
||||||
|
;; Every paragraph must have line-height to be correctly rendered
|
||||||
|
(nil? (:line-height styles)) (assoc :line-height (:line-height txt/default-text-attrs))
|
||||||
|
true (assoc :font-size font-size))))
|
||||||
|
|
||||||
|
(defn get-root-styles
|
||||||
|
[root]
|
||||||
|
(get-styles-from-attrs root txt/root-attrs))
|
||||||
|
|
||||||
|
(defn get-inline-styles
|
||||||
|
[inline paragraph]
|
||||||
|
(let [node (if (= "" (:text inline)) paragraph inline)
|
||||||
|
styles (get-styles-from-attrs node txt/text-node-attrs)]
|
||||||
|
(dissoc styles :line-height)))
|
||||||
|
|
||||||
|
(defn get-inline-children
|
||||||
|
[inline]
|
||||||
|
[(if (= "" (:text inline))
|
||||||
|
(dom/create-element "br")
|
||||||
|
(dom/create-text (:text inline)))])
|
||||||
|
|
||||||
|
(defn create-inline
|
||||||
|
[inline paragraph]
|
||||||
|
(create-element
|
||||||
|
"span"
|
||||||
|
{:id (:key inline)
|
||||||
|
:data {:itype "inline"}
|
||||||
|
:style (get-inline-styles inline paragraph)}
|
||||||
|
(get-inline-children inline)))
|
||||||
|
|
||||||
|
(defn create-paragraph
|
||||||
|
[paragraph]
|
||||||
|
(create-element
|
||||||
|
"div"
|
||||||
|
{:id (:key paragraph)
|
||||||
|
:data {:itype "paragraph"}
|
||||||
|
:style (get-paragraph-styles paragraph)}
|
||||||
|
(mapv #(create-inline % paragraph) (:children paragraph))))
|
||||||
|
|
||||||
|
(defn create-root
|
||||||
|
[root]
|
||||||
|
(let [root-styles (get-root-styles root)]
|
||||||
|
(create-element
|
||||||
|
"div"
|
||||||
|
{:id (:key root)
|
||||||
|
:data {:itype "root"}
|
||||||
|
:style root-styles}
|
||||||
|
(mapv create-paragraph (get-in root [:children 0 :children])))))
|
43
frontend/src/app/util/text/ui.cljs
Normal file
43
frontend/src/app/util/text/ui.cljs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
;; 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.util.text.ui
|
||||||
|
(:require
|
||||||
|
[app.main.features :as features]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.util.dom :as dom]))
|
||||||
|
|
||||||
|
(defn v1-closest-text-editor-content
|
||||||
|
[target]
|
||||||
|
(.closest ^js target ".public-DraftEditor-content"))
|
||||||
|
|
||||||
|
(defn v2-closest-text-editor-content
|
||||||
|
[target]
|
||||||
|
(.closest ^js target ".text-editor-content"))
|
||||||
|
|
||||||
|
(defn closest-text-editor-content
|
||||||
|
[target]
|
||||||
|
(if (features/active-feature? @st/state "text-editor/v2")
|
||||||
|
(v2-closest-text-editor-content target)
|
||||||
|
(v1-closest-text-editor-content target)))
|
||||||
|
|
||||||
|
(defn some-text-editor-content?
|
||||||
|
[target]
|
||||||
|
(some? (closest-text-editor-content target)))
|
||||||
|
|
||||||
|
(defn v1-get-text-editor-content
|
||||||
|
[]
|
||||||
|
(dom/get-element-by-class "public-DraftEditor-content"))
|
||||||
|
|
||||||
|
(defn v2-get-text-editor-content
|
||||||
|
[]
|
||||||
|
(dom/get-element-by-class "text-editor-content"))
|
||||||
|
|
||||||
|
(defn get-text-editor-content
|
||||||
|
[]
|
||||||
|
(if (features/active-feature? @st/state "text-editor/v2")
|
||||||
|
(v2-get-text-editor-content)
|
||||||
|
(v1-get-text-editor-content)))
|
Loading…
Add table
Add a link
Reference in a new issue