This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
import ProseMirrorPlugin from "./plugin.mjs";
import {Plugin} from "prosemirror-state";
/**
* A class responsible for handling the display of automated link recommendations when a user highlights text in a
* ProseMirror editor.
* @param {EditorView} view The editor view.
*/
class PossibleMatchesTooltip {
/**
* @param {EditorView} view The editor view.
*/
constructor(view) {
this.update(view, null);
}
/* -------------------------------------------- */
/**
* A reference to any existing tooltip that has been generated as part of a highlight match.
* @type {HTMLElement}
*/
tooltip;
/* -------------------------------------------- */
/**
* Update the tooltip based on changes to the selected text.
* @param {EditorView} view The editor view.
* @param {State} lastState The previous state of the document.
*/
async update(view, lastState) {
if ( !game.settings.get("core", "pmHighlightDocumentMatches") ) return;
const state = view.state;
// Deactivate tooltip if the document/selection didn't change or is empty
const stateUnchanged = lastState && (lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection));
if ( stateUnchanged || state.selection.empty ) return this._deactivateTooltip();
const selection = state.selection.content().content;
const highlighted = selection.textBetween(0, selection.size);
// If the user selected fewer than a certain amount of characters appropriate for the language, we bail out.
if ( highlighted.length < CONFIG.i18n.searchMinimumCharacterLength ) return this._deactivateTooltip();
// Look for any matches based on the contents of the selection
let html = this._findMatches(highlighted);
// If html is an empty string bail out and deactivate tooltip
if ( !html ) return this._deactivateTooltip();
// Enrich the matches HTML to get proper content links
html = await TextEditor.enrichHTML(html);
html = html.replace(/data-tooltip="[^"]+"/g, "");
const {from, to} = state.selection;
// In-screen coordinates
const start = view.coordsAtPos(from);
const end = view.coordsAtPos(to);
// Position the tooltip. This needs to be very close to the user's cursor, otherwise the locked tooltip will be
// immediately dismissed for being too far from the tooltip.
// TODO: We use the selection endpoints here which works fine for single-line selections, but not multi-line.
const left = (start.left + 3) + "px";
const bottom = window.innerHeight - start.bottom + 25 + "px";
const position = {bottom, left};
if ( this.tooltip ) this._updateTooltip(html);
else this._createTooltip(position, html, {cssClass: "link-matches"});
}
/* -------------------------------------------- */
/**
* Create a locked tooltip at the given position.
* @param {object} position A position object with coordinates for where the tooltip should be placed
* @param {string} position.top Explicit top position for the tooltip
* @param {string} position.right Explicit right position for the tooltip
* @param {string} position.bottom Explicit bottom position for the tooltip
* @param {string} position.left Explicit left position for the tooltip
* @param {string} text Explicit tooltip text or HTML to display.
* @param {object} [options={}] Additional options which can override tooltip behavior.
* @param {array} [options.cssClass] An optional, space-separated list of CSS classes to apply to the activated
* tooltip.
*/
_createTooltip(position, text, options) {
this.tooltip = game.tooltip.createLockedTooltip(position, text, options);
}
/* -------------------------------------------- */
/**
* Update the tooltip with new HTML
* @param {string} html The HTML to be included in the tooltip
*/
_updateTooltip(html) {
this.tooltip.innerHTML = html;
}
/* -------------------------------------------- */
/**
* Dismiss all locked tooltips and set this tooltip to undefined.
*/
_deactivateTooltip() {
if ( !this.tooltip ) return;
game.tooltip.dismissLockedTooltip(this.tooltip);
this.tooltip = undefined;
}
/* -------------------------------------------- */
/**
* Find all Documents in the world/compendia with names that match the selection insensitive to case.
* @param {string} text A string which will be matched against document names
* @returns {string}
*/
_findMatches(text) {
let html = "";
const matches = game.documentIndex.lookup(text, { ownership: "OBSERVER" });
for ( const [type, collection] of Object.entries(matches) ) {
if ( collection.length === 0 ) continue;
html += `<section><h4>${type}</h4><p>`;
for ( const document of collection ) {
html += document.entry?.link ? document.entry.link : `@UUID[${document.uuid}]{${document.entry.name}}`;
}
html += "</p></section>";
}
return html;
}
}
/**
* A ProseMirrorPlugin wrapper around the {@link PossibleMatchesTooltip} class.
* @extends {ProseMirrorPlugin}
*/
export default class ProseMirrorHighlightMatchesPlugin extends ProseMirrorPlugin {
/**
* @param {Schema} schema The ProseMirror schema.
* @param {ProseMirrorMenuOptions} [options] Additional options to configure the plugin's behaviour.
*/
constructor(schema, options={}) {
super(schema);
this.options = options;
}
/* -------------------------------------------- */
/** @inheritdoc */
static build(schema, options={}) {
return new Plugin({
view(editorView) {
return new PossibleMatchesTooltip(editorView);
},
isHighlightMatchesPlugin: true
});
}
}