@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
146 lines (140 loc) • 4.95 kB
JavaScript
import { Mark } from '@atlaskit/editor-prosemirror/model';
import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
/**
* Determine if a mark of a specific type exists anywhere in the selection.
*/
export const anyMarkActive = (state, markType) => {
const {
$from,
from,
to,
empty
} = state.selection;
if (empty) {
return !!markType.isInSet(state.storedMarks || $from.marks());
}
let rangeHasMark = false;
if (state.selection instanceof CellSelection) {
state.selection.forEachCell((cell, cellPos) => {
const from = cellPos;
const to = cellPos + cell.nodeSize;
if (!rangeHasMark) {
rangeHasMark = state.doc.rangeHasMark(from, to, markType);
}
});
} else {
rangeHasMark = state.doc.rangeHasMark(from, to, markType);
}
return rangeHasMark;
};
const allMarksAreRuledOut = marks => Array.from(marks.values()).every(mark => !mark);
/**
* Check if a selection contains any of the specifiged marks. The whole selection
* must have the mark for it to be considered active.
* @param state The editor state
* @param markTypes The marks to check for
* @returns A map indicating which marks are present in the selection
* @example
* const markTypes = editorState.schema.marks;
* const activeMarks = wholeSelectionHasMarks(editorState, [markTypes.strong, markTypes.em, markTypes.etc]);
*/
export const wholeSelectionHasMarks = (state, markTypes) => {
const {
$from,
from,
to,
empty
} = state.selection;
if (empty) {
return new Map(markTypes.map(markType => [markType, !!markType.isInSet(state.storedMarks || $from.marks())]));
}
if (state.selection instanceof CellSelection) {
return cellSelectionHasMarks(state.doc, state.selection, markTypes);
}
return wholeRangeHasMarks(from, to, state.doc, markTypes);
};
const cellSelectionHasMarks = (doc, selection, markTypes) => {
// Warning: This is micro-optimized and is a total pain to actually read.
// Will attempt to explain in comments.
// Start with map of booleans for each mark
let cellsHaveMarks = new Map(markTypes.map(markType => [markType, true]));
selection.forEachCell((cell, cellPos) => {
// Early exit if all marks are already ruled out
if (allMarksAreRuledOut(cellsHaveMarks)) {
return;
}
const from = cellPos;
const to = cellPos + cell.nodeSize;
// On first cell just do a regular check
if (!cellsHaveMarks) {
cellsHaveMarks = wholeRangeHasMarks(from, to, doc, markTypes);
} else {
// Find the marks that are still true ie the ones that haven't been ruled out in
// previous cells. The idea here is to whittle down the list of marks so that we check
// less and less as we go, giving `wholeRangeHasMarks` more opportunities to exit early
const marksToCheck = [];
for (const [markType, hasMark] of cellsHaveMarks) {
if (hasMark) {
marksToCheck.push(markType);
}
}
// Look specifically for the marks that are not yet ruled out.
const cellHasMarks = wholeRangeHasMarks(from, to, doc, marksToCheck);
// Map these results back into the original array of results and repeat!
for (const [markType, hasMark] of cellHasMarks) {
cellsHaveMarks.set(markType, hasMark);
}
}
});
return cellsHaveMarks;
};
const wholeRangeHasMarks = (from, to, doc, markTypes) => {
const hasMarks = new Map(markTypes.map(markType => [markType, true]));
const hasNoMarks = new Map(markTypes.map(markType => [markType, false]));
let isTextContent = false;
doc.nodesBetween(from, to, node => {
if (allMarksAreRuledOut(hasMarks)) {
// This won't be a true early exit, but will prevent diving into nodes and
// any checks further down the function.
return false;
}
if (!node.type.isText) {
return true; // continue traversing
}
isTextContent = true;
for (const [markType, hasMark] of hasMarks) {
if (!hasMark) {
continue; // already ruled out the mark, skip further checks
}
const value = markType instanceof Mark ? markType.isInSet(node.marks) : node.marks.some(mark => mark.type === markType);
hasMarks.set(markType, value);
}
});
return isTextContent ? hasMarks : hasNoMarks;
};
export const isMarkAllowedInRange = (doc, ranges, type) => {
for (let i = 0; i < ranges.length; i++) {
const {
$from,
$to
} = ranges[i];
let can = $from.depth === 0 ? doc.type.allowsMarkType(type) : false;
doc.nodesBetween($from.pos, $to.pos, node => {
if (can) {
return false;
}
can = node.inlineContent && node.type.allowsMarkType(type);
return;
});
if (can) {
return can;
}
}
return false;
};
export const isMarkExcluded = (type, marks) => {
if (marks) {
return marks.some(mark => mark.type !== type && mark.type.excludes(type));
}
return false;
};