From 26467187c48687e4eae1f01dab62e5b577838587 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 14 Jul 2021 14:45:03 +0200 Subject: [PATCH] :sparkles: Fix text editor issues --- .../src/app/main/data/workspace/texts.cljs | 10 ++ .../main/ui/workspace/shapes/text/editor.cljs | 67 +++++++-- .../workspace/sidebar/options/menus/text.cljs | 5 +- frontend/src/app/util/text_editor.cljs | 36 ++++- frontend/src/app/util/text_editor_impl.js | 127 ++++++++++++++++-- 5 files changed, 218 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 289800da8..11dad4b70 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -316,3 +316,13 @@ (rx/race resize-batch change-page) (rx/of #(dissoc % ::handling-texts)))) (rx/empty)))))) + +(defn save-font + [data] + (ptk/reify ::save-font + ptk/UpdateEvent + (update [_ state] + ;; Check if the data has any multiple + (assoc-in state + [:workspace-local :defaults :font] + data)))) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 0ba3a332f..26617eb3e 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] + [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.text :as txt] [app.main.data.workspace :as dw] @@ -32,7 +33,7 @@ (let [bprops (obj/get props "blockProps") data (obj/get bprops "data") style (sts/generate-paragraph-styles (obj/get bprops "shape") - (obj/get bprops "data")) + (obj/get bprops "data")) dir (:text-direction data "auto")] @@ -56,12 +57,35 @@ :shape shape}} nil))) +(defn styles-fn [styles content] + (if (= (.getText content) "") + (-> (.getData content) + (.toJS) + (js->clj :keywordize-keys true) + (sts/generate-text-styles)) + (-> (txt/styles-to-attrs styles) + (sts/generate-text-styles)))) + (def default-decorator (ted/create-decorator "PENPOT_SELECTION" selection-component)) (def empty-editor-state (ted/create-editor-state nil default-decorator)) +(defn get-content-changes + [old-state state] + (let [old-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js old-state))) + :keywordize-keys false) + new-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js state))) + :keywordize-keys false)] + (->> old-blocks + (d/mapm + (fn [bkey bstate] + {:old (get bstate "text") + :new (get-in new-blocks [bkey "text"])})) + (filter #(contains? new-blocks (first %))) + (into {})))) + (mf/defc text-shape-edit-html {::mf/wrap [mf/memo] ::mf/wrap-props false @@ -106,13 +130,38 @@ (fn [_] (reset! blured false))) + prev-value (mf/use-ref state) + + ;; Effect that keeps updated the `prev-value` reference + _ (mf/use-effect + (mf/deps state) + #(mf/set-ref-val! prev-value state)) + + handle-change + (mf/use-callback + (fn [state] + (let [old-state (mf/ref-val prev-value)] + (if (and (some? state) (some? old-state)) + (let [block-states (get-content-changes old-state state) + + block-to-add-styles + (->> block-states + (filter + (fn [[_ v]] + (and (not= (:old v) (:new v)) + (= (:old v) "")))) + (mapv first))] + (ted/apply-block-styles-to-content state block-to-add-styles)) + state)))) + on-change (mf/use-callback (fn [val] - (let [val (if (true? @blured) - (ted/add-editor-blur-selection val) - (ted/remove-editor-blur-selection val))] - (st/emit! (dwt/update-editor-state shape val))))) + (let [val (handle-change val)] + (let [val (if (true? @blured) + (ted/add-editor-blur-selection val) + (ted/remove-editor-blur-selection val))] + (st/emit! (dwt/update-editor-state shape val)))))) on-editor (mf/use-callback @@ -124,7 +173,9 @@ handle-return (mf/use-callback (fn [_ state] - (st/emit! (dwt/update-editor-state shape (ted/editor-split-block state))) + (let [state (ted/editor-split-block state) + state (handle-change state)] + (st/emit! (dwt/update-editor-state shape state))) "handled")) on-click @@ -152,9 +203,7 @@ :on-focus on-focus :handle-return handle-return :strip-pasted-styles true - :custom-style-fn (fn [styles _] - (-> (txt/styles-to-attrs styles) - (sts/generate-text-styles))) + :custom-style-fn styles-fn :block-renderer-fn #(render-block % shape) :ref on-editor :editor-state state}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index d9f9ec455..6bd83ade7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -220,7 +220,10 @@ emit-update! (mf/use-callback + (mf/deps values) (fn [id attrs] + (st/emit! (dwt/save-font (merge values attrs))) + (let [attrs (select-keys attrs root-attrs)] (when-not (empty? attrs) (st/emit! (dwt/update-root-attrs {:id id :attrs attrs})))) @@ -235,7 +238,7 @@ on-change (mf/use-callback - (mf/deps ids) + (mf/deps ids emit-update!) (fn [attrs] (run! #(emit-update! % attrs) ids))) diff --git a/frontend/src/app/util/text_editor.cljs b/frontend/src/app/util/text_editor.cljs index c5336cf68..790609f6f 100644 --- a/frontend/src/app/util/text_editor.cljs +++ b/frontend/src/app/util/text_editor.cljs @@ -70,8 +70,11 @@ (defn get-editor-current-inline-styles [state] - (-> (.getCurrentInlineStyle ^js state) - (txt/styles-to-attrs))) + (if (impl/isCurrentEmpty state) + (let [block (impl/getCurrentBlock state)] + (get-editor-block-data block)) + (-> (.getCurrentInlineStyle ^js state) + (txt/styles-to-attrs)))) (defn update-editor-current-block-data [state attrs] @@ -79,7 +82,18 @@ (defn update-editor-current-inline-styles [state attrs] - (impl/applyInlineStyle state (txt/attrs-to-styles attrs))) + (let [update-blocks + (fn [state block-key] + (if (empty? (impl/getBlockContent state block-key)) + (impl/updateBlockData state block-key (clj->js attrs)) + + (let [attrs (-> (impl/getInlineStyle state block-key 0) + (txt/styles-to-attrs))] + (impl/updateBlockData state block-key (clj->js attrs))))) + + state (impl/applyInlineStyle state (txt/attrs-to-styles attrs)) + selected (impl/getSelectedBlocks state)] + (reduce update-blocks state selected))) (defn editor-split-block [state] @@ -96,3 +110,19 @@ (defn cursor-to-end [state] (impl/cursorToEnd state)) + +(defn apply-block-styles-to-content + [state blocks] + (if (empty? blocks) + state + (let [selection (impl/getSelection state) + redfn + (fn [state bkey] + (let [attrs (-> (impl/getBlockData state bkey) + (js->clj :keywordize-keys true))] + (-> state + (impl/selectBlock bkey) + (impl/applyInlineStyle (txt/attrs-to-styles attrs)))))] + (as-> state $ + (reduce redfn $ blocks) + (impl/setSelection $ selection))))) diff --git a/frontend/src/app/util/text_editor_impl.js b/frontend/src/app/util/text_editor_impl.js index c966f0527..6eb1983cb 100644 --- a/frontend/src/app/util/text_editor_impl.js +++ b/frontend/src/app/util/text_editor_impl.js @@ -22,6 +22,23 @@ function isDefined(v) { return v !== undefined && v !== null; } +function mergeBlockData(block, newData) { + let data = block.getData(); + + for (let key of Object.keys(newData)) { + const oldVal = data.get(key); + if (oldVal === newData[key]) { + data = data.delete(key); + } else { + data = data.set(key, newData[key]); + } + } + + return block.merge({ + data: data + }); +} + export function createEditorState(content, decorator) { if (content === null) { return EditorState.createEmpty(decorator); @@ -95,26 +112,19 @@ export function updateCurrentBlockData(state, attrs) { let content = state.getCurrentContent(); content = modifySelectedBlocks(content, selection, (block) => { - let data = block.getData(); - for (let key of Object.keys(attrs)) { - const oldVal = data.get(key); - if (oldVal === attrs[key]) { - data = data.delete(key); - } else { - data = data.set(key, attrs[key]); - } - } - - return block.merge({ - data: data - }); + return mergeBlockData(block, attrs); }); return EditorState.push(state, content, "change-block-data"); } export function applyInlineStyle(state, styles) { - const selection = state.getSelection(); + let selection = state.getSelection(); + + if (selection.isCollapsed()) { + selection = selection.set("anchorOffset", 0); + } + let content = null; for (let style of styles) { @@ -234,3 +244,92 @@ export function cursorToEnd(state) { return state; } + +export function isCurrentEmpty(state) { + const selection = state.getSelection(); + + if (!selection.isCollapsed()) { + return false; + } + + const blockKey = selection.getStartKey(); + const content = state.getCurrentContent(); + + const block = content.getBlockForKey(blockKey); + + return block.getText() === ""; +} + +/* + Returns the block keys between a selection +*/ +export function getSelectedBlocks(state) { + const selection = state.getSelection(); + const startKey = selection.getStartKey(); + const endKey = selection.getEndKey(); + const content = state.getCurrentContent(); + const result = [ startKey ]; + + let currentKey = startKey; + + while (currentKey !== endKey) { + const currentBlock = content.getBlockAfter(currentKey); + currentKey = currentBlock.getKey(); + result.push(currentKey); + } + + return result; +} + +export function getBlockContent(state, blockKey) { + const content = state.getCurrentContent(); + const block = content.getBlockForKey(blockKey); + return block.getText(); +} + +export function getBlockData(state, blockKey) { + const content = state.getCurrentContent(); + const block = content.getBlockForKey(blockKey); + return block && block.getData().toJS(); +} + +export function updateBlockData(state, blockKey, data) { + const userSelection = state.getSelection(); + const content = state.getCurrentContent(); + const block = content.getBlockForKey(blockKey); + const newBlock = mergeBlockData(block, data); + + const blockData = newBlock.getData(); + + const newContent = Modifier.setBlockData( + state.getCurrentContent(), + SelectionState.createEmpty(blockKey), + blockData + ); + + const result = EditorState.push(state, newContent, 'change-block-data'); + return EditorState.acceptSelection(result, userSelection) +} + +export function getSelection(state) { + return state.getSelection(); +} + +export function setSelection(state, selection) { + return EditorState.acceptSelection(state, selection); +} + +export function selectBlock(state, blockKey) { + const block = state.getCurrentContent().getBlockForKey(blockKey); + const length = block.getText().length; + const selection = SelectionState.createEmpty(blockKey).merge({ + focusOffset: length + }); + return EditorState.acceptSelection(state, selection); +} + +export function getInlineStyle(state, blockKey, offset) { + const content = state.getCurrentContent(); + const block = content.getBlockForKey(blockKey); + return block.getInlineStyleAt(offset).toJS(); +}