scripts/core/editor3/helpers/highlights.ts
import {RichUtils, EditorState} from 'draft-js';
import {getHighlightsConfig} from '../highlightsConfig';
import {editor3DataKeys, getCustomDataFromEditor, setCustomDataForEditor__deprecated} from './editor3CustomData';
import {getDraftCharacterListForSelection} from './getDraftCharacterListForSelection';
import {getDraftSelectionForEntireContent} from './getDraftSelectionForEntireContent';
import {resizeDraftSelection} from './resizeDraftSelection';
import {clearInlineStyles} from './clearInlineStyles';
import {changeSuggestionsTypes, paragraphSuggestionTypes} from '../highlightsConfig';
import {has} from 'lodash';
import {gettext} from 'core/utils';
export const paragraphSeparator = 'ΒΆ';
export function getAvailableHighlights() {
const highlightsConfig = getHighlightsConfig();
return Object.keys(highlightsConfig).reduce((obj, key) => {
obj[key] = highlightsConfig[key].draftStyleMap;
return obj;
}, {});
}
export function getTypeByInlineStyle(inlineStyle) {
const highlightsConfig = getHighlightsConfig();
const mapInlineStyleToHighlightType = Object.keys(highlightsConfig).reduce((obj, key) => {
if (highlightsConfig[key].type === 'STYLE') {
obj[highlightsConfig[key].style] = key;
}
return obj;
}, {});
return mapInlineStyleToHighlightType[inlineStyle];
}
export function getInlineStyleByType(type) {
const highlightsConfig = getHighlightsConfig();
const mapHighlightTypeToInlineStyle = Object.keys(highlightsConfig).reduce((obj, key) => {
if (highlightsConfig[key].type === 'STYLE') {
obj[key] = highlightsConfig[key].style;
}
return obj;
}, {});
return mapHighlightTypeToInlineStyle[type];
}
export function getHighlightDescription(suggestionsType) {
const highlight = getHighlightsConfig()[suggestionsType];
if (highlight != null && highlight.description) {
return highlight.description;
}
return suggestionsType;
}
export function getBlockStylesDescription(blockStyle) {
const blockStylesDescription = {
'header-one': 'H1',
'header-two': 'H2',
'header-three': 'H3',
'header-four': 'H4',
'header-five': 'H5',
'header-six': 'H6',
blockquote: gettext('quote'),
'unordered-list-item': gettext('unordered list'),
'ordered-list-item': gettext('ordered list'),
'code-block': gettext('preformatted'),
};
if (blockStyle == null || !(blockStyle in blockStylesDescription)) {
return '';
}
return blockStylesDescription[blockStyle];
}
function getInitialHighlightsState() {
return {
highlightsStyleMap: {},
highlightsData: {},
lastHighlightIds: Object.keys(getAvailableHighlights()).reduce((obj, key) => {
obj[key] = 0;
return obj;
}, {}),
};
}
function getHighlightsState(editorState) {
const initialHighlightsState = getInitialHighlightsState();
const highlightsDataFromEditor = getCustomDataFromEditor(editorState, editor3DataKeys.MULTIPLE_HIGHLIGHTS);
if (highlightsDataFromEditor == null) {
return initialHighlightsState;
}
const dataFromEditorContainsRequiredKeys = Object.keys(initialHighlightsState)
.filter((key) => Object.keys(highlightsDataFromEditor).includes(key) === false)
.length === 0;
if (dataFromEditorContainsRequiredKeys === false) {
return initialHighlightsState;
}
return highlightsDataFromEditor;
}
function setHighlightsState(editorState, hightlightsState) {
return setCustomDataForEditor__deprecated(editorState, editor3DataKeys.MULTIPLE_HIGHLIGHTS, hightlightsState);
}
function getHighlightType(styleName) {
const delimiterIndex = styleName.lastIndexOf('-');
if (delimiterIndex === -1) {
throw new Error('styleName doesn\'t belong to a highlight');
}
return styleName.slice(0, delimiterIndex);
}
/**
* @ngdoc method
* @name getHighlightsStyleMap
* @param {Object} editorState
* @return {Object}
* @description return the current highlights style map.
*/
export function getHighlightsStyleMap(editorState) {
const highlightsState = getHighlightsState(editorState);
return highlightsState.highlightsStyleMap;
}
/**
* @ngdoc method
* @name highlightTypeValid
* @param {String} highlightType
* @return {Boolean}
* @description return true if the highlightType is a valid highlight type for current editor.
*/
function highlightTypeValid(highlightType) {
return Object.keys(getAvailableHighlights()).includes(highlightType);
}
/**
* @ngdoc method
* @name styleNameBelongsToHighlight
* @param {String} styleName
* @return {Boolean}
* @description return true if the styleName is a valid style for current editor.
*/
export function styleNameBelongsToHighlight(styleName) {
if (typeof styleName !== 'string') {
return false;
}
const delimiterIndex = styleName.lastIndexOf('-');
if (delimiterIndex === -1) {
return false;
}
return Object.keys(getAvailableHighlights()).includes(styleName.slice(0, delimiterIndex));
}
/**
* @ngdoc method
* @name getHighlightTypeFromStyleName
* @param {String} styleName
* @return {String}
* @description return the highlight type for styleName.
*/
export function getHighlightTypeFromStyleName(styleName) {
if (typeof styleName !== 'string') {
throw new Error('string expected');
}
const delimiterIndex = styleName.lastIndexOf('-');
if (delimiterIndex === -1) {
throw new Error('Style name does not contain a highlight type');
}
const highlightType = styleName.slice(0, delimiterIndex);
if (highlightTypeValid(highlightType) === false) {
throw new Error(`Invalid highlight type '${highlightType}'`);
}
return highlightType;
}
/**
* @ngdoc method
* @name isHighlightStyle
* @param {String} styleName
* @return {Boolean}
* @description return true if styleName coresponds to a valid type.
*/
export function isHighlightStyle(styleName) {
const delimiterIndex = styleName.lastIndexOf('-');
if (delimiterIndex === -1) {
return false;
}
return highlightTypeValid(styleName.slice(0, delimiterIndex));
}
/**
* @ngdoc method
* @name getHighlightsCount
* @param {Object} editorState
* @param {String} styleName
* @return {String}
* @description return the number of individual highlights.
*/
export function getHighlightsCount(editorState, highlightType) {
const highlightsState = getHighlightsState(editorState);
if (highlightType !== undefined) {
if (highlightTypeValid(highlightType) === false) {
throw new Error(`Invalid highlight type '${highlightType}'`);
}
return highlightsState.lastHighlightIds[highlightType];
} else {
// count highlights of all types
return Object.keys(highlightsState.lastHighlightIds)
.reduce((count, key) => count + highlightsState.lastHighlightIds[key], 0);
}
}
/**
* @ngdoc method
* @name canAddHighlight
* @param {Object} editorState
* @param {String} highlightType
* @return {Boolean}
* @description return true if a highlight of type highlightType can be set
* for current selection
*/
export function canAddHighlight(editorState, highlightType) {
if (highlightTypeValid(highlightType) !== true) {
return false;
}
function characterHasAHighlightOfTheSameType(character) {
if (
character.getStyle()
.some((styleName) =>
this.styleNameBelongsToHighlight(styleName)
&& getHighlightType(styleName) === highlightType)
) {
return true;
}
return false;
}
if (editorState.getSelection().isCollapsed()) {
return false;
}
// selection is expanded to include edges
// so you can't add a highlight right next to another
const selection = resizeDraftSelection(
1,
1,
editorState.getSelection(),
editorState,
true,
);
return getDraftCharacterListForSelection(editorState, selection)
.some(characterHasAHighlightOfTheSameType.bind(this)) === false;
}
/**
* @ngdoc method
* @name getHighlightStyleAtOffset
* @param {Object} editorState
* @param {List} types
* @param {Object} selection
* @param {Integer} offset
* @param {Boolean} fromEnd - if true start from end of the selection
* @param {Boolean} firstFound - if true return first style found otherwise return all styles found
* @description the highlight style from the new possition specified by offset.
*/
export function getHighlightStyleAtOffset(
editorState, types, selection, offset, fromEnd = false, firstFound = true): string | Array<string> {
const {block, newOffset} = getBlockAndOffset(editorState, selection, offset, fromEnd);
if (block == null) {
return null;
}
const inlineStyles = block.getInlineStyleAt(newOffset);
let highlightStyle = firstFound ? null : [];
inlineStyles.forEach((style) => {
if (styleNameBelongsToHighlight(style)) {
const type = getHighlightTypeFromStyleName(style);
if (type != null && types.indexOf(type) !== -1 && type !== style) {
if (firstFound) {
highlightStyle = highlightStyle || style;
} else {
highlightStyle.push(style);
}
}
}
});
return highlightStyle;
}
/**
* @ngdoc method
* @name getHighlightStyleAtCurrentPosition
* @param {Object} editorState
* @param {List} types
* @param {Boolean} fromEnd - if true start from end of the selection
* @param {Boolean} firstFound - if true return first style found otherwise return all styles found
* @return {String}
* @description return the style of type for current character position.
*/
export function getHighlightStyleAtCurrentPosition(editorState, types, fromEnd = false, firstFound = true) {
const selection = editorState.getSelection();
return getHighlightStyleAtOffset(editorState, types, selection, 0, fromEnd, firstFound);
}
/**
* @ngdoc method
* @name getHighlightData
* @param {Object} editorstate
* @return {String} style
* @description returns the data associated to the style.
*/
export function getHighlightData(editorState, style) {
const highlightsState = getHighlightsState(editorState);
if (highlightsState.highlightsData[style] === undefined) {
throw new Error('Highlight doesn\'t exist.');
}
return highlightsState.highlightsData[style];
}
/**
* @ngdoc method
* @name getHighlightAuthor
* @param {Object} editorstate
* @return {String} style
* @description returns the author associated to the style.
*/
export function getHighlightAuthor(editorState, style) {
const data = getHighlightData(editorState, style);
if (data == null) {
return null;
}
return data.author;
}
/**
* @ngdoc method
* @name getHighlightDataAtOffset
* @param {Object} editorState
* @param {Array} types
* @param {Object} selection
* @param {Integer} offset
* @return {Object}
* @description the highlight associated data from the new possition specified by offset.
*/
export function getHighlightDataAtOffset(editorState, types, selection, offset, fromEnd = false) {
const style = getHighlightStyleAtOffset(editorState, types, selection, offset, fromEnd) as string;
if (style == null) {
return null;
}
const highlightsState = getHighlightsState(editorState);
return highlightsState.highlightsData[style];
}
/**
* @ngdoc method
* @name getHighlightDataAtCurrentPosition
* @param {Object} editorState
* @param {Array} types
* @return {Object}
* @description the highlight associated data from current position.
*/
export function getHighlightDataAtCurrentPosition(editorState, types) {
const selection = editorState.getSelection();
return getHighlightDataAtOffset(editorState, types, selection, 0);
}
/**
* Test if current selection has given type style applied
*
* @param {EditorState} editorState
* @param {String} type
* @returns {Boolean}
*/
function selectionHasType(editorState, type) {
const characters = getDraftCharacterListForSelection(editorState, editorState.getSelection());
return characters.every((character) => character.getStyle().some((style) => style.indexOf(type) === 0));
}
/**
* @ngdoc method
* @name addHighlight
* @param {Object} editorState
* @param {String} type
* @param {Object} data
* @return {Object} new editor state
* @description add a new highlight for the current selection.
*/
export function addHighlight(editorState, type, data, single = false) {
let nextEditorState = editorState;
const initialSelection = nextEditorState.getSelection().merge({
hasFocus: true,
});
const highlightsState = getHighlightsState(nextEditorState);
if (highlightTypeValid(type) !== true) {
throw new Error('Highlight type invalid');
}
if (single && selectionHasType(editorState, type)) {
return editorState;
}
let newIndex = 0;
if (highlightsState.lastHighlightIds && has(highlightsState.lastHighlightIds, type)) {
newIndex = highlightsState.lastHighlightIds[type] + 1;
}
const styleName = type + '-' + newIndex;
const newHighlightsState = {
lastHighlightIds: {
...highlightsState.lastHighlightIds,
[type]: newIndex,
},
highlightsStyleMap: {
...highlightsState.highlightsStyleMap,
[styleName]: getAvailableHighlights()[type],
},
highlightsData: {
...highlightsState.highlightsData,
[styleName]: {
...data,
type,
},
},
};
nextEditorState = RichUtils.toggleInlineStyle(nextEditorState, styleName);
// prevent recording the changes to undo stack so user doesn't have to undo twice
// for the highlight to be completelly(both inline styles and related data) undone
nextEditorState = EditorState.set(nextEditorState, {allowUndo: false});
nextEditorState = setHighlightsState(nextEditorState, newHighlightsState);
// make sure the cursor is at the right position after undo
// it used to always end up at the position 0 of the first block
// when undoing after changing block data with `allowUndo` set to false
nextEditorState = EditorState.push(
nextEditorState,
nextEditorState.getCurrentContent().set('selectionBefore', initialSelection),
'change-block-data',
);
// restore focus lost after clicking a toolbar action or entering highlight data OR pushing editorState
// so the selection is visible after undo
nextEditorState = EditorState.acceptSelection(
nextEditorState,
initialSelection,
);
nextEditorState = EditorState.set(nextEditorState, {allowUndo: true});
return nextEditorState;
}
/**
* @ngdoc method
* @name updateHighlightData
* @param {Object} editorState
* @param {String} styleName
* @param {Object} nextData
* @return {Object} new editor state
* @description update highlight data associated to given style.
*/
export function updateHighlightData(editorState, styleName, nextData) {
const highlightsState = getHighlightsState(editorState);
if (highlightsState.highlightsData[styleName] === undefined) {
throw new Error('Highlight doesn\'t exist.');
}
const newHighlightsState = {
...highlightsState,
highlightsData: {
...highlightsState.highlightsData,
[styleName]: nextData,
},
};
return setHighlightsState(editorState, newHighlightsState);
}
/**
* @ngdoc method
* @name removeHighlight
* @param {Object} editorState
* @param {String} styleName
* @return {Object} new editor state
* @description for current selection remove the highlight style and associated data.
*/
export function removeHighlight(editorState, styleName) {
const highlightsState = getHighlightsState(editorState);
if (highlightsState.highlightsData[styleName] === undefined) {
return editorState;
}
const nextHighlightsStyleMap = {...highlightsState.highlightsStyleMap};
delete nextHighlightsStyleMap[styleName];
const nextHighlightsData = {...highlightsState.highlightsData};
delete nextHighlightsData[styleName];
const newHighlightsState = {
lastHighlightIds: highlightsState.lastHighlightIds,
highlightsStyleMap: nextHighlightsStyleMap,
highlightsData: nextHighlightsData,
};
let newEditorState = clearInlineStyles(
editorState,
getDraftSelectionForEntireContent(editorState),
[styleName],
);
// prevent recording the changes to undo stack so user doesn't have to undo twice
// for the highlight deletion to be completelly(both inline styles and related data) undone
newEditorState = EditorState.set(newEditorState, {allowUndo: false});
newEditorState = setHighlightsState(newEditorState, newHighlightsState);
newEditorState = EditorState.set(newEditorState, {allowUndo: true});
return newEditorState;
}
export function hadHighlightsChanged(prevEditorState, nextEditorState) {
return getHighlightsState(prevEditorState) !== getHighlightsState(nextEditorState);
}
/**
* @ngdoc method
* @name resetHighlightForCurrentSelection
* @param {Object} editorState
* @param {String} style
* @return {Object} editor state
* @description For current selection reset the highlight style. If it was the last
* selection with given style, delete the associated data too.
*/
export function resetHighlightForCurrentSelection(editorState, style) {
const type = getHighlightType(style);
const selection = editorState.getSelection();
const content = editorState.getCurrentContent();
const block = content.getBlockForKey(selection.getStartKey());
const offset = selection.getStartOffset() === block.getLength() - 1 ? 1 : 0;
const styleBefore = getHighlightStyleAtOffset(editorState, [type], selection, -1);
const styleAfter = getHighlightStyleAtOffset(editorState, [type], selection, offset, true);
if (styleBefore !== style && styleAfter !== style) {
return removeHighlight(editorState, style);
}
return RichUtils.toggleInlineStyle(editorState, style);
}
/**
* @ngdoc method
* @name getCharByOffset
* @param {Object} content
* @param {Object} selection
* @param {Integer} offset
* @return {Char} return char
* @description the char from the new possition specified by offset.
*/
export function getCharByOffset(editorState, selection, offset) {
const {block, newOffset} = getBlockAndOffset(editorState, selection, offset);
if (block == null) {
return null;
}
return block.getText()[newOffset];
}
/**
* @ngdoc method
* @name changeEditorSelection
* @param {Object} editorState
* @param {Integer} startOffset - the anchor offset relative to current start offset
* @param {Integer} endOffset - the focus offset relative to current end offset
* @param {Boolean} force - apply accept or force selection
* @return {Object} returns new state
* @description Change the current editor selection.
*/
export function changeEditorSelection(editorState, startOffset, endOffset, force) {
const selection = editorState.getSelection();
const {block: startBlock, newOffset: newStartOffset} = getBlockAndOffset(
editorState, selection, startOffset, false);
const {block: endBlock, newOffset: newEndOffset} = getBlockAndOffset(
editorState, selection, endOffset, true);
if (startBlock == null || endBlock == null) {
return editorState;
}
const newSelection = selection.merge({
anchorOffset: newStartOffset,
anchorKey: startBlock.getKey(),
focusOffset: newEndOffset,
focusKey: endBlock.getKey(),
isBackward: false,
});
if (force) {
return EditorState.forceSelection(editorState, newSelection);
}
return EditorState.acceptSelection(editorState, newSelection);
}
/**
* @ngdoc method
* @name getBlockAndOffset
* @param {Object} editorState
* @param {Object} selection
* @param {Integer} offset
* @param {Boolean} startFromEnd
* @return {Object} return block and offset at new position
* @description find the block and offset for the new position specified by offset starting
* from beggining of selection if startFromEnd is false or from end of selection otherwise.
*/
export const getBlockAndOffset = (
editorState,
selection,
offset,
startFromEnd = false,
limitedToSingleBlock = false,
) => {
const noValue = {block: null, newOffset: null};
const content = editorState.getCurrentContent();
let newOffset;
let block;
if (startFromEnd) {
newOffset = selection.getEndOffset() + offset;
block = content.getBlockForKey(selection.getEndKey());
} else {
newOffset = selection.getStartOffset() + offset;
block = content.getBlockForKey(selection.getStartKey());
}
if (block == null) {
return noValue;
}
if (limitedToSingleBlock === true) {
const offsetWithinBlock = startFromEnd === true
? Math.min(newOffset, block.getLength())
: Math.max(newOffset, 0);
return {block: block, newOffset: offsetWithinBlock};
}
while (newOffset < 0) {
block = content.getBlockBefore(block.getKey());
if (block == null) {
return noValue;
}
newOffset = block.getLength() + newOffset + 1;
}
while (newOffset > block.getLength()) {
newOffset = newOffset - block.getLength() - 1;
block = content.getBlockAfter(block.getKey());
if (block == null) {
return noValue;
}
}
return {block, newOffset};
};
function getLeftRangeAndTextForStyle(editorState, style) {
const type = getHighlightTypeFromStyleName(style);
const selection = editorState.getSelection();
const content = editorState.getCurrentContent();
let startBlock = content.getBlockForKey(selection.getStartKey());
let startOffset = selection.getStartOffset();
let startText = '';
let block = startBlock;
let offset = startOffset < block.getLength() ? startOffset : block.getLength() - 1;
let characterMetadataList;
let characterMetadata;
let blockText;
let found;
let newBlock = false;
if (paragraphSuggestionTypes.indexOf(type) !== -1) {
startText = block.getText()
.substring(0, startOffset) + paragraphSeparator;
return {startOffset, startBlock, startText};
}
while (block) {
if (block.getLength() === 0) {
block = content.getBlockBefore(block.getKey());
continue;
}
found = false;
offset = (offset == null) ? (block.getLength() - 1) : offset;
characterMetadataList = block.getCharacterList();
blockText = block.getText();
for (let i = offset; i >= 0; i--) {
characterMetadata = characterMetadataList.get(i);
if (!characterMetadata.hasStyle(style)) {
continue;
}
if (newBlock) {
startText = paragraphSeparator + startText;
newBlock = false;
}
startText = blockText[i] + startText;
startOffset = i;
startBlock = block;
found = true;
}
if (found) {
block = content.getBlockBefore(block.getKey());
offset = null;
newBlock = true;
} else {
block = null;
}
}
return {startOffset, startBlock, startText};
}
function getRightRangeAndTextForStyle(editorState, style) {
/* eslint-disable complexity */
const type = getHighlightTypeFromStyleName(style);
const selection = editorState.getSelection();
const content = editorState.getCurrentContent();
let endText = '';
let endBlock = content.getBlockForKey(selection.getStartKey());
let endOffset = selection.getStartOffset() + 1;
let block = endBlock;
let offset = endOffset;
let characterMetadataList;
let characterMetadata;
let blockText;
let found;
let newBlock = false;
if (paragraphSuggestionTypes.indexOf(type) !== -1) {
endText = block.getText()
.substring(endOffset);
if (endText === '') {
block = content.getBlockAfter(block.getKey());
endText = block.getText();
}
return {endOffset, endBlock, endText};
}
if (block.getLength() === offset && offset !== 0) {
block = content.getBlockAfter(block.getKey());
if (block == null) {
return {endOffset, endBlock, endText};
}
offset = null;
newBlock = true;
}
while (block) {
if (block.getLength() === 0) {
block = content.getBlockAfter(block.getKey());
continue;
}
found = false;
offset = offset == null ? 0 : offset;
characterMetadataList = block.getCharacterList();
blockText = block.getText();
for (let i = offset; i < block.getLength(); i++) {
characterMetadata = characterMetadataList.get(i);
if (!characterMetadata.hasStyle(style)) {
continue;
}
if (newBlock) {
endText = endText + paragraphSeparator;
newBlock = false;
}
endText = endText + blockText[i];
endOffset = i + 1;
endBlock = block;
found = true;
}
if (found) {
block = content.getBlockAfter(block.getKey());
offset = null;
newBlock = true;
} else {
block = null;
}
}
return {endOffset, endBlock, endText};
}
/**
* @ngdoc method
* @name getRangeAndTextForStyle
* @param {Object} editorState
* @param {String} style
* @return {Object} return a selection and text that are associated to given highlight style.
*/
export function getRangeAndTextForStyle(editorState, style, leftOnly = false) {
const selection = editorState.getSelection();
if (selection.isCollapsed() === false) {
throw new Error('Only collapsed selection supported');
}
const {startOffset, startBlock, startText} = getLeftRangeAndTextForStyle(editorState, style);
const {endOffset, endBlock, endText} = getRightRangeAndTextForStyle(editorState, style);
const newSelection = selection.merge({
anchorOffset: startOffset,
anchorKey: startBlock.getKey(),
focusOffset: leftOnly ? selection.getEndOffset() : endOffset,
focusKey: leftOnly ? selection.getEndKey() : endBlock.getKey(),
isBackward: false,
});
return {
selection: newSelection,
highlightedText: leftOnly ? startText + endText[0] : startText + endText,
};
}
export function getRangeAndTextForStyleInRawState(rawEditorState, highlightId) {
let highlightedText = '';
for (const {inlineStyleRanges, text} of rawEditorState.blocks) {
for (const {offset, length, style} of inlineStyleRanges) {
if (style === highlightId) {
const textRange = text.substring(offset, offset + length);
highlightedText = highlightedText.length > 0
? highlightedText = `${highlightedText}${paragraphSeparator}${textRange}`
: highlightedText = textRange;
}
}
}
return {highlightedText};
}
/**
* @ngdoc method
* @name isPeerHighlight
* @param {Object} editorState
* @param {String} style
* @param {String} type
* @param {String} author
* @return {Object} return true if the current suggestion is a complementary to type
* (one is ADD_SUGGESTION and one is DELETE_SUGGESTION) and they have the same author
*/
function isPeerHighlight(editorState, style, type, author) {
return style != null && getHighlightType(style) !== type
&& getHighlightAuthor(editorState, style) === author;
}
/**
* @ngdoc method
* @name getSuggestionData
* @param {Object} editorState
* @param {String} style
* @return {Object} return the data associated with a suggestion; for replace suggestion returns
* both old text and the suggested text and the selection returned wrap both texts.
*/
export function getSuggestionData(editorState, styleName) {
const type = getHighlightType(styleName);
const {selection, highlightedText} = getRangeAndTextForStyle(editorState, styleName);
const data = {
...getHighlightData(editorState, styleName),
suggestionText: highlightedText,
selection: selection,
styleName: styleName,
};
if (changeSuggestionsTypes.indexOf(type) === -1) {
return data;
}
let peerStyleName = getHighlightStyleAtOffset(editorState, changeSuggestionsTypes, selection, 0, true);
let afterPeer = true;
if (!isPeerHighlight(editorState, peerStyleName, type, data.author)) {
peerStyleName = getHighlightStyleAtOffset(editorState, changeSuggestionsTypes, selection, -1, false);
if (!isPeerHighlight(editorState, peerStyleName, type, data.author)) {
return data;
}
afterPeer = false;
}
const beforeSelection = selection.merge({
anchorOffset: selection.getStartOffset(),
anchorKey: selection.getStartKey(),
focusOffset: selection.getStartOffset(),
focusKey: selection.getStartKey(),
isBackward: false,
});
const afterSelection = selection.merge({
anchorOffset: selection.getEndOffset(),
anchorKey: selection.getEndKey(),
focusOffset: selection.getEndOffset(),
focusKey: selection.getEndKey(),
isBackward: false,
});
const beforeEditorState = EditorState.acceptSelection(editorState, beforeSelection);
const afterEditorState = EditorState.acceptSelection(editorState, afterSelection);
const peerRangeAndText = getRangeAndTextForStyle(afterPeer ? afterEditorState : beforeEditorState, peerStyleName);
const suggestionSelection = selection.merge({
anchorOffset: afterPeer ? selection.getStartOffset() : peerRangeAndText.selection.getStartOffset(),
anchorKey: afterPeer ? selection.getStartKey() : peerRangeAndText.selection.getStartKey(),
focusOffset: afterPeer ? peerRangeAndText.selection.getEndOffset() : selection.getEndOffset(),
focusKey: afterPeer ? peerRangeAndText.selection.getEndKey() : selection.getEndKey(),
isBackward: false,
});
if (type === 'ADD_SUGGESTION') {
return {
...data,
type: 'REPLACE_SUGGESTION',
oldText: peerRangeAndText.highlightedText,
selection: suggestionSelection,
};
}
return {
...data,
type: 'REPLACE_SUGGESTION',
suggestionText: peerRangeAndText.highlightedText,
oldText: data.suggestionText,
selection: suggestionSelection,
};
}
/**
* @ngdoc method
* @name addCommentsForServer
* @param {EditorState} editorState
* @return {EditorState}
*/
function addCommentsForServer(editorState) {
const multipleHighlights = getCustomDataFromEditor(editorState, editor3DataKeys.MULTIPLE_HIGHLIGHTS);
if (multipleHighlights === undefined || multipleHighlights.highlightsData === undefined) {
return editorState;
}
const highlightsData = multipleHighlights.highlightsData;
const comments = Object.keys(highlightsData)
.filter((key) => key.indexOf(getHighlightsConfig().COMMENT.type) === 0)
.map((key) => highlightsData[key].data);
return setCustomDataForEditor__deprecated(editorState, editor3DataKeys.__PUBLIC_API__comments, comments);
}
/**
* @ngdoc method
* @name applyHighlightsStyleMap
* @param {EditorState} editorState
* @return {EditorState}
* @description highlightsStyleMap is not stored on the server and needs to be generated
* If it were stored, changing highights' styles wouldn't be possible for already existing highlights
*/
function applyHighlightsStyleMap(editorState): EditorState {
const highlights = getCustomDataFromEditor(editorState, editor3DataKeys.MULTIPLE_HIGHLIGHTS);
if (highlights === undefined || highlights.highlightsData === undefined) {
return editorState;
}
const highlightsWithStyleMapApplied = {
...highlights,
highlightsStyleMap: Object.keys(highlights.highlightsData).reduce((obj, styleName) => {
obj[styleName] = getAvailableHighlights()[getHighlightType(styleName)];
return obj;
}, {}),
};
return setCustomDataForEditor__deprecated(
editorState,
editor3DataKeys.MULTIPLE_HIGHLIGHTS,
highlightsWithStyleMapApplied,
);
}
export const initializeHighlights = applyHighlightsStyleMap;
/**
* @ngdoc method
* @name removeHighlightsStyleMap
* @param {EditorState} editorState
* @return {EditorState}
* @description highlightsStyleMap needs to be removed, since it's generated data
* and not removing it, might lead to old styles being used
*/
function removeHighlightsStyleMap(editorState) {
const highlights = getCustomDataFromEditor(editorState, editor3DataKeys.MULTIPLE_HIGHLIGHTS);
const nextHighlights = {...highlights};
delete nextHighlights.highlightsStyleMap;
return setCustomDataForEditor__deprecated(
editorState,
editor3DataKeys.MULTIPLE_HIGHLIGHTS,
nextHighlights,
);
}
export const prepareHighlightsForExport = (editorState) => addCommentsForServer(removeHighlightsStyleMap(editorState));
/**
* Highlight current entity
*
* @param {EditorState} initialState
* @param {String} type
* @param {Object} data
* @param {Boolean} single
* @returns {EditorState}
*/
export function highlightEntity(initialState, type, data, single) {
let editorState = initialState;
const selection = editorState.getSelection();
const content = editorState.getCurrentContent();
const block = content.getBlockForKey(selection.getStartKey());
const entity = block.getEntityAt(selection.getStartOffset());
block.findEntityRanges((characterMeta) => characterMeta.getEntity() === entity,
(start, end) => {
editorState = EditorState.acceptSelection(editorState, selection.merge({
isBackward: false,
anchorOffset: start,
focusOffset: end,
}));
editorState = addHighlight(editorState, type, data, single);
editorState = EditorState.push(editorState, editorState.getCurrentContent(), 'change-block-data');
editorState = EditorState.acceptSelection(editorState, selection);
});
return editorState;
}