mirror of
https://github.com/penpot/penpot.git
synced 2026-03-30 00:00:45 +02:00
When pasting an image (with no text content) into the text editor, Draft.js calls handlePastedText with null/empty text. The previous fix guarded splitTextIntoTextBlocks against null, but insertText still attempted to build a fragment from an empty block array, causing Modifier.replaceWithFragment to crash with 'Cannot read properties of undefined (reading getLength)'. Fix insertText to return the original state unchanged when there are no text blocks to insert. Also guard handle-pasted-text in the ClojureScript editor to skip the insert-text call entirely when text is nil or empty. Signed-off-by: Andrey Antukh <niwi@niwi.nz>
419 lines
12 KiB
JavaScript
419 lines
12 KiB
JavaScript
/**
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*
|
|
* Copyright (c) KALEIDOS INC
|
|
*/
|
|
import pkg from "draft-js";
|
|
|
|
export const {
|
|
BlockMapBuilder,
|
|
CharacterMetadata,
|
|
CompositeDecorator,
|
|
EditorState,
|
|
Modifier,
|
|
RichTextEditorUtil,
|
|
SelectionState,
|
|
convertFromRaw,
|
|
convertToRaw,
|
|
EditorBlock,
|
|
Editor
|
|
} = pkg;
|
|
|
|
import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js';
|
|
import {Map, OrderedSet} from "immutable";
|
|
|
|
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.mergeDeep({
|
|
data: data
|
|
});
|
|
}
|
|
|
|
export function createEditorState(content, decorator) {
|
|
if (content === null) {
|
|
return EditorState.createEmpty(decorator);
|
|
} else {
|
|
return EditorState.createWithContent(content, decorator);
|
|
}
|
|
}
|
|
|
|
export function createDecorator(type, component) {
|
|
const strategy = (block, callback, content) => {
|
|
return block.findEntityRanges((cmeta) => {
|
|
const entityKey = cmeta.getEntity();
|
|
return isDefined(entityKey) && (type === content.getEntity(entityKey).getType());
|
|
}, callback);
|
|
};
|
|
|
|
return new CompositeDecorator([
|
|
{"strategy": strategy, "component": component}
|
|
]);
|
|
}
|
|
|
|
function getSelectAllSelection(state) {
|
|
const content = state.getCurrentContent();
|
|
const firstBlock = content.getBlockMap().first();
|
|
const lastBlock = content.getBlockMap().last();
|
|
|
|
return new SelectionState({
|
|
"anchorKey": firstBlock.getKey(),
|
|
"anchorOffset": 0,
|
|
"focusKey": lastBlock.getKey(),
|
|
"focusOffset": lastBlock.getLength()
|
|
});
|
|
}
|
|
|
|
function getCursorInEndPosition(state) {
|
|
const content = state.getCurrentContent();
|
|
const lastBlock = content.getBlockMap().last();
|
|
|
|
return new SelectionState({
|
|
"anchorKey": lastBlock.getKey(),
|
|
"anchorOffset": lastBlock.getLength(),
|
|
"focusKey": lastBlock.getKey(),
|
|
"focusOffset": lastBlock.getLength()
|
|
});
|
|
}
|
|
|
|
export function selectAll(state) {
|
|
return EditorState.forceSelection(state, getSelectAllSelection(state));
|
|
}
|
|
|
|
function modifySelectedBlocks(contentState, selectionState, operation) {
|
|
var startKey = selectionState.getStartKey();
|
|
var endKey = selectionState.getEndKey();
|
|
var blockMap = contentState.getBlockMap();
|
|
|
|
var newBlocks = blockMap.toSeq().skipUntil(function (_, k) {
|
|
return k === startKey;
|
|
}).takeUntil(function (_, k) {
|
|
return k === endKey;
|
|
}).concat(Map([[endKey, blockMap.get(endKey)]])).map(operation);
|
|
|
|
return contentState.merge({
|
|
"blockMap": blockMap.merge(newBlocks),
|
|
"selectionBefore": selectionState,
|
|
"selectionAfter": selectionState
|
|
});
|
|
}
|
|
|
|
export function updateCurrentBlockData(state, attrs) {
|
|
const selection = state.getSelection();
|
|
let content = state.getCurrentContent();
|
|
|
|
content = modifySelectedBlocks(content, selection, (block) => {
|
|
return mergeBlockData(block, attrs);
|
|
});
|
|
|
|
return EditorState.push(state, content, "change-block-data");
|
|
}
|
|
|
|
function addStylesToOverride(styles, other) {
|
|
let result = styles;
|
|
|
|
for (let style of other) {
|
|
const [p, k, v] = style.split("$$$");
|
|
const prefix = [p, k, ""].join("$$$");
|
|
|
|
const curValue = result.find((it) => it.startsWith(prefix))
|
|
if (curValue) {
|
|
result = result.remove(curValue);
|
|
}
|
|
result = result.add(style);
|
|
}
|
|
return result
|
|
}
|
|
|
|
export function applyInlineStyle(state, styles) {
|
|
const userSelection = state.getSelection();
|
|
let selection = userSelection;
|
|
let result = state;
|
|
|
|
if (selection.isCollapsed()) {
|
|
const currentOverride = state.getCurrentInlineStyle() || new OrderedSet();
|
|
const styleOverride = addStylesToOverride(currentOverride, styles)
|
|
return EditorState.setInlineStyleOverride(state, styleOverride);
|
|
}
|
|
|
|
let content = null;
|
|
|
|
for (let style of styles) {
|
|
const [p, k, v] = style.split("$$$");
|
|
const prefix = [p, k, ""].join("$$$");
|
|
|
|
content = result.getCurrentContent();
|
|
content = removeInlineStylePrefix(content, selection, prefix);
|
|
|
|
if (v !== "z:null") {
|
|
content = Modifier.applyInlineStyle(content, selection, style);
|
|
}
|
|
|
|
result = EditorState.push(result, content, "change-inline-style");
|
|
}
|
|
|
|
return EditorState.acceptSelection(result, userSelection);
|
|
}
|
|
|
|
export function splitBlockPreservingData(state) {
|
|
let content = state.getCurrentContent();
|
|
const selection = state.getSelection();
|
|
|
|
content = Modifier.splitBlock(content, selection);
|
|
|
|
const blockData = content.blockMap.get(content.selectionBefore.getStartKey()).getData();
|
|
const blockKey = content.selectionAfter.getStartKey();
|
|
const blockMap = content.blockMap.update(blockKey, (block) => {
|
|
return block.set("data", blockData);
|
|
});
|
|
|
|
content = content.set("blockMap", blockMap);
|
|
|
|
return EditorState.push(state, content, "split-block");
|
|
}
|
|
|
|
export function addBlurSelectionEntity(state) {
|
|
let content = state.getCurrentContent(state);
|
|
const selection = state.getSelection();
|
|
|
|
content = content.createEntity("PENPOT_SELECTION", "MUTABLE");
|
|
const entityKey = content.getLastCreatedEntityKey();
|
|
|
|
content = Modifier.applyEntity(content, selection, entityKey);
|
|
return EditorState.push(state, content, "apply-entity");
|
|
}
|
|
|
|
export function removeBlurSelectionEntity(state) {
|
|
const selectionAll = getSelectAllSelection(state);
|
|
const selection = state.getSelection();
|
|
|
|
let content = state.getCurrentContent();
|
|
content = Modifier.applyEntity(content, selectionAll, null);
|
|
|
|
state = EditorState.push(state, content, "apply-entity");
|
|
state = EditorState.forceSelection(state, selection);
|
|
|
|
return state;
|
|
}
|
|
|
|
export function getCurrentBlock(state) {
|
|
const content = state.getCurrentContent();
|
|
const selection = state.getSelection();
|
|
const startKey = selection.getStartKey();
|
|
return content.getBlockForKey(startKey);
|
|
}
|
|
|
|
export function getCurrentEntityKey(state) {
|
|
const block = getCurrentBlock(state);
|
|
const selection = state.getSelection();
|
|
const startOffset = selection.getStartOffset();
|
|
return block.getEntityAt(startOffset);
|
|
}
|
|
|
|
export function removeInlineStylePrefix(contentState, selectionState, stylePrefix) {
|
|
const startKey = selectionState.getStartKey();
|
|
const startOffset = selectionState.getStartOffset();
|
|
const endKey = selectionState.getEndKey();
|
|
const endOffset = selectionState.getEndOffset();
|
|
|
|
return modifySelectedBlocks(contentState, selectionState, (block, blockKey) => {
|
|
let sliceStart;
|
|
let sliceEnd;
|
|
|
|
if (startKey === endKey) {
|
|
sliceStart = startOffset;
|
|
sliceEnd = endOffset;
|
|
} else {
|
|
sliceStart = blockKey === startKey ? startOffset : 0;
|
|
sliceEnd = blockKey === endKey ? endOffset : block.getLength();
|
|
}
|
|
|
|
let chars = block.getCharacterList();
|
|
let current;
|
|
|
|
while (sliceStart < sliceEnd) {
|
|
current = chars.get(sliceStart);
|
|
current = current.set("style", current.getStyle().filter((s) => !s.startsWith(stylePrefix)))
|
|
chars = chars.set(sliceStart, CharacterMetadata.create(current));
|
|
|
|
sliceStart++;
|
|
}
|
|
|
|
return block.set("characterList", chars);
|
|
});
|
|
}
|
|
|
|
export function cursorToEnd(state) {
|
|
const newSelection = getCursorInEndPosition(state);
|
|
const selection = state.getSelection();
|
|
|
|
let content = state.getCurrentContent();
|
|
content = Modifier.applyEntity(content, newSelection, null);
|
|
|
|
state = EditorState.forceSelection(state, newSelection);
|
|
state = EditorState.push(state, content, "apply-entity");
|
|
|
|
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 inlineStyleOverride = state.getInlineStyleOverride();
|
|
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
|
|
);
|
|
|
|
let result = EditorState.push(state, newContent, 'change-block-data');
|
|
result = EditorState.acceptSelection(result, userSelection);
|
|
result = EditorState.setInlineStyleOverride(result, inlineStyleOverride);
|
|
return result;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
const NEWLINE_REGEX = /\r\n?|\n/g;
|
|
|
|
function splitTextIntoTextBlocks(text) {
|
|
if (text == null) {
|
|
return [];
|
|
}
|
|
return text.split(NEWLINE_REGEX);
|
|
}
|
|
|
|
export function insertText(state, text, attrs, inlineStyles) {
|
|
const blocks = splitTextIntoTextBlocks(text);
|
|
|
|
if (blocks.length === 0) {
|
|
return state;
|
|
}
|
|
|
|
const character = CharacterMetadata.create({style: OrderedSet(inlineStyles)});
|
|
|
|
let blockArray = DraftPasteProcessor.processText(
|
|
blocks,
|
|
character,
|
|
"unstyled",
|
|
);
|
|
|
|
blockArray = blockArray.map((b) => {
|
|
return mergeBlockData(b, attrs);
|
|
});
|
|
|
|
const fragment = BlockMapBuilder.createFromArray(blockArray);
|
|
const content = state.getCurrentContent();
|
|
const selection = state.getSelection();
|
|
|
|
const newContent = Modifier.replaceWithFragment(
|
|
content,
|
|
selection,
|
|
fragment
|
|
);
|
|
|
|
const resultSelection = SelectionState.createEmpty(selection.getStartKey());
|
|
return EditorState.push(state, newContent, 'insert-fragment');
|
|
}
|
|
|
|
export function setInlineStyleOverride(state, inlineStyles) {
|
|
return EditorState.setInlineStyleOverride(state, inlineStyles);
|
|
}
|
|
|
|
export function selectionEquals(selection, other) {
|
|
return selection.getAnchorKey() === other.getAnchorKey() &&
|
|
selection.getAnchorOffset() === other.getAnchorOffset() &&
|
|
selection.getFocusKey() === other.getFocusKey() &&
|
|
selection.getFocusOffset() === other.getFocusOffset() &&
|
|
selection.getIsBackward() === other.getIsBackward();
|
|
}
|