Skip to content

Commit

Permalink
Merge pull request #589 from getformwork/feature/improved-tooltips
Browse files Browse the repository at this point in the history
Better tooltips positioning
  • Loading branch information
giuscris authored Oct 21, 2024
2 parents 70a9a94 + 14ed1b2 commit fd689c4
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 68 deletions.
13 changes: 10 additions & 3 deletions panel/src/scss/components/_tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
z-index: 20;
display: none;
min-width: 2rem;
padding: 0.25rem 0.5rem;
max-width: 20rem;
padding: 0.25rem 0.375rem;
border-radius: $border-radius;
background-color: var(--color-tooltip-inverted);
box-shadow: $box-shadow-md;
color: var(--color-base-900);
cursor: default;
font-size: $font-size-xs;
pointer-events: none;
text-align: center;
@include user-select-none;
}

.tooltip .icon {
min-width: auto;
min-height: auto;
}

.tooltip a {
color: inherit;
text-decoration: underline;
}
3 changes: 3 additions & 0 deletions panel/src/ts/components/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class Form {
form.addEventListener("submit", removeBeforeUnload);

$$('a[href]:not([href^="#"]):not([target="_blank"]):not([target^="formwork-"])').forEach((element: HTMLAnchorElement) => {
if (element.closest(".editor-wrap")) {
return;
}
element.addEventListener("click", (event) => {
if (this.hasChanged()) {
event.preventDefault();
Expand Down
4 changes: 2 additions & 2 deletions panel/src/ts/components/inputs/editor-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class EditorInput {
textarea.dispatchEvent(new Event("change", { bubbles: true }));
}, 500);

let editor: MarkdownView | CodeView = new MarkdownView(editorWrap, addBaseUri(textarea.value, baseUri), inputEventHandler, attributes);
let editor: MarkdownView | CodeView = new MarkdownView(editorWrap, addBaseUri(textarea.value, baseUri), inputEventHandler, attributes, baseUri);
editor.view.dom.style.height = `${textareaHeight}px`;

const codeSwitch = $("[data-command=toggle-markdown]", editorWrap) as HTMLButtonElement;
Expand All @@ -44,7 +44,7 @@ export class EditorInput {
editor.view.dom.style.height = `${textareaHeight}px`;
} else {
editor.destroy();
editor = new MarkdownView(editorWrap, addBaseUri(editor.content, baseUri), inputEventHandler, attributes);
editor = new MarkdownView(editorWrap, addBaseUri(editor.content, baseUri), inputEventHandler, attributes, baseUri);
editor.view.dom.style.height = `${textareaHeight}px`;
}
codeSwitch.blur();
Expand Down
73 changes: 35 additions & 38 deletions panel/src/ts/components/inputs/editor/markdown/linktooltip.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { EditorState, Plugin } from "prosemirror-state";
import { Mark, MarkType } from "prosemirror-model";
import { debounce } from "../../../../utils/events";
import { EditorView } from "prosemirror-view";
import { Mark } from "prosemirror-model";
import { passIcon } from "../../../icons";
import { Plugin } from "prosemirror-state";
import { schema } from "prosemirror-markdown";
import { Tooltip } from "../../../tooltip";

function addBaseUri(text: string, baseUri: string) {
return text.replace(/^\/?(?!https?:\/\/)(.+)/, `${baseUri}$1`);
}

class LinkTooltipView {
editorView: EditorView;
tooltip?: Tooltip;
currentLink?: Mark;
baseUri: string;

constructor(view: EditorView) {
constructor(view: EditorView, baseUri: string) {
this.editorView = view;

this.baseUri = baseUri;

this.editorView.dom.addEventListener(
"scroll",
debounce(() => this.destroy(), 100, true),
Expand All @@ -24,59 +31,49 @@ class LinkTooltipView {
update(view: EditorView) {
const state = view.state;

if (isMarkActive(state, schema.marks.link)) {
const link = state.selection.$head.marks().find((mark) => mark.type === schema.marks.link);

if (link) {
if (this.currentLink && link.eq(this.currentLink)) {
return;
}

this.currentLink = link;

if (this.tooltip) {
this.tooltip.remove();
}
const link = state.selection.$head.marks().find((mark) => mark.type === schema.marks.link);

const domAtPos = view?.domAtPos(state.selection.$head.pos);
if (link) {
if (this.tooltip) {
this.tooltip.remove();
}

passIcon("link", (icon) => {
this.tooltip = new Tooltip(`${icon}${link.attrs.href}`, {
referenceElement: domAtPos?.node.parentElement as HTMLElement,
container: view.dom.parentElement as HTMLElement,
removeOnMouseout: false,
delay: 0,
});
const domAtPos = view?.domAtPos(state.selection.$head.pos);
const coordsAtPos = view?.coordsAtPos(state.selection.$head.pos);

this.tooltip.show();
passIcon("link", (icon) => {
this.tooltip = new Tooltip(`${icon} <a href="${addBaseUri(link.attrs.href, this.baseUri)}" target="_blank">${link.attrs.href}</a>`, {
referenceElement: domAtPos?.node.parentElement as HTMLElement,
position: {
x: coordsAtPos.left + window.scrollX,
y: coordsAtPos.top + window.scrollY,
},
offset: { x: 0, y: -24 },
removeOnMouseout: false,
delay: 0,
zIndex: 7,
});
}

this.tooltip.show();
});
} else {
this.destroy();
}
}

destroy() {
if (this.tooltip) {
this.tooltip.remove();
const tooltip = this.tooltip;
setTimeout(() => tooltip.remove(), 100);
}
this.tooltip = undefined;
this.currentLink = undefined;
}
}

export function linkTooltip(): Plugin {
export function linkTooltip(baseUri: string): Plugin {
return new Plugin({
view(editorView: EditorView) {
return new LinkTooltipView(editorView);
return new LinkTooltipView(editorView, baseUri);
},
});
}

function isMarkActive(state: EditorState, type: MarkType) {
const { from, $from, to, empty } = state.selection;
if (empty) {
return !!type.isInSet(state.storedMarks || $from.marks());
}
return state.doc.rangeHasMark(from, to, type);
}
4 changes: 2 additions & 2 deletions panel/src/ts/components/inputs/editor/markdown/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { menuPlugin } from "./menu";
export class MarkdownView {
view: EditorView;

constructor(target: Element, content: string, inputEventHandler: (content: string) => void, attributes: { [key: string]: string } = {}) {
constructor(target: Element, content: string, inputEventHandler: (content: string) => void, attributes: { [key: string]: string } = {}, baseUri: string) {
this.view = new EditorView(target, {
state: EditorState.create({
doc: defaultMarkdownParser.parse(content) as any,
Expand All @@ -22,7 +22,7 @@ export class MarkdownView {
keymap(baseKeymap),
history(),
menuPlugin(),
linkTooltip(),
linkTooltip(baseUri),
new Plugin({
props: {
handleDOMEvents: {
Expand Down
60 changes: 55 additions & 5 deletions panel/src/ts/components/tooltip.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { clamp } from "../utils/math";

interface TooltipOptions {
container: HTMLElement;
referenceElement: HTMLElement;
position: "top" | "right" | "bottom" | "left" | "center";
position: "top" | "right" | "bottom" | "left" | "center" | { x: number; y: number };
offset: {
x: number;
y: number;
Expand All @@ -10,6 +12,7 @@ interface TooltipOptions {
timeout: number | null;
removeOnMouseout: boolean;
removeOnClick: boolean;
zIndex: number | null;
}

export class Tooltip {
Expand All @@ -19,6 +22,10 @@ export class Tooltip {
timeoutTimer: number;
tooltipElement: HTMLElement;

get removed() {
return this.tooltipElement === undefined || !this.options.container.contains(this.tooltipElement);
}

constructor(text: string, options: Partial<TooltipOptions> = {}) {
const defaults = {
container: document.body,
Expand All @@ -32,6 +39,7 @@ export class Tooltip {
timeout: null,
removeOnMouseout: true,
removeOnClick: false,
zIndex: null,
};

this.text = text;
Expand All @@ -49,9 +57,11 @@ export class Tooltip {
tooltip.style.display = "block";
tooltip.innerHTML = this.text;

const getTooltipPosition = (tooltip: HTMLElement) => {
const referenceElement = options.referenceElement;
const getRelativePosition = (tooltip: HTMLElement) => {
const offset = options.offset;

const referenceElement = options.referenceElement;

const rect = referenceElement.getBoundingClientRect();

const top = rect.top + window.scrollY;
Expand All @@ -62,6 +72,7 @@ export class Tooltip {

switch (options.position) {
case "top":
default:
return {
top: Math.round(top - tooltip.offsetHeight + offset.y),
left: Math.round(left + hw + offset.x),
Expand Down Expand Up @@ -89,28 +100,67 @@ export class Tooltip {
}
};

const getTooltipPosition = (tooltip: HTMLElement) => {
const position =
typeof options.position === "string"
? getRelativePosition(tooltip)
: {
top: options.position.y + options.offset.y,
left: options.position.x + options.offset.x,
};

const min = {
top: window.scrollY + 4,
left: window.scrollX + 4,
};

const max = {
top: window.innerHeight + window.scrollY - tooltip.offsetHeight - 20,
left: window.innerWidth + window.scrollX - tooltip.offsetWidth - 4,
};

return {
top: clamp(position.top, min.top, max.top),
left: clamp(position.left, min.left, max.left),
};
};

container.appendChild(tooltip);

const position = getTooltipPosition(tooltip);
tooltip.style.top = `${position.top}px`;
tooltip.style.left = `${position.left}px`;

if (options.zIndex !== null) {
tooltip.style.zIndex = `${options.zIndex}`;
}

if (options.timeout !== null) {
this.timeoutTimer = setTimeout(() => this.remove(), options.timeout);
}

if (options.removeOnMouseout) {
tooltip.addEventListener("mouseout", () => this.remove());
}

this.tooltipElement = tooltip;
}, options.delay);

const referenceElement = options.referenceElement;

if (referenceElement.tagName.toLowerCase() === "button" || referenceElement.classList.contains("button")) {
referenceElement.addEventListener("click", () => this.remove());
referenceElement.addEventListener("click", () => {
this.remove();
});
referenceElement.addEventListener("blur", () => this.remove());
}

if (options.removeOnMouseout) {
referenceElement.addEventListener("mouseout", () => this.remove());
referenceElement.addEventListener("mouseout", (event: MouseEvent) => {
if (event.relatedTarget !== this.tooltipElement) {
this.remove();
}
});
}
if (options.removeOnClick) {
referenceElement.addEventListener("click", () => this.remove());
Expand Down
Loading

0 comments on commit fd689c4

Please sign in to comment.