Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add footnotes functionnality to richtext #2071

Merged
merged 63 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
e1b087b
wip: works: loads ckeditor5-custom-build
derschnee68 Dec 13, 2024
940534e
wip: tooltip
derschnee68 Dec 27, 2024
afd26e7
wip: footnote display tooltip
derschnee68 Dec 27, 2024
f358609
wip
derschnee68 Jan 3, 2025
7918966
feat: add footnotes to the property value
derschnee68 Jan 6, 2025
8c5167a
fix
derschnee68 Jan 8, 2025
a2440b9
fix: improve style
derschnee68 Jan 8, 2025
3704f8a
fix
derschnee68 Jan 8, 2025
8c7250f
fix
derschnee68 Jan 8, 2025
59fde3f
fix
derschnee68 Jan 8, 2025
2bff43a
fix
derschnee68 Jan 8, 2025
d917c44
fix: updating bug
derschnee68 Jan 9, 2025
a619ef7
fix: two footnotes updating
derschnee68 Jan 10, 2025
76dc506
fix: style
derschnee68 Jan 13, 2025
63b9b19
fix: edit animation
derschnee68 Jan 13, 2025
8295a03
wip
derschnee68 Jan 13, 2025
99db9ce
fix
derschnee68 Jan 13, 2025
4b33f51
fix
derschnee68 Jan 13, 2025
1e57f28
Merge remote-tracking branch 'origin/main' into julien/footnote-display
derschnee68 Jan 13, 2025
e022c2d
fix
derschnee68 Jan 13, 2025
dfe67de
fix
derschnee68 Jan 13, 2025
4974f49
fix
derschnee68 Jan 13, 2025
8bbe2c4
fix
derschnee68 Jan 14, 2025
6d67af0
fix
derschnee68 Jan 14, 2025
d104624
Merge remote-tracking branch 'origin/main' into julien/footnote-display
derschnee68 Jan 14, 2025
4e80bd8
fix: new package lock
derschnee68 Jan 14, 2025
225f26e
fix lint
derschnee68 Jan 14, 2025
5e72717
fix: lint circular dependency
derschnee68 Jan 14, 2025
860079e
Merge remote-tracking branch 'origin/main' into julien/fixfootnotes-i…
derschnee68 Jan 22, 2025
6b22e5a
fix
derschnee68 Jan 22, 2025
25cce2a
fix
derschnee68 Jan 22, 2025
764401d
fix: first setup for footnotes
derschnee68 Jan 22, 2025
1838468
fix
derschnee68 Jan 23, 2025
ce0f593
review: wip
derschnee68 Jan 27, 2025
52d1857
Merge remote-tracking branch 'origin/main' into julien/fixfootnotes-i…
derschnee68 Jan 28, 2025
1d67dc0
fix
derschnee68 Jan 28, 2025
cb4ceec
Merge remote-tracking branch 'origin/main' into julien/fixfootnotes-i…
derschnee68 Jan 28, 2025
a11eff9
fix
derschnee68 Jan 28, 2025
149a8dd
fix
derschnee68 Jan 28, 2025
8a600c4
wip
derschnee68 Jan 29, 2025
dadeef0
wip: better
derschnee68 Jan 29, 2025
827562c
wip: better
derschnee68 Jan 29, 2025
6173bf8
fix
derschnee68 Jan 29, 2025
556e8bf
fix
derschnee68 Jan 30, 2025
1b840bf
fix
derschnee68 Jan 31, 2025
ff43971
fix
derschnee68 Jan 31, 2025
d57dd4d
fix
derschnee68 Jan 31, 2025
f133e21
fix
derschnee68 Jan 31, 2025
fb0635d
fix
derschnee68 Jan 31, 2025
5187cd6
fix
derschnee68 Jan 31, 2025
52b6a7d
fix
derschnee68 Jan 31, 2025
234ccb7
fix
derschnee68 Jan 31, 2025
80f7103
fix
derschnee68 Jan 31, 2025
8c17c71
Merge branch 'main' into julien/fixfootnotes-issues
derschnee68 Jan 31, 2025
45f3479
Revert "fix"
derschnee68 Jan 31, 2025
53d38a5
fix
derschnee68 Jan 31, 2025
ccfe100
fix
derschnee68 Jan 31, 2025
a7f0464
fix
derschnee68 Jan 31, 2025
4097edb
fix
derschnee68 Jan 31, 2025
9d89854
fix
derschnee68 Jan 31, 2025
2c127c5
fix: scroll to view
derschnee68 Jan 31, 2025
ac86f64
fix: scroll to view
derschnee68 Jan 31, 2025
8df20c6
fix: ckeditor v2.1.4
derschnee68 Jan 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions apps/dsp-app/cypress/e2e/system-admin/resource.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Project00FFPayloads } from '../../fixtures/project00FF-resource-payload
import { ClassPropertyPayloads } from '../../fixtures/property-definition-payloads';
import { ResourceRequests, ResponseUtil } from '../../fixtures/requests';
import { AddResourceInstancePage } from '../../support/pages/add-resource-instance-page';
import { ResourcePage } from '../../support/pages/resource-page';

describe('Resource', () => {
let finalLastModificationDate: string;
Expand All @@ -22,6 +23,51 @@ describe('Resource', () => {
});
});

describe('footnotes', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there might be more tests with footnotes, I suggest moving them to a separate test file

it.only('should be displayed, and can be edited', () => {
const footnote = {
'@type': 'http://0.0.0.0:3333/ontology/00FF/images/v2#datamodelclass',
'http://www.w3.org/2000/01/rdf-schema#label': 'rlabel',
'http://api.knora.org/ontology/knora-api/v2#attachedToProject': {
'@id': 'http://rdfh.ch/projects/00FF',
},
'http://0.0.0.0:3333/ontology/00FF/images/v2#property': {
'@type': 'http://api.knora.org/ontology/knora-api/v2#TextValue',
'http://api.knora.org/ontology/knora-api/v2#textValueAsXml':
'<?xml version="1.0" encoding="UTF-8"?><text><p>footnote1<footnote content="&amp;lt;p&amp;gt;fn1&amp;lt;/p&amp;gt;">[Footnote]</footnote> and footnote2<footnote content="&amp;lt;p&amp;gt;fn2&amp;lt;/p&amp;gt;">[Footnote]</footnote></p></text>',
'http://api.knora.org/ontology/knora-api/v2#textValueHasMapping': {
'@id': 'http://rdfh.ch/standoff/mappings/StandardMapping',
},
},
};

ResourceRequests.resourceRequest(ClassPropertyPayloads.richText(finalLastModificationDate));
cy.request('POST', `${Cypress.env('apiUrl')}/v2/resources`, footnote).then(v => {
const id = v.body['@id'].match(/\/([^\/]+)$/)[1];
const page = new ResourcePage();
page.visit(id);
cy.get('[data-cy=footnote]').should('have.length', 2);
cy.get('[data-cy=footnote]').eq(0).should('contain', 'fn1');
cy.get('[data-cy=footnote]').eq(1).should('contain', 'fn2');
});
cy.get('app-rich-text-switch').trigger('mouseenter');
cy.get('[data-cy="edit-button"]').click();
cy.get('[content="&lt;p&gt;fn1&lt;/p&gt;"]').click();
cy.get('.ck-content[contenteditable=true]')
.eq(1)
.then(el => {
// @ts-ignore
const editor = el[0].ckeditorInstance; // If you're using TS, this is ReturnType<typeof InlineEditor['create']>
editor.setData('Typing some stuff');
});
cy.get('.ck-button-save').click({ force: true });
cy.get('[data-cy="save-button"] > .mat-mdc-button-touch-target').click();
cy.get('[data-cy=footnote]').should('have.length', 2);
cy.get('[data-cy=footnote]').eq(0).should('contain', 'Typing some stuff');
cy.get('[data-cy=footnote]').eq(1).should('contain', 'fn2');
});
});

describe('can add an instance, edit, and delete for a property', () => {
it('text', () => {
const initialValue = faker.lorem.word();
Expand Down
18 changes: 18 additions & 0 deletions apps/dsp-app/cypress/fixtures/property-definition-payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ export class ClassPropertyPayloads {
};
}

static richText(lastModificationDate: string) {
return {
...this.baseData(lastModificationDate),
'@graph': [
{
...this.baseGraph,
...this.hasValue,
'http://api.knora.org/ontology/knora-api/v2#objectType': {
'@id': 'http://api.knora.org/ontology/knora-api/v2#TextValue',
},
'http://api.knora.org/ontology/salsah-gui/v2#guiElement': {
'@id': 'http://api.knora.org/ontology/salsah-gui/v2#Richtext',
},
},
],
};
}

static number(lastModificationDate: string) {
return {
...this.baseData(lastModificationDate),
Expand Down
5 changes: 5 additions & 0 deletions apps/dsp-app/cypress/support/pages/resource-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class ResourcePage {
visit(id: string) {
cy.visit(`/project/00FF/ontology/00FF/images/${id}`);
}
}
8 changes: 5 additions & 3 deletions apps/dsp-app/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ $warn: mat.define-palette(config.$velvet_red_palette, 700, 500, 900);

// Define a theme.
$theme: mat.define-light-theme((
color: (primary: $primary, accent: $accent, warn: $warn),
density: 0
color: (primary: $primary, accent: $accent, warn: $warn),
density: 0
));

// Include all theme styles for the components.
Expand Down Expand Up @@ -67,7 +67,7 @@ body {
// The following styles will override material design!

// mat-form-field overrides
.mdc-text-field--filled:not(.mdc-text-field--disabled){
.mdc-text-field--filled:not(.mdc-text-field--disabled) {
background-color: transparent;
}

Expand Down Expand Up @@ -111,9 +111,11 @@ body {
.mat-mdc-text-field-wrapper {
width: 100%;
padding-left: 0px;

.mat-mdc-form-field-flex {
border-left: 1px solid rgba(0, 0, 0, 0.12);
min-height: calc(4 * 36px + 2px);

.mat-mdc-form-field-infix {
// negative values are not the best choice,
// but with this margin-top the placeholder is at the
Expand Down
4 changes: 4 additions & 0 deletions libs/vre/resource-editor/resource-properties/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ export * from './lib/sortByKeys';
export * from './lib/upload-control.component';
export * from './resource-properties.components';
export * from './lib/upload.component';
export * from './lib/footnote.service';
export * from './lib/footnotes.component';
export * from './lib/footnote-tooltip.component';
export * from './lib/footnote.directive';
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, Input } from '@angular/core';
import { SafeHtml } from '@angular/platform-browser';

@Component({
selector: 'app-footnote-tooltip',
template: ` <div class="content" [@fadeIn]="'in'">
<div [innerHTML]="content"></div>
</div>`,
animations: [
trigger('fadeIn', [
state('in', style({ opacity: 1 })),
transition(':enter', [style({ opacity: 0 }), animate(100)]),
]),
],
styles: [
`
:host {
position: absolute;
z-index: 1000;
}

.content {
font-size: 0.8em;
background: white;
color: black;
padding: 8px;
min-width: 200px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
`,
],
})
export class FootnoteTooltipComponent {
@Input({ required: true }) content!: SafeHtml;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Overlay, OverlayPositionBuilder, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { Directive, HostListener } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { AppError } from '@dasch-swiss/vre/core/error-handler';
import { FootnoteTooltipComponent } from './footnote-tooltip.component';

@Directive({
selector: '[appFootnote]',
})
export class FootnoteDirective {
private overlayRef: OverlayRef | null = null;
private hideTimeout: any;
derschnee68 marked this conversation as resolved.
Show resolved Hide resolved

constructor(
private overlay: Overlay,
private positionBuilder: OverlayPositionBuilder,
private sanitizer: DomSanitizer
derschnee68 marked this conversation as resolved.
Show resolved Hide resolved
) {}

@HostListener('mouseover', ['$event'])
onMouseOver(event: MouseEvent) {
const targetElement = event.target as HTMLElement;
if (targetElement.nodeName.toLowerCase() === 'footnote') {
const content = targetElement.getAttribute('content');
if (content === null) {
throw new AppError('Footnote content is null');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use translations

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intended for the dev

derschnee68 marked this conversation as resolved.
Show resolved Hide resolved
}
this.showTooltip(content, event.clientX, event.clientY);
}
}

@HostListener('mouseout', ['$event.target'])
onMouseOut(targetElement: HTMLElement) {
if (targetElement.nodeName.toLowerCase() === 'footnote') {
this.hideTooltipWithDelay();
}
}

private showTooltip(content: string, mouseX: number, mouseY: number) {
if (!this.overlayRef) {
const positionStrategy = this.positionBuilder.flexibleConnectedTo({ x: mouseX, y: mouseY }).withPositions([
{
overlayX: 'center',
overlayY: 'top',
originX: 'center',
originY: 'bottom',
offsetY: 10,
},
]);

this.overlayRef = this.overlay.create({
positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition(),
});
}

const tooltipPortal = new ComponentPortal(FootnoteTooltipComponent);
if (this.overlayRef.hasAttached()) {
this.overlayRef.detach();
clearTimeout(this.hideTimeout);
}

const tooltipRef = this.overlayRef.attach(tooltipPortal);
tooltipRef.instance.content = this.sanitizer.bypassSecurityTrustHtml(content);

this.overlayRef.overlayElement.addEventListener('mouseenter', () => {
clearTimeout(this.hideTimeout);
});

this.overlayRef.overlayElement.addEventListener('mouseleave', () => {
this.hideTooltipWithDelay();
});
}

private hideTooltipWithDelay() {
this.hideTimeout = setTimeout(() => {
this.hideTooltip();
}, 300);
}

private hideTooltip() {
if (this.overlayRef) {
this.overlayRef.detach();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { SafeHtml } from '@angular/platform-browser';

@Injectable()
export class FootnoteService {
footnotes: { uid: string; content: SafeHtml }[] = [];
derschnee68 marked this conversation as resolved.
Show resolved Hide resolved

addFootnote(uid: string, content: SafeHtml): void {
const property = this.footnotes.find(footnote => footnote.uid === uid);
if (property) {
property.content = content;
} else {
this.footnotes.push({ uid, content });
}
}

reset() {
this.footnotes = [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Component } from '@angular/core';
import { AppError } from '@dasch-swiss/vre/core/error-handler';
import { FootnoteService } from './footnote.service';

@Component({
selector: 'app-footnotes',
template: `<h5>Footnotes</h5>
derschnee68 marked this conversation as resolved.
Show resolved Hide resolved
<div
*ngFor="let footnote of footnoteService.footnotes; let index = index"
derschnee68 marked this conversation as resolved.
Show resolved Hide resolved
(click)="goToFootnote(footnote.uid)"
class="footnote"
data-cy="footnote">
{{ index + 1 }}. <span [innerHTML]="footnote.content"></span>
</div>`,
styles: [
`
.footnote {
display: flex;
align-items: center;
gap: 5px;
}
`,
],
})
export class FootnotesComponent {
constructor(public readonly footnoteService: FootnoteService) {}

goToFootnote(uid: string) {
const element = document.getElementById(uid);
if (!element) {
throw new AppError(`Element with uid ${uid} is not found on page.`);
derschnee68 marked this conversation as resolved.
Show resolved Hide resolved
}

element.scrollIntoView();
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
import {
AfterViewInit,
Component,
ElementRef,
Input,
OnChanges,
OnDestroy,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { RouteConstants } from '@dasch-swiss/vre/core/config';
import { PropertyInfoValues } from '@dasch-swiss/vre/shared/app-common';
import { of, Subject } from 'rxjs';
import { takeUntil, takeWhile } from 'rxjs/operators';
import { FootnoteService } from './footnote.service';

@Component({
selector: 'app-property-row',
template: ` <div [class.border-bottom]="borderBottom" #rowElement style="display: flex; padding: 8px 0;">
<h3 class="label mat-subtitle-2" [matTooltip]="tooltip ?? ''" matTooltipPosition="above">{{ label }}</h3>
<div style="flex: 1">
<ng-content></ng-content>
<app-footnotes *ngIf="footnoteService.footnotes.length > 0" />
derschnee68 marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>`,
providers: [FootnoteService],
styleUrls: ['./property-row.component.scss'],
})
export class PropertyRowComponent implements AfterViewInit, OnDestroy {
export class PropertyRowComponent implements AfterViewInit, OnDestroy, OnChanges {
@Input({ required: true }) label!: string;
@Input({ required: true }) borderBottom!: boolean;
@Input() tooltip: string | undefined;
Expand All @@ -29,7 +41,16 @@ export class PropertyRowComponent implements AfterViewInit, OnDestroy {
return this.prop && this.prop.values.length > 0 ? this.prop.values[0]?.id.split('/').pop() : undefined;
}

constructor(private route: ActivatedRoute) {}
constructor(
private route: ActivatedRoute,
public footnoteService: FootnoteService
derschnee68 marked this conversation as resolved.
Show resolved Hide resolved
) {}

ngOnChanges(changes: SimpleChanges) {
if (changes['prop']) {
this.footnoteService.reset();
}
}

ngAfterViewInit() {
this.highlightArkValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ export class PropertyValueSwitcherComponent implements OnInit, OnChanges, AfterV

ngOnInit() {
this._setupData();
this._cd.detectChanges();
}

_setupData() {
Expand Down
Loading
Loading