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:
parent
352efcb610
commit
ffadf29ad7
5 changed files with 175 additions and 129 deletions
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)}]]]]))
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
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
|
//# sourceMappingURL=TextEditor.js.map
|
||||||
|
|
Loading…
Add table
Reference in a new issue