Skip to content

Commit

Permalink
Integrate Ckeditor into Property View (#199)
Browse files Browse the repository at this point in the history
* enhancement (property view): integrate ckeditor into property view

* refactor (viewer): handle XML validation in text value as XML comp.

* tests (viewer): adapt tests since XML values are editable now

* feature (viewer): convert CKEditor XML to Knora XML on validation

* feature (viewer): convert CKEditor XML to Knora XML on validation

* feature (viewer): convert CKEditor XML to Knora XML on validation

* feature (viewer): move xml conversion in comp.

* feature (viewer): treat XML text values as read-only values if the do not use the standard mapping

* feature (viewer): different read-modes for XML text values depending on mapping

* tests (viewer): add test cases for ReadTextValueAsXML with different mappings

* tests (viewer): add test cases for ReadTextValueAsXML with different mappings

* fix (add-value): add xml value directive to add value template

* refactor (viewer): adapt message

* css (xml value): override default margin for paragraphs

Co-authored-by: Tobias Schweizer <t.schweizer@unibas.ch>
Co-authored-by: Tobias Schweizer <tobiasschweizer@users.noreply.github.com>
  • Loading branch information
3 people committed Oct 6, 2020
1 parent 29f259d commit 2835864
Show file tree
Hide file tree
Showing 14 changed files with 221 additions and 172 deletions.
Expand Up @@ -3,6 +3,7 @@
<span [ngSwitch]="resourcePropertyDefinition.objectType">
<dsp-text-value-as-string #createVal *ngSwitchCase="'ReadTextValueAsString'" [mode]="mode"></dsp-text-value-as-string>
<dsp-text-value-as-html #createVal *ngSwitchCase="'ReadTextValueAsHtml'" [mode]="mode"></dsp-text-value-as-html>
<dsp-text-value-as-xml #createVal *ngSwitchCase="'ReadTextValueAsXml'" [mode]="mode"></dsp-text-value-as-xml>
<dsp-int-value #createVal *ngSwitchCase="constants.IntValue" [mode]="mode"></dsp-int-value>
<dsp-boolean-value #createVal *ngSwitchCase="constants.BooleanValue" [mode]="mode"></dsp-boolean-value>
<dsp-uri-value #createVal *ngSwitchCase="constants.UriValue" [mode]="mode"></dsp-uri-value>
Expand Down
Expand Up @@ -7,6 +7,7 @@
<!-- display value is cast as 'any' because the compiler cannot infer the value type expected by the child component -->
<dsp-text-value-as-string class="parent-value-component" #displayVal *ngSwitchCase="'ReadTextValueAsString'" [mode]="mode" [displayValue]="$any(displayValue)"></dsp-text-value-as-string>
<dsp-text-value-as-html class="parent-value-component" #displayVal *ngSwitchCase="'ReadTextValueAsHtml'" [mode]="mode" [displayValue]="$any(displayValue)"></dsp-text-value-as-html>
<dsp-text-value-as-xml class="parent-value-component" #displayVal *ngSwitchCase="'ReadTextValueAsXml'" [mode]="mode" [displayValue]="$any(displayValue)"></dsp-text-value-as-xml>
<dsp-int-value class="parent-value-component" #displayVal *ngSwitchCase="constants.IntValue" [mode]="mode" [displayValue]="$any(displayValue)"></dsp-int-value>
<dsp-boolean-value class="parent-value-component" #displayVal *ngSwitchCase="constants.BooleanValue" [mode]="mode" [displayValue]="$any(displayValue)"></dsp-boolean-value>
<dsp-uri-value class="parent-value-component" #displayVal *ngSwitchCase="constants.UriValue" [mode]="mode" [displayValue]="$any(displayValue)"></dsp-uri-value>
Expand Down
Expand Up @@ -513,7 +513,7 @@ describe('DisplayEditComponent', () => {
it('should return the type of a integer value as not readonly', () => {
expect(valueTypeService.getValueTypeOrClass(testHostComponent.displayEditValueComponent.displayValue)).toEqual(Constants.IntValue);

expect(valueTypeService.isReadOnly(Constants.IntValue)).toBe(false);
expect(valueTypeService.isReadOnly(Constants.IntValue, testHostComponent.displayEditValueComponent.displayValue)).toBe(false);
});

it('should return the class of a html text value as readonly', () => {
Expand All @@ -523,18 +523,7 @@ describe('DisplayEditComponent', () => {

expect(valueTypeService.getValueTypeOrClass(htmlTextVal)).toEqual('ReadTextValueAsHtml');

expect(valueTypeService.isReadOnly('ReadTextValueAsHtml')).toBe(true);

});

it('should return the class of an XML text value as readonly', () => {

const xmlTextVal = new ReadTextValueAsXml();
xmlTextVal.type = Constants.TextValue;

expect(valueTypeService.getValueTypeOrClass(xmlTextVal)).toEqual('ReadTextValueAsXml');

expect(valueTypeService.isReadOnly('ReadTextValueAsXml')).toBe(true);
expect(valueTypeService.isReadOnly('ReadTextValueAsHtml', htmlTextVal)).toBe(true);

});

Expand All @@ -545,7 +534,7 @@ describe('DisplayEditComponent', () => {

expect(valueTypeService.getValueTypeOrClass(plainTextVal)).toEqual('ReadTextValueAsString');

expect(valueTypeService.isReadOnly('ReadTextValueAsString')).toBe(false);
expect(valueTypeService.isReadOnly('ReadTextValueAsString', plainTextVal)).toBe(false);

});

Expand Down
Expand Up @@ -126,7 +126,7 @@ export class DisplayEditComponent implements OnInit {

this.valueTypeOrClass = this._valueTypeService.getValueTypeOrClass(this.displayValue);

this.readOnlyValue = this._valueTypeService.isReadOnly(this.valueTypeOrClass);
this.readOnlyValue = this._valueTypeService.isReadOnly(this.valueTypeOrClass, this.displayValue);

this._dspApiConnection.admin.usersEndpoint.getUserByIri(this.displayValue.attachedToUser).subscribe(
(response: ApiResponseData<UserResponse>) => {
Expand Down
22 changes: 19 additions & 3 deletions projects/dsp-ui/src/lib/viewer/services/value-type.service.spec.ts
@@ -1,6 +1,6 @@
import { TestBed } from '@angular/core/testing';

import { ReadIntValue, ReadTextValueAsHtml, ReadTextValueAsString } from '@dasch-swiss/dsp-js';
import { ReadIntValue, ReadTextValueAsHtml, ReadTextValueAsString, ReadTextValueAsXml } from '@dasch-swiss/dsp-js';
import { ValueTypeService } from './value-type.service';

describe('ValueTypeService', () => {
Expand Down Expand Up @@ -36,14 +36,30 @@ describe('ValueTypeService', () => {
const readTextValueAsString = new ReadTextValueAsString();
readTextValueAsString.type = 'http://api.knora.org/ontology/knora-api/v2#TextValue';
const valueClass = service.getValueTypeOrClass(readTextValueAsString);
expect(service.isReadOnly(valueClass)).toBeFalsy();
expect(service.isReadOnly(valueClass, readTextValueAsString)).toBeFalsy();
});

it('should mark ReadTextValueAsHtml as ReadOnly', () => {
const readTextValueAsHtml = new ReadTextValueAsHtml();
readTextValueAsHtml.type = 'http://api.knora.org/ontology/knora-api/v2#TextValue';
const valueClass = service.getValueTypeOrClass(readTextValueAsHtml);
expect(service.isReadOnly(valueClass)).toBeTruthy();
expect(service.isReadOnly(valueClass, readTextValueAsHtml)).toBeTruthy();
});

it('should not mark ReadTextValueAsXml with standard mapping as ReadOnly', () => {
const readTextValueAsXml = new ReadTextValueAsXml();
readTextValueAsXml.type = 'http://api.knora.org/ontology/knora-api/v2#TextValue';
readTextValueAsXml.mapping = 'http://rdfh.ch/standoff/mappings/StandardMapping';
const valueClass = service.getValueTypeOrClass(readTextValueAsXml);
expect(service.isReadOnly(valueClass, readTextValueAsXml)).toBeFalsy();
});

it('should mark ReadTextValueAsXml with custom mapping as ReadOnly', () => {
const readTextValueAsXml = new ReadTextValueAsXml();
readTextValueAsXml.type = 'http://api.knora.org/ontology/knora-api/v2#TextValue';
readTextValueAsXml.mapping = 'http://rdfh.ch/standoff/mappings/CustomMapping';
const valueClass = service.getValueTypeOrClass(readTextValueAsXml);
expect(service.isReadOnly(valueClass, readTextValueAsXml)).toBeTruthy();
});
});

Expand Down
12 changes: 8 additions & 4 deletions projects/dsp-ui/src/lib/viewer/services/value-type.service.ts
Expand Up @@ -56,7 +56,7 @@ export class ValueTypeService {
if (resourcePropDef.guiElement === 'http://api.knora.org/ontology/salsah-gui/v2#SimpleText') {
return this._readTextValueAsString;
} else if (resourcePropDef.guiElement === 'http://api.knora.org/ontology/salsah-gui/v2#Richtext') {
return this._readTextValueAsHtml;
return this._readTextValueAsXml;
} else {
throw new Error(`unknown TextValue class ${resourcePropDef}`);
}
Expand Down Expand Up @@ -85,10 +85,14 @@ export class ValueTypeService {
* Determines if the given value is readonly.
*
* @param valueTypeOrClass the type or class of the given value.
* @param value the given value.
*/
isReadOnly(valueTypeOrClass: string): boolean {
isReadOnly(valueTypeOrClass: string, value: ReadValue): boolean {
const xmlValueNonStandardMapping
= valueTypeOrClass === this._readTextValueAsXml
&& (value instanceof ReadTextValueAsXml && value.mapping !== 'http://rdfh.ch/standoff/mappings/StandardMapping');

return valueTypeOrClass === this._readTextValueAsHtml ||
valueTypeOrClass === this._readTextValueAsXml ||
valueTypeOrClass === this.constants.GeomValue;
valueTypeOrClass === this.constants.GeomValue || xmlValueNonStandardMapping;
}
}
@@ -1,18 +1,30 @@
<div *ngIf="editor; else noConfig" [formGroup]="form">
<ckeditor formControlName="xmlValue" [config]="editorConfig" [editor]="editor"></ckeditor>
<mat-form-field class="large-field value-component-comment">
<textarea matInput
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="6"
[formControlName]="'comment'"
class="comment"
placeholder="Comment"
type="text"
spellcheck="false">
</textarea>
</mat-form-field>
</div>
<ng-template #noConfig>
No mapping or apt configuration was provided for "xmlTransform" in the app's configuration object.
<span *ngIf="mode === 'read'; else showForm" class="read-mode-view">
<div *ngIf="this.displayValue?.mapping === standardMapping; else sourceMode" class="rm-value" [innerHTML]="valueFormControl.value"></div>
<ng-template #sourceMode>
<span class="rm-value">{{displayValue?.xml}}</span>
</ng-template>
<span class="rm-comment" *ngIf="shouldShowComment">{{commentFormControl.value}}</span>
</span>
<ng-template #showForm>
<div *ngIf="editor; else noConfig" [formGroup]="form">
<span [formGroup]="form">
<ckeditor [formControlName]="'xmlValue'" [config]="editorConfig" [editor]="editor"></ckeditor>
<mat-form-field class="large-field value-component-comment">
<textarea matInput
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="6"
[formControlName]="'comment'"
class="comment"
placeholder="Comment"
type="text"
spellcheck="false">
</textarea>
</mat-form-field>
</span>
</div>
<ng-template #noConfig>
No class was provided for CKEditor.
</ng-template>
</ng-template>

Expand Up @@ -5,3 +5,9 @@
::ng-deep .ck-content code {
font-family: monospace;
}

.rm-value {
::ng-deep p {
margin: 0;
}
}
Expand Up @@ -105,23 +105,6 @@ class TestHostCreateValueComponent implements OnInit {

describe('TextValueAsXMLComponent', () => {

const appInitServiceMock = {
config: {
xmlTransform: {
'http://rdfh.ch/standoff/mappings/StandardMapping': {
'<hr>': '<hr/>',
'</hr>': '',
'<s>': '<strike>',
'</s>': '</strike>',
'<i>': '<em>',
'</i>': '</em>',
'<figure class="table">': '',
'</figure>': ''
}
}
}
};

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
Expand All @@ -134,9 +117,6 @@ describe('TextValueAsXMLComponent', () => {
ReactiveFormsModule,
MatInputModule,
BrowserAnimationsModule
],
providers: [
{provide: AppInitService, useValue: appInitServiceMock}
]
})
.compileComponents();
Expand All @@ -145,6 +125,12 @@ describe('TextValueAsXMLComponent', () => {
describe('display and edit a text value with xml markup', () => {
let testHostComponent: TestHostDisplayValueComponent;
let testHostFixture: ComponentFixture<TestHostDisplayValueComponent>;

let valueComponentDe: DebugElement;

let valueReadModeDebugElement: DebugElement;
let valueReadModeNativeElement;

let ckeditorDe: DebugElement;

beforeEach(() => {
Expand All @@ -153,21 +139,50 @@ describe('TextValueAsXMLComponent', () => {
testHostFixture.detectChanges();

const hostCompDe = testHostFixture.debugElement;
valueComponentDe = hostCompDe.query(By.directive(TextValueAsXMLComponent));

valueReadModeDebugElement = valueComponentDe.query(By.css('.rm-value'));
valueReadModeNativeElement = valueReadModeDebugElement.nativeElement;

// reset before each it
ckeditorDe = undefined;

ckeditorDe = hostCompDe.query(By.directive(TestCKEditorComponent));
});

it('should display an existing value', () => {
it('should display an existing value for the standard mapping as formatted text', () => {

expect(testHostComponent.inputValueComponent.displayValue.xml).toEqual('<?xml version="1.0" encoding="UTF-8"?>\n<text><p>test with <strong>markup</strong></p></text>');

expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy();

expect(testHostComponent.inputValueComponent.mode).toEqual('read');

expect(testHostComponent.inputValueComponent.valueFormControl.disabled).toBeTruthy();
expect(valueReadModeNativeElement.innerHTML).toEqual('\n<p>test with <strong>markup</strong></p>');

expect(ckeditorDe.componentInstance.value).toEqual('\n<p>test with <strong>markup</strong></p>');
});

it('should display an existing value for a custom mapping as XML source code', () => {

const newXml = new ReadTextValueAsXml();

newXml.xml = '<?xml version="1.0" encoding="UTF-8"?><text><p>my updated text</p></text>';
newXml.mapping = 'http://rdfh.ch/standoff/mappings/customMapping';

newXml.id = 'id';

testHostComponent.displayInputVal = newXml;

testHostFixture.detectChanges();

valueReadModeDebugElement = valueComponentDe.query(By.css('.rm-value'));

valueReadModeNativeElement = valueReadModeDebugElement.nativeElement;

expect(valueReadModeNativeElement.innerText).toEqual(
'<?xml version="1.0" encoding="UTF-8"?><text><p>my updated text</p></text>');

// custom mappings are not supported by this component
expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy();

});

Expand All @@ -177,6 +192,8 @@ describe('TextValueAsXMLComponent', () => {

testHostFixture.detectChanges();

ckeditorDe = valueComponentDe.query(By.directive(TestCKEditorComponent));

expect(testHostComponent.inputValueComponent.mode).toEqual('update');

expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy();
Expand Down Expand Up @@ -208,6 +225,8 @@ describe('TextValueAsXMLComponent', () => {

testHostFixture.detectChanges();

ckeditorDe = valueComponentDe.query(By.directive(TestCKEditorComponent));

expect(testHostComponent.inputValueComponent.mode).toEqual('update');

expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy();
Expand All @@ -234,6 +253,8 @@ describe('TextValueAsXMLComponent', () => {

testHostFixture.detectChanges();

ckeditorDe = valueComponentDe.query(By.directive(TestCKEditorComponent));

expect(testHostComponent.inputValueComponent.mode).toEqual('update');

expect(testHostComponent.inputValueComponent.form.valid).toBeFalsy();
Expand All @@ -258,7 +279,7 @@ describe('TextValueAsXMLComponent', () => {

const newXml = new ReadTextValueAsXml();

newXml.xml = '<?xml version="1.0" encoding="UTF-8"?><text><p>my updated text<p></text>';
newXml.xml = '<?xml version="1.0" encoding="UTF-8"?><text><p>my updated text</p></text>';
newXml.mapping = 'http://rdfh.ch/standoff/mappings/StandardMapping';

newXml.id = 'updatedId';
Expand All @@ -267,7 +288,7 @@ describe('TextValueAsXMLComponent', () => {

testHostFixture.detectChanges();

expect(ckeditorDe.componentInstance.value).toEqual('<p>my updated text<p>');
expect(valueReadModeNativeElement.innerHTML).toEqual('<p>my updated text</p>');

expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy();

Expand All @@ -279,6 +300,8 @@ describe('TextValueAsXMLComponent', () => {

testHostFixture.detectChanges();

ckeditorDe = valueComponentDe.query(By.directive(TestCKEditorComponent));

// simulate input in ckeditor
ckeditorDe.componentInstance.value = '<p>test <i>with</i> a lot of <i>markup</i></p>';
ckeditorDe.componentInstance._handleInput();
Expand All @@ -296,6 +319,8 @@ describe('TextValueAsXMLComponent', () => {

testHostFixture.detectChanges();

ckeditorDe = valueComponentDe.query(By.directive(TestCKEditorComponent));

// simulate input in ckeditor
ckeditorDe.componentInstance.value = '<p>test with horizontal line <hr></hr></p>';
ckeditorDe.componentInstance._handleInput();
Expand All @@ -313,6 +338,8 @@ describe('TextValueAsXMLComponent', () => {

testHostFixture.detectChanges();

ckeditorDe = valueComponentDe.query(By.directive(TestCKEditorComponent));

// simulate input in ckeditor
ckeditorDe.componentInstance.value = '<p>test with <s>struck</s> word</p>';
ckeditorDe.componentInstance._handleInput();
Expand All @@ -330,6 +357,8 @@ describe('TextValueAsXMLComponent', () => {

testHostFixture.detectChanges();

ckeditorDe = valueComponentDe.query(By.directive(TestCKEditorComponent));

// simulate input in ckeditor
ckeditorDe.componentInstance.value = '<p><figure class="table"><table><tbody><tr><td>test</td><td>test</td></tr><tr><td>test</td><td>test</td></tr></tbody></table></figure></p>';
ckeditorDe.componentInstance._handleInput();
Expand Down

0 comments on commit 2835864

Please sign in to comment.