Skip to content
Permalink
Browse files
fix(value): display ckEditor in case of rich-text property (DEV-182) (#…
…571)

* chore(deps): update ckEditor

* chore(deps): update ckEditor

* style(value): set ckEditor height

* feat(value): consider rich-text gui element in text value

* test(value): fix tests

* Merge branch 'main' into wip/dev-182-edit-richt-text

* fix(deps): use correct ckeditor custom build branch

* refactor(style): clean up style imports

* fix(value): consider gui element in text value
  • Loading branch information
kilchenmann committed Nov 5, 2021
1 parent bddc2f1 commit 6bfe25403d42fe76b3239fda088b7029c78c1912
Show file tree
Hide file tree
Showing 39 changed files with 2,321 additions and 7,487 deletions.

Large diffs are not rendered by default.

@@ -32,14 +32,14 @@
"@angular/platform-browser": "^11.2.9",
"@angular/platform-browser-dynamic": "^11.2.9",
"@angular/router": "^11.2.9",
"@ckeditor/ckeditor5-angular": "^1.2.3",
"@ckeditor/ckeditor5-angular": "^2.0.2",
"@dasch-swiss/dsp-js": "^4.2.0",
"@datadog/browser-rum": "^3.6.3",
"@ngx-translate/core": "^12.1.2",
"@ngx-translate/http-loader": "5.0.0",
"3d-force-graph": "^1.60.12",
"angular-split": "^4.0.0",
"ckeditor5-custom-build": "github:dasch-swiss/ckeditor_custom_build#v1.0.0",
"ckeditor5-custom-build": "github:dasch-swiss/ckeditor_custom_build#main",
"core-js": "^3.6.5",
"d3": "^5.15.1",
"d3-force-3d": "^2.1.0",
@@ -1 +0,0 @@
@import "../../../../assets/style/viewer";
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
@@ -1,7 +1,7 @@
<div class="grid-container">
<div class="value-component">
<span [ngSwitch]="resourcePropertyDefinition.objectType">
<app-text-value-as-string #createVal *ngSwitchCase="'ReadTextValueAsString'" [mode]="mode"></app-text-value-as-string>
<app-text-value-as-string #createVal *ngSwitchCase="'ReadTextValueAsString'" [mode]="mode" [guiElement]="textValueGuiEle"></app-text-value-as-string>
<app-text-value-as-html #createVal *ngSwitchCase="'ReadTextValueAsHtml'" [mode]="mode"></app-text-value-as-html>
<app-text-value-as-xml #createVal *ngSwitchCase="'ReadTextValueAsXml'" [mode]="mode"></app-text-value-as-xml>
<app-int-value #createVal *ngSwitchCase="constants.IntValue" [mode]="mode"></app-int-value>
@@ -35,6 +35,6 @@
(click)="cancelAddValue()">
<mat-icon>cancel</mat-icon>
</button>
<app-progress-indicator *ngIf="submittingValue" [status]="progressIndicatorStatus" [color]="progressIndicatorColor"></app-progress-indicator>
<app-progress-indicator *ngIf="submittingValue" [status]="progressIndicatorStatus"></app-progress-indicator>
</div>
</div>
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
@@ -50,7 +50,12 @@ export class AddValueComponent implements OnInit, AfterViewInit {
// 0 will display a loading animation
progressIndicatorStatus = 0;

progressIndicatorColor = 'blue';
// type of given displayValue
// or knora-api-js-lib class representing the value
valueTypeOrClass: string;

// gui element in case of textValue
textValueGuiEle: 'simpleText' | 'textArea' | 'richText';

constructor(
@Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection,
@@ -65,6 +70,11 @@ export class AddValueComponent implements OnInit, AfterViewInit {
// we need to use the ValueTypeService in order to assign it the correct object type for the ngSwitch in the template
if (this.resourcePropertyDefinition.objectType === 'http://api.knora.org/ontology/knora-api/v2#TextValue') {
this.resourcePropertyDefinition.objectType = this._valueService.getTextValueClass(this.resourcePropertyDefinition);

if (this.resourcePropertyDefinition.objectType === 'ReadTextValueAsString') {
// handle the correct gui element depending on guiEle property
this.textValueGuiEle = this._valueService.getTextValueGuiEle(this.resourcePropertyDefinition.guiElement);
}
}

}
@@ -5,7 +5,7 @@
[ngClass]='backgroundColor'>
<span [ngSwitch]="valueTypeOrClass">
<!-- display value is cast as 'any' because the compiler cannot infer the value type expected by the child component -->
<app-text-value-as-string class="parent-value-component" #displayVal *ngSwitchCase="'ReadTextValueAsString'" [mode]="mode" [displayValue]="$any(displayValue)" [textArea]="textArea"></app-text-value-as-string>
<app-text-value-as-string class="parent-value-component" #displayVal *ngSwitchCase="'ReadTextValueAsString'" [mode]="mode" [displayValue]="$any(displayValue)" [guiElement]="textValueGuiEle"></app-text-value-as-string>
<app-text-value-as-html class="parent-value-component" #displayVal *ngSwitchCase="'ReadTextValueAsHtml'" [mode]="mode" [displayValue]="$any(displayValue)"></app-text-value-as-html>
<app-text-value-as-xml class="parent-value-component" #displayVal *ngSwitchCase="'ReadTextValueAsXml'" [mode]="mode" [displayValue]="$any(displayValue)"
(internalLinkClicked)="standoffLinkClicked($event)" (internalLinkHovered)="standoffLinkHovered($event)"></app-text-value-as-xml>
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
@@ -357,7 +357,7 @@ describe('DisplayEditComponent', () => {

const userServiceSpy = jasmine.createSpyObj('UserService', ['getUser']);

const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValueTypeOrClass', 'isReadOnly']);
const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValueTypeOrClass', 'getTextValueGuiEle', 'isReadOnly']);

TestBed.configureTestingModule({
imports: [
@@ -440,6 +440,11 @@ describe('DisplayEditComponent', () => {
(value: ReadValue) => valueService.getValueTypeOrClass(value)
);

// spy for getTextValueGuiEle
(valueServiceSpy as jasmine.SpyObj<ValueService>).getTextValueGuiEle.and.callFake(
(guiEle: string) => valueService.getTextValueGuiEle(guiEle)
);

// spy for isReadOnly
(valueServiceSpy as jasmine.SpyObj<ValueService>).isReadOnly.and.callFake(
(typeOrClass: string, value: ReadValue, propDef: ResourcePropertyDefinition) => valueService.isReadOnly(typeOrClass, value, propDef)
@@ -50,15 +50,15 @@ import { ValueService } from '../../services/value.service';

// fade in when created.
transition(':enter', [
// the styles start from this point when the element appears
// the styles start from this point when the element appears
style({ opacity: 0 }),
// and animate toward the "in" state above
animate(150)
]),

// fade out when destroyed.
transition(':leave',
// fading out uses a different syntax, with the "style" being passed into animate()
// fading out uses a different syntax, with the "style" being passed into animate()
animate(150, style({ opacity: 0 })))
])
]
@@ -110,7 +110,8 @@ export class DisplayEditComponent implements OnInit {

showDateLabels = false;

textArea = false;
// gui element in case of textValue
textValueGuiEle: 'simpleText' | 'textArea' | 'richText';

dateFormat: string;

@@ -146,8 +147,12 @@ export class DisplayEditComponent implements OnInit {
(propDef: ResourcePropertyDefinition) => propDef.id === this.displayValue.property
);

if(resPropDef[0].guiElement === Constants.SalsahGui + Constants.HashDelimiter + 'Textarea') {
this.textArea = true;
// we should also take the gui element into account in case of text value
// since simple text values and rich text values share the same object type 'TextValue',
// we need to use the ValueTypeService in order to assign it the correct object type for the ngSwitch in the template
if (this.valueTypeOrClass === 'ReadTextValueAsString') {
// handle the correct gui element depending on guiEle property
this.textValueGuiEle = this._valueService.getTextValueGuiEle(resPropDef[0].guiElement);
}

if (resPropDef.length !== 1) {
@@ -312,7 +317,7 @@ export class DisplayEditComponent implements OnInit {

this._dspApiConnection.v2.values.deleteValue(updateRes as UpdateResource<DeleteValue>).pipe(
mergeMap((res: DeleteValueResponse) => {
// emit a ValueDeleted event to the listeners in resource-view component to trigger an update of the UI
// emit a ValueDeleted event to the listeners in resource-view component to trigger an update of the UI
this._valueOperationEventService.emit(new EmitEvent(Events.ValueDeleted, new DeletedEventValue(deleteVal)));
return res.result;
})).subscribe();
@@ -388,7 +393,7 @@ export class DisplayEditComponent implements OnInit {
// find the corresponding standoff link value
const referredResStandoffLinkVal: ReadValue[] = standoffLinkPropInfoVals[0].values.filter(
(standoffLinkVal: ReadValue) => standoffLinkVal instanceof ReadLinkValue
&& (standoffLinkVal as ReadLinkValue).linkedResourceIri === resIri
&& (standoffLinkVal as ReadLinkValue).linkedResourceIri === resIri
);

// if no corresponding standoff link value was found,
@@ -72,7 +72,22 @@ export class ValueService {
default:
return this._readTextValueAsString;
}
}

/**
* given a salsah gui element IRI, determines the short type of it
*
* @param guiEle the given salsah gui element iri.
*/
getTextValueGuiEle(guiEle: string): 'simpleText' | 'textArea' | 'richText' {
switch (guiEle) {
case 'http://api.knora.org/ontology/salsah-gui/v2#Textarea':
return 'textArea';
case 'http://api.knora.org/ontology/salsah-gui/v2#Richtext':
return 'richText';
default:
return 'simpleText';
}
}

/**
@@ -1,5 +1,3 @@
@import "../../../../../assets/style/viewer";

.read-mode-view .mat-icon,
.boolValue {
margin-top: 4px;
@@ -1,3 +1,4 @@
.child-input-component {
height: 48px;
max-width: 256px;
}
@@ -1,5 +1,3 @@
@import "../../../../../assets/style/viewer";

:host ::ng-deep .child-value-component {
width: fit-content;

@@ -1,5 +1,3 @@
@import "../../../../../../../assets/style/viewer";

.era-radio {
margin-bottom: 15px;
}
@@ -1,5 +1,3 @@
@import "../../../../../../assets/style/viewer";

.period-checkbox {
display: inline-block;
padding-bottom: 20px;
@@ -1,5 +1,3 @@
@import "../../../../../../assets/style/viewer";

.period-checkbox {
display: inline-block;
padding-bottom: 20px;
@@ -1,5 +1,3 @@
@import "../../../../../assets/style/viewer";

:host ::ng-deep .child-value-component {
.mat-form-field-underline {
display: none;
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
@@ -1,5 +1,3 @@
@import "../../../../../assets/style/viewer";

.read-mode-view .more-info,
.form-fields-container .mat-form-field .more-info {
cursor: pointer;
@@ -1,5 +1,3 @@
@import "../../../../../assets/style/viewer";

// Firefox:
// Hide number picker
input[type=number] {
@@ -1 +0,0 @@
@import "../../../../../../assets/style/viewer";
@@ -1,5 +1,3 @@
@import "../../../../../assets/style/viewer";

:host ::ng-deep .child-value-component {
.mat-form-field-underline {
display: none;
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
@@ -0,0 +1,81 @@
import { Constants } from '@dasch-swiss/dsp-js';

export class ckEditor {

static config = {
entities: false,
toolbar: {
items: [
'heading',
'|',
'bold',
'italic',
'underline',
'strikethrough',
'subscript',
'superscript',
'|',
'removeFormat',
'|',
'undo',
'redo',
'-',
'link',
'|',
'bulletedList',
'numberedList',
'horizontalLine',
'|',
'blockQuote',
'code',
'codeBlock',
'insertTable',
'specialCharacters',
'|',
'sourceEditing',
],
shouldNotGroupWhenFull: true
},
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3' },
{ model: 'heading4', view: 'h4', title: 'Heading 4' },
{ model: 'heading5', view: 'h5', title: 'Heading 5' },
{ model: 'heading6', view: 'h6', title: 'Heading 6' },
{ model: 'formatted', view: 'pre', title: 'Formatted' },
{ model: 'cite', view: 'cite', title: 'Cited' }
]
},
codeBlock: {
languages: [
{ language: 'plaintext', label: 'Plain text', class: '' }
]
},
language: 'en',
link: {
addTargetToExternalLinks: false,
decorators: {
isInternal: {
// label: 'internal link to a Knora resource',
mode: 'automatic', // automatic requires callback -> but the callback is async and the user could save the text before the check ...
callback: url => // console.log(url, url.startsWith( 'http://rdfh.ch/' ));
!!url && url.startsWith('http://rdfh.ch/') // --> TODO: get this from config via AppInitService
,
attributes: {
class: Constants.SalsahLink
}
}
}
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells'
]
}
};
}
@@ -1 +0,0 @@
@import "../../../../../../assets/style/viewer";
@@ -4,9 +4,19 @@
</span>
<ng-template #showForm>
<span [formGroup]="form">
<mat-form-field class="large-field child-value-component" floatLabel="never">
<input *ngIf="!textArea" matInput [formControlName]="'value'" class="value" placeholder="Text value" type="text" [errorStateMatcher]="matcher">
<textarea *ngIf="textArea" matInput [formControlName]="'value'" class="value" placeholder="Text value" type="text" [errorStateMatcher]="matcher"></textarea>
<mat-form-field *ngIf="guiElement !== 'richText'; else showRichText" class="large-field child-value-component"
floatLabel="never">

<!-- default gui-element: simple text -->
<input *ngIf="guiElement === 'simpleText'" matInput [formControlName]="'value'" class="value"
placeholder="Text value" type="text" [errorStateMatcher]="matcher">

<!-- gui-element: text area -->
<textarea *ngIf="guiElement === 'textArea'" matInput [formControlName]="'value'" class="value"
placeholder="Text value" type="text" [errorStateMatcher]="matcher" [mat-autosize]="true"
[matAutosizeMaxRows]="12" [matAutosizeMinRows]="6"></textarea>
<!-- <textarea *ngSwitchCase="'richText'" matInput [formControlName]="'value'" MatTextareaAutosize [mat-autosize]="true" [matAutosizeMaxRows]="10" class="value" placeholder="Text value" type="text" [errorStateMatcher]="matcher"></textarea> -->

<mat-error *ngIf="valueFormControl.hasError('valueNotChanged') &&
(valueFormControl.touched || valueFormControl.dirty)">
<span class="custom-error-message">New value must be different than the current value.</span>
@@ -15,19 +25,19 @@
A text value is <strong>required</strong>.
</mat-error>
<mat-error *ngIf="valueFormControl.hasError('duplicateValue')">
<span class="custom-error-message">This value already exists for this property. Duplicate values are not allowed.</span>
<span class="custom-error-message">This value already exists for this property. Duplicate values are not
allowed.</span>
</mat-error>
</mat-form-field>
<!-- gui-element: rich-text -->
<ng-template #showRichText>
<ckeditor [formControlName]="'value'" [config]="editorConfig" [editor]="editor"></ckeditor>
</ng-template>

<!-- comment -->
<mat-form-field *ngIf="!commentDisabled" class="large-field value-component-comment">
<textarea matInput
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="6"
[formControlName]="'comment'"
class="comment"
placeholder="Comment"
type="text"
spellcheck="false">
<textarea matInput cdkTextareaAutosize cdkAutosizeMinRows="1" cdkAutosizeMaxRows="6"
[formControlName]="'comment'" class="comment" placeholder="Comment" type="text" spellcheck="false">
</textarea>
</mat-form-field>
</span>
@@ -1 +0,0 @@
@import "../../../../../../assets/style/viewer";

0 comments on commit 6bfe254

Please sign in to comment.