Skip to content

Commit

Permalink
Merge pull request #4846 from dodona-edu/enhancement/overlay-layout
Browse files Browse the repository at this point in the history
Split code rendering into different functional layers
  • Loading branch information
jorg-vr authored Jul 31, 2023
2 parents 4eefc74 + 1f2812a commit 7ae4c56
Show file tree
Hide file tree
Showing 15 changed files with 429 additions and 475 deletions.
2 changes: 1 addition & 1 deletion app/assets/javascripts/code_listing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "components/annotations/annotation_options";
import "components/annotations/annotations_count_badge";
import { annotationState } from "state/Annotations";
import { exerciseState } from "state/Exercises";
import { triggerSelectionEnd } from "components/annotations/select";
import { triggerSelectionEnd } from "components/annotations/selectionHelpers";

const MARKING_CLASS = "marked";

Expand Down
109 changes: 21 additions & 88 deletions app/assets/javascripts/components/annotations/annotation_marker.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { customElement, property } from "lit/decorators.js";
import { render, html, LitElement, TemplateResult } from "lit";
import tippy, { Instance as Tippy, createSingleton } from "tippy.js";
import { AnnotationData, annotationState, compareAnnotationOrders, isUserAnnotation } from "state/Annotations";
import { StateController } from "state/state_system/StateController";
import { createDelayer } from "util.js";

const setInstancesDelayer = createDelayer();
import { html, LitElement, TemplateResult } from "lit";
import {
AnnotationData,
annotationState,
compareAnnotationOrders,
isUserAnnotation
} from "state/Annotations";
import { MachineAnnotationData } from "state/MachineAnnotations";
/**
* A marker that styles the slotted content and shows a tooltip with annotations.
* A marker that styles the slotted content based on the relevant annotations.
* It applies a background color to user annotations and a wavy underline to machine annotations.
*
* @prop {AnnotationData[]} annotations The annotations to show in the tooltip.
* @prop {AnnotationData[]} annotations The annotations to use for styling.
*
* @element d-annotation-marker
*/
Expand All @@ -18,9 +20,6 @@ export class AnnotationMarker extends LitElement {
@property({ type: Array })
annotations: AnnotationData[];

state = new StateController(this);


static colors = {
"error": "var(--error-color, red)",
"warning": "var(--warning-color, yellow)",
Expand All @@ -33,87 +32,23 @@ export class AnnotationMarker extends LitElement {

static getStyle(annotation: AnnotationData): string {
if (["error", "warning", "info"].includes(annotation.type)) {
return `text-decoration: wavy underline ${AnnotationMarker.colors[annotation.type]} 1px;`;
return `
text-decoration: wavy underline ${AnnotationMarker.colors[annotation.type]} 1px;
text-decoration-skip-ink: none;
`;
} else {
return `
background: ${AnnotationMarker.colors[annotation.type]};
padding-top: 2px;
padding-bottom: 2px;
margin-top: -2px;
margin-bottom: -2px;
`;
}
}

static tippyInstances: Tippy[] = [];
// Using a singleton to avoid multiple tooltips being open at the same time.
static tippySingleton = createSingleton([], {
placement: "bottom-start",
interactive: true,
interactiveDebounce: 25,
delay: [500, 25],
offset: [-10, -2],
// This transition fixes a bug where overlap with the previous tooltip was taken into account when positioning
moveTransition: "transform 0.001s ease-out",
appendTo: () => document.querySelector(".code-table"),
});
static updateSingletonInstances(): void {
setInstancesDelayer(() => this.tippySingleton.setInstances(this.tippyInstances), 100);
}
static registerTippyInstance(instance: Tippy): void {
this.tippyInstances.push(instance);
this.updateSingletonInstances();
}
static unregisterTippyInstance(instance: Tippy): void {
this.tippyInstances = this.tippyInstances.filter(i => i !== instance);
this.updateSingletonInstances();
}

// Annotations that are displayed inline should show up as tooltips.
get hiddenAnnotations(): AnnotationData[] {
return this.annotations.filter(a => !annotationState.isVisible(a)).sort(compareAnnotationOrders);
}

tippyInstance: Tippy;

renderTooltip(): void {
if (this.tippyInstance) {
AnnotationMarker.unregisterTippyInstance(this.tippyInstance);
this.tippyInstance.destroy();
this.tippyInstance = undefined;
}

if (this.hiddenAnnotations.length === 0) {
return;
}

const tooltip = document.createElement("div");
tooltip.classList.add("marker-tooltip");
render(this.hiddenAnnotations.map(a => isUserAnnotation(a) ?
html`<d-user-annotation .data=${a}></d-user-annotation>` :
html`<d-machine-annotation .data=${a}></d-machine-annotation>`), tooltip);

this.tippyInstance = tippy(this, {
content: tooltip,
});
AnnotationMarker.registerTippyInstance(this.tippyInstance);
}

disconnectedCallback(): void {
super.disconnectedCallback();
if (this.tippyInstance) {
AnnotationMarker.unregisterTippyInstance(this.tippyInstance);
this.tippyInstance.destroy();
this.tippyInstance = undefined;
}
}

get sortedAnnotations(): AnnotationData[] {
return this.annotations.sort( compareAnnotationOrders );
}

get machineAnnotationMarkerSVG(): TemplateResult | undefined {
const firstMachineAnnotation = this.sortedAnnotations.find(a => !isUserAnnotation(a));
const firstMachineAnnotation = this.sortedAnnotations.find(a => !isUserAnnotation(a)) as MachineAnnotationData | undefined;
const size = 14;
return firstMachineAnnotation && html`<svg style="position: absolute; top: ${16 - size/2}px; left: -${size/2}px" width="${size}" height="${size}" viewBox="0 0 24 24">
<path fill="${AnnotationMarker.colors[firstMachineAnnotation.type]}" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6l-6 6l1.41 1.41Z"/>
Expand All @@ -125,13 +60,11 @@ export class AnnotationMarker extends LitElement {
}

render(): TemplateResult {
this.renderTooltip();

return html`<style>
:host {
position: relative;
${this.annotationStyles}
}
</style><slot>${this.machineAnnotationMarkerSVG}</slot>`;
:host {
position: relative;
${this.annotationStyles}
}
</style><slot>${this.machineAnnotationMarkerSVG}</slot>`;
}
}
108 changes: 108 additions & 0 deletions app/assets/javascripts/components/annotations/annotation_tooltip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { customElement, property } from "lit/decorators.js";
import { render, html, LitElement, TemplateResult, css } from "lit";
import tippy, { Instance as Tippy, createSingleton } from "tippy.js";
import { AnnotationData, annotationState, compareAnnotationOrders, isUserAnnotation } from "state/Annotations";
import { StateController } from "state/state_system/StateController";
import { createDelayer } from "util.js";

const setInstancesDelayer = createDelayer();
/**
* Adds tooltips with annotations to slotted content
*
* @prop {AnnotationData[]} annotations The annotations to show in the tooltip.
*
* @element d-annotation-tooltip
*/
@customElement("d-annotation-tooltip")
export class AnnotationTooltip extends LitElement {
@property({ type: Array })
annotations: AnnotationData[];

static styles = css`:host { position: relative; }`;

state = new StateController(this);

// we need to keep track of all tippy instances to update the singleton
static tippyInstances: Tippy[] = [];
// Using a singleton to avoid multiple tooltips being open at the same time.
static tippySingleton = createSingleton([], {
placement: "bottom-start",
interactive: true,
interactiveDebounce: 25,
delay: [500, 25],
offset: [-10, 0],
// This transition fixes a bug where overlap with the previous tooltip was taken into account when positioning
moveTransition: "transform 0.001s ease-out",
appendTo: () => document.querySelector(".code-table"),
});

/**
* Updates the tippy singleton with the current list of tippy instances.
* This method is debounced to avoid updating the singleton too often as it is expensive.
*/
static updateSingletonInstances(): void {
setInstancesDelayer(() => this.tippySingleton.setInstances(this.tippyInstances), 100);
}

/**
* Adds a tippy instance to the list of instances, which will be used to update the singleton.
*/
static registerTippyInstance(instance: Tippy): void {
this.tippyInstances.push(instance);
this.updateSingletonInstances();
}

/**
* Removes a tippy instance from the list of instances, which will be used to update the singleton.
*/
static unregisterTippyInstance(instance: Tippy): void {
this.tippyInstances = this.tippyInstances.filter(i => i !== instance);
this.updateSingletonInstances();
}

// Annotations that are not displayed inline should show up as tooltips.
get hiddenAnnotations(): AnnotationData[] {
return this.annotations.filter(a => !annotationState.isVisible(a)).sort(compareAnnotationOrders);
}

tippyInstance: Tippy;

disconnectedCallback(): void {
super.disconnectedCallback();
// before destroying this element, we need to clean up the tippy instance
// and make sure it is removed from the singleton
if (this.tippyInstance) {
AnnotationTooltip.unregisterTippyInstance(this.tippyInstance);
this.tippyInstance.destroy();
this.tippyInstance = undefined;
}
}

render(): TemplateResult {
// Clean up the previous tippy instance if it exists.
if (this.tippyInstance) {
AnnotationTooltip.unregisterTippyInstance(this.tippyInstance);
this.tippyInstance.destroy();
this.tippyInstance = undefined;
}

if (this.hiddenAnnotations.length > 0) {
const tooltip = document.createElement("div");
tooltip.classList.add("marker-tooltip");
render(this.hiddenAnnotations.map(a => isUserAnnotation(a) ?
html`
<d-user-annotation .data=${a}></d-user-annotation>` :
html`
<d-machine-annotation .data=${a}></d-machine-annotation>`), tooltip);

this.tippyInstance = tippy(this, {
content: tooltip,
});
AnnotationTooltip.registerTippyInstance(this.tippyInstance);
}

// if slot is empty, render an empty svg to make sure the tooltip is positioned correctly
return html`<slot><svg style="position: absolute; top: 9px; left: -7px" width="14" height="14" viewBox="0 0 24 24">
</svg></slot>`;
}
}
Loading

0 comments on commit 7ae4c56

Please sign in to comment.