From f0087e11b05bf572df62b1fd0510371100487001 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Mar 2021 11:31:28 +0100 Subject: [PATCH] :bug: Proper handle visual selection on blured editor. --- .../src/app/main/data/workspace/texts.cljs | 20 +-- .../main/ui/workspace/shapes/text/editor.cljs | 49 +++++--- .../src/app/main/ui/workspace/viewport.cljs | 9 +- frontend/src/app/util/draft_helpers.js | 57 +++++++++ frontend/src/app/util/text_editor.cljs | 119 +++++++++++++++--- 5 files changed, 202 insertions(+), 52 deletions(-) create mode 100644 frontend/src/app/util/draft_helpers.js diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 94eb3076f..96556aa0e 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -55,14 +55,15 @@ (update state :workspace-editor-state dissoc id))))) (defn initialize-editor-state - [{:keys [id content] :as shape}] + [{:keys [id content] :as shape} decorator] (ptk/reify ::initialize-editor-state ptk/UpdateEvent (update [_ state] (update-in state [:workspace-editor-state id] (fn [_] (ted/create-editor-state - (some->> content ted/import-content))))))) + (some->> content ted/import-content) + decorator)))))) (defn finalize-editor-state [{:keys [id] :as shape}] @@ -136,8 +137,7 @@ shape-ids (cond (= (:type shape) :text) [id] (= (:type shape) :group) (cp/get-children id objects))] - (rx/of (dwc/update-shapes shape-ids update-fn) - (focus-editor)))))) + (rx/of (dwc/update-shapes shape-ids update-fn)))))) (defn update-paragraph-attrs [{:keys [id attrs]}] @@ -149,11 +149,7 @@ ptk/WatchEvent (watch [_ state stream] - (cond - (some? (get-in state [:workspace-editor-state id])) - (rx/of (focus-editor)) - - :else + (when-not (some? (get-in state [:workspace-editor-state id])) (let [objects (dwc/lookup-page-objects state) shape (get objects id) @@ -173,11 +169,7 @@ ptk/WatchEvent (watch [_ state stream] - (cond - (some? (get-in state [:workspace-editor-state id])) - (rx/of (focus-editor)) - - :else + (when-not (some? (get-in state [:workspace-editor-state id])) (let [objects (dwc/lookup-page-objects state) shape (get objects id) 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 47577da8c..e10a1ced5 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -55,6 +55,12 @@ [:div {:style style :dir "auto"} [:> draft/EditorBlock props]])) +(mf/defc selection-component + {::mf/wrap-props false} + [props] + (let [children (obj/get props "children")] + [:span {:style {:background "#ccc" :display "inline-block"}} children])) + (defn render-block [block shape] (let [type (ted/get-editor-block-type block)] @@ -66,8 +72,11 @@ :shape shape}} nil))) +(def default-decorator + (ted/create-decorator "PENPOT_SELECTION" selection-component)) + (def empty-editor-state - (ted/create-editor-state)) + (ted/create-editor-state nil default-decorator)) (mf/defc text-shape-edit-html {::mf/wrap [mf/memo] @@ -79,9 +88,10 @@ zoom (mf/deref refs/selected-zoom) state-map (mf/deref refs/workspace-editor-state) state (get state-map id empty-editor-state) - self-ref (mf/use-ref) + blured (mf/use-var false) + on-click-outside (fn [event] (let [target (dom/get-target event) @@ -111,7 +121,7 @@ (let [keys [(events/listen js/document EventType.MOUSEDOWN on-click-outside) (events/listen js/document EventType.CLICK on-click-outside) (events/listen js/document EventType.KEYUP on-key-up)]] - (st/emit! (dwt/initialize-editor-state shape) + (st/emit! (dwt/initialize-editor-state shape default-decorator) (dwt/select-all shape)) #(do (st/emit! (dwt/finalize-editor-state shape)) @@ -119,14 +129,26 @@ (events/unlistenByKey key))))) on-blur - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event)) + (mf/use-callback + (mf/deps shape state) + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (reset! blured true))) + + on-focus + (mf/use-callback + (mf/deps shape state) + (fn [event] + (reset! blured false))) on-change (mf/use-callback (fn [val] - (st/emit! (dwt/update-editor-state shape 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 @@ -140,17 +162,6 @@ (fn [event state] (st/emit! (dwt/update-editor-state shape (ted/editor-split-block state))) "handled")) - - on-pointer-down - (mf/use-callback - (fn [event] - (let [target (dom/get-target event) - closest (.closest ^js target "foreignObject")] - ;; Capture mouse pointer to detect the movements even if cursor - ;; leaves the viewport or the browser itself - ;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture - (when closest - (.setPointerCapture closest (.-pointerId event)))))) ] (mf/use-layout-effect on-mount) @@ -158,7 +169,6 @@ [:div.text-editor {:ref self-ref :style {:cursor cur/text} - :on-pointer-down on-pointer-down :class (dom/classnames :align-top (= (:vertical-align content "top") "top") :align-center (= (:vertical-align content) "center") @@ -166,6 +176,7 @@ [:> draft/Editor {:on-change on-change :on-blur on-blur + :on-focus on-focus :handle-return handle-return :strip-pasted-styles true :custom-style-fn (fn [styles _] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 3a3b4de04..bb235e5f0 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -434,11 +434,16 @@ on-pointer-down (mf/use-callback (fn [event] - (let [target (dom/get-target event)] + ;; We need to handle editor related stuff here because + ;; handling on editor dom node does not works properly. + (let [target (dom/get-target event) + editor (.closest ^js target ".public-DraftEditor-content")] ;; Capture mouse pointer to detect the movements even if cursor ;; leaves the viewport or the browser itself ;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture - (.setPointerCapture target (.-pointerId event))))) + (if editor + (.setPointerCapture editor (.-pointerId event)) + (.setPointerCapture target (.-pointerId event)))))) on-pointer-up (mf/use-callback diff --git a/frontend/src/app/util/draft_helpers.js b/frontend/src/app/util/draft_helpers.js new file mode 100644 index 000000000..f75ee0277 --- /dev/null +++ b/frontend/src/app/util/draft_helpers.js @@ -0,0 +1,57 @@ +/** + * Copyright (c) UXBOX Labs SL + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +import {CharacterMetadata} from "draft-js"; +import {Map} from "immutable"; + +function removeStylePrefix(chmeta, stylePrefix) { + var withoutStyle = chmeta.set('style', chmeta.getStyle().filter((s) => !s.startsWith(stylePrefix))) + return CharacterMetadata.create(withoutStyle); +}; + +export function removeInlineStylePrefix(contentState, selectionState, stylePrefix) { + var blockMap = contentState.getBlockMap(); + var startKey = selectionState.getStartKey(); + var startOffset = selectionState.getStartOffset(); + var endKey = selectionState.getEndKey(); + var endOffset = selectionState.getEndOffset(); + var newBlocks = blockMap.skipUntil(function (_, k) { + return k === startKey; + }).takeUntil(function (_, k) { + return k === endKey; + }).concat(Map([[endKey, blockMap.get(endKey)]])).map(function (block, blockKey) { + var sliceStart; + var sliceEnd; + + if (startKey === endKey) { + sliceStart = startOffset; + sliceEnd = endOffset; + } else { + sliceStart = blockKey === startKey ? startOffset : 0; + sliceEnd = blockKey === endKey ? endOffset : block.getLength(); + } + + var chars = block.getCharacterList(); + var current; + + while (sliceStart < sliceEnd) { + current = chars.get(sliceStart); + chars = chars.set(sliceStart, removeStylePrefix(current, stylePrefix)); + sliceStart++; + } + + return block.set('characterList', chars); + }); + + return contentState.merge({ + blockMap: blockMap.merge(newBlocks), + selectionBefore: selectionState, + selectionAfter: selectionState + }); +} diff --git a/frontend/src/app/util/text_editor.cljs b/frontend/src/app/util/text_editor.cljs index 4eabbf4ef..26a3fa8b2 100644 --- a/frontend/src/app/util/text_editor.cljs +++ b/frontend/src/app/util/text_editor.cljs @@ -11,6 +11,7 @@ "Draft related abstraction functions." (:require ["draft-js" :as draft] + ["./draft_helpers.js" :as helpers] [app.common.attrs :as attrs] [app.common.text :as txt] [app.common.data :as d] @@ -49,6 +50,16 @@ v (encode-style-value val)] (str "PENPOT$$$" k "$$$" v))) +(defn encode-style-prefix + [key] + (let [k (d/name key)] + (str "PENPOT$$$" k "$$$"))) + +(defn decode-style + [style] + (let [[_ k v] (str/split style "$$$" 3)] + [(keyword k) (decode-style-value v)])) + (defn attrs-to-styles [attrs] (reduce-kv (fn [res k v] @@ -60,8 +71,12 @@ [styles] (persistent! (reduce (fn [result style] - (let [[_ k v] (str/split style "$$$" 3)] - (assoc! result (keyword k) (decode-style-value v)))) + (if (str/starts-with? style "PENPOT") + (if (= style "PENPOT_SELECTION") + (assoc! result :penpot-selection true) + (let [[_ k v] (str/split style "$$$" 3)] + (assoc! result (keyword k) (decode-style-value v)))) + result)) (transient {}) (seq styles)))) @@ -71,14 +86,15 @@ "Parses draft-js style ranges, converting encoded style name into a key/val pair of data." [styles] - (map (fn [item] - (let [[_ k v] (-> (obj/get item "style") - (str/split "$$$" 3))] - {:key (keyword k) - :val (decode-style-value v) - :offset (obj/get item "offset") - :length (obj/get item "length")})) - styles)) + (->> styles + (filter #(str/starts-with? (obj/get % "style") "PENPOT$$$")) + (map (fn [item] + (let [[_ k v] (-> (obj/get item "style") + (str/split "$$$" 3))] + {:key (keyword k) + :val (decode-style-value v) + :offset (obj/get item "offset") + :length (obj/get item "length")}))))) (defn- build-style-index "Generates a character based index with associated styles map." @@ -123,7 +139,6 @@ (assoc :key key) (assoc :type "paragraph") (assoc :children (split-texts text styles)))))] - {:type "root" :children [{:type "paragraph-set" @@ -193,9 +208,25 @@ ([] (.createEmpty ^js draft/EditorState)) ([content] + (.createWithContent ^js draft/EditorState content)) + ([content decorator] (if (some? content) - (.createWithContent ^js draft/EditorState content) - (.createEmpty ^js draft/EditorState)))) + (.createWithContent ^js draft/EditorState content decorator) + (.createEmpty ^js draft/EditorState decorator)))) + +(defn create-decorator + [type component] + (letfn [(find-entity [block callback content] + (.findEntityRanges ^js block + (fn [cmeta] + (let [ekey (.getEntity ^js cmeta)] + (boolean + (and (some? ekey) + (= type (.. ^js content (getEntity ekey) (getType))))))) + callback))] + (draft/CompositeDecorator. + #js [#js {:strategy find-entity + :component component}]))) (defn import-content [content] @@ -276,17 +307,33 @@ (.mergeBlockData ^js draft/Modifier content target (clj->js attrs)) "change-block-data")))) +(defn get-editor-current-entity-key + [state] + (let [content (.getCurrentContent ^js state) + selection (.getSelection ^js state) + start-key (.getStartKey ^js selection) + start-offset (.getStartOffset ^js selection) + block (.getBlockForKey ^js content start-key)] + (.getEntityAt ^js block start-offset))) + (defn update-editor-current-inline-styles [state attrs] (let [selection (.getSelection ^js state) - content (.getCurrentContent ^js state) styles (attrs-to-styles attrs)] (reduce (fn [state style] - (let [modifier (.applyInlineStyle draft/Modifier - (.getCurrentContent ^js state) + (let [[sk sv] (decode-style style) + prefix (encode-style-prefix sk) + + content (.getCurrentContent ^js state) + content (helpers/removeInlineStylePrefix content + selection + prefix) + + content (.applyInlineStyle ^js draft/Modifier + content selection style)] - (.push draft/EditorState state modifier "change-inline-style"))) + (.push ^js draft/EditorState state content "change-inline-style"))) state styles))) @@ -299,3 +346,41 @@ block-key (.. ^js content -selectionAfter getStartKey) block-map (.. ^js content -blockMap (update block-key (fn [block] (.set ^js block "data" block-data))))] (.push ^js draft/EditorState state (.set ^js content "blockMap" block-map) "split-block"))) + +(defn add-editor-blur-selection + [state] + (let [content (.getCurrentContent ^js state) + selection (.getSelection ^js state) + content (.createEntity ^js content "PENPOT_SELECTION" "MUTABLE") + ekey (.getLastCreatedEntityKey ^js content) + content (.applyEntity draft/Modifier + content + selection + ekey)] + (.push draft/EditorState state content "apply-entity"))) + + +(defn remove-editor-blur-selection + [state] + (let [content (get-editor-current-content state) + fblock (.. ^js content getBlockMap first) + lblock (.. ^js content getBlockMap last) + fbk (.getKey ^js fblock) + lbk (.getKey ^js lblock) + lbl (.getLength ^js lblock) + params #js {:anchorKey fbk + :anchorOffset 0 + :focusKey lbk + :focusOffset lbl} + + prev-selection (.getSelection state) + + selection (draft/SelectionState. params) + content (.applyEntity draft/Modifier + content + selection + nil)] + (as-> state $ + (.push draft/EditorState $ content "apply-entity") + (.forceSelection ^js draft/EditorState $ prev-selection)))) +