@atlaskit/editor-plugin-selection-marker
Version:
Selection marker plugin for @atlaskit/editor-core.
124 lines (120 loc) • 5.47 kB
JavaScript
/**
* @jsxRuntime classic
* @jsx jsx
*/
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
import { Decoration } from '@atlaskit/editor-prosemirror/view';
const selectionMarkerHighlightStyles = {
content: "''",
position: 'absolute',
backgroundImage: "url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMyIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDMgMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMSAxSDBMMSAxLjg1NzE0VjE4LjE0MzNMMCAxOS4wMDA0SDNMMiAxOC4xNDMzVjEuODU3MTRMMyAxSDJIMVoiIGZpbGw9IiM1NzlERkYiLz4KPHJlY3QgeT0iMTkiIHdpZHRoPSIzIiBoZWlnaHQ9IjEiIGZpbGw9IiM1NzlERkYiLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMSIgZmlsbD0iIzU3OURGRiIvPgo8L3N2Zz4K')",
top: "var(--ds-space-0, 0px)",
bottom: "var(--ds-space-negative-025, -2px)",
backgroundRepeat: 'no-repeat',
backgroundPositionX: 'center',
backgroundPositionY: 'center',
backgroundSize: 'contain',
aspectRatio: '3/20',
left: '0px',
marginLeft: "var(--ds-space-negative-025, -2px)",
right: '0px',
marginRight: "var(--ds-space-negative-025, -2px)",
pointerEvents: 'none'
};
const selectionMarkerBlockCursorStyles = {
content: "''",
position: 'absolute',
background: "var(--ds-text, #292A2E)",
width: '1px',
display: 'inline-block',
top: "var(--ds-space-0, 0px)",
bottom: "var(--ds-space-negative-025, -2px)",
left: '1px',
marginLeft: "var(--ds-space-negative-025, -2px)",
right: '0px',
marginRight: "var(--ds-space-negative-025, -2px)",
pointerEvents: 'none'
};
// Same as above but defined as an inline element to avoid breaking long words
const selectionMarkerInlineCursorStyles = {
content: "''",
position: 'relative',
pointerEvents: 'none',
borderLeft: `${"var(--ds-border-width, 1px)"} solid ${"var(--ds-text, #292A2E)"}`,
marginLeft: '-1px',
left: '0.5px'
};
/**
* Converts a camelCased CSS property name to a hyphenated CSS property name.
*
* @param property - CamelCased CSS property name.
* @returns Hyphenated CSS property name.
*/
function hyphenate(property) {
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
return property.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`).replace(/^ms/, '-ms');
}
const Widget = ({
type,
isHighlight,
isInWord
}) => {
const span = document.createElement('span');
const selectionMarkerCursorStyles = isInWord ? selectionMarkerInlineCursorStyles : selectionMarkerBlockCursorStyles;
const styles = isHighlight ? selectionMarkerHighlightStyles : selectionMarkerCursorStyles;
for (const [rule, value] of Object.entries(styles)) {
span.style.setProperty(hyphenate(rule), value);
}
span.setAttribute('contentEditable', 'false');
span.dataset.testid = `selection-marker-${type}-cursor`;
return span;
};
const toDOM = (type, isHighlight, isInWord) => {
const element = document.createElement('span');
element.contentEditable = 'false';
element.setAttribute('style', `position: relative;`);
element.appendChild(Widget({
type,
isHighlight,
isInWord
}));
return element;
};
const containsText = resolvedPos => {
const {
nodeBefore,
nodeAfter
} = resolvedPos;
return (nodeBefore === null || nodeBefore === void 0 ? void 0 : nodeBefore.isInline) || (nodeAfter === null || nodeAfter === void 0 ? void 0 : nodeAfter.isInline);
};
export const createWidgetDecoration = (resolvedPos, type, selection, isHighlight) => {
var _nodeBefore$textConte, _nodeAfter$textConten;
// We don't want the cursor to show if it's not text selection
// ie. if it's on media selection
if (!(selection instanceof TextSelection) || containsText(resolvedPos) === false || !selection.empty) {
return [];
}
// We're inside a word if the parent, before, and after nodes are all text nodes
// and the before/after nodes are appended/prepended with non-whitespace characters
// Also if we're making a selection and not just a cursor, this isn't relevant
const {
nodeBefore,
nodeAfter,
parent
} = resolvedPos;
// Check if the parent is a text node and the before/after nodes are also text nodes
const areTextNodes = parent.isTextblock && (nodeBefore === null || nodeBefore === void 0 ? void 0 : nodeBefore.isText) && (nodeAfter === null || nodeAfter === void 0 ? void 0 : nodeAfter.isText);
const lastCharacterOfBeforeNode = nodeBefore === null || nodeBefore === void 0 ? void 0 : (_nodeBefore$textConte = nodeBefore.textContent) === null || _nodeBefore$textConte === void 0 ? void 0 : _nodeBefore$textConte.slice(-1);
const firstCharacterOfAfterNode = nodeAfter === null || nodeAfter === void 0 ? void 0 : (_nodeAfter$textConten = nodeAfter.textContent) === null || _nodeAfter$textConten === void 0 ? void 0 : _nodeAfter$textConten.slice(0, 1);
const areAdjacentCharactersNonWhitespace =
// @ts-ignore - TS1501 Older versions of TypeScript don't play nice with the u flag. With the current AFM TypeScript version, this *should* be fine, but the pipeline type check fails, hence why a ts-ignore is needed (over a ts-expect-error)
/\S/u.test(lastCharacterOfBeforeNode || '') && /\S/u.test(firstCharacterOfAfterNode || '');
const isInWord = Boolean(areTextNodes && areAdjacentCharactersNonWhitespace);
return [Decoration.widget(resolvedPos.pos, toDOM(type, isHighlight, isInWord), {
side: -1,
key: `${type}WidgetDecoration`,
stopEvent: () => true,
ignoreSelection: true
})];
};