mirror of
https://github.com/penpot/penpot.git
synced 2025-05-19 04:06:14 +02:00
🐛 Proper handle visual selection on blured editor.
This commit is contained in:
parent
5519cdfd7c
commit
f0087e11b0
5 changed files with 202 additions and 52 deletions
|
@ -55,14 +55,15 @@
|
||||||
(update state :workspace-editor-state dissoc id)))))
|
(update state :workspace-editor-state dissoc id)))))
|
||||||
|
|
||||||
(defn initialize-editor-state
|
(defn initialize-editor-state
|
||||||
[{:keys [id content] :as shape}]
|
[{:keys [id content] :as shape} decorator]
|
||||||
(ptk/reify ::initialize-editor-state
|
(ptk/reify ::initialize-editor-state
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update-in state [:workspace-editor-state id]
|
(update-in state [:workspace-editor-state id]
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(ted/create-editor-state
|
(ted/create-editor-state
|
||||||
(some->> content ted/import-content)))))))
|
(some->> content ted/import-content)
|
||||||
|
decorator))))))
|
||||||
|
|
||||||
(defn finalize-editor-state
|
(defn finalize-editor-state
|
||||||
[{:keys [id] :as shape}]
|
[{:keys [id] :as shape}]
|
||||||
|
@ -136,8 +137,7 @@
|
||||||
shape-ids (cond (= (:type shape) :text) [id]
|
shape-ids (cond (= (:type shape) :text) [id]
|
||||||
(= (:type shape) :group) (cp/get-children id objects))]
|
(= (:type shape) :group) (cp/get-children id objects))]
|
||||||
|
|
||||||
(rx/of (dwc/update-shapes shape-ids update-fn)
|
(rx/of (dwc/update-shapes shape-ids update-fn))))))
|
||||||
(focus-editor))))))
|
|
||||||
|
|
||||||
(defn update-paragraph-attrs
|
(defn update-paragraph-attrs
|
||||||
[{:keys [id attrs]}]
|
[{:keys [id attrs]}]
|
||||||
|
@ -149,11 +149,7 @@
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(cond
|
(when-not (some? (get-in state [:workspace-editor-state id]))
|
||||||
(some? (get-in state [:workspace-editor-state id]))
|
|
||||||
(rx/of (focus-editor))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(let [objects (dwc/lookup-page-objects state)
|
(let [objects (dwc/lookup-page-objects state)
|
||||||
shape (get objects id)
|
shape (get objects id)
|
||||||
|
|
||||||
|
@ -173,11 +169,7 @@
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(cond
|
(when-not (some? (get-in state [:workspace-editor-state id]))
|
||||||
(some? (get-in state [:workspace-editor-state id]))
|
|
||||||
(rx/of (focus-editor))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(let [objects (dwc/lookup-page-objects state)
|
(let [objects (dwc/lookup-page-objects state)
|
||||||
shape (get objects id)
|
shape (get objects id)
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,12 @@
|
||||||
[:div {:style style :dir "auto"}
|
[:div {:style style :dir "auto"}
|
||||||
[:> draft/EditorBlock props]]))
|
[:> 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
|
(defn render-block
|
||||||
[block shape]
|
[block shape]
|
||||||
(let [type (ted/get-editor-block-type block)]
|
(let [type (ted/get-editor-block-type block)]
|
||||||
|
@ -66,8 +72,11 @@
|
||||||
:shape shape}}
|
:shape shape}}
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
|
(def default-decorator
|
||||||
|
(ted/create-decorator "PENPOT_SELECTION" selection-component))
|
||||||
|
|
||||||
(def empty-editor-state
|
(def empty-editor-state
|
||||||
(ted/create-editor-state))
|
(ted/create-editor-state nil default-decorator))
|
||||||
|
|
||||||
(mf/defc text-shape-edit-html
|
(mf/defc text-shape-edit-html
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]
|
||||||
|
@ -79,9 +88,10 @@
|
||||||
zoom (mf/deref refs/selected-zoom)
|
zoom (mf/deref refs/selected-zoom)
|
||||||
state-map (mf/deref refs/workspace-editor-state)
|
state-map (mf/deref refs/workspace-editor-state)
|
||||||
state (get state-map id empty-editor-state)
|
state (get state-map id empty-editor-state)
|
||||||
|
|
||||||
self-ref (mf/use-ref)
|
self-ref (mf/use-ref)
|
||||||
|
|
||||||
|
blured (mf/use-var false)
|
||||||
|
|
||||||
on-click-outside
|
on-click-outside
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
|
@ -111,7 +121,7 @@
|
||||||
(let [keys [(events/listen js/document EventType.MOUSEDOWN on-click-outside)
|
(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.CLICK on-click-outside)
|
||||||
(events/listen js/document EventType.KEYUP on-key-up)]]
|
(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))
|
(dwt/select-all shape))
|
||||||
#(do
|
#(do
|
||||||
(st/emit! (dwt/finalize-editor-state shape))
|
(st/emit! (dwt/finalize-editor-state shape))
|
||||||
|
@ -119,14 +129,26 @@
|
||||||
(events/unlistenByKey key)))))
|
(events/unlistenByKey key)))))
|
||||||
|
|
||||||
on-blur
|
on-blur
|
||||||
(fn [event]
|
(mf/use-callback
|
||||||
(dom/stop-propagation event)
|
(mf/deps shape state)
|
||||||
(dom/prevent-default event))
|
(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
|
on-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [val]
|
(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
|
on-editor
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
@ -140,17 +162,6 @@
|
||||||
(fn [event state]
|
(fn [event state]
|
||||||
(st/emit! (dwt/update-editor-state shape (ted/editor-split-block state)))
|
(st/emit! (dwt/update-editor-state shape (ted/editor-split-block state)))
|
||||||
"handled"))
|
"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)
|
(mf/use-layout-effect on-mount)
|
||||||
|
@ -158,7 +169,6 @@
|
||||||
[:div.text-editor
|
[:div.text-editor
|
||||||
{:ref self-ref
|
{:ref self-ref
|
||||||
:style {:cursor cur/text}
|
:style {:cursor cur/text}
|
||||||
:on-pointer-down on-pointer-down
|
|
||||||
:class (dom/classnames
|
:class (dom/classnames
|
||||||
: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")
|
||||||
|
@ -166,6 +176,7 @@
|
||||||
[:> draft/Editor
|
[:> draft/Editor
|
||||||
{:on-change on-change
|
{:on-change on-change
|
||||||
:on-blur on-blur
|
:on-blur on-blur
|
||||||
|
:on-focus on-focus
|
||||||
:handle-return handle-return
|
:handle-return handle-return
|
||||||
:strip-pasted-styles true
|
:strip-pasted-styles true
|
||||||
:custom-style-fn (fn [styles _]
|
:custom-style-fn (fn [styles _]
|
||||||
|
|
|
@ -434,11 +434,16 @@
|
||||||
on-pointer-down
|
on-pointer-down
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [event]
|
(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
|
;; Capture mouse pointer to detect the movements even if cursor
|
||||||
;; leaves the viewport or the browser itself
|
;; leaves the viewport or the browser itself
|
||||||
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
|
;; 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
|
on-pointer-up
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
|
57
frontend/src/app/util/draft_helpers.js
Normal file
57
frontend/src/app/util/draft_helpers.js
Normal file
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
|
@ -11,6 +11,7 @@
|
||||||
"Draft related abstraction functions."
|
"Draft related abstraction functions."
|
||||||
(:require
|
(:require
|
||||||
["draft-js" :as draft]
|
["draft-js" :as draft]
|
||||||
|
["./draft_helpers.js" :as helpers]
|
||||||
[app.common.attrs :as attrs]
|
[app.common.attrs :as attrs]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
@ -49,6 +50,16 @@
|
||||||
v (encode-style-value val)]
|
v (encode-style-value val)]
|
||||||
(str "PENPOT$$$" k "$$$" v)))
|
(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
|
(defn attrs-to-styles
|
||||||
[attrs]
|
[attrs]
|
||||||
(reduce-kv (fn [res k v]
|
(reduce-kv (fn [res k v]
|
||||||
|
@ -60,8 +71,12 @@
|
||||||
[styles]
|
[styles]
|
||||||
(persistent!
|
(persistent!
|
||||||
(reduce (fn [result style]
|
(reduce (fn [result style]
|
||||||
(let [[_ k v] (str/split style "$$$" 3)]
|
(if (str/starts-with? style "PENPOT")
|
||||||
(assoc! result (keyword k) (decode-style-value v))))
|
(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 {})
|
(transient {})
|
||||||
(seq styles))))
|
(seq styles))))
|
||||||
|
|
||||||
|
@ -71,14 +86,15 @@
|
||||||
"Parses draft-js style ranges, converting encoded style name into a
|
"Parses draft-js style ranges, converting encoded style name into a
|
||||||
key/val pair of data."
|
key/val pair of data."
|
||||||
[styles]
|
[styles]
|
||||||
(map (fn [item]
|
(->> styles
|
||||||
(let [[_ k v] (-> (obj/get item "style")
|
(filter #(str/starts-with? (obj/get % "style") "PENPOT$$$"))
|
||||||
(str/split "$$$" 3))]
|
(map (fn [item]
|
||||||
{:key (keyword k)
|
(let [[_ k v] (-> (obj/get item "style")
|
||||||
:val (decode-style-value v)
|
(str/split "$$$" 3))]
|
||||||
:offset (obj/get item "offset")
|
{:key (keyword k)
|
||||||
:length (obj/get item "length")}))
|
:val (decode-style-value v)
|
||||||
styles))
|
:offset (obj/get item "offset")
|
||||||
|
:length (obj/get item "length")})))))
|
||||||
|
|
||||||
(defn- build-style-index
|
(defn- build-style-index
|
||||||
"Generates a character based index with associated styles map."
|
"Generates a character based index with associated styles map."
|
||||||
|
@ -123,7 +139,6 @@
|
||||||
(assoc :key key)
|
(assoc :key key)
|
||||||
(assoc :type "paragraph")
|
(assoc :type "paragraph")
|
||||||
(assoc :children (split-texts text styles)))))]
|
(assoc :children (split-texts text styles)))))]
|
||||||
|
|
||||||
{:type "root"
|
{:type "root"
|
||||||
:children
|
:children
|
||||||
[{:type "paragraph-set"
|
[{:type "paragraph-set"
|
||||||
|
@ -193,9 +208,25 @@
|
||||||
([]
|
([]
|
||||||
(.createEmpty ^js draft/EditorState))
|
(.createEmpty ^js draft/EditorState))
|
||||||
([content]
|
([content]
|
||||||
|
(.createWithContent ^js draft/EditorState content))
|
||||||
|
([content decorator]
|
||||||
(if (some? content)
|
(if (some? content)
|
||||||
(.createWithContent ^js draft/EditorState content)
|
(.createWithContent ^js draft/EditorState content decorator)
|
||||||
(.createEmpty ^js draft/EditorState))))
|
(.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
|
(defn import-content
|
||||||
[content]
|
[content]
|
||||||
|
@ -276,17 +307,33 @@
|
||||||
(.mergeBlockData ^js draft/Modifier content target (clj->js attrs))
|
(.mergeBlockData ^js draft/Modifier content target (clj->js attrs))
|
||||||
"change-block-data"))))
|
"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
|
(defn update-editor-current-inline-styles
|
||||||
[state attrs]
|
[state attrs]
|
||||||
(let [selection (.getSelection ^js state)
|
(let [selection (.getSelection ^js state)
|
||||||
content (.getCurrentContent ^js state)
|
|
||||||
styles (attrs-to-styles attrs)]
|
styles (attrs-to-styles attrs)]
|
||||||
(reduce (fn [state style]
|
(reduce (fn [state style]
|
||||||
(let [modifier (.applyInlineStyle draft/Modifier
|
(let [[sk sv] (decode-style style)
|
||||||
(.getCurrentContent ^js state)
|
prefix (encode-style-prefix sk)
|
||||||
|
|
||||||
|
content (.getCurrentContent ^js state)
|
||||||
|
content (helpers/removeInlineStylePrefix content
|
||||||
|
selection
|
||||||
|
prefix)
|
||||||
|
|
||||||
|
content (.applyInlineStyle ^js draft/Modifier
|
||||||
|
content
|
||||||
selection
|
selection
|
||||||
style)]
|
style)]
|
||||||
(.push draft/EditorState state modifier "change-inline-style")))
|
(.push ^js draft/EditorState state content "change-inline-style")))
|
||||||
state
|
state
|
||||||
styles)))
|
styles)))
|
||||||
|
|
||||||
|
@ -299,3 +346,41 @@
|
||||||
block-key (.. ^js content -selectionAfter getStartKey)
|
block-key (.. ^js content -selectionAfter getStartKey)
|
||||||
block-map (.. ^js content -blockMap (update block-key (fn [block] (.set ^js block "data" block-data))))]
|
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")))
|
(.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))))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue