mirror of
https://github.com/penpot/penpot.git
synced 2025-07-30 02:57:21 +02:00
✨ Fix text editor issues
This commit is contained in:
parent
69e256ab86
commit
26467187c4
5 changed files with 218 additions and 27 deletions
|
@ -316,3 +316,13 @@
|
||||||
(rx/race resize-batch change-page)
|
(rx/race resize-batch change-page)
|
||||||
(rx/of #(dissoc % ::handling-texts))))
|
(rx/of #(dissoc % ::handling-texts))))
|
||||||
(rx/empty))))))
|
(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))))
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.ui.workspace.shapes.text.editor
|
(ns app.main.ui.workspace.shapes.text.editor
|
||||||
(:require
|
(:require
|
||||||
["draft-js" :as draft]
|
["draft-js" :as draft]
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
|
@ -56,12 +57,35 @@
|
||||||
:shape shape}}
|
:shape shape}}
|
||||||
nil)))
|
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
|
(def default-decorator
|
||||||
(ted/create-decorator "PENPOT_SELECTION" selection-component))
|
(ted/create-decorator "PENPOT_SELECTION" selection-component))
|
||||||
|
|
||||||
(def empty-editor-state
|
(def empty-editor-state
|
||||||
(ted/create-editor-state nil default-decorator))
|
(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/defc text-shape-edit-html
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]
|
||||||
::mf/wrap-props false
|
::mf/wrap-props false
|
||||||
|
@ -106,13 +130,38 @@
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(reset! blured false)))
|
(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
|
on-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [val]
|
(fn [val]
|
||||||
|
(let [val (handle-change val)]
|
||||||
(let [val (if (true? @blured)
|
(let [val (if (true? @blured)
|
||||||
(ted/add-editor-blur-selection val)
|
(ted/add-editor-blur-selection val)
|
||||||
(ted/remove-editor-blur-selection val))]
|
(ted/remove-editor-blur-selection val))]
|
||||||
(st/emit! (dwt/update-editor-state shape val)))))
|
(st/emit! (dwt/update-editor-state shape val))))))
|
||||||
|
|
||||||
on-editor
|
on-editor
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
@ -124,7 +173,9 @@
|
||||||
handle-return
|
handle-return
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [_ state]
|
(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"))
|
"handled"))
|
||||||
|
|
||||||
on-click
|
on-click
|
||||||
|
@ -152,9 +203,7 @@
|
||||||
:on-focus on-focus
|
: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 styles-fn
|
||||||
(-> (txt/styles-to-attrs styles)
|
|
||||||
(sts/generate-text-styles)))
|
|
||||||
:block-renderer-fn #(render-block % shape)
|
:block-renderer-fn #(render-block % shape)
|
||||||
:ref on-editor
|
:ref on-editor
|
||||||
:editor-state state}]]))
|
:editor-state state}]]))
|
||||||
|
|
|
@ -220,7 +220,10 @@
|
||||||
|
|
||||||
emit-update!
|
emit-update!
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
(mf/deps values)
|
||||||
(fn [id attrs]
|
(fn [id attrs]
|
||||||
|
(st/emit! (dwt/save-font (merge values attrs)))
|
||||||
|
|
||||||
(let [attrs (select-keys attrs root-attrs)]
|
(let [attrs (select-keys attrs root-attrs)]
|
||||||
(when-not (empty? attrs)
|
(when-not (empty? attrs)
|
||||||
(st/emit! (dwt/update-root-attrs {:id id :attrs attrs}))))
|
(st/emit! (dwt/update-root-attrs {:id id :attrs attrs}))))
|
||||||
|
@ -235,7 +238,7 @@
|
||||||
|
|
||||||
on-change
|
on-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps ids)
|
(mf/deps ids emit-update!)
|
||||||
(fn [attrs]
|
(fn [attrs]
|
||||||
(run! #(emit-update! % attrs) ids)))
|
(run! #(emit-update! % attrs) ids)))
|
||||||
|
|
||||||
|
|
|
@ -70,8 +70,11 @@
|
||||||
|
|
||||||
(defn get-editor-current-inline-styles
|
(defn get-editor-current-inline-styles
|
||||||
[state]
|
[state]
|
||||||
|
(if (impl/isCurrentEmpty state)
|
||||||
|
(let [block (impl/getCurrentBlock state)]
|
||||||
|
(get-editor-block-data block))
|
||||||
(-> (.getCurrentInlineStyle ^js state)
|
(-> (.getCurrentInlineStyle ^js state)
|
||||||
(txt/styles-to-attrs)))
|
(txt/styles-to-attrs))))
|
||||||
|
|
||||||
(defn update-editor-current-block-data
|
(defn update-editor-current-block-data
|
||||||
[state attrs]
|
[state attrs]
|
||||||
|
@ -79,7 +82,18 @@
|
||||||
|
|
||||||
(defn update-editor-current-inline-styles
|
(defn update-editor-current-inline-styles
|
||||||
[state attrs]
|
[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
|
(defn editor-split-block
|
||||||
[state]
|
[state]
|
||||||
|
@ -96,3 +110,19 @@
|
||||||
(defn cursor-to-end
|
(defn cursor-to-end
|
||||||
[state]
|
[state]
|
||||||
(impl/cursorToEnd 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)))))
|
||||||
|
|
|
@ -22,6 +22,23 @@ function isDefined(v) {
|
||||||
return v !== undefined && v !== null;
|
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) {
|
export function createEditorState(content, decorator) {
|
||||||
if (content === null) {
|
if (content === null) {
|
||||||
return EditorState.createEmpty(decorator);
|
return EditorState.createEmpty(decorator);
|
||||||
|
@ -95,26 +112,19 @@ export function updateCurrentBlockData(state, attrs) {
|
||||||
let content = state.getCurrentContent();
|
let content = state.getCurrentContent();
|
||||||
|
|
||||||
content = modifySelectedBlocks(content, selection, (block) => {
|
content = modifySelectedBlocks(content, selection, (block) => {
|
||||||
let data = block.getData();
|
return mergeBlockData(block, attrs);
|
||||||
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 EditorState.push(state, content, "change-block-data");
|
return EditorState.push(state, content, "change-block-data");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyInlineStyle(state, styles) {
|
export function applyInlineStyle(state, styles) {
|
||||||
const selection = state.getSelection();
|
let selection = state.getSelection();
|
||||||
|
|
||||||
|
if (selection.isCollapsed()) {
|
||||||
|
selection = selection.set("anchorOffset", 0);
|
||||||
|
}
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
|
|
||||||
for (let style of styles) {
|
for (let style of styles) {
|
||||||
|
@ -234,3 +244,92 @@ export function cursorToEnd(state) {
|
||||||
|
|
||||||
return 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();
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue