0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-22 14:39:45 -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$"
: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
{:output-feature-set :es2020
:output-wrapper false

View file

@ -6,6 +6,7 @@
(ns app.main.data.workspace.texts
(:require
["penpot/vendor/text-editor-v2" :as editor.v2]
[app.common.attrs :as attrs]
[app.common.data :as d]
[app.common.data.macros :as dm]
@ -34,7 +35,12 @@
[cuerdas.core :as str]
[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-editor-styles)
@ -463,13 +469,12 @@
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 ^js text-editor-instance styles))))))))
(let [instance (:workspace-editor state)
styles (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration)
((comp update-node-fn migrate-node))
(styles/attrs->styles))]
(editor.v2/applyStylesToSelection instance styles))))))
;; --- RESIZE UTILS
@ -729,10 +734,9 @@
ptk/EffectEvent
(effect [_ state _]
(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)]
(when (some? text-editor-instance)
(.applyStylesToSelection ^js text-editor-instance styles)))))))
(editor.v2/applyStylesToSelection instance styles))))))
(defn update-all-attrs
[ids attrs]

View file

@ -7,7 +7,6 @@
(ns app.main.ui.workspace.shapes.text.v2-editor
(:require-macros [app.main.style :as stl])
(:require
["penpot/vendor/text-editor-v2" :as editor.v2]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
@ -21,148 +20,143 @@
[app.main.store :as st]
[app.main.ui.css-cursors :as cur]
[app.util.dom :as dom]
[app.util.globals :as global]
[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]))
(def ^:private TextEditor
editor.v2/default)
(defn- initialize-event-handlers
"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
[element options]
(new TextEditor element (obj/clone options)))
selection-node
(mf/ref-val selection-ref)
(defn- set-editor-root!
[instance root]
(set! (.-root ^TextEditor instance) root)
instance)
;; Gets the default font from the workspace refs.
default-font
(deref refs/default-font)
(defn- get-editor-root
[instance]
(.-root ^TextEditor instance))
style-defaults
(styles/get-style-defaults
(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
"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)
::mf/props :obj}
[{:keys [shape]}]
(let [content (:content shape)
shape-id (dm/get-prop shape :id)
;; This is a reference to the dom element that
;; should contain the TextEditor.
text-editor-ref (mf/use-ref nil)
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)
container-ref (mf/use-ref nil)
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 (get-editor-root 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 (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))))))
;; WARN: we explicitly do not pass content on effect dependency
;; array because we only need to initialize this once with initial
;; content
(mf/with-effect [shape-id]
(initialize-event-handlers shape-id
content
selection-ref
editor-ref
container-ref))
[:div
{:class (dm/str (cur/get-dynamic "text" (:rotation shape))
" "
(stl/css :text-editor-container))
:ref text-editor-container-ref
:ref 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)}}
;; 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}]
:ref selection-ref}]
[:div
{:class (dm/str
"mousetrap "
@ -174,7 +168,7 @@
:align-top (= (:vertical-align content "top") "top")
:align-center (= (:vertical-align content) "center")
:align-bottom (= (:vertical-align content) "bottom")))
:ref text-editor-ref
:ref editor-ref
:data-testid "text-editor-content"
:data-x (dm/get-prop shape :x)
:data-y (dm/get-prop shape :y)
@ -198,7 +192,7 @@
(mf/defc text-editor
"Text editor wrapper component"
{::mf/wrap [mf/memo]
::mf/wrap-props false
::mf/props :obj
::mf/forward-ref true}
[{:keys [shape modifiers] :as props} _]
(let [shape-id (dm/get-prop shape :id)
@ -272,4 +266,3 @@
[:div {:style style}
[:& text-editor-html {:shape shape
:key (dm/str shape-id)}]]]]))

View file

@ -92,7 +92,6 @@
"</style>")]
(.insertAdjacentHTML ^js node "beforeend" style)))
(defn get-element-by-class
([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