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:
parent
352efcb610
commit
ffadf29ad7
5 changed files with 175 additions and 129 deletions
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)}]]]]))
|
||||
|
||||
|
|
|
@ -92,7 +92,6 @@
|
|||
"</style>")]
|
||||
(.insertAdjacentHTML ^js node "beforeend" style)))
|
||||
|
||||
|
||||
(defn get-element-by-class
|
||||
([classname]
|
||||
(dom/getElementByClass classname))
|
||||
|
|
46
frontend/vendor/text_editor_v2.js
vendored
46
frontend/vendor/text_editor_v2.js
vendored
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue