From 4ed4fff309c2b893643dde9778538b8563c50089 Mon Sep 17 00:00:00 2001 From: Jonathon Herbert Date: Sat, 3 Aug 2024 10:33:00 +0100 Subject: [PATCH] Add wavy underline on error, using widget decoration as position anchor as getting position of char from ProseMirror was a bit flaky; problems at start of doc, end of doc --- prosemirror-client/src/cqlInput/CqlInput.ts | 18 ++++------- .../src/cqlInput/ErrorPopover.ts | 30 +++++++++---------- prosemirror-client/src/cqlInput/Popover.ts | 24 ++++----------- prosemirror-client/src/cqlInput/plugin.ts | 14 +++++++-- prosemirror-client/src/cqlInput/utils.ts | 11 +++++++ 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/prosemirror-client/src/cqlInput/CqlInput.ts b/prosemirror-client/src/cqlInput/CqlInput.ts index 570d103..977863e 100644 --- a/prosemirror-client/src/cqlInput/CqlInput.ts +++ b/prosemirror-client/src/cqlInput/CqlInput.ts @@ -56,6 +56,7 @@ template.innerHTML = ` } #cql-input { + display: block; position: relative; padding: 5px; font-size: ${baseFontSize}px; @@ -98,29 +99,22 @@ template.innerHTML = ` } .Cql__TypeaheadPopover, .Cql__ErrorPopover { + position: absolute; width: 500px; margin: 0; padding: 0; - top: anchor(end); font-size: ${baseFontSize}px; border-radius: ${baseBorderRadius}px; - position-anchor: --cql-input; overflow: visible; } .Cql__ErrorPopover { width: max-content; + background: transparent; + border: none; + color: red; } - .Cql__PopoverArrow { - position: absolute; - width: 0; - height: 0; - border-left: ${popoverArrowSize}px solid transparent; - border-right: ${popoverArrowSize}px solid transparent; - border-bottom: ${popoverArrowSize}px solid white; - top: -${popoverArrowSize}px; - } `; @@ -144,7 +138,7 @@ export const createCqlInput = ( shadow.innerHTML = `
-
+
~
`; shadow.appendChild(template.content.cloneNode(true)); const cqlInput = shadow.getElementById(cqlInputId)!; diff --git a/prosemirror-client/src/cqlInput/ErrorPopover.ts b/prosemirror-client/src/cqlInput/ErrorPopover.ts index 5a01bec..903810e 100644 --- a/prosemirror-client/src/cqlInput/ErrorPopover.ts +++ b/prosemirror-client/src/cqlInput/ErrorPopover.ts @@ -2,11 +2,11 @@ import { Mapping } from "prosemirror-transform"; import { EditorView } from "prosemirror-view"; import { CqlError } from "../services/CqlService"; import { Popover, VirtualElement } from "./Popover"; +import { ERROR_CLASS } from "./plugin"; export class ErrorPopover extends Popover { private debugContainer: HTMLElement | undefined; private contentEl: HTMLElement; - private arrowEl: HTMLElement; public constructor( public view: EditorView, @@ -15,10 +15,6 @@ export class ErrorPopover extends Popover { ) { super(view, popoverEl); - this.arrowEl = document.createElement("div"); - this.arrowEl.classList.add("Cql__PopoverArrow"); - popoverEl.appendChild(this.arrowEl); - this.contentEl = document.createElement("div"); popoverEl.appendChild(this.contentEl); @@ -30,10 +26,7 @@ export class ErrorPopover extends Popover { popoverEl.hidePopover?.(); } - public updateErrorMessage = async ( - error: CqlError | undefined, - mapping: Mapping - ) => { + public updateErrorMessage = async (error: CqlError | undefined) => { if (!error) { this.contentEl.innerHTML = ""; this.popoverEl.hidePopover?.(); @@ -42,14 +35,18 @@ export class ErrorPopover extends Popover { this.updateDebugContainer(error); - this.contentEl.innerHTML = error.message; + const referenceEl = this.view.dom.getElementsByClassName(ERROR_CLASS)?.[0]; - const referenceEl = this.getVirtualElementFromView( - error.position ? mapping.map(error.position) - 1 : undefined - ); + if (!referenceEl) { + console.warn( + "Attempt to render element popover, but no position widget found in document" + ); + return; + } - const xOffset = -30; - await this.renderPopover(referenceEl, this.arrowEl, xOffset); + const xOffset = 0; + const yOffset = -25; + await this.renderPopover(referenceEl, xOffset, yOffset); this.popoverEl.showPopover?.(); }; @@ -57,7 +54,7 @@ export class ErrorPopover extends Popover { private updateDebugContainer = (error: CqlError) => { if (this.debugContainer) { this.debugContainer.innerHTML = `
-

Error

+

Error

Position: ${error.position ?? "No position given"}
Message: ${error.message}
`; @@ -70,6 +67,7 @@ export class ErrorPopover extends Popover { if (position) { try { const { top, right, bottom, left } = this.view.coordsAtPos(position); + return { getBoundingClientRect: () => ({ width: right - left, diff --git a/prosemirror-client/src/cqlInput/Popover.ts b/prosemirror-client/src/cqlInput/Popover.ts index fc37b0f..ca7211c 100644 --- a/prosemirror-client/src/cqlInput/Popover.ts +++ b/prosemirror-client/src/cqlInput/Popover.ts @@ -1,4 +1,4 @@ -import { arrow, computePosition, flip, offset, shift } from "@floating-ui/dom"; +import { computePosition, flip, offset, shift } from "@floating-ui/dom"; import { EditorView } from "prosemirror-view"; export type VirtualElement = { @@ -22,31 +22,19 @@ export abstract class Popover { protected async renderPopover( referenceElement: VirtualElement, - arrowEl?: HTMLElement, - xOffset: number = 0 + xOffset: number = 0, + yOffset: number = 0 ) { - const { - x, - y, - middlewareData: { arrow: arrowData }, - } = await computePosition(referenceElement, this.popoverEl, { + const { x, y } = await computePosition(referenceElement, this.popoverEl, { placement: "bottom-start", middleware: [ flip(), shift(), - offset({ mainAxis: 15, crossAxis: xOffset }), - ...(arrowEl ? [arrow({ element: arrowEl })] : []), + offset({ mainAxis: yOffset, crossAxis: xOffset }), ], }); this.popoverEl.style.left = `${x}px`; - this.popoverEl.style.right = `${y}px`; - - if (arrowEl && arrowData) { - const { x, y } = arrowData; - - arrowEl.style.left = x !== undefined ? `${x}px` : ""; - arrowEl.style.top = y !== undefined ? `${y}px` : ""; - } + this.popoverEl.style.top = `${y}px`; } } diff --git a/prosemirror-client/src/cqlInput/plugin.ts b/prosemirror-client/src/cqlInput/plugin.ts index 8767b02..b00fcbd 100644 --- a/prosemirror-client/src/cqlInput/plugin.ts +++ b/prosemirror-client/src/cqlInput/plugin.ts @@ -20,6 +20,7 @@ import { toProseMirrorTokens, ProseMirrorToken, applyDeleteIntent, + errorToDecoration, } from "./utils"; import { Mapping } from "prosemirror-transform"; import { TypeaheadPopover } from "./TypeaheadPopover"; @@ -45,6 +46,8 @@ type ServiceState = { const NEW_STATE = "NEW_STATE"; +export const ERROR_CLASS = "Cql__ErrorWidget"; + /** * The CQL plugin handles most aspects of the editor behaviour, including * - fetching results from the language server, and applying them to the document @@ -178,9 +181,16 @@ export const createCqlPlugin = ({ }, }, decorations: (state) => { - const { tokens } = cqlPluginKey.getState(state)!; + const { tokens, error, mapping } = cqlPluginKey.getState(state)!; + + const maybeErrorDeco = error?.position + ? [errorToDecoration(mapping.map(error.position))] + : []; - return DecorationSet.create(state.doc, tokensToDecorations(tokens)); + return DecorationSet.create(state.doc, [ + ...maybeErrorDeco, + ...tokensToDecorations(tokens), + ]); }, handleKeyDown(view, event) { switch (event.code) { diff --git a/prosemirror-client/src/cqlInput/utils.ts b/prosemirror-client/src/cqlInput/utils.ts index 5ac4cbb..d53dc59 100644 --- a/prosemirror-client/src/cqlInput/utils.ts +++ b/prosemirror-client/src/cqlInput/utils.ts @@ -12,6 +12,7 @@ import { } from "./schema"; import { Node, NodeType } from "prosemirror-model"; import { Selection, TextSelection } from "prosemirror-state"; +import { ERROR_CLASS } from "./plugin"; const tokensToPreserve = ["QUERY_FIELD_KEY", "QUERY_VALUE"]; @@ -326,3 +327,13 @@ export const logNode = (doc: Node) => { ); }); }; + +export const errorToDecoration = (position: number): Decoration => { + const toDOM = () => { + const el = document.createElement("span"); + el.classList.add(ERROR_CLASS); + return el; + }; + + return Decoration.widget(position, toDOM); +};