mirror of
https://github.com/penpot/penpot.git
synced 2025-05-02 21:25:54 +02:00
Mainly leave shadow-cljs for build cljs stuff and use esbuild for bundle all js dependencies, completly avoiding all possible incompatibility issues between js libraries and google closure compiler.
415 lines
12 KiB
JavaScript
415 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
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import {
|
|
BlockMapBuilder,
|
|
CharacterMetadata,
|
|
CompositeDecorator,
|
|
EditorState,
|
|
Modifier,
|
|
RichTextEditorUtil,
|
|
SelectionState,
|
|
convertFromRaw,
|
|
convertToRaw
|
|
} from "draft-js";
|
|
|
|
import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor';
|
|
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) {
|
|
return text.split(NEWLINE_REGEX);
|
|
}
|
|
|
|
export function insertText(state, text, attrs, inlineStyles) {
|
|
const blocks = splitTextIntoTextBlocks(text);
|
|
|
|
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();
|
|
}
|
|
|
|
export {
|
|
convertToRaw,
|
|
convertFromRaw
|
|
};
|