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

fix(value): display ckEditor in case of rich-text property (DEV-182) #571

Merged
merged 11 commits into from Nov 5, 2021
9,511 changes: 2,136 additions & 7,375 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -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",
Expand Down
@@ -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>
Expand Down Expand Up @@ -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";
Expand Up @@ -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,
Expand All @@ -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);
}
}

}
Expand Down
Expand Up @@ -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>
Expand Down
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
Expand Up @@ -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: [
Expand Down Expand Up @@ -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)
Expand Down
Expand Up @@ -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 })))
])
]
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions src/app/workspace/resource/services/value.service.ts
Expand Up @@ -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';
}
}

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

.read-mode-view .mat-icon,
.boolValue {
margin-top: 4px;
Expand Down
@@ -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;

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

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

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

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

:host ::ng-deep .child-value-component {
.mat-form-field-underline {
display: none;
Expand Down
@@ -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;
Expand Down
@@ -1,5 +1,3 @@
@import "../../../../../assets/style/viewer";

// Firefox:
// Hide number picker
input[type=number] {
Expand Down
@@ -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;
Expand Down
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
@@ -1 +0,0 @@
@import "../../../../../assets/style/viewer";
81 changes: 81 additions & 0 deletions src/app/workspace/resource/values/text-value/ck-editor.ts
@@ -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";
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
@@ -1 +0,0 @@
@import "../../../../../../assets/style/viewer";