@ -0,0 +1,53 @@ |
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
|
|||
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> |
|||
<script src="editor-sdk.js"></script> |
|||
|
|||
<style> |
|||
.editor { |
|||
border: 1px solid #eee; |
|||
border-radius: 4px; |
|||
height: 500px; |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
|
|||
</head> |
|||
|
|||
<body style="margin: 0px; padding: 0px;"> |
|||
<textarea name="content" class="editor" id="editor"></textarea> |
|||
|
|||
<script> |
|||
var element = document.getElementById('editor'); |
|||
|
|||
// When the field is instantiated it notifies the UI that it has been loaded. |
|||
// |
|||
// Furthermore it sends the current size to the parent. |
|||
var field = new SquidexFormField(); |
|||
|
|||
field.onValueChanged(function (value) { |
|||
if (value) { |
|||
element.value = JSON.stringify(value); |
|||
} else { |
|||
element.value = ''; |
|||
} |
|||
}); |
|||
|
|||
field.onDisabled(function (disabled) { |
|||
element.disabled = disabled; |
|||
}); |
|||
|
|||
element.addEventListener('change', function () { |
|||
var value = element.value; |
|||
|
|||
if (value) { |
|||
field.valueChanged(JSON.parse(value)); |
|||
} else { |
|||
field.valueChanged(undefined); |
|||
} |
|||
}); |
|||
</script> |
|||
</body> |
|||
</html> |
|||
|
Before Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 703 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 102 KiB |
@ -0,0 +1,153 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { FormControl, FormGroup } from '@angular/forms'; |
|||
import { FormArrayTemplate, TemplatedFormArray } from './templated-form-array'; |
|||
|
|||
describe('TemplatedFormArray', () => { |
|||
class Template implements FormArrayTemplate { |
|||
public clearCalled = 0; |
|||
public removeCalled: number[] = []; |
|||
|
|||
public createControl() { |
|||
return new FormGroup({ |
|||
value: new FormControl(), |
|||
}); |
|||
} |
|||
|
|||
public clearControls() { |
|||
this.clearCalled++; |
|||
} |
|||
|
|||
public removeControl(index: number) { |
|||
this.removeCalled.push(index); |
|||
} |
|||
} |
|||
|
|||
let formTemplate: Template; |
|||
let formArray: TemplatedFormArray; |
|||
|
|||
beforeEach(() => { |
|||
formTemplate = new Template(); |
|||
formArray = new TemplatedFormArray(formTemplate); |
|||
}); |
|||
|
|||
type Test = [ (value: any) => void, string]; |
|||
|
|||
const methods: Test[] = [ |
|||
[x => formArray.setValue(x), 'setValue'], |
|||
[x => formArray.patchValue(x), 'patchValue'], |
|||
[x => formArray.reset(x), 'reset'], |
|||
]; |
|||
|
|||
methods.forEach(([method, name]) => { |
|||
it(`Should call template to construct items for ${name}`, () => { |
|||
const value1 = [{ |
|||
value: 1, |
|||
}, { |
|||
value: 2, |
|||
}]; |
|||
|
|||
method(value1); |
|||
|
|||
expect(formArray.value).toEqual(value1); |
|||
}); |
|||
|
|||
it(`Should call template to remove items for ${name}`, () => { |
|||
const value1 = [{ |
|||
value: 1, |
|||
}, { |
|||
value: 2, |
|||
}, { |
|||
value: 3, |
|||
}, { |
|||
value: 4, |
|||
}]; |
|||
|
|||
const value2 = [{ |
|||
value: 1, |
|||
}, { |
|||
value: 2, |
|||
}]; |
|||
|
|||
method(value1); |
|||
method(value2); |
|||
|
|||
expect(formArray.value).toEqual(value2); |
|||
expect(formTemplate.clearCalled).toEqual(0); |
|||
expect(formTemplate.removeCalled).toEqual([3, 2]); |
|||
}); |
|||
|
|||
it(`Should call template to clear items with undefined for ${name}`, () => { |
|||
const value1 = [{ |
|||
value: 1, |
|||
}, { |
|||
value: 2, |
|||
}]; |
|||
|
|||
method(value1); |
|||
method(undefined); |
|||
|
|||
expect(formArray.value).toEqual(undefined); |
|||
expect(formTemplate.clearCalled).toEqual(1); |
|||
expect(formTemplate.removeCalled).toEqual([]); |
|||
}); |
|||
|
|||
it(`Should call template to clear items with empty array for ${name}`, () => { |
|||
const value1 = [{ |
|||
value: 1, |
|||
}, { |
|||
value: 2, |
|||
}]; |
|||
|
|||
method(value1); |
|||
method([]); |
|||
|
|||
expect(formArray.value).toEqual([]); |
|||
expect(formTemplate.clearCalled).toEqual(1); |
|||
expect(formTemplate.removeCalled).toEqual([]); |
|||
}); |
|||
}); |
|||
|
|||
it('should add control', () => { |
|||
formArray.add(); |
|||
formArray.add(); |
|||
|
|||
expect(formArray.value).toEqual([{ |
|||
value: null, |
|||
}, { |
|||
value: null, |
|||
}]); |
|||
}); |
|||
|
|||
it('should call template when cleared', () => { |
|||
formArray.add(); |
|||
formArray.clear(); |
|||
|
|||
expect(formTemplate.clearCalled).toEqual(1); |
|||
}); |
|||
|
|||
it('should not call template when clearing empty form', () => { |
|||
formArray.clear(); |
|||
|
|||
expect(formTemplate.clearCalled).toEqual(0); |
|||
}); |
|||
|
|||
it('should call template when item removed', () => { |
|||
formArray.add(); |
|||
formArray.removeAt(0); |
|||
|
|||
expect(formTemplate.removeCalled).toEqual([0]); |
|||
}); |
|||
|
|||
it('should not call template when item to remove out of bounds', () => { |
|||
formArray.add(); |
|||
formArray.removeAt(1); |
|||
|
|||
expect(formTemplate.removeCalled).toEqual([]); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,86 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, ValidatorFn } from '@angular/forms'; |
|||
import { Types } from '@app/framework/internal'; |
|||
import { UndefinableFormArray } from './undefinable-form-array'; |
|||
|
|||
export interface FormArrayTemplate { |
|||
createControl(value: any, initialValue?: any): AbstractControl; |
|||
|
|||
removeControl?(index: number, control: AbstractControl) : void; |
|||
|
|||
clearControls?(): void; |
|||
} |
|||
|
|||
export class TemplatedFormArray extends UndefinableFormArray { |
|||
constructor(public readonly template: FormArrayTemplate, |
|||
validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, |
|||
) { |
|||
super([], validatorOrOpts, asyncValidator); |
|||
} |
|||
|
|||
public setValue(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) { |
|||
this.prepare(value); |
|||
|
|||
super.setValue(value, options); |
|||
} |
|||
|
|||
public patchValue(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) { |
|||
this.prepare(value); |
|||
|
|||
super.patchValue(value, options); |
|||
} |
|||
|
|||
public reset(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) { |
|||
this.prepare(value); |
|||
|
|||
super.reset(value, options); |
|||
} |
|||
|
|||
public add(initialValue?: any) { |
|||
const control = this.template.createControl({}, initialValue); |
|||
|
|||
this.push(control); |
|||
|
|||
return control; |
|||
} |
|||
|
|||
public removeAt(index: number, options?: { emitEvent?: boolean }) { |
|||
if (this.template.removeControl && index >= 0 && index < this.controls.length) { |
|||
this.template.removeControl(index, this.controls[index]); |
|||
} |
|||
|
|||
super.removeAt(index, options); |
|||
} |
|||
|
|||
public clear(options?: { emitEvent?: boolean }) { |
|||
if (this.template.clearControls && this.controls.length > 0) { |
|||
this.template.clearControls(); |
|||
} |
|||
|
|||
super.clear(options); |
|||
} |
|||
|
|||
private prepare(value?: any[]) { |
|||
if (Types.isArray(value) && value.length > 0) { |
|||
let index = this.controls.length; |
|||
|
|||
while (this.controls.length < value.length) { |
|||
this.add(value[index]); |
|||
|
|||
index++; |
|||
} |
|||
|
|||
while (this.controls.length > value.length) { |
|||
this.removeAt(this.controls.length - 1, { emitEvent: false }); |
|||
} |
|||
} else { |
|||
this.clear(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { FormControl, FormGroup } from '@angular/forms'; |
|||
import { FormGroupTemplate, TemplatedFormGroup } from './templated-form-group'; |
|||
|
|||
describe('TemplatedFormGroup', () => { |
|||
class Template implements FormGroupTemplate { |
|||
public clearCalled = 0; |
|||
public removeCalled: number[] = []; |
|||
|
|||
public setControls(form: FormGroup) { |
|||
form.setControl('value', new FormControl()); |
|||
} |
|||
|
|||
public clearControls() { |
|||
this.clearCalled++; |
|||
} |
|||
} |
|||
|
|||
let formTemplate: Template; |
|||
let formArray: TemplatedFormGroup; |
|||
|
|||
beforeEach(() => { |
|||
formTemplate = new Template(); |
|||
formArray = new TemplatedFormGroup(formTemplate); |
|||
}); |
|||
|
|||
type Test = [ (value: any) => void, string]; |
|||
|
|||
const methods: Test[] = [ |
|||
[x => formArray.setValue(x), 'setValue'], |
|||
[x => formArray.patchValue(x), 'patchValue'], |
|||
[x => formArray.reset(x), 'reset'], |
|||
]; |
|||
|
|||
methods.forEach(([method, name]) => { |
|||
it(`Should call template to construct controls for ${name}`, () => { |
|||
const value1 = { |
|||
value: 1, |
|||
}; |
|||
|
|||
method(value1); |
|||
|
|||
expect(formArray.value).toEqual(value1); |
|||
}); |
|||
it(`Should call template to clear items with for ${name}`, () => { |
|||
const value1 = { |
|||
value: 1, |
|||
}; |
|||
|
|||
method(value1); |
|||
method(undefined); |
|||
|
|||
expect(formArray.value).toEqual(undefined); |
|||
expect(formTemplate.clearCalled).toEqual(1); |
|||
expect(formTemplate.removeCalled).toEqual([]); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,50 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms'; |
|||
import { Types } from '@app/framework/internal'; |
|||
import { UndefinableFormGroup } from './undefinable-form-group'; |
|||
|
|||
export interface FormGroupTemplate { |
|||
setControls(form: FormGroup, value: any): void; |
|||
|
|||
clearControls?(): void; |
|||
} |
|||
|
|||
export class TemplatedFormGroup extends UndefinableFormGroup { |
|||
constructor(public readonly template: FormGroupTemplate, |
|||
validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, |
|||
) { |
|||
super({}, validatorOrOpts, asyncValidator); |
|||
} |
|||
|
|||
public setValue(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) { |
|||
this.build(value); |
|||
|
|||
super.setValue(value, options); |
|||
} |
|||
|
|||
public patchValue(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) { |
|||
this.build(value); |
|||
|
|||
super.patchValue(value, options); |
|||
} |
|||
|
|||
public reset(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) { |
|||
this.build(value); |
|||
|
|||
super.reset(value, options); |
|||
} |
|||
|
|||
public build(value?: {}) { |
|||
if (Types.isObject(value)) { |
|||
this.template?.setControls(this, value); |
|||
} else if (this.template?.clearControls) { |
|||
this.template?.clearControls(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,157 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
/* eslint-disable @typescript-eslint/no-implied-eval */ |
|||
/* eslint-disable no-useless-return */ |
|||
|
|||
import { Types } from '@app/framework'; |
|||
import { FieldRule, SchemaDto } from './../services/schemas.service'; |
|||
|
|||
export type RuleContext = { data: any; user?: any }; |
|||
export type RuleForm = { fieldPath: string }; |
|||
|
|||
export interface CompiledRules { |
|||
get rules(): ReadonlyArray<CompiledRule>; |
|||
} |
|||
|
|||
export interface RulesProvider { |
|||
compileRules(schema: SchemaDto): ReadonlyArray<CompiledRule>; |
|||
|
|||
getRules(form: RuleForm): CompiledRules; |
|||
} |
|||
|
|||
export class CompiledRule { |
|||
private readonly function: Function; |
|||
|
|||
public get field() { |
|||
return this.rule.field; |
|||
} |
|||
|
|||
public get action() { |
|||
return this.rule.action; |
|||
} |
|||
|
|||
constructor( |
|||
private readonly rule: FieldRule, |
|||
private readonly useItemData: boolean, |
|||
) { |
|||
try { |
|||
this.function = new Function(`return function(user, ctx, data, itemData) { return ${rule.condition} }`)(); |
|||
} catch { |
|||
this.function = () => false; |
|||
} |
|||
} |
|||
|
|||
public eval(context: RuleContext, itemData: any) { |
|||
try { |
|||
const data = this.useItemData ? itemData || context.data : context.data; |
|||
|
|||
return this.function(context.user, context, data, itemData); |
|||
} catch { |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
const EMPTY_RULES: CompiledRule[] = []; |
|||
const EMPTY_RULES_STATIC = { rules: EMPTY_RULES }; |
|||
|
|||
class ComponentRules implements ComponentRules { |
|||
private previouSchema: SchemaDto; |
|||
private compiledRules: ReadonlyArray<CompiledRule> = []; |
|||
|
|||
public get rules() { |
|||
const schema = this.schema(); |
|||
|
|||
if (schema !== this.previouSchema) { |
|||
if (schema) { |
|||
this.compiledRules = Types.fastMerge(this.parent.getRules(this.form).rules, this.getRelativeRules(this.form, schema)); |
|||
} else { |
|||
this.compiledRules = EMPTY_RULES; |
|||
} |
|||
} |
|||
|
|||
return this.compiledRules; |
|||
} |
|||
|
|||
constructor( |
|||
private readonly form: RuleForm, |
|||
private readonly parentPath: string, |
|||
private readonly parent: RulesProvider, |
|||
private readonly schema: () => SchemaDto | undefined, |
|||
) { |
|||
} |
|||
|
|||
private getRelativeRules(form: RuleForm, schema: SchemaDto) { |
|||
const rules = this.parent.compileRules(schema); |
|||
|
|||
if (rules.length === 0) { |
|||
return EMPTY_RULES; |
|||
} |
|||
|
|||
const pathField = form.fieldPath.substr(this.parentPath.length + 1); |
|||
const pathSimplified = pathField.replace('.iv.', '.'); |
|||
|
|||
return rules.filter(x => x.field === pathField || x.field === pathSimplified); |
|||
} |
|||
} |
|||
|
|||
export class ComponentRulesProvider implements RulesProvider { |
|||
constructor( |
|||
private readonly parentPath: string, |
|||
private readonly parent: RulesProvider, |
|||
private readonly schema: () => SchemaDto | undefined, |
|||
) { |
|||
} |
|||
|
|||
public compileRules(schema: SchemaDto) { |
|||
return this.parent.compileRules(schema); |
|||
} |
|||
|
|||
public getRules(form: RuleForm) { |
|||
return new ComponentRules(form, this.parentPath, this.parent, this.schema); |
|||
} |
|||
} |
|||
|
|||
export class RootRulesProvider implements RulesProvider { |
|||
private readonly rulesCache: { [id: string]: ReadonlyArray<CompiledRule> } = {}; |
|||
private readonly rules: ReadonlyArray<CompiledRule>; |
|||
|
|||
constructor(schema: SchemaDto) { |
|||
this.rules = this.compileRules(schema); |
|||
} |
|||
|
|||
public compileRules(schema: SchemaDto) { |
|||
if (!schema) { |
|||
return EMPTY_RULES; |
|||
} |
|||
|
|||
let result = this.rulesCache[schema.id]; |
|||
|
|||
if (!result) { |
|||
result = schema.fieldRules.map(x => new CompiledRule(x, true)); |
|||
|
|||
this.rulesCache[schema.id] = result; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public getRules(form: RuleForm) { |
|||
const allRules = this.rules; |
|||
|
|||
if (allRules.length === 0) { |
|||
return EMPTY_RULES_STATIC; |
|||
} |
|||
|
|||
const pathField = form.fieldPath; |
|||
const pathSimplified = pathField.replace('.iv.', '.'); |
|||
|
|||
const rules = allRules.filter(x => x.field === pathField || x.field === pathSimplified); |
|||
|
|||
return { rules }; |
|||
} |
|||
} |
|||