@atlaskit/editor-plugin-code-block-advanced
Version:
CodeBlockAdvanced plugin for @atlaskit/editor-core
157 lines (141 loc) • 5.13 kB
text/typescript
import type { Facet } from '@codemirror/state';
import { ViewPlugin, WidgetType, Decoration as CodeMirrorDecoration } from '@codemirror/view';
import type { EditorView as CodeMirror, DecorationSet, ViewUpdate } from '@codemirror/view';
import type { EditorView, Decoration, DecorationSource } from '@atlaskit/editor-prosemirror/view';
import { fg } from '@atlaskit/platform-feature-flags';
class PMWidget extends WidgetType {
constructor(readonly toDOMElement: HTMLElement) {
super();
}
toDOM() {
return this.toDOMElement;
}
ignoreEvent() {
return false;
}
}
// This type is not exposed publically by ProseMirror but we need it to map to CodeMirror
// See: https://github.com/ProseMirror/prosemirror-view/blob/master/src/decoration.ts
type WidgetConstructor = ((view: EditorView, getPos: () => number | undefined) => Node) | Node;
// This type is not exposed publically by ProseMirror but we need it to map to CodeMirror
// See: https://github.com/ProseMirror/prosemirror-view/blob/master/src/decoration.ts
interface ExtendedProseMirrorDecoration extends Decoration {
inline: boolean;
type: {
attrs?: Record<string, string>;
side?: number;
toDOM?: WidgetConstructor;
};
widget: boolean;
}
// This type is not exposed publically by ProseMirror but we need it to map to CodeMirror
// See: https://github.com/ProseMirror/prosemirror-view/blob/master/src/decoration.ts
function isExtendedDecoration(decoration: Decoration): decoration is ExtendedProseMirrorDecoration {
return (
(decoration as ExtendedProseMirrorDecoration).inline !== undefined &&
(decoration as ExtendedProseMirrorDecoration).widget !== undefined &&
(decoration as ExtendedProseMirrorDecoration).type !== undefined
);
}
const getHTMLElement = (
toDOM: WidgetConstructor | undefined,
view: EditorView,
getPos: () => number | undefined,
): HTMLElement | undefined => {
if (toDOM instanceof Function) {
const element = toDOM(view, getPos);
return element instanceof HTMLElement ? element : undefined;
} else if (toDOM instanceof HTMLElement) {
return toDOM;
}
};
const mapPMDecorationToCMDecoration = (
decoration: Decoration,
view: EditorView,
getPos: () => number | undefined,
) => {
if (!isExtendedDecoration(decoration)) {
return undefined;
}
if (decoration.inline) {
const markDecoration = CodeMirrorDecoration.mark({
attributes: decoration.type.attrs,
});
return markDecoration.range(decoration.from, decoration.to);
} else if (decoration.widget) {
const toDOM = getHTMLElement(decoration?.type?.toDOM, view, getPos);
if (!toDOM) {
return undefined;
}
const widgetDecoration = CodeMirrorDecoration.widget({
widget: new PMWidget(toDOM),
side: decoration.type.side,
});
return widgetDecoration.range(decoration.from, decoration.to);
}
};
function isDefined<TValue>(value: TValue | undefined): value is TValue {
return value !== undefined;
}
export const sortDecorationsByPositionAndSide = (
a: { from: number; value: { startSide: number } },
b: { from: number; value: { startSide: number } },
): number => a.from - b.from || a.value.startSide - b.value.startSide;
/**
* Creates CodeMirror versions of the decorations provided by ProseMirror.
*
* Inline ProseMirror decorations -> Mark CodeMirror decorations
* Widget ProseMirror decorations -> Widget CodeMirror decorations
*
* This way any decorations applied in ProseMirror land should automatically be supported
* by the CodeMirror editor
*
* @param updateDecorationsEffect Facet for the prosemirror decorations
* @returns CodeMirror extension
*/
export const prosemirrorDecorationPlugin = (
updateDecorationsEffect: Facet<DecorationSource>,
editorView: EditorView,
getPos: () => number | undefined,
): ViewPlugin<{
decorations: DecorationSet;
update: (update: ViewUpdate) => void;
updateDecorations: (view: CodeMirror) => DecorationSet;
}> =>
ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: CodeMirror) {
this.decorations = this.updateDecorations(view);
}
updateDecorations(view: CodeMirror) {
const { from, to } = view.viewport;
const innnerDecorations = view.state.facet(updateDecorationsEffect);
const allDecorations: Decoration[] = [];
innnerDecorations?.map((source) => {
source?.forEachSet((set) => {
const decorations = set
.find(from, to)
// Do not render the code block line decorations
.filter((dec) => dec.spec.type !== 'decorationWidgetType');
allDecorations.push(...decorations);
});
});
const cmDecorations = allDecorations
.sort((a, b) => (a.from < b.from ? -1 : 1))
.map((decoration) => mapPMDecorationToCMDecoration(decoration, editorView, getPos))
.filter(isDefined);
if (fg('platform_editor_fix_decoration_edge_case')) {
return CodeMirrorDecoration.set(cmDecorations.sort(sortDecorationsByPositionAndSide));
} else {
return CodeMirrorDecoration.set(cmDecorations);
}
}
update(update: ViewUpdate) {
this.decorations = this.updateDecorations(update.view);
}
},
{
decorations: (v) => v.decorations,
},
);