scripts/core/editor3/reducers/suggestions.tsx
import {EditorState, Modifier, RichUtils} from 'draft-js';
import {onChange} from './editor3';
import {acceptedInlineStyles, sanitizeContent} from '../helpers/inlineStyles';
import {
changeSuggestionsTypes, getStyleSuggestionsTypes,
blockSuggestionTypes, paragraphSuggestionTypes,
} from '../highlightsConfig';
import * as Highlights from '../helpers/highlights';
import {initSelectionIterator, hasNextSelection} from '../helpers/selection';
import {
editor3DataKeys, getCustomDataFromEditor, setCustomDataForEditor__deprecated,
getAllCustomDataFromEditor, setAllCustomDataForEditor__deprecated,
} from '../helpers/editor3CustomData';
import * as Links from '../helpers/links';
import {replaceSelectedEntityData} from '../components/links/entityUtils';
const suggestions = (state = {}, action) => {
switch (action.type) {
case 'TOGGLE_SUGGESTING_MODE':
return toggleSuggestingMode(state);
case 'CREATE_ADD_SUGGESTION':
return createAddSuggestion(state, action.payload);
case 'CREATE_DELETE_SUGGESTION':
return createDeleteSuggestion(state, action.payload);
case 'CREATE_CHANGE_STYLE_SUGGESTION':
return createChangeStyleSuggestion(state, action.payload);
case 'CREATE_CHANGE_BLOCK_STYLE_SUGGESTION':
return createChangeBlockStyleSuggestion(state, action.payload);
case 'CREATE_SPLIT_PARAGRAPH_SUGGESTION':
return createSplitParagraphSuggestion(state, action.payload);
case 'PASTE_ADD_SUGGESTION':
return pasteAddSuggestion(state, action.payload);
case 'CREATE_LINK_SUGGESTION':
return createLinkSuggestion(state, action.payload);
case 'CHANGE_LINK_SUGGESTION':
return changeLinkSuggestion(state, action.payload);
case 'REMOVE_LINK_SUGGESTION':
return removeLinkSuggestion(state, action.payload);
case 'ACCEPT_SUGGESTION':
return processSuggestion(state, action.payload, true);
case 'REJECT_SUGGESTION':
return processSuggestion(state, action.payload, false);
default:
return state;
}
};
/**
* @ngdoc method
* @name toggleSuggestingMode
* @param {Object} state
* @return {Object} returns new state
* @description Disable/enable the suggesting mode.
*/
const toggleSuggestingMode = (state) => {
const {suggestingMode} = state;
return {
...state,
suggestingMode: !suggestingMode,
};
};
/**
* @ngdoc method
* @name saveEditorStatus
* @param {Object} state
* @param {Object} tmpEditorState
* @param {String} changeType
* @return {Object} returns new state
* @description Save the changes as a single change in undo stack.
*/
const saveEditorStatus = (state, tmpEditorState, changeType, restoreSelection = false) => {
const {editorState} = state;
const content = tmpEditorState.getCurrentContent();
const selection = restoreSelection ? editorState.getSelection() : tmpEditorState.getSelection();
let newEditorState;
newEditorState = EditorState.push(editorState, content, changeType);
newEditorState = EditorState.forceSelection(newEditorState, selection);
return onChange(state, newEditorState);
};
/**
* @ngdoc method
* @name createAddSuggestion
* @param {Object} state
* @param {String} text - the suggestion added text
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Add a new suggestion of type ADD.
*/
export const createAddSuggestion = (state, {text, data}, selection = null) => {
let {editorState} = state;
const inlineStyle = editorState.getCurrentInlineStyle();
if (selection) {
editorState = EditorState.acceptSelection(editorState, selection);
}
editorState = deleteCurrentSelection(editorState, data);
for (let i = 0; i < text.length; i++) {
// for every character from inserted text apply add suggestion
editorState = setAddSuggestionForCharacter(editorState, data, text[i], inlineStyle);
}
return saveEditorStatus(state, editorState, 'insert-characters');
};
/**
* @ngdoc method
* @name createDeleteSuggestion
* @param {Object} state
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Add a new suggestion of type DELETE.
*/
const createDeleteSuggestion = (state, {action, data}) => {
let {editorState} = state;
const selection = editorState.getSelection();
if (selection.isCollapsed()) {
if (action === 'backspace') {
editorState = Highlights.changeEditorSelection(editorState, -1, 0, false);
} else {
editorState = Highlights.changeEditorSelection(editorState, 0, 1, false);
}
}
editorState = deleteCurrentSelection(editorState, data, action);
return saveEditorStatus(state, editorState, 'change-inline-style');
};
/**
* @ngdoc method
* @name createAddSuggestion
* @param {Object} state
* @param {String} style - the suggestion style
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Add a new suggestion of type ADD.
*/
const createChangeStyleSuggestion = (state, {style, data}) => {
let {editorState} = state;
const type = Highlights.getTypeByInlineStyle(style);
editorState = applyStyleSuggestion(editorState, type, style, data);
editorState = RichUtils.toggleInlineStyle(editorState, style);
return saveEditorStatus(state, editorState, 'change-inline-style', true);
};
function applyStyleSuggestion(editorState, type, style, data) {
const selection = editorState.getSelection();
let newEditorState = editorState;
let tmpEditorState;
let currentStyle;
let changeStyle = true;
newEditorState = initSelectionIterator(newEditorState);
while (hasNextSelection(newEditorState, selection)) {
currentStyle = Highlights.getHighlightStyleAtCurrentPosition(newEditorState, type);
if (currentStyle) {
tmpEditorState = resetSuggestion(newEditorState, currentStyle);
} else {
const currentSelection = newEditorState.getSelection();
const content = newEditorState.getCurrentContent();
const block = content.getBlockForKey(currentSelection.getStartKey());
if (block.getLength() !== currentSelection.getStartOffset()) {
changeStyle = false;
}
tmpEditorState = Highlights.changeEditorSelection(newEditorState, 1, 1, false);
}
if (tmpEditorState === newEditorState) {
break;
}
newEditorState = tmpEditorState;
}
if (changeStyle && currentStyle != null) {
const oldData: any = Highlights.getHighlightData(editorState, currentStyle);
if (oldData.originalStyle === style && data.originalStyle === '' ||
oldData.originalStyle === '' && data.originalStyle === style) {
// the style is toggled back, so no suggestion is added
// restore the selection
newEditorState = EditorState.acceptSelection(newEditorState, selection);
return newEditorState;
} else {
data.originalStyle = oldData.originalStyle;
}
}
newEditorState = EditorState.acceptSelection(newEditorState, selection);
newEditorState = Highlights.addHighlight(newEditorState, type, data);
return newEditorState;
}
/**
* @ngdoc method
* @name createLinkSuggestion
* @param {Object} state
* @param {Object} data - info about the suggestion (includes link object)
* @return {Object} returns new state
* @description Add a new suggestion of type ADD link to text
*/
const createLinkSuggestion = (state, {data}) => {
const {editorState} = state;
const stateWithLink = Links.createLink(editorState, data.link);
const newState = Highlights.addHighlight(stateWithLink, 'ADD_LINK_SUGGESTION', data);
return saveEditorStatus(state, newState, 'apply-entity');
};
/**
* @ngdoc method
* @name changeLinkSuggestion
* @param {Object} state
* @param {Object} data - info about the suggestion
* @param {Object} link - the new link
* @param {Object} entity - the link entity
* @return {Object} returns new state
* @description Add a new suggestion of type CHANGE link
*/
const changeLinkSuggestion = (state, {data, link, entity}) => {
const {editorState} = state;
let newState = Highlights.highlightEntity(editorState, 'CHANGE_LINK_SUGGESTION',
{
...data,
to: link,
from: entity.getData().link,
},
undefined,
);
newState = replaceSelectedEntityData(newState, {link});
return saveEditorStatus(state, newState, 'apply-entity');
};
/**
* @ngdoc method
* @name changeLinkSuggestion
* @param {Object} state
* @param {Object} data - info about the suggestion
* @return {Object} returns new state
* @description Add a new suggestion of type CHANGE link
*/
const removeLinkSuggestion = (state, {data}) => {
const {editorState} = state;
const newState = Highlights.highlightEntity(editorState, 'REMOVE_LINK_SUGGESTION', data, true);
return saveEditorStatus(state, newState, 'apply-entity');
};
/**
* @ngdoc method
* @name createChangeBlockStyleSuggestion
* @param {Object} state
* @param {String} blockType - the suggestion block type
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Add a new suggestion of type change block style.
*/
const createChangeBlockStyleSuggestion = (state, {blockType, data}) => {
let {editorState} = state;
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const firstBlock = content.getBlockForKey(selection.getStartKey());
let lastBlock = content.getBlockForKey(selection.getEndKey());
if (selection.getEndOffset() === 0 && selection.getStartKey() !== selection.getEndKey()) {
lastBlock = content.getBlockBefore(selection.getEndKey());
}
const blocksSelection = selection.merge({
anchorOffset: 0,
anchorKey: firstBlock.getKey(),
focusOffset: lastBlock.getLength(),
focusKey: lastBlock.getKey(),
isBackward: false,
});
const type = 'BLOCK_STYLE_SUGGESTION';
const newData = {
...data,
blockType,
};
editorState = EditorState.acceptSelection(editorState, blocksSelection);
editorState = applyStyleSuggestion(editorState, type, blockType, newData);
editorState = RichUtils.toggleBlockType(editorState, blockType);
return saveEditorStatus(state, editorState, 'change-block-type', true);
};
/**
* @ngdoc method
* @name isMergeParagraph
* @param {Object} editorState
* @param {Object} data - info about the author of suggestion
* @return {Object} returns true if there is a merge paragraph suggestion for the same user
* @description Check if at current position there is a merge paragraph suggestion.
*/
const isMergeParagraph = (editorState, data) => {
const suggestionStyle = Highlights.getHighlightStyleAtCurrentPosition(editorState, ['MERGE_PARAGRAPHS_SUGGESTION']);
if (suggestionStyle == null) {
return false;
}
const suggestionAuthor = Highlights.getHighlightAuthor(editorState, suggestionStyle);
return suggestionAuthor != null && suggestionAuthor === data.author;
};
/**
* @ngdoc method
* @name createSplitParagraphSuggestion
* @param {Object} state
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Add a new suggestion of type split block.
*/
const createSplitParagraphSuggestion = (state, {data}) => {
const type = 'SPLIT_PARAGRAPH_SUGGESTION';
let {editorState} = state;
const isMergeParagraphAfter = isMergeParagraph(editorState, data);
const checkState = Highlights.changeEditorSelection(editorState, -1, -1, false);
const isMergeParagraphBefore = isMergeParagraph(checkState, data);
let selection = editorState.getSelection();
let content = Modifier.splitBlock(editorState.getCurrentContent(), selection);
const firstBlock = content.getBlockForKey(selection.getStartKey());
const secondBlock = content.getBlockAfter(selection.getStartKey());
if (isMergeParagraphBefore || isMergeParagraphAfter) {
if (isMergeParagraphBefore) {
selection = selection.merge({
anchorOffset: firstBlock.getLength() - 1,
anchorKey: firstBlock.getKey(),
focusOffset: firstBlock.getLength(),
focusKey: firstBlock.getKey(),
isBackward: false,
});
} else {
selection = selection.merge({
anchorOffset: 0,
anchorKey: secondBlock.getKey(),
focusOffset: 1,
focusKey: secondBlock.getKey(),
isBackward: false,
});
}
content = Modifier.removeRange(content, selection, 'backward');
editorState = EditorState.push(editorState, content, 'remove-range');
} else {
selection = selection.merge({
anchorOffset: firstBlock.getLength(),
anchorKey: firstBlock.getKey(),
focusOffset: firstBlock.getLength(),
focusKey: firstBlock.getKey(),
isBackward: false,
});
content = Modifier.insertText(content, selection, Highlights.paragraphSeparator);
editorState = EditorState.push(editorState, content, 'insert-characters');
editorState = EditorState.acceptSelection(editorState, selection);
editorState = Highlights.changeEditorSelection(editorState, 0, 1, false);
editorState = Highlights.addHighlight(editorState, type, data);
}
selection = selection.merge({
anchorOffset: 0,
anchorKey: secondBlock.getKey(),
focusOffset: 0,
focusKey: secondBlock.getKey(),
isBackward: false,
});
editorState = EditorState.acceptSelection(editorState, selection);
return saveEditorStatus(state, editorState, 'change-inline-style', false);
};
/**
* @ngdoc method
* @name pasteAddSuggestion
* @param {Object} state
* @param {Object} content - the suggestion added content
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Add a new suggestion of type ADD based on content.
*/
const pasteAddSuggestion = (state, {content, data}) => {
let {editorState} = state;
let selection = editorState.getSelection();
const beforeStyle =
Highlights.getHighlightStyleAtOffset(editorState, changeSuggestionsTypes, selection, -1) as string;
const beforeData: any = beforeStyle != null ? Highlights.getHighlightData(editorState, beforeStyle) : null;
// if text is selected mark it as removed and collapse the selection before replacing
if (!selection.isCollapsed()) {
editorState = deleteCurrentSelection(editorState, data);
selection = editorState.getSelection();
}
// only get it now after adding delete suggestions
const customData = getAllCustomDataFromEditor(editorState);
// add content to editor state
const mergedContent = Modifier.replaceWithFragment(
editorState.getCurrentContent(),
editorState.getSelection(),
sanitizeContent(EditorState.createWithContent(content))
.getCurrentContent()
.getBlockMap(),
);
// push new content
editorState = EditorState.push(editorState, mergedContent, 'insert-fragment');
// store current selection for later
const finalSelection = editorState.getSelection();
const newSelection = selection.merge({
anchorKey: selection.getStartKey(),
anchorOffset: selection.getStartOffset(),
focusKey: finalSelection.getStartKey(),
focusOffset: finalSelection.getStartOffset(),
hasFocus: true,
isBackward: false,
});
// for the first block recover the initial block data because on replaceWithFragment the block data is
// replaced with the data from pasted fragment
editorState = setAllCustomDataForEditor__deprecated(editorState, customData);
// select pasted content
editorState = EditorState.acceptSelection(editorState, newSelection);
// apply highlights
if (beforeData != null && beforeData.type === 'ADD_SUGGESTION' && beforeData.author === data.author) {
editorState = RichUtils.toggleInlineStyle(editorState, beforeStyle);
} else {
editorState = Highlights.addHighlight(editorState, 'ADD_SUGGESTION', data);
}
// reset selection
editorState = EditorState.forceSelection(editorState, finalSelection);
return saveEditorStatus(state, editorState, 'change-block-type');
};
/**
* @ngdoc method
* @name moveToSuggestionsHistory
* @param {Object} editorState
* @param {Object} data - info about the author
* @param {Object} suggestion
* @param {Boolean} accepted
* @return {editorState} returns new state
*/
function moveToSuggestionsHistory(editorState, data, suggestion, accepted) {
const resolvedSuggestions = getCustomDataFromEditor(
editorState,
editor3DataKeys.RESOLVED_SUGGESTIONS_HISTORY,
) || [];
let nextEditorState = editorState;
nextEditorState = setCustomDataForEditor__deprecated(
editorState,
editor3DataKeys.RESOLVED_SUGGESTIONS_HISTORY,
resolvedSuggestions.concat({
suggestionText: suggestion.suggestionText,
oldText: suggestion.oldText,
suggestionInfo: {
...suggestion,
},
resolutionInfo: {
resolverUserId: data.author,
date: data.date,
accepted: accepted,
},
}),
);
nextEditorState = Highlights.removeHighlight(nextEditorState, suggestion.styleName);
return nextEditorState;
}
/**
* @ngdoc method
* @name processSplitBlockSuggestion
* @param {Object} state
* @param {Object} data - info about the author
* @param {Object} suggestion
* @param {Boolean} accepted - the suggestion is accepted
* @return {Object} returns new state
* @description Accept or reject the split suggestions in the selection.
*/
const processSplitBlockSuggestion = (state, data, suggestion, accepted) => {
const {selection} = suggestion;
let {editorState} = state;
editorState = moveToSuggestionsHistory(editorState, data, suggestion, accepted);
let content = editorState.getCurrentContent();
const block = content.getBlockAfter(selection.getStartKey());
const newSelection = selection.merge({
anchorOffset: selection.getStartOffset(),
anchorKey: selection.getStartKey(),
focusOffset: accepted ? selection.getEndOffset() : 0,
focusKey: accepted ? selection.getEndKey() : block.getKey(),
isBackward: false,
});
content = Modifier.removeRange(content, newSelection, 'backward');
editorState = EditorState.push(editorState, content, 'remove-range');
return saveEditorStatus(state, editorState, 'change-inline-style', false);
};
/**
* @ngdoc method
* @name processMergeBlocksSuggestion
* @param {Object} state
* @param {Object} data - info about the author
* @param {Object} suggestion
* @param {Boolean} accepted - the suggestion is accepted
* @return {Object} returns new state
* @description Accept or reject the merge suggestions in the selection.
*/
const processMergeBlocksSuggestion = (state, data, suggestion, accepted) => {
const {selection} = suggestion;
let {editorState} = state;
const crtSelection = editorState.getSelection();
editorState = moveToSuggestionsHistory(editorState, data, suggestion, accepted);
let content = editorState.getCurrentContent();
content = Modifier.removeRange(content, selection, 'backward');
if (!accepted) {
content = Modifier.splitBlock(content, crtSelection);
}
editorState = EditorState.push(editorState, content, 'remove-range');
editorState = EditorState.acceptSelection(editorState, crtSelection);
return saveEditorStatus(state, editorState, 'change-inline-style', false);
};
/**
* @ngdoc method
* @name processSuggestion
* @param {Object} state
* @param {Object} data - info about the author
* @param {Object} suggestion
* @param {Boolean} accepted - the suggestion is accepted
* @return {Object} returns new state
* @description Accept or reject the suggestions in the selection.
*/
const processSuggestion = (state, {data, suggestion}, accepted) => {
if (accepted === true || accepted === false) {
// after clicking accept/reject editor focus is lost
// restore the focus so undo stack is correct
// and pop up can be positioned properly on undo SDFID-401
state.editorState = EditorState.acceptSelection(
state.editorState,
state.editorState.getSelection().merge({
hasFocus: true,
}),
);
}
let {selection} = suggestion;
let {editorState} = state;
let style;
if (suggestion.type === 'SPLIT_PARAGRAPH_SUGGESTION') {
return processSplitBlockSuggestion(state, data, suggestion, accepted);
}
if (suggestion.type === 'MERGE_PARAGRAPHS_SUGGESTION') {
return processMergeBlocksSuggestion(state, data, suggestion, accepted);
}
// If link it's rejected we remove the entity
if (suggestion.type === 'ADD_LINK_SUGGESTION') {
editorState = moveToSuggestionsHistory(editorState, data, suggestion, accepted);
if (!accepted) {
editorState = Links.removeLink(editorState);
}
return saveEditorStatus(state, editorState, 'apply-entity', true);
}
// remove link if remove link is accepted
if (suggestion.type === 'REMOVE_LINK_SUGGESTION') {
editorState = moveToSuggestionsHistory(editorState, data, suggestion, accepted);
if (accepted) {
editorState = Links.removeLink(editorState);
}
return saveEditorStatus(state, editorState, 'apply-entity', true);
}
if (suggestion.type === 'CHANGE_LINK_SUGGESTION') {
editorState = moveToSuggestionsHistory(editorState, data, suggestion, accepted);
if (!accepted) {
editorState = replaceSelectedEntityData(editorState, {link: suggestion.from});
}
return saveEditorStatus(state, editorState, 'apply-entity', true);
}
editorState = EditorState.acceptSelection(editorState, selection);
if (suggestion.type === 'BLOCK_STYLE_SUGGESTION') {
editorState = moveToSuggestionsHistory(editorState, data, suggestion, accepted);
if (!accepted) {
editorState = RichUtils.toggleBlockType(editorState, suggestion.blockType);
}
return saveEditorStatus(state, editorState, 'change-inline-style', true);
}
if (getStyleSuggestionsTypes().indexOf(suggestion.type) !== -1) {
editorState = moveToSuggestionsHistory(editorState, data, suggestion, accepted);
if (!accepted) {
style = Highlights.getInlineStyleByType(suggestion.type);
editorState = RichUtils.toggleInlineStyle(editorState, style);
}
return saveEditorStatus(state, editorState, 'change-inline-style', true);
}
editorState = EditorState.acceptSelection(editorState, selection);
editorState = applyChangeSuggestion(editorState, accepted);
selection = editorState.getSelection();
selection = selection.merge({
anchorOffset: selection.getEndOffset(),
anchorKey: selection.getEndKey(),
focusOffset: selection.getEndOffset(),
focusKey: selection.getEndKey(),
isBackward: false,
});
editorState = EditorState.acceptSelection(editorState, selection);
editorState = moveToSuggestionsHistory(editorState, data, suggestion, accepted);
return saveEditorStatus(state, editorState, 'change-block-data');
};
/**
* @ngdoc method
* @name applyChangeSuggestion
* @param {Object} editorState
* @param {Boolean} accepted - the suggestion is accepted
* @return {Object} returns new state
* @description Accept or reject the change suggestions in current editor selection.
*/
const applyChangeSuggestion = (editorState, accepted) => {
const suggestionTypes = [...changeSuggestionsTypes, ...paragraphSuggestionTypes];
let selection = editorState.getSelection();
let content = editorState.getCurrentContent();
let lastBlock = content.getBlockForKey(selection.getEndKey());
const afterSectionLength = lastBlock.getLength() - selection.getEndOffset();
const nextBlock = content.getBlockAfter(selection.getEndKey());
let newEditorState;
let oldEditorState;
let style;
let data;
newEditorState = removeDeleteParagraphSuggestions(editorState);
newEditorState = initSelectionIterator(newEditorState, true);
while (hasNextSelection(newEditorState, selection, true)) {
oldEditorState = newEditorState;
newEditorState = Highlights.changeEditorSelection(newEditorState, -1, -1, false);
style = Highlights.getHighlightStyleAtCurrentPosition(newEditorState, suggestionTypes);
if (style == null) {
continue;
}
data = Highlights.getHighlightData(newEditorState, style);
if (paragraphSuggestionTypes.indexOf(data.type) !== -1) {
// delete any paragraph suggestion
newEditorState = Highlights.changeEditorSelection(newEditorState, 1, 1, false);
newEditorState = deleteCharacter(newEditorState, style);
if (newEditorState === oldEditorState) {
break;
}
continue;
}
const applySuggestion = data.type === 'ADD_SUGGESTION' && accepted ||
data.type === 'DELETE_SUGGESTION' && !accepted;
let {selection: newSelection} = Highlights.getRangeAndTextForStyle(newEditorState, style, true);
let offset = 0;
if (selection.getStartKey() === newSelection.getStartKey() &&
selection.getStartOffset() > newSelection.getStartOffset()) {
offset = selection.getStartOffset() - newSelection.getStartOffset();
}
newEditorState = EditorState.acceptSelection(newEditorState, newSelection);
newEditorState = Highlights.changeEditorSelection(newEditorState, offset, 1, false);
newSelection = newEditorState.getSelection();
newEditorState = Highlights.resetHighlightForCurrentSelection(newEditorState, style);
if (!applySuggestion) {
// delete current selection
const _content = newEditorState.getCurrentContent();
const newContent = Modifier.removeRange(_content, newSelection, 'forward');
newEditorState = EditorState.push(newEditorState, newContent, 'backspace-character');
}
newSelection = newSelection.merge({
anchorOffset: newSelection.getStartOffset(),
anchorKey: newSelection.getStartKey(),
focusOffset: newSelection.getStartOffset(),
focusKey: newSelection.getStartKey(),
isBackward: false,
});
newEditorState = EditorState.acceptSelection(newEditorState, newSelection);
if (newEditorState === oldEditorState) {
break;
}
}
content = newEditorState.getCurrentContent();
if (nextBlock != null) {
lastBlock = content.getBlockBefore(nextBlock.getKey());
} else {
lastBlock = content.getLastBlock();
}
selection = selection.merge({
anchorKey: selection.getStartKey(),
anchorOffset: selection.getStartOffset(),
focusKey: lastBlock.getKey(),
focusOffset: lastBlock.getLength() - afterSectionLength,
isBackward: false,
});
return EditorState.acceptSelection(newEditorState, selection);
};
/**
* @ngdoc method
* @name addDeleteParagraphSuggestions
* @param {Object} editorState
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Add delete paragraph suggestion for every empty block.
*/
const addDeleteParagraphSuggestions = (editorState, data) => {
const selection = editorState.getSelection();
let newEditorState = editorState;
let content = editorState.getCurrentContent();
let block = content.getBlockForKey(selection.getStartKey());
let offset = 0;
let changed = false;
while (block != null) {
if (block.getLength() === 0 && block.getType() !== 'atomic') {
const newSelection = selection.merge({
anchorOffset: 0,
anchorKey: block.getKey(),
focusOffset: 0,
focusKey: block.getKey(),
isBackward: false,
});
content = Modifier.insertText(content, newSelection, Highlights.paragraphSeparator);
newEditorState = EditorState.push(newEditorState, content, 'insert-characters');
newEditorState = EditorState.acceptSelection(newEditorState, newSelection);
newEditorState = Highlights.changeEditorSelection(newEditorState, 0, 1, false);
newEditorState = Highlights.addHighlight(newEditorState, 'DELETE_EMPTY_PARAGRAPH_SUGGESTION', data);
content = newEditorState.getCurrentContent();
changed = true;
}
if (block.getKey() === selection.getEndKey()) {
if (block.getLength() === 0) {
offset = 1;
}
break;
}
block = content.getBlockAfter(block.getKey());
}
if (!changed) {
return editorState;
}
newEditorState = EditorState.acceptSelection(newEditorState, selection);
return Highlights.changeEditorSelection(newEditorState, 0, offset, false);
};
/**
* @ngdoc method
* @name removeDeleteParagraphSuggestions
* @param {Object} editorState
* @return {Object} returns new state
* @description Remove all delete paragraph suggestions.
*/
const removeDeleteParagraphSuggestions = (editorState) => {
const selection = editorState.getSelection();
let content = editorState.getCurrentContent();
let block = content.getBlockForKey(selection.getStartKey());
let offset = 0;
let changed = false;
let newEditorState;
while (block != null) {
const key = block.getKey();
const checkBlock = block.getLength() === 1 && block.getType() !== 'atomic';
const checkMiddleBlock = key !== selection.getStartKey() && key !== selection.getEndKey();
const checkFirstBlock = key === selection.getStartKey() && selection.getStartOffset() === 0;
const checkLastBlock = key === selection.getEndKey() && selection.getEndOffset() === 1;
if (checkBlock && (checkMiddleBlock || checkFirstBlock || checkLastBlock)) {
const newSelection = selection.merge({
anchorOffset: 0,
anchorKey: block.getKey(),
focusOffset: 1,
focusKey: block.getKey(),
isBackward: false,
});
newEditorState = EditorState.acceptSelection(editorState, newSelection);
const style = Highlights.getHighlightStyleAtCurrentPosition(
newEditorState, ['DELETE_EMPTY_PARAGRAPH_SUGGESTION']);
if (style != null) {
content = Modifier.removeRange(content, newSelection, 'forward');
changed = true;
offset = checkLastBlock ? -1 : 0;
}
}
if (block.getKey() === selection.getEndKey()) {
break;
}
block = content.getBlockAfter(block.getKey());
}
if (!changed) {
return editorState;
}
newEditorState = EditorState.push(editorState, content, 'backspace-character');
newEditorState = EditorState.acceptSelection(newEditorState, selection);
return Highlights.changeEditorSelection(newEditorState, 0, offset, false);
};
/**
* @ngdoc method
* @name deleteCurrentSelection
* @param {Object} editorState
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Set the delete suggestion for current editor selection.
*/
const deleteCurrentSelection = (editorState, data, action = 'delete') => {
let selection = editorState.getSelection();
let newEditorState;
if (selection.isCollapsed()) {
return editorState;
}
const content = editorState.getCurrentContent();
const block = content.getBlockForKey(selection.getStartKey());
if (selection.getEndOffset() === 0 && selection.getStartOffset() === block.getLength()) {
newEditorState = setMergeParagraphSuggestion(editorState, data);
if (action !== 'delete') {
return newEditorState;
}
return Highlights.changeEditorSelection(newEditorState, 1, 1, false);
}
if (selection.getStartKey() === selection.getEndKey() &&
selection.getStartOffset() === selection.getEndOffset() - 1) {
newEditorState = Highlights.changeEditorSelection(editorState, 1, 0, false);
newEditorState = setDeleteSuggestionForCharacter(newEditorState, data);
if (action !== 'delete') {
return newEditorState;
}
return Highlights.changeEditorSelection(newEditorState, 1, 1, false);
}
// if there are insert or delete suggestion, reject them and then set delete suggestion
newEditorState = applyChangeSuggestion(editorState, false);
newEditorState = addDeleteParagraphSuggestions(newEditorState, data);
selection = newEditorState.getSelection();
selection = selection.merge({
anchorOffset: action === 'delete' ? selection.getEndOffset() : selection.getStartOffset(),
anchorKey: action === 'delete' ? selection.getEndKey() : selection.getStartKey(),
focusOffset: action === 'delete' ? selection.getEndOffset() : selection.getStartOffset(),
focusKey: action === 'delete' ? selection.getEndKey() : selection.getStartKey(),
isBackward: false,
});
newEditorState = Highlights.addHighlight(newEditorState, 'DELETE_SUGGESTION', data);
return EditorState.acceptSelection(newEditorState, selection);
};
/**
* @ngdoc method
* @name setAddSuggestionForCharacter
* @param {Object} state
* @param {Object} data - info about the author of suggestion
* @param {String} text - the suggestion added text
* @param {Object} inlineStyle - the style for the text
* @return {Object} returns new state
* @description Set the add suggestion for current character.
* On suggestion mode:
* 1. next neighbor is 'delete suggestion' with same user and the same char as added one -> reset related data
* 1.1. if both neighbors are 'new suggestion' and has the same user -> concatenate them?
* 2. at least one of neighbors is 'new suggestion' and has same user -> set the same data
* 3. other cases -> add new 'new suggestion'
*/
const setAddSuggestionForCharacter = (editorState, data, text, inlineStyle = null) => {
const crtInlineStyle = inlineStyle || editorState.getCurrentInlineStyle();
const selection = editorState.getSelection();
const beforeStyle =
Highlights.getHighlightStyleAtOffset(editorState, changeSuggestionsTypes, selection, -1) as string;
const beforeData: any = beforeStyle != null ? Highlights.getHighlightData(editorState, beforeStyle) : null;
const currentStyle =
Highlights.getHighlightStyleAtOffset(editorState, changeSuggestionsTypes, selection, 0) as string;
const currentData: any = currentStyle != null ? Highlights.getHighlightData(editorState, currentStyle) : null;
let content = editorState.getCurrentContent();
let newState = editorState;
content = Modifier.insertText(content, selection, text);
newState = EditorState.push(newState, content, 'insert-characters');
newState = Highlights.changeEditorSelection(newState, -1, 0, false);
newState = applyStyleForSuggestion(newState, crtInlineStyle);
if (beforeData != null && beforeData.type === 'ADD_SUGGESTION'
&& beforeData.author === data.author) {
// if previous character is an add suggestion of the same user, set the same data
newState = RichUtils.toggleInlineStyle(newState, beforeStyle);
} else if (currentData != null && currentData.type === 'ADD_SUGGESTION'
&& currentData.author === data.author) {
// if next character is an add suggestion of the same user, set the same data
newState = RichUtils.toggleInlineStyle(newState, currentStyle);
} else {
// create a new suggestion
newState = Highlights.addHighlight(newState, 'ADD_SUGGESTION', data);
}
newState = Highlights.changeEditorSelection(newState, 1, 0, true);
return newState;
};
/**
* @ngdoc method
* @name isSplitParagraph
* @param {Object} editorState
* @param {Object} data - info about the author of suggestion
* @return {Object} returns true if there is a split paragraph suggestion for the same user
* @description Check if at current position there is a split paragraph suggestion.
*/
const isSplitParagraph = (editorState, data) => {
const suggestionStyle = Highlights.getHighlightStyleAtCurrentPosition(editorState, ['SPLIT_PARAGRAPH_SUGGESTION']);
if (suggestionStyle == null) {
return false;
}
const suggestionAuthor = Highlights.getHighlightAuthor(editorState, suggestionStyle);
return suggestionAuthor != null && suggestionAuthor === data.author;
};
/**
* @ngdoc method
* @name setMergeParagraphSuggestion
* @param {Object} editorState
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Set the merge paragraph suggestion for current adjacent blocks.
*/
const setMergeParagraphSuggestion = (editorState, data) => {
const type = 'MERGE_PARAGRAPHS_SUGGESTION';
const checkState = Highlights.changeEditorSelection(editorState, -2, 0, false);
const deleteSplitSuggestion = isSplitParagraph(checkState, data);
const offset = deleteSplitSuggestion ? -1 : 0;
let newState = Highlights.changeEditorSelection(editorState, offset, 0, false);
let content = newState.getCurrentContent();
const selection = newState.getSelection();
if (deleteSplitSuggestion) {
content = Modifier.removeRange(content, selection, 'backward');
newState = EditorState.push(newState, content, 'remove-range');
} else {
content = Modifier.replaceText(content, selection, Highlights.paragraphSeparator);
newState = EditorState.push(newState, content, 'insert-characters');
newState = Highlights.changeEditorSelection(newState, -1, 0, false);
newState = Highlights.addHighlight(newState, type, data);
newState = Highlights.changeEditorSelection(newState, 0, -1, false);
}
return newState;
};
/**
* @ngdoc method
* @name createDeleteSuggestion
* @param {Object} state
* @param {Object} data - info about the author of suggestion
* @return {Object} returns new state
* @description Set the delete suggestion for current character.
* On suggestion mode:
* 1. if previous neighbor is 'new suggestion' with the same user -> delete char
* 1.1. if both new neighbors are 'new suggestion' and has the same user -> concatenate them?
* 2. at least one of neighbors is 'delete suggestion' and has same user -> set the same suggestion data
* 3. other cases -> add new 'delete suggestion'
*/
const setDeleteSuggestionForCharacter = (editorState, data) => {
const selection = editorState.getSelection();
const paragraphStyle = Highlights.getHighlightStyleAtOffset(editorState, paragraphSuggestionTypes, selection, -1);
if (paragraphStyle != null) {
// if current character is marked as paragraph suggestion, skip
return Highlights.changeEditorSelection(editorState, -1, -1, true);
}
const currentStyle = Highlights.getHighlightStyleAtOffset(editorState, changeSuggestionsTypes, selection, -1);
const currentData: any = currentStyle != null ? Highlights.getHighlightData(editorState, currentStyle) : null;
if (currentData != null && currentData.type === 'DELETE_SUGGESTION') {
// if current character is already marked as a delete suggestion, skip
return Highlights.changeEditorSelection(editorState, -1, -1, true);
}
if (currentData != null && currentData.type === 'ADD_SUGGESTION' &&
currentData.author === data.author) {
// if current character already a suggestion of current user, delete the character
return deleteCharacter(editorState, currentStyle);
}
const beforeStyle =
Highlights.getHighlightStyleAtOffset(editorState, changeSuggestionsTypes, selection, -2) as string;
const beforeData: any = beforeStyle != null ? Highlights.getHighlightData(editorState, beforeStyle) : null;
const afterParagraphStyle = Highlights.getHighlightStyleAtOffset(
editorState, paragraphSuggestionTypes, selection, 0);
const offset = afterParagraphStyle == null ? 0 : 1;
const afterStyle =
Highlights.getHighlightStyleAtOffset(editorState, changeSuggestionsTypes, selection, offset) as string;
const afterData: any = afterStyle != null ? Highlights.getHighlightData(editorState, afterStyle) : null;
let newState = Highlights.changeEditorSelection(editorState, -1, 0, false);
if (beforeData != null && beforeData.type === 'DELETE_SUGGESTION'
&& beforeData.author === data.author) {
// if previous character is a delete suggestion of the same user, set the same data
newState = RichUtils.toggleInlineStyle(newState, beforeStyle);
} else if (afterData != null && afterData.type === 'DELETE_SUGGESTION'
&& afterData.author === data.author) {
// if next character is a delete suggestion of the same user, set the same data
newState = RichUtils.toggleInlineStyle(newState, afterStyle);
} else {
// create a new suggestion
newState = Highlights.addHighlight(newState, 'DELETE_SUGGESTION', data);
}
return Highlights.changeEditorSelection(newState, 0, -1, true);
};
/**
* @ngdoc method
* @name applyStyleForSuggestion
* @param {Object} editorState
* @param {Objest} inlineStyle
* @param {String} style
* @return {Object} returns new state
* @description Apply the style for current selection.
*/
const applyStyleForSuggestion = (editorState, inlineStyle) => {
let newState = editorState;
inlineStyle.filter((style) => acceptedInlineStyles.indexOf(style) !== -1)
.forEach((style) => {
newState = RichUtils.toggleInlineStyle(newState, style);
});
const nextInlineStyle = Highlights.getHighlightStyleAtCurrentPosition(
newState, getStyleSuggestionsTypes(), true, false);
if (nextInlineStyle == null) {
return newState;
}
inlineStyle.forEach((style) => {
const type = Highlights.isHighlightStyle(style) ? Highlights.getHighlightTypeFromStyleName(style) : null;
if (type != null && getStyleSuggestionsTypes().indexOf(type) !== -1 && nextInlineStyle.indexOf(style) !== -1 ||
blockSuggestionTypes.indexOf(type) !== -1) {
newState = RichUtils.toggleInlineStyle(newState, style);
}
});
return newState;
};
/**
* @ngdoc method
* @name resetSuggestion
* @param {Object} editorState
* @param {String} style
* @return {Object} returns new state
* @description For type suggestion reset both style and data for
* current character position.
*/
const resetSuggestion = (editorState, style) => {
let newState = editorState;
newState = Highlights.changeEditorSelection(newState, 0, 1, false);
newState = Highlights.resetHighlightForCurrentSelection(newState, style);
newState = Highlights.changeEditorSelection(newState, 1, 0, false);
return newState;
};
/**
* @ngdoc method
* @name deleteCharacter
* @param {Object} editorState
* @param {String} style - style of the current selection (optional)
* @return {Object} returns new state
* @description Delete the current character.
*/
const deleteCharacter = (editorState, style = null) => {
let newState = Highlights.changeEditorSelection(editorState, -1, 0, false);
let content = newState.getCurrentContent();
const selection = newState.getSelection();
content = Modifier.removeRange(content, selection, 'forward');
newState = EditorState.push(newState, content, 'backspace-character');
if (style) {
const textForHighlight = Highlights.getRangeAndTextForStyle(newState, style);
if (textForHighlight.highlightedText === '') {
// also delete the suggestion if it was the last character of that suggestion
newState = Highlights.removeHighlight(newState, style);
}
}
return newState;
};
export default suggestions;