2022-11-18 15:04:37 +00:00
|
|
|
// Forked from https://codeberg.org/retronav/ixora
|
|
|
|
// Original author: Pranav Karawale
|
|
|
|
// License: Apache License 2.0.
|
|
|
|
import {
|
|
|
|
Decoration,
|
2022-12-09 15:09:53 +00:00
|
|
|
DecorationSet,
|
2022-11-18 15:04:37 +00:00
|
|
|
EditorState,
|
|
|
|
EditorView,
|
|
|
|
foldedRanges,
|
2022-12-09 15:09:53 +00:00
|
|
|
StateField,
|
|
|
|
Transaction,
|
2022-11-28 15:42:54 +00:00
|
|
|
WidgetType,
|
2022-11-18 15:04:37 +00:00
|
|
|
} from "../deps.ts";
|
|
|
|
|
2022-11-30 14:28:43 +00:00
|
|
|
type LinkOptions = {
|
|
|
|
text: string;
|
|
|
|
href?: string;
|
|
|
|
title: string;
|
|
|
|
cssClass: string;
|
|
|
|
callback: (e: MouseEvent) => void;
|
|
|
|
};
|
2022-11-28 15:42:54 +00:00
|
|
|
export class LinkWidget extends WidgetType {
|
|
|
|
constructor(
|
2022-11-30 14:28:43 +00:00
|
|
|
readonly options: LinkOptions,
|
2022-11-28 15:42:54 +00:00
|
|
|
) {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
toDOM(): HTMLElement {
|
|
|
|
const anchor = document.createElement("a");
|
2022-11-30 14:28:43 +00:00
|
|
|
anchor.className = this.options.cssClass;
|
|
|
|
anchor.textContent = this.options.text;
|
2022-11-28 15:42:54 +00:00
|
|
|
anchor.addEventListener("click", (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
2022-11-30 14:28:43 +00:00
|
|
|
this.options.callback(e);
|
2022-11-28 15:42:54 +00:00
|
|
|
});
|
2022-11-30 14:28:43 +00:00
|
|
|
anchor.setAttribute("title", this.options.title);
|
|
|
|
anchor.href = this.options.href || "#";
|
2022-11-28 15:42:54 +00:00
|
|
|
return anchor;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-09 15:09:53 +00:00
|
|
|
export function decoratorStateField(
|
|
|
|
stateToDecoratorMapper: (state: EditorState) => DecorationSet,
|
|
|
|
) {
|
|
|
|
return StateField.define<DecorationSet>({
|
|
|
|
create(state: EditorState) {
|
|
|
|
return stateToDecoratorMapper(state);
|
|
|
|
},
|
|
|
|
|
|
|
|
update(value: DecorationSet, tr: Transaction) {
|
|
|
|
// if (tr.docChanged || tr.selection) {
|
|
|
|
return stateToDecoratorMapper(tr.state);
|
|
|
|
// }
|
|
|
|
// return value;
|
|
|
|
},
|
|
|
|
|
|
|
|
provide: (f) => EditorView.decorations.from(f),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-29 08:11:23 +00:00
|
|
|
export class ButtonWidget extends WidgetType {
|
|
|
|
constructor(
|
|
|
|
readonly text: string,
|
|
|
|
readonly title: string,
|
|
|
|
readonly cssClass: string,
|
|
|
|
readonly callback: (e: MouseEvent) => void,
|
|
|
|
) {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
toDOM(): HTMLElement {
|
|
|
|
const anchor = document.createElement("button");
|
|
|
|
anchor.className = this.cssClass;
|
|
|
|
anchor.textContent = this.text;
|
|
|
|
anchor.addEventListener("click", (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
this.callback(e);
|
|
|
|
});
|
|
|
|
anchor.setAttribute("title", this.title);
|
|
|
|
return anchor;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-18 15:04:37 +00:00
|
|
|
/**
|
|
|
|
* Check if two ranges overlap
|
|
|
|
* Based on the visual diagram on https://stackoverflow.com/a/25369187
|
|
|
|
* @param range1 - Range 1
|
|
|
|
* @param range2 - Range 2
|
|
|
|
* @returns True if the ranges overlap
|
|
|
|
*/
|
|
|
|
export function checkRangeOverlap(
|
|
|
|
range1: [number, number],
|
|
|
|
range2: [number, number],
|
|
|
|
) {
|
|
|
|
return range1[0] <= range2[1] && range2[0] <= range1[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a range is inside another range
|
|
|
|
* @param parent - Parent (bigger) range
|
|
|
|
* @param child - Child (smaller) range
|
|
|
|
* @returns True if child is inside parent
|
|
|
|
*/
|
|
|
|
export function checkRangeSubset(
|
|
|
|
parent: [number, number],
|
|
|
|
child: [number, number],
|
|
|
|
) {
|
|
|
|
return child[0] >= parent[0] && child[1] <= parent[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if any of the editor cursors is in the given range
|
|
|
|
* @param state - Editor state
|
|
|
|
* @param range - Range to check
|
|
|
|
* @returns True if the cursor is in the range
|
|
|
|
*/
|
|
|
|
export function isCursorInRange(state: EditorState, range: [number, number]) {
|
|
|
|
return state.selection.ranges.some((selection) =>
|
|
|
|
checkRangeOverlap(range, [selection.from, selection.to])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Decoration to simply hide anything.
|
|
|
|
*/
|
|
|
|
export const invisibleDecoration = Decoration.replace({});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the lines of the editor that are in the given range and not folded.
|
|
|
|
* This function is of use when you need to get the lines of a particular
|
|
|
|
* block node and add line decorations to each line of it.
|
|
|
|
*
|
|
|
|
* @param view - Editor view
|
|
|
|
* @param from - Start of the range
|
|
|
|
* @param to - End of the range
|
|
|
|
* @returns A list of line blocks that are in the range
|
|
|
|
*/
|
|
|
|
export function editorLines(view: EditorView, from: number, to: number) {
|
|
|
|
let lines = view.viewportLineBlocks.filter((block) =>
|
|
|
|
// Keep lines that are in the range
|
|
|
|
checkRangeOverlap([block.from, block.to], [from, to])
|
|
|
|
);
|
|
|
|
|
|
|
|
const folded = foldedRanges(view.state).iter();
|
|
|
|
while (folded.value) {
|
|
|
|
lines = lines.filter(
|
|
|
|
(line) =>
|
|
|
|
!checkRangeOverlap(
|
|
|
|
[folded.from, folded.to],
|
|
|
|
[line.from, line.to],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
folded.next();
|
|
|
|
}
|
|
|
|
|
|
|
|
return lines;
|
|
|
|
}
|