0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-02 04:19:08 -05:00

Add minor improvements to text editor v2 events handling

Also updates the editor code to the latest version
This commit is contained in:
Andrey Antukh 2024-10-03 09:54:53 +02:00
parent 352efcb610
commit ffadf29ad7
5 changed files with 175 additions and 129 deletions

View file

@ -151,6 +151,12 @@
:ns-regexp "^frontend-tests.*-test$" :ns-regexp "^frontend-tests.*-test$"
:autorun true :autorun true
:js-options
{:entry-keys ["module" "browser" "main"]
:resolve {"penpot/vendor/text-editor-v2"
{:target :file
:file "vendor/text_editor_v2.js"}}}
:compiler-options :compiler-options
{:output-feature-set :es2020 {:output-feature-set :es2020
:output-wrapper false :output-wrapper false

View file

@ -6,6 +6,7 @@
(ns app.main.data.workspace.texts (ns app.main.data.workspace.texts
(:require (:require
["penpot/vendor/text-editor-v2" :as editor.v2]
[app.common.attrs :as attrs] [app.common.attrs :as attrs]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
@ -34,7 +35,12 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
;; -- V2 Editor ;; -- V2 Editor Helpers
(def ^function create-editor editor.v2/create)
(def ^function set-editor-root! editor.v2/setRoot)
(def ^function get-editor-root editor.v2/getRoot)
(def ^function dispose! editor.v2/dispose)
(declare v2-update-text-shape-content) (declare v2-update-text-shape-content)
(declare v2-update-text-editor-styles) (declare v2-update-text-editor-styles)
@ -463,13 +469,12 @@
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(when (features/active-feature? state "text-editor/v2") (when (features/active-feature? state "text-editor/v2")
(let [text-editor-instance (:workspace-editor state)] (let [instance (:workspace-editor state)
(when (some? text-editor-instance) styles (some-> (editor.v2/getCurrentStyle instance)
(let [attrs (-> (.-currentStyle text-editor-instance) (styles/get-styles-from-style-declaration)
(styles/get-styles-from-style-declaration) ((comp update-node-fn migrate-node))
((comp update-node-fn migrate-node))) (styles/attrs->styles))]
styles (styles/attrs->styles attrs)] (editor.v2/applyStylesToSelection instance styles))))))
(.applyStylesToSelection ^js text-editor-instance styles))))))))
;; --- RESIZE UTILS ;; --- RESIZE UTILS
@ -729,10 +734,9 @@
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(when (features/active-feature? state "text-editor/v2") (when (features/active-feature? state "text-editor/v2")
(let [text-editor-instance (:workspace-editor state) (let [instance (:workspace-editor state)
styles (styles/attrs->styles attrs)] styles (styles/attrs->styles attrs)]
(when (some? text-editor-instance) (editor.v2/applyStylesToSelection instance styles))))))
(.applyStylesToSelection ^js text-editor-instance styles)))))))
(defn update-all-attrs (defn update-all-attrs
[ids attrs] [ids attrs]

View file

@ -7,7 +7,6 @@
(ns app.main.ui.workspace.shapes.text.v2-editor (ns app.main.ui.workspace.shapes.text.v2-editor
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
["penpot/vendor/text-editor-v2" :as editor.v2]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
@ -21,148 +20,143 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.css-cursors :as cur] [app.main.ui.css-cursors :as cur]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.globals :as global]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.text.content :as content] [app.util.text.content :as content]
[app.util.text.content.styles :as styles] [app.util.text.content.styles :as styles]
[goog.events :as events]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private TextEditor (defn- initialize-event-handlers
editor.v2/default) "Internal editor events handler initializer/destructor"
[shape-id content selection-ref editor-ref container-ref]
(let [editor-node
(mf/ref-val editor-ref)
(defn- create-editor selection-node
[element options] (mf/ref-val selection-ref)
(new TextEditor element (obj/clone options)))
(defn- set-editor-root! ;; Gets the default font from the workspace refs.
[instance root] default-font
(set! (.-root ^TextEditor instance) root) (deref refs/default-font)
instance)
(defn- get-editor-root style-defaults
[instance] (styles/get-style-defaults
(.-root ^TextEditor instance)) (merge txt/default-attrs default-font))
options
#js {:styleDefaults style-defaults
:selectionImposterElement selection-node}
instance
(dwt/create-editor editor-node options)
on-key-up
(fn [event]
(dom/stop-propagation event)
(when (kbd/esc? event)
(st/emit! :interrupt (dw/clear-edition-mode))))
on-blur
(fn []
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content true)))
(let [container-node (mf/ref-val container-ref)]
(dom/set-style! container-node "opacity" 0)))
on-focus
(fn []
(let [container-node (mf/ref-val container-ref)]
(dom/set-style! container-node "opacity" 1)))
on-style-change
(fn [event]
(let [styles (styles/get-styles-from-event event)]
(st/emit! (dwt/v2-update-text-editor-styles shape-id styles))))
on-needs-layout
(fn []
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id 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
(fn []
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content true))))]
(.addEventListener ^js global/document "keyup" on-key-up)
(.addEventListener ^js instance "blur" on-blur)
(.addEventListener ^js instance "focus" on-focus)
(.addEventListener ^js instance "needslayout" on-needs-layout)
(.addEventListener ^js instance "stylechange" on-style-change)
(.addEventListener ^js instance "change" on-change)
(st/emit! (dwt/update-editor instance))
(when (some? content)
(dwt/set-editor-root! instance (content/cljs->dom content)))
(st/emit! (dwt/focus-editor))
;; This function is called when the component is unmount
(fn []
(.removeEventListener ^js global/document "keyup" on-key-up)
(.removeEventListener ^js instance "blur" on-blur)
(.removeEventListener ^js instance "focus" on-focus)
(.removeEventListener ^js instance "needslayout" on-needs-layout)
(.removeEventListener ^js instance "stylechange" on-style-change)
(.removeEventListener ^js instance "change" on-change)
(dwt/dispose! instance)
(st/emit! (dwt/update-editor nil)))))
(mf/defc text-editor-html (mf/defc text-editor-html
"Text editor (HTML)" "Text editor (HTML)"
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
::mf/wrap-props false} ::mf/props :obj}
[{:keys [shape] :as props}] [{:keys [shape]}]
(let [content (:content shape) (let [content (:content shape)
shape-id (:id shape) shape-id (dm/get-prop shape :id)
;; Gets the default font from the workspace refs.
default-font (deref refs/default-font)
;; This is a reference to the dom element that ;; This is a reference to the dom element that
;; should contain the TextEditor. ;; should contain the TextEditor.
text-editor-ref (mf/use-ref nil) editor-ref (mf/use-ref nil)
;; This reference is to the container ;; This reference is to the container
text-editor-container-ref (mf/use-ref nil) container-ref (mf/use-ref nil)
text-editor-instance-ref (mf/use-ref nil) selection-ref (mf/use-ref nil)]
text-editor-selection-ref (mf/use-ref nil)
on-blur ;; WARN: we explicitly do not pass content on effect dependency
(mf/use-fn ;; array because we only need to initialize this once with initial
(fn [] ;; content
(let [text-editor-instance (mf/ref-val text-editor-instance-ref) (mf/with-effect [shape-id]
container (mf/ref-val text-editor-container-ref) (initialize-event-handlers shape-id
new-content (content/dom->cljs (get-editor-root text-editor-instance))] content
(when (some? new-content) selection-ref
(st/emit! (dwt/v2-update-text-shape-content shape-id new-content true))) editor-ref
(dom/set-style! container "opacity" 0)))) container-ref))
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 (get-editor-root 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 (get-editor-root 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 (create-editor 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)
(set-editor-root! 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 [:div
{:class (dm/str (cur/get-dynamic "text" (:rotation shape)) {:class (dm/str (cur/get-dynamic "text" (:rotation shape))
" " " "
(stl/css :text-editor-container)) (stl/css :text-editor-container))
:ref text-editor-container-ref :ref container-ref
:data-testid "text-editor-container" :data-testid "text-editor-container"
:style {:width (:width shape) :style {:width (:width shape)
:height (:height shape)} :height (:height shape)}
;; We hide the editor when is blurred because otherwise the selection won't let us see ;; We hide the editor when is blurred because otherwise the
;; the underlying text. Use opacity because display or visibility won't allow to recover ;; selection won't let us see the underlying text. Use opacity
;; focus afterwards. ;; because display or visibility won't allow to recover focus
;; IMPORTANT! This is now done through DOM mutations (see on-blur and on-focus) ;; afterwards.
;; but I keep this for future references.
;; :opacity (when @blurred 0)}} ;; 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 [:div
{:class (stl/css :text-editor-selection-imposter) {:class (stl/css :text-editor-selection-imposter)
:ref text-editor-selection-ref}] :ref selection-ref}]
[:div [:div
{:class (dm/str {:class (dm/str
"mousetrap " "mousetrap "
@ -174,7 +168,7 @@
:align-top (= (:vertical-align content "top") "top") :align-top (= (:vertical-align content "top") "top")
:align-center (= (:vertical-align content) "center") :align-center (= (:vertical-align content) "center")
:align-bottom (= (:vertical-align content) "bottom"))) :align-bottom (= (:vertical-align content) "bottom")))
:ref text-editor-ref :ref editor-ref
:data-testid "text-editor-content" :data-testid "text-editor-content"
:data-x (dm/get-prop shape :x) :data-x (dm/get-prop shape :x)
:data-y (dm/get-prop shape :y) :data-y (dm/get-prop shape :y)
@ -198,7 +192,7 @@
(mf/defc text-editor (mf/defc text-editor
"Text editor wrapper component" "Text editor wrapper component"
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
::mf/wrap-props false ::mf/props :obj
::mf/forward-ref true} ::mf/forward-ref true}
[{:keys [shape modifiers] :as props} _] [{:keys [shape modifiers] :as props} _]
(let [shape-id (dm/get-prop shape :id) (let [shape-id (dm/get-prop shape :id)
@ -272,4 +266,3 @@
[:div {:style style} [:div {:style style}
[:& text-editor-html {:shape shape [:& text-editor-html {:shape shape
:key (dm/str shape-id)}]]]])) :key (dm/str shape-id)}]]]]))

View file

@ -92,7 +92,6 @@
"</style>")] "</style>")]
(.insertAdjacentHTML ^js node "beforeend" style))) (.insertAdjacentHTML ^js node "beforeend" style)))
(defn get-element-by-class (defn get-element-by-class
([classname] ([classname]
(dom/getElementByClass classname)) (dom/getElementByClass classname))

View file

@ -2740,5 +2740,49 @@ notifyLayout_fn = function(type = LayoutType.FULL, mutations) {
}) })
); );
}; };
export default TextEditor; function isEditor(instance) {
return instance instanceof TextEditor;
}
function getRoot(instance) {
if (isEditor(instance)) {
return instance.root;
} else {
return null;
}
}
function setRoot(instance, root) {
if (isEditor(instance)) {
instance.root = root;
}
return instance;
}
function create(element, options) {
return new TextEditor(element, { ...options });
}
function getCurrentStyle(instance) {
if (isEditor(instance)) {
return instance.currentStyle;
}
}
function applyStylesToSelection(instance, styles) {
if (isEditor(instance)) {
return instance.applyStylesToSelection(styles);
}
}
function dispose(instance) {
if (isEditor(instance)) {
instance.dispose();
}
}
export {
TextEditor,
applyStylesToSelection,
create,
TextEditor as default,
dispose,
getCurrentStyle,
getRoot,
isEditor,
setRoot
};
//# sourceMappingURL=TextEditor.js.map //# sourceMappingURL=TextEditor.js.map