@ -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 }; |
||||
|
} |
||||
|
} |
||||