diff --git a/src/Squidex/app/framework/angular/autocomplete.component.ts b/src/Squidex/app/framework/angular/autocomplete.component.ts
index 8f88ed62f..2a9bc1062 100644
--- a/src/Squidex/app/framework/angular/autocomplete.component.ts
+++ b/src/Squidex/app/framework/angular/autocomplete.component.ts
@@ -26,9 +26,7 @@ export class AutocompleteItem {
const KEY_ENTER = 13;
const KEY_UP = 38;
const KEY_DOWN = 40;
-
/* tslint:disable:no-empty */
-
const NOOP = () => { };
export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
diff --git a/src/Squidex/app/framework/angular/date-time-editor.component.ts b/src/Squidex/app/framework/angular/date-time-editor.component.ts
index 2d5bc9e0a..6e601cd1c 100644
--- a/src/Squidex/app/framework/angular/date-time-editor.component.ts
+++ b/src/Squidex/app/framework/angular/date-time-editor.component.ts
@@ -12,7 +12,6 @@ import * as moment from 'moment';
let Pikaday = require('pikaday/pikaday');
/* tslint:disable:no-empty */
-
const NOOP = () => { };
export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
diff --git a/src/Squidex/app/framework/angular/json-editor.component.html b/src/Squidex/app/framework/angular/json-editor.component.html
new file mode 100644
index 000000000..bb84a4048
--- /dev/null
+++ b/src/Squidex/app/framework/angular/json-editor.component.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/Squidex/app/framework/angular/json-editor.component.scss b/src/Squidex/app/framework/angular/json-editor.component.scss
new file mode 100644
index 000000000..58b58ff8c
--- /dev/null
+++ b/src/Squidex/app/framework/angular/json-editor.component.scss
@@ -0,0 +1,8 @@
+@import '_mixins';
+@import '_vars';
+
+.editor {
+ background: $color-accent-dark;
+ border: 1px solid $color-input;
+ height: 20rem;
+}
\ No newline at end of file
diff --git a/src/Squidex/app/framework/angular/json-editor.component.ts b/src/Squidex/app/framework/angular/json-editor.component.ts
new file mode 100644
index 000000000..6c6ae5629
--- /dev/null
+++ b/src/Squidex/app/framework/angular/json-editor.component.ts
@@ -0,0 +1,137 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Sebastian Stehle. All rights reserved
+ */
+
+import { AfterViewInit, Component, forwardRef, ElementRef, ViewChild } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { ReplaySubject } from 'rxjs';
+
+declare var ace: any;
+
+/* tslint:disable:no-empty */
+const NOOP = () => { };
+
+export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => JsonEditorComponent),
+ multi: true
+};
+
+@Component({
+ selector: 'sqx-json-editor',
+ styleUrls: ['./json-editor.component.scss'],
+ templateUrl: './json-editor.component.html',
+ providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR]
+})
+export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit {
+ private static loaderCallback: ReplaySubject;
+ private static isLoaded: boolean;
+
+ private changeCallback: (value: any) => void = NOOP;
+ private touchedCallback: () => void = NOOP;
+ private aceEditor: any;
+ private value: any;
+ private isDisabled = false;
+
+ @ViewChild('editor')
+ public editor: ElementRef;
+
+ public writeValue(value: any) {
+ this.value = value;
+
+ if (this.aceEditor) {
+ this.setValue(value);
+ }
+ }
+
+ public setDisabledState(isDisabled: boolean): void {
+ this.isDisabled = isDisabled;
+
+ if (this.aceEditor) {
+ this.aceEditor.setReadOnly(isDisabled);
+ }
+ }
+
+ public registerOnChange(fn: any) {
+ this.changeCallback = fn;
+ }
+
+ public registerOnTouched(fn: any) {
+ this.touchedCallback = fn;
+ }
+
+ public ngAfterViewInit() {
+ JsonEditorComponent.loadScript(() => {
+ this.aceEditor = ace.edit(this.editor.nativeElement);
+
+ this.aceEditor.getSession().setMode('ace/mode/javascript');
+ this.aceEditor.setReadOnly(this.isDisabled);
+ this.aceEditor.setFontSize(14);
+
+ this.setValue(this.value);
+
+ this.aceEditor.on('blur', () => {
+ const isValid = this.aceEditor.getSession().getAnnotations().length === 0;
+
+ if (!isValid) {
+ this.changeCallback(null);
+ } else {
+ try {
+ const value = JSON.parse(this.aceEditor.getValue());
+
+ this.changeCallback(value);
+ } catch (e) {
+ this.changeCallback(null);
+ }
+ }
+
+ this.touchedCallback();
+ });
+ });
+ }
+
+ private setValue(value: any) {
+ if (!value) {
+ value = {};
+ }
+
+ const jsonString = JSON.stringify(value, undefined, 4);
+
+ this.aceEditor.setValue(jsonString);
+ }
+
+ private static loadScript(callback: () => void) {
+ if (JsonEditorComponent.isLoaded) {
+ callback();
+
+ return;
+ }
+
+ if (JsonEditorComponent.loaderCallback) {
+ JsonEditorComponent.loaderCallback.subscribe(callback);
+
+ return;
+ }
+
+ JsonEditorComponent.loaderCallback = new ReplaySubject(1);
+ JsonEditorComponent.loaderCallback.subscribe(callback);
+
+ const url = 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js';
+
+ const script = document.createElement('script');
+ script.src = url;
+ script.async = true;
+ script.onload = () => {
+ JsonEditorComponent.loaderCallback.next(null);
+ JsonEditorComponent.loaderCallback = null;
+ JsonEditorComponent.isLoaded = true;
+ };
+
+ const node = document.getElementsByTagName('script')[0];
+
+ node.parentNode.insertBefore(script, node);
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex/app/framework/angular/slider.component.ts b/src/Squidex/app/framework/angular/slider.component.ts
index a9a625d6b..17bfc48a7 100644
--- a/src/Squidex/app/framework/angular/slider.component.ts
+++ b/src/Squidex/app/framework/angular/slider.component.ts
@@ -9,7 +9,6 @@ import { Component, ElementRef, forwardRef, Input, Renderer, ViewChild } from '@
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
/* tslint:disable:no-empty */
-
const NOOP = () => { };
export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = {
diff --git a/src/Squidex/app/framework/angular/tag-editor.component.ts b/src/Squidex/app/framework/angular/tag-editor.component.ts
index bc24b6c1c..73edc74d2 100644
--- a/src/Squidex/app/framework/angular/tag-editor.component.ts
+++ b/src/Squidex/app/framework/angular/tag-editor.component.ts
@@ -8,9 +8,8 @@
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
-/* tslint:disable:no-empty */
-
const KEY_ENTER = 13;
+/* tslint:disable:no-empty */
const NOOP = () => { };
export interface Converter {
diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts
index f64b76af1..079727ef5 100644
--- a/src/Squidex/app/framework/declarations.ts
+++ b/src/Squidex/app/framework/declarations.ts
@@ -16,6 +16,7 @@ export * from './angular/date-time.pipes';
export * from './angular/focus-on-change.directive';
export * from './angular/focus-on-init.directive';
export * from './angular/http-utils';
+export * from './angular/json-editor.component';
export * from './angular/modal-view.directive';
export * from './angular/money.pipe';
export * from './angular/name.pipe';
diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts
index cdff61d46..3bf93d8d6 100644
--- a/src/Squidex/app/framework/module.ts
+++ b/src/Squidex/app/framework/module.ts
@@ -25,6 +25,7 @@ import {
FocusOnChangeDirective,
FocusOnInitDirective,
FromNowPipe,
+ JsonEditorComponent,
LocalStoreService,
MessageBus,
ModalViewDirective,
@@ -67,6 +68,7 @@ import {
FocusOnChangeDirective,
FocusOnInitDirective,
FromNowPipe,
+ JsonEditorComponent,
ModalViewDirective,
MoneyPipe,
MonthPipe,
@@ -94,6 +96,7 @@ import {
FocusOnChangeDirective,
FocusOnInitDirective,
FromNowPipe,
+ JsonEditorComponent,
ModalViewDirective,
MoneyPipe,
MonthPipe,