mirror of
https://github.com/penpot/penpot.git
synced 2025-03-13 00:01:51 -05:00
✨ Allows svg text on test edit and creation
This commit is contained in:
parent
18dded1a00
commit
6cb6adc134
8 changed files with 198 additions and 123 deletions
54
frontend/src/app/main/ui/hooks/mutable_observer.cljs
Normal file
54
frontend/src/app/main/ui/hooks/mutable_observer.cljs
Normal file
|
@ -0,0 +1,54 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.hooks.mutable-observer
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(log/set-level! :warn)
|
||||
|
||||
(defn use-mutable-observer
|
||||
[on-change]
|
||||
|
||||
(let [prev-obs-ref (mf/use-ref nil)
|
||||
node-ref (mf/use-ref nil)
|
||||
|
||||
on-mutation
|
||||
(mf/use-callback
|
||||
(mf/deps on-change)
|
||||
(fn [mutation]
|
||||
(log/debug :action "mutation" :js/mutation mutation)
|
||||
(on-change (mf/ref-val node-ref))))
|
||||
|
||||
set-node
|
||||
(mf/use-callback
|
||||
(mf/deps on-mutation)
|
||||
(fn [^js node]
|
||||
(when (and (some? node) (not= (mf/ref-val node-ref) node))
|
||||
(mf/set-ref-val! node-ref node)
|
||||
|
||||
(when-let [^js prev-obs (mf/ref-val prev-obs-ref)]
|
||||
(.disconnect prev-obs)
|
||||
(mf/set-ref-val! prev-obs-ref nil))
|
||||
|
||||
(when (some? node)
|
||||
(let [options #js {:attributes true
|
||||
:childList true
|
||||
:subtree true
|
||||
:characterData true}
|
||||
mutation-obs (js/MutationObserver. on-mutation)]
|
||||
(mf/set-ref-val! prev-obs-ref mutation-obs)
|
||||
(.observe mutation-obs node options))))))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(fn []
|
||||
(when-let [^js prev-obs (mf/ref-val prev-obs-ref)]
|
||||
(.disconnect prev-obs)
|
||||
(mf/set-ref-val! prev-obs-ref nil)))))
|
||||
|
||||
set-node))
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.shapes.gradients
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
|
@ -106,6 +107,7 @@
|
|||
:gradient gradient
|
||||
:shape shape}]
|
||||
(when gradient
|
||||
(case (:type gradient)
|
||||
:linear [:> linear-gradient gradient-props]
|
||||
:radial [:> radial-gradient gradient-props]))))
|
||||
(case (d/name (:type gradient))
|
||||
"linear" [:> linear-gradient gradient-props]
|
||||
"radial" [:> radial-gradient gradient-props]
|
||||
nil))))
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.transit :as transit]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.text.styles :as sts]
|
||||
|
@ -24,12 +23,7 @@
|
|||
(let [node (obj/get props "node")
|
||||
text (:text node)
|
||||
style (sts/generate-text-styles node)]
|
||||
[:span.text-node {:style style
|
||||
:data-fill-color (:fill-color node)
|
||||
:data-fill-color-gradient (transit/encode-str (:fill-color-gradient node))
|
||||
:data-fill-color-ref-file (transit/encode-str (:fill-color-ref-file node))
|
||||
:data-fill-color-ref-id (transit/encode-str (:fill-color-ref-id node))
|
||||
:data-fill-opacity (:fill-opacity node)}
|
||||
[:span.text-node {:style style}
|
||||
(if (= text "") "\u00A0" text)]))
|
||||
|
||||
(mf/defc render-root
|
||||
|
@ -193,7 +187,10 @@
|
|||
{::mf/wrap-props false
|
||||
::mf/forward-ref true}
|
||||
[props ref]
|
||||
(let [{:keys [id x y width height content] :as shape} (obj/get props "shape")
|
||||
(let [shape (obj/get props "shape")
|
||||
transform (str (geom/transform-matrix shape))
|
||||
|
||||
{:keys [id x y width height content]} shape
|
||||
grow-type (obj/get props "grow-type") ;; This is only needed in workspace
|
||||
;; We add 8px to add a padding for the exporter
|
||||
;; width (+ width 8)
|
||||
|
@ -205,16 +202,17 @@
|
|||
plain-colors?
|
||||
(remap-colors color-mapping))]
|
||||
|
||||
[:foreignObject {:x x
|
||||
:y y
|
||||
:id id
|
||||
:data-colors (->> colors (str/join ","))
|
||||
:data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify))
|
||||
:transform (geom/transform-matrix shape)
|
||||
:width (if (#{:auto-width} grow-type) 100000 width)
|
||||
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)
|
||||
:style (-> (obj/new) (attrs/add-layer-props shape))
|
||||
:ref ref}
|
||||
[:foreignObject
|
||||
{:x x
|
||||
:y y
|
||||
:id id
|
||||
:data-colors (->> colors (str/join ","))
|
||||
:data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify))
|
||||
:transform transform
|
||||
:width (if (#{:auto-width} grow-type) 100000 width)
|
||||
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)
|
||||
:style (-> (obj/new) (attrs/add-layer-props shape))
|
||||
:ref ref}
|
||||
;; We use a class here because react has a bug that won't use the appropriate selector for
|
||||
;; `background-clip`
|
||||
[:style ".text-node { background-clip: text;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.text :as txt]
|
||||
[app.common.transit :as transit]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.util.color :as uc]
|
||||
[app.util.object :as obj]
|
||||
|
@ -71,31 +72,22 @@
|
|||
fill-color (:fill-color data)
|
||||
fill-opacity (:fill-opacity data)
|
||||
|
||||
;; Uncomment this to allow to remove text colors. This could break the texts that already exist
|
||||
;;[r g b a] (if (nil? fill-color)
|
||||
;; [0 0 0 0] ;; Transparent color
|
||||
;; (uc/hex->rgba fill-color fill-opacity))
|
||||
|
||||
[r g b a] (uc/hex->rgba fill-color fill-opacity)
|
||||
text-color (when (and (some? fill-color) (some? fill-opacity))
|
||||
(str/format "rgba(%s, %s, %s, %s)" r g b a))
|
||||
|
||||
fontsdb (deref fonts/fontsdb)
|
||||
|
||||
base #js {:textDecoration text-decoration
|
||||
:textTransform text-transform
|
||||
:lineHeight (or line-height "inherit")
|
||||
:color text-color
|
||||
:caretColor "black"}]
|
||||
:color "transparent"
|
||||
:caretColor (or text-color "black")}
|
||||
|
||||
(when-let [gradient (:fill-color-gradient data)]
|
||||
(let [text-color (-> (update gradient :type keyword)
|
||||
(uc/gradient->css))]
|
||||
(-> base
|
||||
(obj/set! "color" text-color)
|
||||
#_(obj/set! "--text-color" text-color)
|
||||
#_(obj/set! "backgroundImage" "var(--text-color)")
|
||||
#_(obj/set! "WebkitTextFillColor" "transparent")
|
||||
#_(obj/set! "WebkitBackgroundClip" "text"))))
|
||||
base (-> base
|
||||
(obj/set! "--fill-color" fill-color)
|
||||
(obj/set! "--fill-color-gradient" (transit/encode-str (:fill-color-gradient data)))
|
||||
(obj/set! "--fill-opacity" fill-opacity))]
|
||||
|
||||
(when (and (string? letter-spacing)
|
||||
(pos? (alength letter-spacing)))
|
||||
|
|
|
@ -45,10 +45,7 @@
|
|||
[:> :g group-props
|
||||
[:defs
|
||||
[:clipPath {:id clip-id}
|
||||
[:rect.text-clip
|
||||
{:x x :y y
|
||||
:width width :height height
|
||||
:transform (gsh/transform-matrix shape)}]]]
|
||||
[:rect.text-clip {:x x :y y :width width :height height}]]]
|
||||
(for [[index data] (d/enumerate position-data)]
|
||||
(let [props (-> #js {:x (:x data)
|
||||
:y (:y data)
|
||||
|
@ -63,3 +60,5 @@
|
|||
:whiteSpace "pre"}
|
||||
(attrs/add-fill data (get-gradient-id index)))})]
|
||||
[:> :text props (:text data)]))]]]))
|
||||
|
||||
|
||||
|
|
|
@ -7,16 +7,15 @@
|
|||
(ns app.main.ui.workspace.shapes.text
|
||||
(:require
|
||||
[app.common.attrs :as attrs]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
[app.common.transit :as transit]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks.mutable-observer :refer [use-mutable-observer]]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text.fo-text :as fo]
|
||||
[app.main.ui.shapes.text.svg-text :as svg]
|
||||
|
@ -39,7 +38,7 @@
|
|||
(mf/defc text-static-content
|
||||
[{:keys [shape]}]
|
||||
[:& fo/text-shape {:shape shape
|
||||
:grow-type (:grow-type shape)}])
|
||||
:grow-type (:grow-type shape)}])
|
||||
|
||||
(defn- update-with-current-editor-state
|
||||
[{:keys [id] :as shape}]
|
||||
|
@ -114,36 +113,10 @@
|
|||
(mf/use-effect
|
||||
(fn [] #(mf/set-ref-val! mnt false)))
|
||||
|
||||
[:& fo/text-shape {:ref text-ref-cb :shape shape :grow-type (:grow-type shape)}]))
|
||||
|
||||
|
||||
(defn calc-position-data
|
||||
[base-node]
|
||||
(let [viewport (dom/get-element "render")
|
||||
zoom (get-in @st/state [:workspace-local :zoom])
|
||||
text-data (utp/calc-text-node-positions base-node viewport zoom)]
|
||||
(->> text-data
|
||||
(map (fn [{:keys [node position text]}]
|
||||
(let [{:keys [x y width height]} position
|
||||
rtl? (= "rtl" (.-dir (.-parentElement ^js node)))
|
||||
styles (.computedStyleMap ^js node)]
|
||||
(d/without-nils
|
||||
{:rtl? rtl?
|
||||
:x (if rtl? (+ x width) x)
|
||||
:y (+ y height)
|
||||
:width width
|
||||
:height height
|
||||
:font-family (str (.get styles "font-family"))
|
||||
:font-size (str (.get styles "font-size"))
|
||||
:font-weight (str (.get styles "font-weight"))
|
||||
:text-transform (str (.get styles "text-transform"))
|
||||
:text-decoration (str (.get styles "text-decoration"))
|
||||
:font-style (str (.get styles "font-style"))
|
||||
:fill-color (or (dom/get-attribute node "data-fill-color") "#000000")
|
||||
:fill-color-gradient (transit/decode-str (dom/get-attribute node "data-fill-color-gradient"))
|
||||
:fill-opacity (d/parse-double (or (:fill-opacity node) "1"))
|
||||
:text text})))))))
|
||||
|
||||
[:& fo/text-shape {:ref text-ref-cb
|
||||
:shape shape
|
||||
:grow-type (:grow-type shape)
|
||||
:key (str "shape-" (:id shape))}]))
|
||||
|
||||
|
||||
(mf/defc text-wrapper
|
||||
|
@ -154,13 +127,13 @@
|
|||
edition? (mf/deref edition-ref)
|
||||
shape-ref (mf/use-ref nil)
|
||||
|
||||
prev-obs-ref (mf/use-ref nil)
|
||||
local-position-data (mf/use-state nil)
|
||||
|
||||
handle-change-foreign-object
|
||||
(fn []
|
||||
(when-let [node (mf/ref-val shape-ref)]
|
||||
(let [position-data (calc-position-data node)
|
||||
(fn [node]
|
||||
(when (some? node)
|
||||
(mf/set-ref-val! shape-ref node)
|
||||
(let [position-data (utp/calc-position-data node)
|
||||
parent (dom/get-parent node)
|
||||
parent-transform (dom/get-attribute parent "transform")
|
||||
node-transform (dom/get-attribute node "transform")
|
||||
|
@ -173,40 +146,20 @@
|
|||
mtx (-> (gmt/multiply parent-mtx node-mtx)
|
||||
(gmt/inverse))
|
||||
|
||||
position-data'
|
||||
position-data
|
||||
(->> position-data
|
||||
(mapv #(merge % (-> (select-keys % [:x :y :width :height])
|
||||
(gsh/transform-rect mtx)))))]
|
||||
(reset! local-position-data position-data'))))
|
||||
(reset! local-position-data position-data))))
|
||||
|
||||
on-change-node
|
||||
(fn [^js node]
|
||||
(mf/set-ref-val! shape-ref node)
|
||||
|
||||
(when-let [^js prev-obs (mf/ref-val prev-obs-ref)]
|
||||
(.disconnect prev-obs)
|
||||
(mf/set-ref-val! prev-obs-ref nil))
|
||||
|
||||
(when (some? node)
|
||||
(let [fo-node (dom/query node "foreignObject")
|
||||
options #js {:attributes true
|
||||
:childList true
|
||||
:subtree true}
|
||||
mutation-obs (js/MutationObserver. handle-change-foreign-object)]
|
||||
(mf/set-ref-val! prev-obs-ref mutation-obs)
|
||||
(.observe mutation-obs fo-node options))))]
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(fn []
|
||||
(when-let [^js prev-obs (mf/ref-val prev-obs-ref)]
|
||||
(.disconnect prev-obs)
|
||||
(mf/set-ref-val! prev-obs-ref nil)))))
|
||||
on-change-node (use-mutable-observer handle-change-foreign-object)]
|
||||
|
||||
;; When the text is "dirty?" we get recalculate the positions
|
||||
(mf/use-layout-effect
|
||||
(mf/deps id dirty?)
|
||||
(fn []
|
||||
(let [node (mf/ref-val shape-ref)
|
||||
position-data (calc-position-data node)]
|
||||
position-data (utp/calc-position-data node)]
|
||||
(reset! local-position-data nil)
|
||||
(st/emit! (dch/update-shapes
|
||||
[id]
|
||||
|
@ -233,9 +186,10 @@
|
|||
:edition? edition?
|
||||
:key (str id edition?)}]]
|
||||
|
||||
[:g.text-svg {:opacity (when edition? 0)
|
||||
:pointer-events "none"}
|
||||
(when (some? (:position-data shape))
|
||||
[:& svg/text-shape {:shape (cond-> shape
|
||||
(some? @local-position-data)
|
||||
(assoc :position-data @local-position-data))}])]]]))
|
||||
(when (and (not edition?) (or (some? (:position-data shape)) (some? local-position-data)))
|
||||
(let [shape
|
||||
(cond-> shape
|
||||
(some? @local-position-data)
|
||||
(assoc :position-data @local-position-data))]
|
||||
[:g.text-svg {:pointer-events "none"}
|
||||
[:& svg/text-shape {:shape shape}]]))]]))
|
||||
|
|
|
@ -14,11 +14,15 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.cursors :as cur]
|
||||
[app.main.ui.hooks.mutable-observer :refer [use-mutable-observer]]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text.styles :as sts]
|
||||
[app.main.ui.shapes.text.svg-text :as svg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.text-svg-position :as utp]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
(:import
|
||||
|
@ -233,17 +237,56 @@
|
|||
::mf/forward-ref true}
|
||||
[props _]
|
||||
(let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape")
|
||||
clip-id (str "clip-" id)]
|
||||
[:g.text-editor {:clip-path (str "url(#" clip-id ")")}
|
||||
[:defs
|
||||
;; This clippath will cut the huge foreign object we use to calculate the automatic resize
|
||||
[:clipPath {:id clip-id}
|
||||
[:rect {:x x :y y
|
||||
:width (+ width 8) :height (+ height 8)
|
||||
:transform (gsh/transform-matrix shape)}]]]
|
||||
[:foreignObject {:transform (gsh/transform-matrix shape)
|
||||
:x x :y y
|
||||
:width (if (#{:auto-width} grow-type) 100000 width)
|
||||
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)}
|
||||
transform (str (gsh/transform-matrix shape))
|
||||
|
||||
[:& text-shape-edit-html {:shape shape :key (str id)}]]]))
|
||||
clip-id (str "clip-" id)
|
||||
|
||||
shape-ref (mf/use-ref nil)
|
||||
local-position-data (mf/use-state nil)
|
||||
|
||||
handle-change-foreign-object
|
||||
(mf/use-callback
|
||||
(fn [node]
|
||||
(when node
|
||||
(mf/set-ref-val! shape-ref node)
|
||||
(let [position-data (utp/calc-position-data node)]
|
||||
(reset! local-position-data position-data)))))
|
||||
|
||||
handle-interaction
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(handle-change-foreign-object (mf/ref-val shape-ref))))
|
||||
|
||||
on-change-node (use-mutable-observer handle-change-foreign-object)]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/use-callback handle-interaction)
|
||||
(fn []
|
||||
(let [keys [(events/listen js/document EventType.KEYUP handle-interaction)
|
||||
(events/listen js/document EventType.KEYDOWN handle-interaction)
|
||||
(events/listen js/document EventType.MOUSEDOWN handle-interaction)]]
|
||||
#(doseq [key keys]
|
||||
(events/unlistenByKey key)))))
|
||||
[:*
|
||||
[:> shape-container {:shape shape
|
||||
:pointer-events "none"}
|
||||
[:& svg/text-shape {:shape (cond-> shape
|
||||
(some? @local-position-data)
|
||||
(assoc :position-data @local-position-data))}]]
|
||||
|
||||
[:g.text-editor {:clip-path (str "url(#" clip-id ")")
|
||||
:ref on-change-node
|
||||
:key (str "editor-" id)}
|
||||
[:defs
|
||||
;; This clippath will cut the huge foreign object we use to calculate the automatic resize
|
||||
[:clipPath {:id clip-id}
|
||||
[:rect {:x x :y y
|
||||
:width (+ width 8) :height (+ height 8)
|
||||
:transform transform}]]]
|
||||
|
||||
[:foreignObject {:transform transform
|
||||
:x x :y y
|
||||
:width (if (#{:auto-width} grow-type) 100000 width)
|
||||
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)}
|
||||
|
||||
[:& text-shape-edit-html {:shape shape :key (str id)}]]]]))
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.transit :as transit]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as global]))
|
||||
|
||||
|
@ -79,7 +81,7 @@
|
|||
:width (- (:x p2) (:x p1))
|
||||
:height (- (:y p2) (:y p1)))))
|
||||
|
||||
text-nodes (dom/query-all base-node ".text-node")]
|
||||
text-nodes (dom/query-all base-node ".text-node, span[data-text]")]
|
||||
|
||||
(->> text-nodes
|
||||
(mapcat
|
||||
|
@ -90,3 +92,34 @@
|
|||
(map #(update % :position translate-rect))))))
|
||||
|
||||
|
||||
|
||||
(defn calc-position-data
|
||||
[base-node]
|
||||
(let [viewport (dom/get-element "render")
|
||||
zoom (get-in @st/state [:workspace-local :zoom])
|
||||
text-data (calc-text-node-positions base-node viewport zoom)]
|
||||
(->> text-data
|
||||
(map (fn [{:keys [node position text]}]
|
||||
(let [{:keys [x y width height]} position
|
||||
rtl? (= "rtl" (.-dir (.-parentElement ^js node)))
|
||||
styles (js/getComputedStyle ^js node)
|
||||
get (fn [prop]
|
||||
(let [value (.getPropertyValue styles prop)]
|
||||
(when (and value (not= value ""))
|
||||
value)))]
|
||||
(d/without-nils
|
||||
{:rtl? rtl?
|
||||
:x (if rtl? (+ x width) x)
|
||||
:y (+ y height)
|
||||
:width width
|
||||
:height height
|
||||
:font-family (str (get "font-family"))
|
||||
:font-size (str (get "font-size"))
|
||||
:font-weight (str (get "font-weight"))
|
||||
:text-transform (str (get "text-transform"))
|
||||
:text-decoration (str (get "text-decoration"))
|
||||
:font-style (str (get "font-style"))
|
||||
:fill-color (or (get "--fill-color") "#000000")
|
||||
:fill-color-gradient (transit/decode-str (get "--fill-color-gradient"))
|
||||
:fill-opacity (d/parse-double (or (get "--fill-opacity") "1"))
|
||||
:text text})))))))
|
||||
|
|
Loading…
Add table
Reference in a new issue