UNPKG

@atlaskit/editor-plugin-selection-marker

Version:

Selection marker plugin for @atlaskit/editor-core.

124 lines (120 loc) 5.47 kB
/** * @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 })]; };