diff --git a/package-lock.json b/package-lock.json
index 915c0ff..70c5219 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1935,6 +1935,21 @@
"schema-utils": "^2.7.0"
}
},
+ "@ng-stack/contenteditable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@ng-stack/contenteditable/-/contenteditable-1.1.0.tgz",
+ "integrity": "sha512-bmu0PFWNgAw+shTDlQQV6gBiUcbp6VEwl51fGCUci5GAhKtLwYEe/biPA6Q6tsqSP4l1/XV/HUvroKek1+csOg==",
+ "requires": {
+ "tslib": "^2.0.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
+ "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
+ }
+ }
+ },
"@ngtools/webpack": {
"version": "10.1.6",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.1.6.tgz",
diff --git a/package.json b/package.json
index a646a21..a8230b0 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"@ionic-native/splash-screen": "^5.0.0",
"@ionic-native/status-bar": "^5.0.0",
"@ionic/angular": "^5.3.5",
+ "@ng-stack/contenteditable": "^1.1.0",
"ag-grid-angular": "^22.1.1",
"ag-grid-community": "^22.1.1",
"core-js": "^2.5.4",
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index 77e97a3..cb93bca 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -29,4 +29,10 @@
color: var(--noded-background-code);
}
}
+
+ &.files {
+ .tree-node-icon {
+ color: var(--noded-background-files);
+ }
+ }
}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 153e63e..82ad08a 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -11,7 +11,8 @@ import {OptionPickerComponent} from './components/option-picker/option-picker.co
import {OptionMenuComponent} from './components/option-menu/option-menu.component';
import {SelectorComponent} from './components/sharing/selector/selector.component';
import {SessionService} from './service/session.service';
-import {SearchComponent} from "./components/search/Search.component";
+import {SearchComponent} from './components/search/Search.component';
+import {NodeTypeIcons} from './structures/node-types';
@Component({
selector: 'app-root',
@@ -64,13 +65,7 @@ export class AppComponent implements OnInit {
}
};
- public typeIcons = {
- branch: 'fa fa-folder',
- node: 'fa fa-quote-left',
- page: 'fa fa-sticky-note',
- db: 'fa fa-database',
- code: 'fa fa-code',
- };
+ public typeIcons = NodeTypeIcons;
public get appName(): string {
return this.session.appName || 'Noded';
diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts
index b79e22a..5d4b644 100644
--- a/src/app/components/components.module.ts
+++ b/src/app/components/components.module.ts
@@ -1,12 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { HostComponent } from './editor/host/host.component';
import {NodePickerComponent} from './editor/node-picker/node-picker.component';
import {IonicModule} from '@ionic/angular';
import {DatabaseComponent} from './editor/database/database.component';
import {AgGridModule} from 'ag-grid-angular';
import {ColumnsComponent} from './editor/database/columns/columns.component';
-import {FormsModule} from '@angular/forms';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {ContenteditableModule} from '@ng-stack/contenteditable';
import {CodeComponent} from './editor/code/code.component';
import {MonacoEditorModule} from 'ngx-monaco-editor';
import {FilesComponent} from './editor/files/files.component';
@@ -26,9 +26,11 @@ import {CurrencyRendererComponent} from './editor/database/renderers/currency-re
import {BooleanRendererComponent} from './editor/database/renderers/boolean-renderer.component';
import {SearchComponent} from './search/Search.component';
+import {NormComponent} from './nodes/norm/norm.component';
+import {DirectivesModule} from '../directives/directives.module';
+
@NgModule({
declarations: [
- HostComponent,
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
@@ -49,16 +51,20 @@ import {SearchComponent} from './search/Search.component';
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
+
+ NormComponent,
],
imports: [
CommonModule,
IonicModule,
AgGridModule,
FormsModule,
- MonacoEditorModule
+ ReactiveFormsModule,
+ ContenteditableModule,
+ MonacoEditorModule,
+ DirectivesModule,
],
entryComponents: [
- HostComponent,
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
@@ -79,9 +85,10 @@ import {SearchComponent} from './search/Search.component';
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
+
+ NormComponent,
],
exports: [
- HostComponent,
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
@@ -102,6 +109,8 @@ import {SearchComponent} from './search/Search.component';
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
+
+ NormComponent,
]
})
export class ComponentsModule {}
diff --git a/src/app/components/editor/code/code.component.html b/src/app/components/editor/code/code.component.html
index b6bea5f..7db4cd6 100644
--- a/src/app/components/editor/code/code.component.html
+++ b/src/app/components/editor/code/code.component.html
@@ -7,7 +7,7 @@
-
+
-
-
- Drop Editor
- Save Changes
-
-
diff --git a/src/app/components/editor/code/code.component.ts b/src/app/components/editor/code/code.component.ts
index d13c5b2..4d559f5 100644
--- a/src/app/components/editor/code/code.component.ts
+++ b/src/app/components/editor/code/code.component.ts
@@ -1,209 +1,215 @@
-import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {Component, Input, OnInit} from '@angular/core';
import {v4} from 'uuid';
-import HostRecord from '../../../structures/HostRecord';
-import {Observable} from 'rxjs';
import {ApiService} from '../../../service/api.service';
-import {AlertController, LoadingController} from '@ionic/angular';
+import {EditorNodeContract} from '../../nodes/EditorNode.contract';
+import {EditorService} from '../../../service/editor.service';
@Component({
- selector: 'editor-code',
- templateUrl: './code.component.html',
- styleUrls: ['./code.component.scss'],
+ selector: 'editor-code',
+ templateUrl: './code.component.html',
+ styleUrls: ['./code.component.scss'],
})
-export class CodeComponent implements OnInit {
- @Input() readonly = false;
- @Input() hostRecord: HostRecord;
- @Output() hostRecordChange = new EventEmitter();
- @Output() requestParentSave = new EventEmitter();
- @Output() requestParentDelete = new EventEmitter();
- @ViewChild('theEditor') theEditor;
+export class CodeComponent extends EditorNodeContract implements OnInit {
+ @Input() nodeId: string;
+ public dirty = false;
+ protected dbRecord: any = {};
+ protected codeRefId!: string;
- public dirty = false;
- public pendingSetup = true;
- protected dbRecord: any = {};
+ public editorOptions = {
+ language: 'javascript',
+ uri: v4(),
+ readOnly: false,
+ };
- public languageOptions: Array = [
- 'ABAP',
- 'AES',
- 'Apex',
- 'AZCLI',
- 'Bat',
- 'C',
- 'Cameligo',
- 'Clojure',
- 'CoffeeScript',
- 'Cpp',
- 'Csharp',
- 'CSP',
- 'CSS',
- 'Dockerfile',
- 'Fsharp',
- 'Go',
- 'GraphQL',
- 'Handlebars',
- 'HTML',
- 'INI',
- 'Java',
- 'JavaScript',
- 'JSON',
- 'Kotlin',
- 'LeSS',
- 'Lua',
- 'Markdown',
- 'MiPS',
- 'MSDAX',
- 'MySQL',
- 'Objective-C',
- 'Pascal',
- 'Pascaligo',
- 'Perl',
- 'pgSQL',
- 'PHP',
- 'Plaintext',
- 'Postiats',
- 'PowerQuery',
- 'PowerShell',
- 'Pug',
- 'Python',
- 'R',
- 'Razor',
- 'Redis',
- 'RedShift',
- 'RestructuredText',
- 'Ruby',
- 'Rust',
- 'SB',
- 'Scheme',
- 'SCSS',
- 'Shell',
- 'SOL',
- 'SQL',
- 'St',
- 'Swift',
- 'TCL',
- 'Twig',
- 'TypeScript',
- 'VB',
- 'XML',
- 'YAML',
- ];
+ public editorValue = '';
+ public get readonly() {
+ return !this.node || !this.editorService.canEdit();
+ }
- public editorOptions = {
- language: 'javascript',
- uri: v4(),
- readOnly: this.readonly,
- };
- public editorValue = '';
+ public languageOptions: Array = [
+ 'ABAP',
+ 'AES',
+ 'Apex',
+ 'AZCLI',
+ 'Bat',
+ 'C',
+ 'Cameligo',
+ 'Clojure',
+ 'CoffeeScript',
+ 'Cpp',
+ 'Csharp',
+ 'CSP',
+ 'CSS',
+ 'Dockerfile',
+ 'Fsharp',
+ 'Go',
+ 'GraphQL',
+ 'Handlebars',
+ 'HTML',
+ 'INI',
+ 'Java',
+ 'JavaScript',
+ 'JSON',
+ 'Kotlin',
+ 'LeSS',
+ 'Lua',
+ 'Markdown',
+ 'MiPS',
+ 'MSDAX',
+ 'MySQL',
+ 'Objective-C',
+ 'Pascal',
+ 'Pascaligo',
+ 'Perl',
+ 'pgSQL',
+ 'PHP',
+ 'Plaintext',
+ 'Postiats',
+ 'PowerQuery',
+ 'PowerShell',
+ 'Pug',
+ 'Python',
+ 'R',
+ 'Razor',
+ 'Redis',
+ 'RedShift',
+ 'RestructuredText',
+ 'Ruby',
+ 'Rust',
+ 'SB',
+ 'Scheme',
+ 'SCSS',
+ 'Shell',
+ 'SOL',
+ 'SQL',
+ 'St',
+ 'Swift',
+ 'TCL',
+ 'Twig',
+ 'TypeScript',
+ 'VB',
+ 'XML',
+ 'YAML',
+ ];
+ protected hadLoad = false;
- constructor(
- protected api: ApiService,
- protected loader: LoadingController,
- protected alerts: AlertController,
- ) { }
+ constructor(
+ public readonly editorService: EditorService,
+ public readonly api: ApiService,
+ ) { super(); }
- ngOnInit() {
- this.loader.create({message: 'Loading code...'}).then(loader => {
- loader.present().then(() => {
- this.getInitObservable().subscribe(() => {
- this.editorOptions.language = this.dbRecord.Language;
- this.editorOptions.readOnly = this.readonly;
- this.onSelectChange(false);
- loader.dismiss();
+ public isDirty(): boolean | Promise {
+ return this.dirty;
+ }
+
+ public needsSave(): boolean | Promise {
+ return this.dirty;
+ }
+
+ public writeChangesToNode(): void | Promise {
+ this.node.Value.Mode = 'code';
+ this.node.Value.Value = this.codeRefId;
+ this.node.value = this.codeRefId;
+ }
+
+ public needsLoad(): boolean | Promise {
+ return this.node && !this.hadLoad;
+ }
+
+ public performLoad(): void | Promise {
+ return new Promise((res, rej) => {
+ if ( !this.node.Value ) {
+ this.node.Value = {};
+ }
+
+ if ( !this.node.Value.Value && this.editorService.canEdit() ) {
+ this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/create`).subscribe({
+ next: result => {
+ this.dbRecord = result.data;
+ this.node.Value.Mode = 'code';
+ this.node.Value.Value = result.data.UUID;
+ this.node.value = result.data.UUID;
+ this.codeRefId = result.data.UUID;
+ this.editorOptions.readOnly = this.readonly;
+ this.onSelectChange(false);
+ this.hadLoad = true;
+ res();
+ },
+ error: rej,
+ });
+ } else {
+ this.api.get(`/code/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}`).subscribe({
+ next: result => {
+ this.dbRecord = result.data;
+ this.initialValue = this.dbRecord.code;
+ this.editorValue = this.dbRecord.code;
+ this.editorOptions.language = this.dbRecord.Language;
+ this.codeRefId = this.node.Value.Value;
+ this.editorOptions.readOnly = this.readonly;
+ this.onSelectChange(false);
+ this.hadLoad = true;
+ res();
+ },
+ error: rej,
+ });
+ }
+ });
+ }
+
+ public performSave(): void | Promise {
+ if ( !this.editorService.canEdit() ) {
+ return;
+ }
+
+ return new Promise((res, rej) => {
+ this.dbRecord.code = this.editorValue;
+ this.dbRecord.Language = this.editorOptions.language;
+
+ this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}`, this.dbRecord)
+ .subscribe({
+ next: result => {
+ this.dbRecord = result.data;
+ this.editorOptions.language = this.dbRecord.Language;
+ this.editorValue = this.dbRecord.code;
+ this.dirty = false;
+ res();
+ },
+ error: rej,
+ });
+ });
+ }
+
+ public performDelete(): void | Promise {
+ return new Promise((res, rej) => {
+ this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/delete/${this.node.Value.Value}`).subscribe({
+ next: result => {
+ res();
+ },
+ error: rej,
});
});
- });
- }
-
- getInitObservable(): Observable {
- return new Observable(sub => {
- if ( this.hostRecord && this.pendingSetup ) {
- if ( !this.hostRecord.Value ) {
- this.hostRecord.Value = {};
- }
-
- if ( !this.hostRecord.Value.Value && !this.readonly ) {
- this.api.post(`/code/${this.hostRecord.PageId}/${this.hostRecord.UUID}/create`).subscribe(res => {
- this.dbRecord = res.data;
- this.hostRecord.Value.Mode = 'code';
- this.hostRecord.Value.Value = res.data.UUID;
- this.hostRecord.value = res.data.UUID;
- this.hostRecordChange.emit(this.hostRecord);
- this.pendingSetup = false;
- sub.next(true);
- sub.complete();
- });
- } else {
- this.api.get(`/code/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}`).subscribe(res => {
- this.dbRecord = res.data;
- this.editorValue = this.dbRecord.code;
- this.editorOptions.language = this.dbRecord.Language;
- this.pendingSetup = false;
- sub.next(true);
- sub.complete();
- });
- }
- } else {
- this.pendingSetup = true;
- }
- });
- }
-
- onSaveClick() {
- if ( this.readonly ) {
- return;
- }
-
- this.dbRecord.code = this.editorValue;
- this.dbRecord.Language = this.editorOptions.language;
- this.api.post(`/code/${this.hostRecord.PageId}/${this.hostRecord.UUID}/set/${this.hostRecord.Value.Value}`, this.dbRecord)
- .subscribe(res => {
- this.dbRecord = res.data;
- this.editorOptions.language = this.dbRecord.Language;
- this.editorValue = this.dbRecord.code;
- this.dirty = false;
- });
- }
-
- async onDropClick() {
- if ( this.readonly ) {
- return;
- }
-
- const alert = await this.alerts.create({
- header: 'Are you sure?',
- message: `You are about to delete this code. This action cannot be undone.`,
- buttons: [
- {
- text: 'Keep It',
- role: 'cancel',
- },
- {
- text: 'Delete It',
- handler: async () => {
- this.api.post(`/code/${this.hostRecord.PageId}/${this.hostRecord.UUID}/delete/${this.hostRecord.Value.Value}`)
- .subscribe(res => {
- this.requestParentDelete.emit(this);
- });
- },
- },
- ],
- });
-
- await alert.present();
- }
-
- public onEditorModelChange($event) {
- if ( this.editorValue !== this.dbRecord.code ) {
- this.dirty = true;
- }
- }
-
- onSelectChange(updateDbRecord = true) {
- if ( updateDbRecord ) {
- this.dbRecord.Language = this.editorOptions.language;
}
- this.editorOptions = {...this.editorOptions};
- }
+ ngOnInit() {
+ this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
+ this.editorOptions.readOnly = !this.editorService.canEdit();
+ });
+ }
+ public onEditorModelChange($event) {
+ if ( this.editorValue !== this.dbRecord.code ) {
+ this.dirty = true;
+ this.editorService.triggerSave();
+ }
+ }
+
+ public onSelectChange(updateDbRecord = true) {
+ if ( updateDbRecord ) {
+ this.dbRecord.Language = this.editorOptions.language;
+ this.editorService.triggerSave();
+ this.dirty = true;
+ }
+
+ this.editorOptions = {...this.editorOptions};
+ }
}
diff --git a/src/app/components/editor/database/database.component.html b/src/app/components/editor/database/database.component.html
index e4e90b4..b22834a 100644
--- a/src/app/components/editor/database/database.component.html
+++ b/src/app/components/editor/database/database.component.html
@@ -1,17 +1,15 @@
-
-
-
+
+
+
Manage Columns
Insert Row
Delete Row
- Sync Records
- Drop Database
diff --git a/src/app/components/editor/database/database.component.ts b/src/app/components/editor/database/database.component.ts
index 7afbe31..5687be4 100644
--- a/src/app/components/editor/database/database.component.ts
+++ b/src/app/components/editor/database/database.component.ts
@@ -1,7 +1,5 @@
-import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
-import HostRecord from '../../../structures/HostRecord';
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {ApiService} from '../../../service/api.service';
-import {Observable} from 'rxjs';
import {AlertController, LoadingController, ModalController} from '@ionic/angular';
import {ColumnsComponent} from './columns/columns.component';
import {AgGridAngular} from 'ag-grid-angular';
@@ -14,18 +12,16 @@ import {DatetimeEditorComponent} from './editors/datetime/datetime-editor.compon
import {DatetimeRendererComponent} from './renderers/datetime-renderer.component';
import {CurrencyRendererComponent} from './renderers/currency-renderer.component';
import {BooleanRendererComponent} from './renderers/boolean-renderer.component';
+import {EditorNodeContract} from '../../nodes/EditorNode.contract';
+import {EditorService} from '../../../service/editor.service';
@Component({
selector: 'editor-database',
templateUrl: './database.component.html',
styleUrls: ['./database.component.scss'],
})
-export class DatabaseComponent implements OnInit {
- @Input() hostRecord: HostRecord;
- @Input() readonly = false;
- @Output() hostRecordChange = new EventEmitter
();
- @Output() requestParentSave = new EventEmitter();
- @Output() requestParentDelete = new EventEmitter();
+export class DatabaseComponent extends EditorNodeContract implements OnInit {
+ @Input() nodeId: string;
@ViewChild('agGridElement') agGridElement: AgGridAngular;
public dbRecord: any;
@@ -33,36 +29,49 @@ export class DatabaseComponent implements OnInit {
public dirty = false;
public lastClickRow = -1;
public dbName = '';
+ protected dbId!: string;
+
+ public get readonly() {
+ return !this.node || !this.editorService.canEdit();
+ }
constructor(
protected api: ApiService,
protected modals: ModalController,
protected alerts: AlertController,
protected loader: LoadingController,
- ) { }
+ public readonly editorService: EditorService,
+ ) { super(); }
title = 'app';
columnDefs = [];
rowData = [];
+ public isDirty(): boolean | Promise {
+ return this.dirty;
+ }
+
+ public needsSave(): boolean | Promise {
+ return this.dirty;
+ }
+
+ public needsLoad(): boolean | Promise {
+ return this.node && this.pendingSetup;
+ }
+
+ public writeChangesToNode(): void | Promise {
+ this.node.Value.Mode = 'database';
+ }
+
ngOnInit() {
- // this.loader.create({message: 'Loading database...'}).then(loader => {
- // setTimeout(() => {
- // loader.present().then(() => {
- this.getInitObservable().subscribe(() => {
- this.getColumnLoadObservable().subscribe(() => {
- this.getDataLoadObservable().subscribe(() => {
- // loader.dismiss();
- });
- });
- });
- // });
- // }, 100);
- // });
+ this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
+
+ });
}
onCellValueChanged() {
this.dirty = true;
+ this.editorService.triggerSave();
}
async onManageColumns() {
@@ -76,14 +85,7 @@ export class DatabaseComponent implements OnInit {
});
modal.onDidDismiss().then(result => {
- if ( result.data ) {
- this.setColumns(result.data).subscribe(() => {
- const endpoint = `/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/set/${this.hostRecord.Value.Value}/columns`
- this.api.post(endpoint, {columns: this.columnDefs}).subscribe(res => {
- this.requestParentSave.emit(this);
- });
- });
- }
+ this.setColumns(result.data);
});
await modal.present();
@@ -97,6 +99,7 @@ export class DatabaseComponent implements OnInit {
this.rowData.push({});
this.agGridElement.api.setRowData(this.rowData);
this.dirty = true;
+ this.editorService.triggerSave();
}
async onRemoveRow() {
@@ -115,14 +118,13 @@ export class DatabaseComponent implements OnInit {
{
text: 'Delete It',
handler: () => {
- const newRows = this.rowData.filter((x, i) => {
+ this.rowData = this.rowData.filter((x, i) => {
return i !== this.lastClickRow;
});
-
- this.rowData = newRows;
this.agGridElement.api.setRowData(this.rowData);
this.lastClickRow = -1;
this.dirty = true;
+ this.editorService.triggerSave();
},
}
],
@@ -131,152 +133,151 @@ export class DatabaseComponent implements OnInit {
await alert.present();
}
- async onDropDatabase() {
- if ( this.readonly ) {
- return;
- }
-
- const alert = await this.alerts.create({
- header: 'Are you sure?',
- message: `You are about to delete this database and all its entries. This action cannot be undone.`,
- buttons: [
- {
- text: 'Keep It',
- role: 'cancel',
- },
- {
- text: 'Delete It',
- handler: async () => {
- this.api.post(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/drop/${this.hostRecord.Value.Value}`).subscribe();
- this.requestParentDelete.emit(this);
- },
- },
- ],
- });
-
- await alert.present();
- }
onRowClicked($event) {
this.lastClickRow = $event.rowIndex;
}
- getDataLoadObservable(): Observable {
- return new Observable(sub => {
- this.api.get(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}/data`).subscribe(res => {
- this.rowData = res.data.map(x => x.RowData);
- this.agGridElement.api.setRowData(this.rowData);
- sub.next();
- sub.complete();
- });
+ setColumns(data) {
+ this.columnDefs = data.map(x => {
+ x.editable = !this.readonly;
+
+ // Set editors and renderers for different types
+ if ( x.Type === 'text' ) {
+ x.editor = 'agTextCellEditor';
+ } else if ( x.Type === 'number' ) {
+ x.cellEditorFramework = NumericEditorComponent;
+ } else if ( x.Type === 'paragraph' ) {
+ x.cellEditorFramework = ParagraphEditorComponent;
+ } else if ( x.Type === 'boolean' ) {
+ x.cellRendererFramework = BooleanRendererComponent;
+ x.cellEditorFramework = BooleanEditorComponent;
+ } else if ( x.Type === 'select' ) {
+ x.cellEditorFramework = SelectEditorComponent;
+ } else if ( x.Type === 'multiselect' ) {
+ x.cellEditorFramework = MultiSelectEditorComponent;
+ } else if ( x.Type === 'datetime' ) {
+ x.cellEditorFramework = DatetimeEditorComponent;
+ x.cellRendererFramework = DatetimeRendererComponent;
+ } else if ( x.Type === 'currency' ) {
+ x.cellEditorFramework = NumericEditorComponent;
+ x.cellRendererFramework = CurrencyRendererComponent;
+ } else if ( x.Type === 'index' ) {
+ x.editable = false;
+ }
+
+ return x;
});
+
+ this.agGridElement.api.setColumnDefs(this.columnDefs);
+ this.dirty = true;
+ this.editorService.triggerSave();
}
- onSyncRecords() {
- if ( this.readonly ) {
- return;
+ public async performLoad(): Promise {
+ if ( !this.node.Value ) {
+ this.node.Value = {};
}
- this.loader.create({message: 'Syncing the database...'}).then(loader => {
- loader.present().then(() => {
- this.api.post(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/set/${this.hostRecord.Value.Value}/data`, this.rowData)
- .subscribe(res => {
- this.rowData = res.data.map(x => x.RowData);
+ // Load the database record itself
+ if ( !this.node.Value.Value && this.editorService.canEdit() ) {
+ await new Promise((res, rej) => {
+ this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/create`).subscribe({
+ next: result => {
+ this.dbRecord = result.data;
+ this.dbName = result.data.Name;
+ this.node.Value.Mode = 'database';
+ this.node.Value.Value = result.data.UUID;
+ this.node.value = result.data.UUID;
+ res();
+ },
+ error: rej,
+ });
+ });
+ } else {
+ await new Promise((res, rej) => {
+ this.api.get(`/db/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}`).subscribe({
+ next: result => {
+ this.dbRecord = result.data;
+ this.dbName = result.data.Name;
+ res();
+ },
+ error: rej,
+ });
+ });
+ }
+
+ // Load the columns
+ await new Promise((res, rej) => {
+ this.api.get(`/db/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}/columns`).subscribe({
+ next: result => {
+ this.setColumns(result.data);
+ res();
+ },
+ error: rej,
+ });
+ });
+
+ // Load the data
+ await new Promise((res, rej) => {
+ this.api.get(`/db/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}/data`).subscribe({
+ next: result => {
+ this.rowData = result.data.map(x => x.RowData);
this.agGridElement.api.setRowData(this.rowData);
+ res();
+ },
+ error: rej,
+ });
+ });
- this.api.post(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/set/${this.hostRecord.Value.Value}/name`,
- { Name: this.dbName })
- .subscribe(resp => {
- this.dirty = false;
- loader.dismiss();
- });
- });
+ this.pendingSetup = false;
+ this.dirty = false;
+ }
+
+ public performDelete(): void | Promise {
+ return new Promise((res, rej) => {
+ this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/drop/${this.node.Value.Value}`).subscribe({
+ next: result => {
+ res();
+ },
+ error: rej,
});
});
}
- getColumnLoadObservable(): Observable {
- return new Observable(sub => {
- this.api.get(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}/columns`).subscribe(res => {
- this.setColumns(res.data).subscribe(() => {
- sub.next();
- sub.complete();
- });
+ public async performSave(): Promise {
+ // Save the columns first
+ await new Promise((res, rej) => {
+ this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}/columns`, {columns: this.columnDefs}).subscribe({
+ next: result => {
+ res();
+ },
+ error: rej,
});
});
- }
- setColumns(data): Observable {
- return new Observable(sub => {
- this.columnDefs = data.map(x => {
- x.editable = !this.readonly;
-
- // Set editors and renderers for different types
- if ( x.Type === 'text' ) {
- x.editor = 'agTextCellEditor';
- } else if ( x.Type === 'number' ) {
- x.cellEditorFramework = NumericEditorComponent;
- } else if ( x.Type === 'paragraph' ) {
- x.cellEditorFramework = ParagraphEditorComponent;
- } else if ( x.Type === 'boolean' ) {
- x.cellRendererFramework = BooleanRendererComponent;
- x.cellEditorFramework = BooleanEditorComponent;
- } else if ( x.Type === 'select' ) {
- x.cellEditorFramework = SelectEditorComponent;
- } else if ( x.Type === 'multiselect' ) {
- x.cellEditorFramework = MultiSelectEditorComponent;
- } else if ( x.Type === 'datetime' ) {
- x.cellEditorFramework = DatetimeEditorComponent;
- x.cellRendererFramework = DatetimeRendererComponent;
- } else if ( x.Type === 'currency' ) {
- x.cellEditorFramework = NumericEditorComponent;
- x.cellRendererFramework = CurrencyRendererComponent;
- } else if ( x.Type === 'index' ) {
- x.editable = false;
- }
-
- console.log({x});
- return x;
+ // Save the data
+ await new Promise((res, rej) => {
+ this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}/data`, this.rowData).subscribe({
+ next: result => {
+ this.rowData = result.data.map(x => x.RowData);
+ this.agGridElement.api.setRowData(this.rowData);
+ res();
+ },
+ error: rej,
});
-
- this.agGridElement.api.setColumnDefs(this.columnDefs);
-
- sub.next();
- sub.complete();
});
- }
- getInitObservable(): Observable {
- return new Observable(sub => {
- if ( this.hostRecord.UUID && this.pendingSetup ) {
- if ( !this.hostRecord.Value ) {
- this.hostRecord.Value = {};
- }
- if ( !this.hostRecord.Value.Value && !this.readonly ) {
- this.api.post(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/create`).subscribe(res => {
- this.dbRecord = res.data;
- this.dbName = res.data.Name;
- this.hostRecord.Value.Mode = 'database';
- this.hostRecord.Value.Value = res.data.UUID;
- this.hostRecord.value = res.data.UUID;
- this.hostRecordChange.emit(this.hostRecord);
- this.pendingSetup = false;
- sub.next(true);
- sub.complete();
- });
- } else {
- this.api.get(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}`).subscribe(res => {
- this.dbRecord = res.data;
- this.dbName = res.data.Name;
- this.pendingSetup = false;
- sub.next(true);
- sub.complete();
- });
- }
- } else {
- this.pendingSetup = true;
- }
+ // Save the name
+ await new Promise((res, rej) => {
+ this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}/name`, { Name: this.dbName }).subscribe({
+ next: result => {
+ res();
+ },
+ error: rej,
+ });
});
- }
+ this.dirty = false;
+ }
}
diff --git a/src/app/components/editor/files/files.component.html b/src/app/components/editor/files/files.component.html
index 867ecb7..654c650 100644
--- a/src/app/components/editor/files/files.component.html
+++ b/src/app/components/editor/files/files.component.html
@@ -4,7 +4,6 @@
Upload
- Drop Files
diff --git a/src/app/components/editor/files/files.component.ts b/src/app/components/editor/files/files.component.ts
index 6111d0c..fc0ce56 100644
--- a/src/app/components/editor/files/files.component.ts
+++ b/src/app/components/editor/files/files.component.ts
@@ -4,38 +4,106 @@ import {ApiService} from '../../../service/api.service';
import {AlertController} from '@ionic/angular';
import {Observable} from 'rxjs';
import { APP_BASE_HREF } from '@angular/common';
+import {EditorService} from '../../../service/editor.service';
+import {EditorNodeContract} from '../../nodes/EditorNode.contract';
@Component({
selector: 'editor-files',
templateUrl: './files.component.html',
styleUrls: ['./files.component.scss'],
})
-export class FilesComponent implements OnInit {
- @Input() readonly = false;
- @Input() hostRecord: HostRecord;
- @Output() hostRecordChange = new EventEmitter
();
- @Output() requestParentSave = new EventEmitter();
- @Output() requestParentDelete = new EventEmitter();
+export class FilesComponent extends EditorNodeContract implements OnInit {
+ @Input() nodeId: string;
@ViewChild('uploadForm') uploadForm: ElementRef;
+ // @Input() readonly = false;
+ // @Input() hostRecord: HostRecord;
+ // @Output() hostRecordChange = new EventEmitter();
+ // @Output() requestParentSave = new EventEmitter();
+ // @Output() requestParentDelete = new EventEmitter();
public fileRecords: Array = [];
public pendingSetup = true;
public dbRecord: any = {};
+ public dirty = false;
+
+ public get readonly() {
+ return !this.node || !this.editorService.canEdit();
+ }
constructor(
protected api: ApiService,
protected alerts: AlertController,
+ public readonly editorService: EditorService,
@Inject(APP_BASE_HREF) private baseHref: string
- ) { }
+ ) { super(); }
+
+ public isDirty(): boolean | Promise {
+ return this.dirty;
+ }
+
+ public writeChangesToNode(): void | Promise {
+ this.node.Value.Mode = 'files';
+ }
+
+ public needsLoad(): boolean | Promise {
+ return this.node && this.pendingSetup;
+ }
+
+ public async performLoad(): Promise {
+ if ( !this.node.Value ) {
+ this.node.Value = {};
+ }
+
+ if ( !this.node.Value.Value && !this.readonly ) {
+ await new Promise((res, rej) => {
+ this.api.post(`/files/${this.page.UUID}/${this.node.UUID}/create`).subscribe({
+ next: result => {
+ this.dbRecord = result.data;
+ this.fileRecords = result.data.files;
+ this.node.Value.Mode = 'files';
+ this.node.Value.Value = result.data.UUID;
+ this.node.value = result.data.UUID;
+ this.dirty = true;
+ res();
+ },
+ error: rej,
+ });
+ });
+ } else {
+ await new Promise((res, rej) => {
+ this.api.get(`/files/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}`).subscribe({
+ next: result => {
+ this.dbRecord = result.data;
+ this.fileRecords = result.data.files;
+ res();
+ },
+ error: rej,
+ });
+ });
+ }
+
+ this.pendingSetup = false;
+ }
+
+ public async performDelete(): Promise {
+ await new Promise((res, rej) => {
+ this.api.post(`/files/${this.page.UUID}/${this.node.UUID}/delete/${this.node.Value.Value}`).subscribe({
+ next: result => {
+ res();
+ },
+ error: rej,
+ });
+ });
+ }
ngOnInit() {
- this.getInitObservable().subscribe(() => {
+ this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
});
}
getApiSubmit() {
- return this.api._build_url(`file/upload/${this.hostRecord.PageId}/${this.hostRecord.UUID}/${this.hostRecord.Value.Value}`);
+ return this.api._build_url(`file/upload/${this.page.UUID}/${this.node.UUID}/${this.node.Value.Value}`);
}
onSubmitClick() {
@@ -47,73 +115,11 @@ export class FilesComponent implements OnInit {
}
getReturnUrl() {
- return `${this.baseHref}editor;id=${this.hostRecord.PageId}`;
+ return `${this.baseHref}editor;id=${this.page.UUID}`;
}
downloadFile(fileRecord) {
// tslint:disable-next-line:max-line-length
- window.open(this.api._build_url(`files/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}/${fileRecord._id}`), '_blank');
+ window.open(this.api._build_url(`files/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}/${fileRecord._id}`), '_blank');
}
-
- async onDestroyClick() {
- if ( this.readonly ) {
- return;
- }
-
- const alert = await this.alerts.create({
- header: 'Are you sure?',
- message: 'You are about to delete these files. This action cannot be undone.',
- buttons: [
- {
- text: 'Keep Them',
- role: 'cancel',
- },
- {
- text: 'Delete Them',
- handler: async () => {
- this.api.post(`/files/${this.hostRecord.PageId}/${this.hostRecord.UUID}/delete/${this.hostRecord.Value.Value}`)
- .subscribe(res => {
- this.requestParentDelete.emit(this);
- });
- },
- },
- ],
- });
-
- await alert.present();
- }
-
- getInitObservable(): Observable {
- return new Observable(sub => {
- if ( this.hostRecord && this.pendingSetup ) {
- if ( !this.hostRecord.Value ) {
- this.hostRecord.Value = {};
- }
-
- if ( !this.hostRecord.Value.Value && !this.readonly ) {
- this.api.post(`/files/${this.hostRecord.PageId}/${this.hostRecord.UUID}/create`).subscribe(res => {
- this.dbRecord = res.data;
- this.fileRecords = res.data.files;
- this.hostRecord.Value.Mode = 'files';
- this.hostRecord.Value.Value = res.data.UUID;
- this.hostRecord.value = res.data.UUID;
- this.hostRecordChange.emit(this.hostRecord);
- this.pendingSetup = false;
- sub.next(true);
- sub.complete();
- });
- } else {
- this.api.get(`/files/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}`).subscribe(res => {
- this.fileRecords = res.data.files;
- this.pendingSetup = false;
- sub.next(true);
- sub.complete();
- });
- }
- } else {
- this.pendingSetup = true;
- }
- });
- }
-
}
diff --git a/src/app/components/editor/host-options/host-options.component.html b/src/app/components/editor/host-options/host-options.component.html
index 506db30..6862e6f 100644
--- a/src/app/components/editor/host-options/host-options.component.html
+++ b/src/app/components/editor/host-options/host-options.component.html
@@ -1,6 +1,9 @@
-
-
+
+
+
+
+
{{ menuItems[i].name }}
diff --git a/src/app/components/editor/host-options/host-options.component.ts b/src/app/components/editor/host-options/host-options.component.ts
index 75cda5a..6dccbed 100644
--- a/src/app/components/editor/host-options/host-options.component.ts
+++ b/src/app/components/editor/host-options/host-options.component.ts
@@ -14,13 +14,33 @@ export class HostOptionsComponent implements OnInit {
@Input() event: Event;
@Input() hostRecord: HostRecord;
- public menuItems: Array<{name: string, icon: string, value: string, type?: string}> = [];
- protected possibleMenuItems: Array<{name: string, icon: string, value: string, type?: string}> = [
+ public menuItems: Array<{name: string, icon?: string, icons?: string[], value: string, type?: string}> = [];
+ protected possibleMenuItems: Array<{name: string, icon?: string, icons?: string[], value: string, type?: string}> = [
+ {
+ name: 'Move Up',
+ icons: ['fa-chevron-up'],
+ value: 'move_up',
+ },
+ {
+ name: 'Add Node Before',
+ icons: ['fa-plus'],
+ value: 'add_before',
+ },
{
name: 'Delete Node',
- icon: 'trash',
+ icon: 'fa-trash',
value: 'delete_node',
},
+ {
+ name: 'Add Node After',
+ icons: ['fa-plus'],
+ value: 'add_after',
+ },
+ {
+ name: 'Move Down',
+ icons: ['fa-chevron-down'],
+ value: 'move_down',
+ },
];
constructor(
@@ -37,7 +57,7 @@ export class HostOptionsComponent implements OnInit {
}
}
- async onSelect(value) {
- await this.popover.dismiss(value);
+ async onSelect(event: MouseEvent, value) {
+ await this.popover.dismiss({event, value});
}
}
diff --git a/src/app/components/editor/host/host.component.html b/src/app/components/editor/host/host.component.html
deleted file mode 100644
index 0ae8f5e..0000000
--- a/src/app/components/editor/host/host.component.html
+++ /dev/null
@@ -1,89 +0,0 @@
-
- ')"
- >
- ')"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/app/components/editor/host/host.component.scss b/src/app/components/editor/host/host.component.scss
deleted file mode 100644
index f3a54b3..0000000
--- a/src/app/components/editor/host/host.component.scss
+++ /dev/null
@@ -1,64 +0,0 @@
-.host-host.header1 {
- font-weight: bold;
- font-size: 24pt;
-}
-
-.host-host.header2 {
- font-weight: bold;
- font-size: 21pt;
-}
-
-.host-host.header3 {
- font-weight: bold;
- font-size: 18pt;
-}
-
-.host-host.header4 {
- font-weight: bold;
- font-size: 16pt;
-}
-
-.host-host.block_code {
- font-family: monospace;
- font-size: 12pt;
- background-color: #ddd;
- margin-bottom: 15px;
-}
-
-.host-host.click_link {
- color: #0141b0;
- cursor: pointer;
-
- &:hover {
- text-decoration: underline;
- }
-}
-
-.hr-wrapper {
- margin: 50px 100px;
-
- & hr {
- background: #ccc;
- height: 2px;
- }
-}
-
-.node-indentation-level-num-1 {
- margin-left: 15px;
-}
-
-.node-indentation-level-num-2 {
- margin-left: 30px;
-}
-
-.node-indentation-level-num-3 {
- margin-left: 45px;
-}
-
-.node-indentation-level-num-4 {
- margin-left: 60px;
-}
-
-.node-indentation-level-num-5 {
- margin-left: 75px;
-}
diff --git a/src/app/components/editor/host/host.component.ts b/src/app/components/editor/host/host.component.ts
deleted file mode 100644
index cd75c6d..0000000
--- a/src/app/components/editor/host/host.component.ts
+++ /dev/null
@@ -1,233 +0,0 @@
-import {Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild, ViewChildren} from '@angular/core';
-import HostRecord from '../../../structures/HostRecord';
-import PageRecord from '../../../structures/PageRecord';
-
-@Component({
- selector: 'editor-host',
- templateUrl: './host.component.html',
- styleUrls: ['./host.component.scss'],
-})
-export class HostComponent implements OnInit {
- @Input() page: PageRecord;
- @Input() record: HostRecord;
- @Output() recordChange = new EventEmitter();
- @Output() newHostRequested = new EventEmitter();
- @Output() destroyHostRequested = new EventEmitter();
- @Output() saveHostRequested = new EventEmitter();
- @ViewChild('hostContainer') hostContainer: ElementRef;
- @ViewChildren('liItems') liItems;
-
- public listLines: Array = [];
-
- constructor() { }
-
- ngOnInit() {
- if ( this.record.type === 'ul' ) {
- const values = JSON.parse(this.record.value);
- values.forEach(group => this.listLines.push(group.value));
- setTimeout(() => {
- values.forEach((group, i) => {
- const el = this.liItems.toArray()[i].nativeElement;
- el.className += ` node-indentation-level-num-${group.indentationLevel}`;
- });
- }, 0);
- }
- }
-
- onRecordChange($event) {
- this.recordChange.emit($event);
- }
-
- onKeyUp($event) {
- const innerText = this.hostContainer.nativeElement.innerText.trim()
- if ( $event.code === 'Enter' && this.record.isNorm() && !$event.shiftKey
- && ( this.record.type !== 'block_code'
- || (innerText.endsWith('```') && (innerText.match(/`/g) || []).length >= 6) // TODO don't add new if cursor in block
- )
- ) {
- this.hostContainer.nativeElement.innerText = this.hostContainer.nativeElement.innerText.trim();
- this.newHostRequested.emit(this);
- } else if ( $event.code === 'Backspace' && !this.hostContainer.nativeElement.innerText.trim() ) {
- this.destroyHostRequested.emit(this);
- }
-
- if ( innerText.startsWith('# ') ) {
- this.record.type = 'header1';
- } else if ( innerText.startsWith('## ') ) {
- this.record.type = 'header2';
- } else if ( innerText.startsWith('### ') ) {
- this.record.type = 'header3';
- } else if ( innerText.startsWith('#### ') ) {
- this.record.type = 'header4';
- } else if ( innerText.startsWith('```') ) {
- this.record.type = 'block_code';
- } else if ( innerText.startsWith('http') ) {
- this.record.type = 'click_link';
- } else if ( innerText === '===' ) {
- this.record.type = 'page_sep';
- } else if ( innerText.startsWith('-') || innerText.startsWith(' -') ) {
- this.record.type = 'ul';
- this.listLines = [this.record.value];
- setTimeout(() => {
- this.focusStart(this.liItems.toArray()[0].nativeElement);
- }, 0);
- }
- }
-
- onUlKeyDown($event, index) {
- if ( $event.code === 'Tab' ) {
- $event.preventDefault();
- const elem = this.liItems.toArray()[index];
- let currentLevel = 0;
-
- elem.nativeElement.className.split(' ').some(x => {
- if ( x.startsWith('node-indentation-level-num-') ) {
- currentLevel = Number(x.replace('node-indentation-level-num-', ''));
- return true;
- }
- });
-
- const newLevel = $event.shiftKey ? currentLevel - 1 : currentLevel + 1;
- if ( newLevel <= 5 && newLevel >= 0 ) {
- const existing = elem.nativeElement.className.split(' ').filter(x => !x.startsWith('node-indentation-level-num-'));
- existing.push(`node-indentation-level-num-${newLevel}`);
-
- elem.nativeElement.className = existing.join(' ');
- }
- }
- }
-
- onUlKeyUp($event, i) {
- if ( $event.code === 'Enter' && !$event.shiftKey ) {
- const e = this.liItems.toArray()[i].nativeElement;
- e.innerText = e.innerText.trim();
- if ( this.liItems.toArray()[i].nativeElement.innerText.trim() === '' ) {
- this.newHostRequested.emit(this);
- } else {
- this.listLines.push('');
- setTimeout(() => {
- this.focusStart(this.liItems.toArray()[i + 1].nativeElement);
-
- let newLevel = 0;
- this.liItems.toArray()[i].nativeElement.className.split(' ').some(x => {
- if ( x.startsWith('node-indentation-level-num-') ) {
- newLevel = Number(x.replace('node-indentation-level-num-', ''));
- return true;
- }
- });
-
- const classes = this.liItems.toArray()[i + 1].nativeElement.className
- .split(' ')
- .filter(x => !x.startsWith('node-indentation-level-num-'));
- classes.push(`node-indentation-level-num-${newLevel}`);
- this.liItems.toArray()[i + 1].nativeElement.className = classes.join(' ');
- }, 0);
- }
- } else if ( $event.code === 'Backspace' && this.liItems.toArray()[i].nativeElement.innerText.trim() === '' ) {
- const newLines = [];
- this.liItems.toArray().forEach((elem, index) => {
- if ( index !== i ) {
- newLines.push(elem.nativeElement.innerText ? elem.nativeElement.innerText.trim() : '');
- }
- });
-
- this.listLines = newLines;
-
- if ( i === 0 && this.listLines.length === 0 ) {
- this.destroyHostRequested.emit(this);
- } else {
- setTimeout(() => {
- this.focusEnd(this.liItems.toArray()[i - 1].nativeElement);
- }, 0);
- }
- } else if ( $event.code === 'ArrowDown' ) {
- const liArr = this.liItems.toArray();
- if ( liArr.length > i + 1 ) {
- setTimeout(() => {
- this.focusStart(this.liItems.toArray()[i + 1].nativeElement);
- }, 0);
- }
- } else if ( $event.code === 'ArrowUp' ) {
- if ( i !== 0 ) {
- setTimeout(() => {
- this.focusStart(this.liItems.toArray()[i - 1].nativeElement);
- }, 0);
- }
- } else {
- const recordValue = this.liItems.toArray().map(item => {
- const elem = item.nativeElement;
- const value = elem.innerText.trim();
- let indentationLevel = 0;
- elem.className.split(' ').some(x => {
- if ( x.startsWith('node-indentation-level-num-') ) {
- indentationLevel = x.replace('node-indentation-level-num-', '');
- return true;
- }
- });
-
- return {value, indentationLevel};
- });
-
- this.record.value = JSON.stringify(recordValue);
- }
- }
-
- onRequestDelete($event) {
- this.destroyHostRequested.emit(this);
- }
-
- onRequestParentSave($event) {
- this.saveHostRequested.emit(this);
- }
-
- onHostDblClick() {
- if ( this.record.type === 'click_link' ) {
- window.open(this.record.value.trim(), '_blank');
- }
- }
-
- takeFocus(fromTop = true) {
- if ( this.record.type === 'ul' ) {
- if ( fromTop ) {
- this.focusStart(this.liItems.toArray()[0].nativeElement);
- } else {
- this.focusEnd(this.liItems.toArray().reverse()[0].nativeElement);
- }
- } else {
- if ( fromTop ) {
- this.focusStart(this.hostContainer.nativeElement);
- } else {
- this.focusEnd(this.hostContainer.nativeElement);
- }
- }
- }
-
- // TODO return an observable here, probably
- focusEnd(item) {
- const s = window.getSelection();
- const r = document.createRange();
- r.setStart(item, 0);
- r.setEnd(item, 0);
- s.removeAllRanges();
- s.addRange(r);
- }
-
- // TODO return an observable here, probably
- focusStart(item) {
- const s = window.getSelection();
- const r = document.createRange();
- r.setStart(item, 0);
- r.setEnd(item, 0);
- s.removeAllRanges();
- s.addRange(r);
-
- setTimeout(() => {
- const r2 = document.createRange();
- r2.selectNodeContents(item);
- r2.collapse(false);
- const s2 = window.getSelection();
- s2.removeAllRanges();
- s2.addRange(r2);
- }, 0);
- }
-}
diff --git a/src/app/components/editor/node-picker/node-picker.component.html b/src/app/components/editor/node-picker/node-picker.component.html
index b14eb8d..41df36f 100644
--- a/src/app/components/editor/node-picker/node-picker.component.html
+++ b/src/app/components/editor/node-picker/node-picker.component.html
@@ -1,50 +1,18 @@
-
-
+
+
Paragraph
-
-
- Heading 1
-
-
-
- Heading 2
-
-
-
- Heading 3
-
-
-
- Heading 4
-
-
-
- Unordered List
-
-
-
- Monospace Block
-
-
-
- Hyperlink
-
-
-
+
+
Database
-
-
+
+
Code Editor
-
-
+
+
Upload Files
-
-
- Horizontal Row
-
diff --git a/src/app/components/editor/node-picker/node-picker.component.scss b/src/app/components/editor/node-picker/node-picker.component.scss
index e69de29..a7f2b95 100644
--- a/src/app/components/editor/node-picker/node-picker.component.scss
+++ b/src/app/components/editor/node-picker/node-picker.component.scss
@@ -0,0 +1,27 @@
+i {
+ min-width: 21px;
+}
+
+.node {
+ i {
+ color: var(--noded-background-node);
+ }
+}
+
+.db {
+ i {
+ color: var(--noded-background-db);
+ }
+}
+
+.code {
+ i {
+ color: var(--noded-background-code);
+ }
+}
+
+.files {
+ i {
+ color: var(--noded-background-files);
+ }
+}
diff --git a/src/app/components/editor/node-picker/node-picker.component.ts b/src/app/components/editor/node-picker/node-picker.component.ts
index d65146e..fa8c4db 100644
--- a/src/app/components/editor/node-picker/node-picker.component.ts
+++ b/src/app/components/editor/node-picker/node-picker.component.ts
@@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import {PopoverController} from '@ionic/angular';
+import {NodeTypeIcons} from '../../../structures/node-types';
@Component({
selector: 'editor-node-picker',
@@ -8,6 +9,8 @@ import {PopoverController} from '@ionic/angular';
})
export class NodePickerComponent implements OnInit {
+ public typeIcons = NodeTypeIcons;
+
constructor(
private popover: PopoverController,
) { }
diff --git a/src/app/components/nodes/EditorNode.contract.ts b/src/app/components/nodes/EditorNode.contract.ts
new file mode 100644
index 0000000..c5fb162
--- /dev/null
+++ b/src/app/components/nodes/EditorNode.contract.ts
@@ -0,0 +1,45 @@
+import PageRecord from '../../structures/PageRecord';
+import HostRecord from '../../structures/HostRecord';
+
+export abstract class EditorNodeContract {
+ protected pageRec!: PageRecord;
+ protected nodeRec!: HostRecord;
+ protected initialValue: any;
+
+ get page() {
+ return this.pageRec;
+ }
+
+ set page(page: PageRecord) {
+ this.pageRec = page;
+ }
+
+ get node() {
+ return this.nodeRec;
+ }
+
+ set node(node: HostRecord) {
+ this.nodeRec = node;
+ }
+
+ get identifier() {
+ return this.nodeRec.UUID;
+ }
+
+ public abstract isDirty(): boolean | Promise;
+ public abstract writeChangesToNode(): void | Promise;
+
+ public needsSave(): boolean | Promise {
+ return false;
+ }
+
+ public needsLoad(): boolean | Promise {
+ return false;
+ }
+
+ public performSave(): void | Promise {}
+
+ public performLoad(): void | Promise {}
+
+ public performDelete(): void | Promise {}
+}
diff --git a/src/app/components/nodes/norm/norm.component.html b/src/app/components/nodes/norm/norm.component.html
new file mode 100644
index 0000000..facbac2
--- /dev/null
+++ b/src/app/components/nodes/norm/norm.component.html
@@ -0,0 +1,78 @@
+
\ No newline at end of file
diff --git a/src/app/components/nodes/norm/norm.component.scss b/src/app/components/nodes/norm/norm.component.scss
new file mode 100644
index 0000000..9f6fafb
--- /dev/null
+++ b/src/app/components/nodes/norm/norm.component.scss
@@ -0,0 +1,36 @@
+.editable-base {
+ padding: 20px;
+
+ &.focused {
+ background: aliceblue;
+ }
+}
+
+.toolbar-base {
+ height: 40px;
+ border: 1px solid lightgray;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+
+ .toolbar-button {
+ height: calc(100% - 6px);
+ min-width: 30px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 3px;
+
+ &:hover {
+ background: lightgrey;
+ cursor: pointer;
+ }
+ }
+
+ .toolbar-sep {
+ height: 100%;
+ width: 1px;
+ border: 1px solid lightgrey;
+ margin: 0 5px;
+ }
+}
diff --git a/src/app/components/nodes/norm/norm.component.ts b/src/app/components/nodes/norm/norm.component.ts
new file mode 100644
index 0000000..5807f3d
--- /dev/null
+++ b/src/app/components/nodes/norm/norm.component.ts
@@ -0,0 +1,110 @@
+import {Component, HostListener, Input, OnInit, ViewChild} from '@angular/core';
+import {EditorNodeContract} from '../EditorNode.contract';
+import {EditorService} from '../../../service/editor.service';
+
+@Component({
+ selector: 'editor-norm',
+ templateUrl: './norm.component.html',
+ styleUrls: ['./norm.component.scss'],
+})
+export class NormComponent extends EditorNodeContract implements OnInit {
+ @ViewChild('editable') editable;
+ @Input() nodeId: string;
+
+ public isFocused = false;
+ public initialValue = 'Click to edit...';
+ public contents = '';
+ private dirtyOverride = false;
+
+ constructor(
+ public readonly editorService: EditorService,
+ ) {
+ super();
+ this.contents = this.initialValue;
+ }
+
+ public isDirty(): boolean | Promise {
+ return this.dirtyOverride || this.contents !== this.initialValue;
+ }
+
+ public writeChangesToNode(): void | Promise {
+ this.nodeRec.value = this.contents;
+ this.initialValue = this.contents;
+ }
+
+ ngOnInit() {
+ this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
+ if ( !this.node.Value ) {
+ this.node.Value = {};
+ }
+
+ if ( this.node.Value.Value ) {
+ this.initialValue = this.node.Value.Value;
+ }
+ this.contents = this.initialValue;
+ });
+ }
+
+ onFocusIn(event: MouseEvent) {
+ this.isFocused = true;
+ }
+
+ onFocusOut(event: MouseEvent) {
+ this.isFocused = false;
+ }
+
+ documentCommand(cmd: string) {
+ // Yes, technically this is deprecated, but it'll be poly-filled if necessary. Bite me.
+ document.execCommand(cmd, false, '');
+ }
+
+ onContentsChanged(contents: string) {
+ const innerHTML = this.editable.nativeElement.innerHTML;
+ if ( this.contents !== innerHTML ) {
+ this.contents = innerHTML;
+ this.editorService.triggerSave();
+ }
+ }
+
+ @HostListener('document:keydown.tab', ['$event'])
+ onIndent(event) {
+ event.preventDefault();
+ this.documentCommand('indent');
+ }
+
+ @HostListener('document:keydown.shift.tab', ['$event'])
+ onOutdent(event) {
+ event.preventDefault();
+ this.documentCommand('outdent');
+ }
+
+ @HostListener('document:keydown.control.b', ['$event'])
+ onBold(event) {
+ event.preventDefault();
+ this.documentCommand('bold');
+ }
+
+ @HostListener('document:keydown.control.i', ['$event'])
+ onItalic(event) {
+ event.preventDefault();
+ this.documentCommand('italic');
+ }
+
+ @HostListener('document:keydown.control.u', ['$event'])
+ onUnderline(event) {
+ event.preventDefault();
+ this.documentCommand('underline');
+ }
+
+ @HostListener('document:keydown.control.z', ['$event'])
+ onUndo(event) {
+ event.preventDefault();
+ this.documentCommand('undo');
+ }
+
+ @HostListener('document:keydown.control.shift.z', ['$event'])
+ onRedo(event) {
+ event.preventDefault();
+ this.documentCommand('redo');
+ }
+}
diff --git a/src/app/components/search/Search.component.scss b/src/app/components/search/Search.component.scss
index aea9b27..b2d1742 100644
--- a/src/app/components/search/Search.component.scss
+++ b/src/app/components/search/Search.component.scss
@@ -36,6 +36,12 @@
color: var(--noded-background-code);
}
}
+
+ &.files {
+ .search-icon {
+ color: var(--noded-background-files);
+ }
+ }
}
.search-assoc {
diff --git a/src/app/components/search/Search.component.ts b/src/app/components/search/Search.component.ts
index 3ec8aec..30348a3 100644
--- a/src/app/components/search/Search.component.ts
+++ b/src/app/components/search/Search.component.ts
@@ -3,6 +3,7 @@ import {IonInput, ModalController} from '@ionic/angular';
import {ApiService} from '../../service/api.service';
import {BehaviorSubject} from 'rxjs';
import {Router} from '@angular/router';
+import {NodeTypeIcons} from '../../structures/node-types';
export interface SearchResult {
title: string;
@@ -26,12 +27,7 @@ export class SearchComponent implements OnInit {
@Input() query = '';
public results: BehaviorSubject = new BehaviorSubject([]);
- public typeIcons = {
- node: 'fa fa-quote-left',
- page: 'fa fa-sticky-note',
- db: 'fa fa-database',
- code: 'fa fa-code',
- };
+ public typeIcons = NodeTypeIcons;
constructor(
protected modal: ModalController,
diff --git a/src/app/directives/directives.module.ts b/src/app/directives/directives.module.ts
new file mode 100644
index 0000000..37c21dd
--- /dev/null
+++ b/src/app/directives/directives.module.ts
@@ -0,0 +1,9 @@
+import {NgModule} from '@angular/core';
+import {DomChangeDirective} from './dom-change.directive';
+
+@NgModule({
+ imports: [],
+ exports: [DomChangeDirective],
+ declarations: [DomChangeDirective],
+})
+export class DirectivesModule {}
diff --git a/src/app/directives/dom-change.directive.ts b/src/app/directives/dom-change.directive.ts
new file mode 100644
index 0000000..5cb2b0d
--- /dev/null
+++ b/src/app/directives/dom-change.directive.ts
@@ -0,0 +1,30 @@
+import {Directive, ElementRef, EventEmitter, OnDestroy, Output} from '@angular/core';
+
+@Directive({
+ selector: '[appDomChange]'
+})
+export class DomChangeDirective implements OnDestroy {
+ private changes: MutationObserver;
+
+ @Output()
+ public domChange = new EventEmitter();
+
+ constructor(private elementRef: ElementRef) {
+ const element = this.elementRef.nativeElement;
+
+ this.changes = new MutationObserver((mutations) => {
+ mutations.forEach(mutation => this.domChange.emit(mutation));
+ });
+
+ this.changes.observe(element, {
+ attributes: true,
+ childList: true,
+ characterData: true,
+ subtree: true,
+ });
+ }
+
+ ngOnDestroy() {
+ this.changes.disconnect();
+ }
+}
diff --git a/src/app/pages/editor/editor.page.html b/src/app/pages/editor/editor.page.html
index 0f19bb3..3c0e3ea 100644
--- a/src/app/pages/editor/editor.page.html
+++ b/src/app/pages/editor/editor.page.html
@@ -1,46 +1,55 @@
-
+
- {{ pageRecord.Name }}
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- Add Node
- Save
+
diff --git a/src/app/pages/editor/editor.page.scss b/src/app/pages/editor/editor.page.scss
index 5092988..e5d6a1a 100644
--- a/src/app/pages/editor/editor.page.scss
+++ b/src/app/pages/editor/editor.page.scss
@@ -5,3 +5,54 @@ ion-icon {
ion-icon.invisible {
opacity: 0;
}
+
+.host-icons {
+ padding: 5px;
+ color: #444;
+ display: flex;
+ flex-direction: column;
+}
+
+.type-icon {
+ margin-bottom: 15px;
+ margin-top: 15px;
+
+ &.node {
+ color: var(--noded-background-node);
+ }
+
+ &.code_ref {
+ color: var(--noded-background-code);
+ }
+
+ &.database_ref {
+ color: var(--noded-background-db);
+ }
+
+ &.file_ref {
+ color: var(--noded-background-files);
+ }
+}
+
+.host-add-button {
+ margin-top: 50px;
+ text-align: center;
+ width: 100%;
+ padding: 10px;
+ border: 1px solid lightgrey;
+ color: darkgrey;
+ border-radius: 5px;
+
+ &:hover {
+ border: 1px solid darkgrey;
+ color: #4d4d4d;
+ }
+}
+
+.save-button {
+ color: #777;
+
+ i {
+ margin-right: 5px;
+ }
+}
diff --git a/src/app/pages/editor/editor.page.ts b/src/app/pages/editor/editor.page.ts
index 82577a3..617d77c 100644
--- a/src/app/pages/editor/editor.page.ts
+++ b/src/app/pages/editor/editor.page.ts
@@ -1,4 +1,4 @@
-import {Component, Host, OnInit, ViewChild, ViewChildren} from '@angular/core';
+import {Component, Host, HostListener, Input, OnInit, ViewChild, ViewChildren} from '@angular/core';
import HostRecord from '../../structures/HostRecord';
import PageRecord from '../../structures/PageRecord';
import {PageService} from '../../service/page.service';
@@ -6,6 +6,8 @@ import {ActivatedRoute, Router} from '@angular/router';
import {LoadingController, PopoverController} from '@ionic/angular';
import {NodePickerComponent} from '../../components/editor/node-picker/node-picker.component';
import {HostOptionsComponent} from '../../components/editor/host-options/host-options.component';
+import {EditorService} from '../../service/editor.service';
+import {NodeTypeIcons} from '../../structures/node-types';
@Component({
selector: 'app-editor',
@@ -13,20 +15,20 @@ import {HostOptionsComponent} from '../../components/editor/host-options/host-op
styleUrls: ['./editor.page.scss'],
})
export class EditorPage implements OnInit {
- public hostRecords: Array = [new HostRecord('Click to edit page...')];
- public pageRecord: PageRecord = new PageRecord();
- public pageId: string;
- public visibleButtons: Array = [];
+ // @ViewChildren('editorHosts') editorHosts;
+ // @ViewChild('titleBar') titleBar;
- @ViewChildren('editorHosts') editorHosts;
- @ViewChild('titleBar') titleBar;
+ public typeIcons = NodeTypeIcons;
+
+ @Input() pageId: string;
+ public pageName = '';
constructor(
- protected pages: PageService,
protected route: ActivatedRoute,
protected router: Router,
- protected popover: PopoverController,
protected loader: LoadingController,
+ protected popover: PopoverController,
+ public readonly editorService: EditorService,
) {
this.route.params.subscribe(params => {
this.pageId = params.id;
@@ -35,215 +37,305 @@ export class EditorPage implements OnInit {
ngOnInit() {}
- buttonIsVisible(index) {
- return this.visibleButtons.includes(index);
- }
-
- makeVisible(index) {
- if ( !this.buttonIsVisible(index) ) {
- this.visibleButtons.push(index);
- }
- }
-
- makeInvisible(index) {
- this.visibleButtons = this.visibleButtons.filter(x => x !== index);
- }
-
ionViewDidEnter() {
if ( this.pageId ) {
- this.pages.load(this.pageId).subscribe(pageRecord => {
- this.pageRecord = pageRecord;
- this.pages.get_nodes(pageRecord).subscribe((hosts: Array) => {
- this.hostRecords = hosts;
- if ( !pageRecord.isViewOnly() ) {
- this.onSaveClick();
- }
- });
- });
+ this.editorService.startEditing(this.pageId);
} else {
this.router.navigate(['/home']);
}
}
- onHostRecordChange($event, i) {
- if ( !this.pageRecord.isViewOnly() ) {
- this.hostRecords[i] = $event;
- }
+ @HostListener('document:keydown.control.s', ['$event'])
+ onManualSave(event) {
+ event.preventDefault();
+ this.editorService.save();
}
- async onAddClick($event) {
- if ( this.pageRecord.isViewOnly() ) {
- return;
- }
-
- const popover = await this.popover.create({
- component: NodePickerComponent,
- event: $event,
- });
-
- popover.onDidDismiss().then(arg => {
- const defValue = this.getDefaultValue(arg.data);
- const hostRec = new HostRecord(defValue);
- hostRec.type = arg.data;
- hostRec.PageId = this.pageRecord.UUID;
-
- if ( hostRec.type === 'ul' ) {
- hostRec.value = JSON.stringify([{value: '', indentationLevel: 0}]);
- }
-
- this.hostRecords.push(hostRec);
- if ( hostRec.isNorm() ) {
- setTimeout(() => {
- this.editorHosts.toArray().reverse()[0].takeFocus();
- }, 0);
- } else {
- this.onSaveClick();
- }
- });
-
- await popover.present();
- }
-
- getDefaultValue(type: string) {
- if ( type === 'paragraph' ) {
- return '';
- } else if ( type === 'header1' ) {
- return '# ';
- } else if ( type === 'header2' ) {
- return '## ';
- } else if ( type === 'header3' ) {
- return '### ';
- } else if ( type === 'header4' ) {
- return '#### ';
- } else if ( type === 'block_code' ) {
- return '```';
- } else if ( type === 'click_link' ) {
- return 'https://';
- } else if ( type === 'page_sep' ) {
- return '===';
- } else {
- return '';
- }
- }
-
- onNewHostRequested($event) {
- if ( this.pageRecord.isViewOnly() ) {
- return;
- }
-
- const insertAfter = this.getIndexFromRecord($event.record);
- const record = new HostRecord('');
- const newHosts = []
- this.hostRecords.forEach((rec, i) => {
- newHosts.push(rec);
- if ( i === insertAfter ) {
- newHosts.push(record);
- }
- })
-
- this.hostRecords = newHosts;
-
- setTimeout(() => {
- this.editorHosts.toArray()[insertAfter + 1].takeFocus();
- }, 0);
- }
-
- onDestroyHostRequested($event) {
- if ( this.pageRecord.isViewOnly() ) {
- return;
- }
-
- let removedIndex = 0;
- const newHostRecords = this.editorHosts.filter((host, i) => {
- if ( $event.record === host.record ) {
- removedIndex = i;
- }
- return host.record !== $event.record;
- });
-
- const removedHost = this.editorHosts[removedIndex];
-
- const hostRecords = newHostRecords.map(host => host.record);
- this.hostRecords = hostRecords;
-
- setTimeout(() => {
- let focusIndex;
- if ( removedIndex === 0 && this.editorHosts.toArray().length ) {
- focusIndex = 0;
- } else if ( removedIndex !== 0 ) {
- focusIndex = removedIndex - 1;
- }
-
- if ( focusIndex >= 0 ) {
- this.editorHosts.toArray()[focusIndex].takeFocus(false);
- }
- }, 0);
- }
-
- protected getIndexFromRecord(record) {
- let index;
- this.editorHosts.toArray().forEach((host, i) => {
- if ( host.record === record ) {
- index = i;
- }
- });
- return index;
- }
-
- onSaveClick() {
- if ( this.pageRecord.isViewOnly() ) {
- return;
- }
-
- this.loader.create({message: 'Saving changes...'}).then(loader => {
- loader.present().then(() => {
- this.pageRecord.Name = this.titleBar.el.innerText.trim();
-
- // First, save the page record itself
- this.pages.save(this.pageRecord).subscribe(pageRecord => {
- this.pageRecord = pageRecord;
-
- // Now, save the nodes
- this.pages.save_nodes(pageRecord, this.hostRecords).subscribe(result => {
- this.hostRecords = result;
- loader.dismiss();
- });
- });
- });
- });
- }
-
- async onOptionsClick($event, i) {
- if ( this.pageRecord.isViewOnly() ) {
+ async onOptionsClick(event: MouseEvent, node: HostRecord) {
+ if ( !this.editorService.canEdit() ) {
return;
}
const popover = await this.popover.create({
component: HostOptionsComponent,
- event: $event,
+ event,
componentProps: {
editor: this,
- index: i,
- event: $event,
- hostRecord: this.hostRecords[i],
+ index: this.editorService.immutableNodes.indexOf(node),
+ event,
+ hostRecord: node,
}
});
- popover.onDidDismiss().then((result) => {
- if ( result.data === 'delete_node' ) {
- $event.record = this.hostRecords[i];
- this.onDestroyHostRequested($event);
+ popover.onDidDismiss().then(result => {
+ const { event: dismissEvent , value } = result.data;
+ if ( value === 'delete_node' ) {
+ this.editorService.deleteNode(node.UUID);
+ } else if ( value === 'move_up' ) {
+ this.editorService.moveNode(node, 'up');
+ } else if ( value === 'move_down' ) {
+ this.editorService.moveNode(node, 'down');
+ } else if ( value === 'add_before' ) {
+ this.onAddClick(dismissEvent, 'before', node.UUID);
+ } else if ( value === 'add_after' ) {
+ this.onAddClick(dismissEvent, 'after', node.UUID);
}
- })
+ });
await popover.present();
}
- onEditorKeydown($event) {
- if ( $event.key === 's' && $event.ctrlKey ) {
- $event.preventDefault();
- this.onSaveClick();
+ async onAddClick(event: MouseEvent, position?: 'before' | 'after', positionNodeId?: string) {
+ if ( !this.editorService.canEdit() ) {
+ return;
}
+
+ const popover = await this.popover.create({
+ component: NodePickerComponent,
+ event,
+ });
+
+ popover.onDidDismiss().then(result => {
+ console.log('adding node', result.data);
+ if ( !result.data ) {
+ return;
+ }
+
+ this.editorService.addNode(result.data, position, positionNodeId);
+ });
+
+ // popover.onDidDismiss().then(arg => {
+ // const defValue = this.getDefaultValue(arg.data);
+ // const hostRec = new HostRecord(defValue);
+ // hostRec.type = arg.data;
+ // hostRec.PageId = this.pageRecord.UUID;
+ //
+ // if ( hostRec.type === 'ul' ) {
+ // hostRec.value = JSON.stringify([{value: '', indentationLevel: 0}]);
+ // }
+ //
+ // this.hostRecords.push(hostRec);
+ // if ( hostRec.isNorm() ) {
+ // setTimeout(() => {
+ // this.editorHosts.toArray().reverse()[0].takeFocus();
+ // }, 0);
+ // } else {
+ // this.onSaveClick();
+ // }
+ // });
+
+ await popover.present();
}
+ // buttonIsVisible(index) {
+ // return this.visibleButtons.includes(index);
+ // }
+ //
+ // makeVisible(index) {
+ // if ( !this.buttonIsVisible(index) ) {
+ // this.visibleButtons.push(index);
+ // }
+ // }
+ //
+ // makeInvisible(index) {
+ // this.visibleButtons = this.visibleButtons.filter(x => x !== index);
+ // }
+ //
+ // ionViewDidEnter() {
+ // if ( this.pageId ) {
+ // this.pages.load(this.pageId).subscribe(pageRecord => {
+ // this.pageRecord = pageRecord;
+ // this.pages.get_nodes(pageRecord).subscribe((hosts: Array) => {
+ // this.hostRecords = hosts;
+ // if ( !pageRecord.isViewOnly() ) {
+ // this.onSaveClick();
+ // }
+ // });
+ // });
+ // } else {
+ // this.router.navigate(['/home']);
+ // }
+ // }
+ //
+ // onHostRecordChange($event, i) {
+ // if ( !this.pageRecord.isViewOnly() ) {
+ // this.hostRecords[i] = $event;
+ // }
+ // }
+ //
+ // async onAddClick($event) {
+ // if ( this.pageRecord.isViewOnly() ) {
+ // return;
+ // }
+ //
+ // const popover = await this.popover.create({
+ // component: NodePickerComponent,
+ // event: $event,
+ // });
+ //
+ // popover.onDidDismiss().then(arg => {
+ // const defValue = this.getDefaultValue(arg.data);
+ // const hostRec = new HostRecord(defValue);
+ // hostRec.type = arg.data;
+ // hostRec.PageId = this.pageRecord.UUID;
+ //
+ // if ( hostRec.type === 'ul' ) {
+ // hostRec.value = JSON.stringify([{value: '', indentationLevel: 0}]);
+ // }
+ //
+ // this.hostRecords.push(hostRec);
+ // if ( hostRec.isNorm() ) {
+ // setTimeout(() => {
+ // this.editorHosts.toArray().reverse()[0].takeFocus();
+ // }, 0);
+ // } else {
+ // this.onSaveClick();
+ // }
+ // });
+ //
+ // await popover.present();
+ // }
+ //
+ // getDefaultValue(type: string) {
+ // if ( type === 'paragraph' ) {
+ // return '';
+ // } else if ( type === 'header1' ) {
+ // return '# ';
+ // } else if ( type === 'header2' ) {
+ // return '## ';
+ // } else if ( type === 'header3' ) {
+ // return '### ';
+ // } else if ( type === 'header4' ) {
+ // return '#### ';
+ // } else if ( type === 'block_code' ) {
+ // return '```';
+ // } else if ( type === 'click_link' ) {
+ // return 'https://';
+ // } else if ( type === 'page_sep' ) {
+ // return '===';
+ // } else {
+ // return '';
+ // }
+ // }
+ //
+ // onNewHostRequested($event) {
+ // if ( this.pageRecord.isViewOnly() ) {
+ // return;
+ // }
+ //
+ // const insertAfter = this.getIndexFromRecord($event.record);
+ // const record = new HostRecord('');
+ // const newHosts = []
+ // this.hostRecords.forEach((rec, i) => {
+ // newHosts.push(rec);
+ // if ( i === insertAfter ) {
+ // newHosts.push(record);
+ // }
+ // })
+ //
+ // this.hostRecords = newHosts;
+ //
+ // setTimeout(() => {
+ // this.editorHosts.toArray()[insertAfter + 1].takeFocus();
+ // }, 0);
+ // }
+ //
+ // onDestroyHostRequested($event) {
+ // if ( this.pageRecord.isViewOnly() ) {
+ // return;
+ // }
+ //
+ // let removedIndex = 0;
+ // const newHostRecords = this.editorHosts.filter((host, i) => {
+ // if ( $event.record === host.record ) {
+ // removedIndex = i;
+ // }
+ // return host.record !== $event.record;
+ // });
+ //
+ // const removedHost = this.editorHosts[removedIndex];
+ //
+ // const hostRecords = newHostRecords.map(host => host.record);
+ // this.hostRecords = hostRecords;
+ //
+ // setTimeout(() => {
+ // let focusIndex;
+ // if ( removedIndex === 0 && this.editorHosts.toArray().length ) {
+ // focusIndex = 0;
+ // } else if ( removedIndex !== 0 ) {
+ // focusIndex = removedIndex - 1;
+ // }
+ //
+ // if ( focusIndex >= 0 ) {
+ // this.editorHosts.toArray()[focusIndex].takeFocus(false);
+ // }
+ // }, 0);
+ // }
+ //
+ // protected getIndexFromRecord(record) {
+ // let index;
+ // this.editorHosts.toArray().forEach((host, i) => {
+ // if ( host.record === record ) {
+ // index = i;
+ // }
+ // });
+ // return index;
+ // }
+ //
+ // onSaveClick() {
+ // if ( this.pageRecord.isViewOnly() ) {
+ // return;
+ // }
+ //
+ // this.loader.create({message: 'Saving changes...'}).then(loader => {
+ // loader.present().then(() => {
+ // this.pageRecord.Name = this.titleBar.el.innerText.trim();
+ //
+ // // First, save the page record itself
+ // this.pages.save(this.pageRecord).subscribe(pageRecord => {
+ // this.pageRecord = pageRecord;
+ //
+ // // Now, save the nodes
+ // this.pages.save_nodes(pageRecord, this.hostRecords).subscribe(result => {
+ // this.hostRecords = result;
+ // loader.dismiss();
+ // });
+ // });
+ // });
+ // });
+ // }
+ //
+ // async onOptionsClick($event, i) {
+ // if ( this.pageRecord.isViewOnly() ) {
+ // return;
+ // }
+ //
+ // const popover = await this.popover.create({
+ // component: HostOptionsComponent,
+ // event: $event,
+ // componentProps: {
+ // editor: this,
+ // index: i,
+ // event: $event,
+ // hostRecord: this.hostRecords[i],
+ // }
+ // });
+ //
+ // popover.onDidDismiss().then((result) => {
+ // if ( result.data === 'delete_node' ) {
+ // $event.record = this.hostRecords[i];
+ // this.onDestroyHostRequested($event);
+ // }
+ // })
+ //
+ // await popover.present();
+ // }
+ //
+ // onEditorKeydown($event) {
+ // if ( $event.key === 's' && $event.ctrlKey ) {
+ // $event.preventDefault();
+ // this.onSaveClick();
+ // }
+ // }
+
}
diff --git a/src/app/service/editor.service.ts b/src/app/service/editor.service.ts
new file mode 100644
index 0000000..003ee95
--- /dev/null
+++ b/src/app/service/editor.service.ts
@@ -0,0 +1,333 @@
+import {Injectable} from '@angular/core';
+import {ApiService} from './api.service';
+import PageRecord from '../structures/PageRecord';
+import HostRecord from '../structures/HostRecord';
+import {EditorNodeContract} from '../components/nodes/EditorNode.contract';
+import {BehaviorSubject, Subscription} from 'rxjs';
+
+export class NoPageLoadedError extends Error {
+ constructor(msg = 'There is no page open for editing.') {
+ super(msg);
+ }
+}
+
+export function debounce(func: (...args: any[]) => any, timeout?: number) {
+ let timer: number | undefined;
+ return (...args: any[]) => {
+ const next = () => func(...args);
+ if (timer) {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(next, timeout > 0 ? timeout : 300);
+ };
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class EditorService {
+ protected currentPage?: PageRecord;
+ protected currentNodes: HostRecord[] = [];
+ protected nodeIdToEditorContract: { [key: string]: EditorNodeContract } = {};
+ protected dirtyOverride = false;
+ protected ready$: BehaviorSubject = new BehaviorSubject(false);
+ protected subs: Subscription[] = [];
+ protected saving = false;
+ protected saveTriggered = false;
+ protected privTriggerSave = debounce(() => {
+ if ( this.saving ) {
+ this.triggerSave();
+ } else {
+ this.save();
+ }
+
+ this.saveTriggered = false;
+ }, 3000);
+
+ public triggerSave() {
+ this.saveTriggered = true;
+ this.privTriggerSave();
+ }
+
+ public get isSaving() {
+ return this.saving;
+ }
+
+ public get willSave() {
+ return this.saveTriggered;
+ }
+
+ public get immutableNodes(): HostRecord[] {
+ return [...this.currentNodes];
+ }
+
+ public get mutablePageName(): string {
+ if ( this.currentPage ) {
+ return this.currentPage.Name;
+ }
+
+ return '';
+ }
+
+ public set mutablePageName(name: string) {
+ if ( this.currentPage && this.canEdit() ) {
+ if ( this.currentPage.Name !== name ) {
+ this.dirtyOverride = true;
+ }
+
+ this.currentPage.Name = name;
+ }
+ }
+
+ constructor(
+ protected api: ApiService,
+ ) { }
+
+ async startEditing(pageId: string) {
+ if ( this.currentPage ) {
+ await this.stopEditing();
+ }
+
+ this.currentPage = await this.loadPage(pageId);
+ this.currentNodes = await this.loadNodes(pageId);
+ await this.ready$.next(true);
+ }
+
+ async stopEditing() {
+ delete this.currentPage;
+ this.currentNodes = [];
+ this.nodeIdToEditorContract = {};
+ this.subs.forEach(sub => sub.unsubscribe());
+ this.subs = [];
+ this.ready$.next(false);
+ }
+
+ async save() {
+ if ( !(await this.needsSave()) || this.saving ) {
+ return;
+ }
+
+ this.saving = true;
+ const editors = Object.values(this.nodeIdToEditorContract);
+
+ // Save all editors that handle their data independently first
+ await Promise.all(editors.map(async editor => {
+ if ( await editor.needsSave() ) {
+ await editor.performSave();
+ }
+ }));
+
+ // Tell the editors to write their state changes to the HostRecords
+ await Promise.all(editors.map(async editor => {
+ await editor.writeChangesToNode();
+ }));
+
+ await this.saveNodesAsPage(this.currentPage, this.currentNodes);
+ this.dirtyOverride = false;
+ this.saving = false;
+ }
+
+ async moveNode(node: HostRecord, direction: 'up' | 'down') {
+ if ( !this.currentPage ) {
+ throw new NoPageLoadedError();
+ }
+
+ const nodeIndex = this.currentNodes.findIndex(maybeNode => maybeNode.UUID === node.UUID);
+ if ( nodeIndex < 0 ) {
+ return;
+ }
+
+ if ( direction === 'up' && nodeIndex > 0 ) {
+ const otherIdx = nodeIndex - 1;
+ const otherNode = this.currentNodes[otherIdx];
+ this.currentNodes[otherIdx] = this.currentNodes[nodeIndex];
+ this.currentNodes[nodeIndex] = otherNode;
+ } else if ( direction === 'down' && nodeIndex !== (this.currentNodes.length - 1) ) {
+ const otherIdx = nodeIndex + 1;
+ const otherNode = this.currentNodes[otherIdx];
+ this.currentNodes[otherIdx] = this.currentNodes[nodeIndex];
+ this.currentNodes[nodeIndex] = otherNode;
+ }
+
+ this.dirtyOverride = true;
+ this.triggerSave();
+ }
+
+ async saveNodesAsPage(page: PageRecord, nodes: HostRecord[]): Promise {
+ return new Promise((res, rej) => {
+ const saveNodes = nodes.map(x => {
+ x.PageId = page.UUID;
+ return x.toSave();
+ });
+
+ this.api.post(`/page/${page.UUID}/nodes/save`, saveNodes).subscribe({
+ next: result => {
+ res(result.data.map(rec => {
+ const host = new HostRecord(rec.Value.Value);
+ host.load(rec);
+ return host;
+ }));
+ },
+ error: rej,
+ });
+ });
+ }
+
+ async saveNodeToPage(page: PageRecord, node: HostRecord): Promise {
+ return new Promise((res, rej) => {
+ node.PageId = page.UUID;
+ const nodeData = node.toSave();
+
+ this.api.post(`/page/${page.UUID}/nodes/save_one`, { nodeData }).subscribe({
+ next: result => {
+ const host = new HostRecord(result.data.Value.Value);
+ host.load(result.data);
+ res(host);
+ },
+ error: rej,
+ });
+ });
+ }
+
+ async needsSave() {
+ if ( this.dirtyOverride ) {
+ return true;
+ }
+
+ const dirties = await Promise.all(Object.values(this.nodeIdToEditorContract).map(editor => editor.isDirty()));
+ const needSaves = await Promise.all(Object.values(this.nodeIdToEditorContract).map(editor => editor.needsSave()));
+
+ return dirties.some(Boolean) || needSaves.some(Boolean);
+ }
+
+ async deleteNode(nodeId: string) {
+ if ( !this.currentPage ) {
+ throw new NoPageLoadedError();
+ }
+
+ const node = this.currentNodes.find(maybeNode => maybeNode.UUID === nodeId);
+ if ( !node ) {
+ throw new Error('Invalid node ID.');
+ }
+
+ const editor = this.nodeIdToEditorContract[nodeId];
+ if ( editor ) {
+ await editor.performDelete();
+ delete this.nodeIdToEditorContract[nodeId];
+ }
+
+ this.currentNodes = this.currentNodes.filter(x => x.UUID !== nodeId);
+ this.dirtyOverride = true;
+ this.triggerSave();
+ }
+
+ async addNode(type: 'paragraph' | 'code_ref' | 'database_ref' | 'file_ref', position?: 'before' | 'after', positionNodeId?: string) {
+ if ( !this.currentPage ) {
+ throw new NoPageLoadedError();
+ }
+
+ const baseHost = new HostRecord();
+ baseHost.type = type;
+ baseHost.PageId = this.currentPage.UUID;
+
+ const host = await this.saveNodeToPage(this.currentPage, baseHost);
+
+ let placed = false;
+ if ( position === 'before' && positionNodeId ) {
+ const index = this.currentNodes.findIndex(node => node.UUID === positionNodeId);
+ if ( index > -1 ) {
+ this.currentNodes.splice(index, 0, host);
+ placed = true;
+ }
+ } else if ( position === 'after' && positionNodeId ) {
+ const index = this.currentNodes.findIndex(node => node.UUID === positionNodeId);
+ if ( index > -1 ) {
+ this.currentNodes.splice(index + 1, 0, host);
+ placed = true;
+ }
+ }
+
+ if ( !placed ) {
+ this.currentNodes.push(host);
+ }
+
+ this.dirtyOverride = true;
+ this.triggerSave();
+ return host;
+ }
+
+ canEdit() {
+ if ( !this.currentPage ) {
+ throw new NoPageLoadedError();
+ }
+
+ return !this.currentPage.isViewOnly();
+ }
+
+ async registerNodeEditor(nodeId: string, editor: EditorNodeContract) {
+ return new Promise((res, rej) => {
+ const sub = this.ready$.subscribe(async val => {
+ if ( val ) {
+ try {
+ if ( !this.currentPage ) {
+ return rej(new NoPageLoadedError());
+ }
+
+ const node = this.currentNodes.find(maybeNode => maybeNode.UUID === nodeId);
+ if ( !node ) {
+ return rej(new Error('Invalid node ID.'));
+ }
+
+ editor.page = this.currentPage;
+ editor.node = node;
+ this.nodeIdToEditorContract[nodeId] = editor;
+
+ if ( editor.needsLoad() ) {
+ await editor.performLoad();
+ }
+
+ res();
+ } catch (e) {
+ rej(e);
+ }
+ }
+ });
+
+ this.subs.push(sub);
+ });
+ }
+
+ async unregisterNodeEditor(nodeId: string) {
+ if ( !this.currentPage ) {
+ throw new NoPageLoadedError();
+ }
+
+ delete this.nodeIdToEditorContract[nodeId];
+ }
+
+ async loadPage(pageId: string): Promise {
+ return new Promise((res, rej) => {
+ this.api.get(`/page/${pageId}`).subscribe({
+ next: result => {
+ res(new PageRecord(result.data));
+ },
+ error: rej,
+ });
+ });
+ }
+
+ async loadNodes(pageId: string): Promise {
+ return new Promise((res, rej) => {
+ this.api.get(`/page/${pageId}/nodes`).subscribe({
+ next: result => {
+ res(result.data.map(rec => {
+ const host = new HostRecord(rec.Value.Value);
+ host.load(rec);
+ return host;
+ }));
+ },
+ error: rej,
+ });
+ });
+ }
+}
diff --git a/src/app/structures/HostRecord.ts b/src/app/structures/HostRecord.ts
index 3fecb07..9fca377 100644
--- a/src/app/structures/HostRecord.ts
+++ b/src/app/structures/HostRecord.ts
@@ -1,16 +1,19 @@
export default class HostRecord {
public value = '';
- public type: 'paragraph'
- |'header1'|'header2'|'header3'|'header4'
- |'block_code'|'click_link'|'database_ref'
- |'ul'|'code_ref'|'file_ref'|'page_sep' = 'paragraph';
+ public type: 'paragraph'|'database_ref'|'code_ref'|'file_ref' = 'paragraph';
public CreatedAt: string;
public PageId: string;
- public UUID: string;
+ private privUUID: string;
public UpdatedAt: string;
public Value: any;
+ public get UUID(): string {
+ return this.privUUID;
+ }
+
+ public set UUID(val: string) {}
+
constructor(value = '') {
this.value = value;
}
@@ -21,11 +24,11 @@ export default class HostRecord {
load(data: any) {
this.type = data.Type;
+ this.privUUID = data.UUID;
[
'CreatedAt',
'PageId',
- 'UUID',
'UpdatedAt',
'Value',
].forEach(field => {
diff --git a/src/app/structures/node-types.ts b/src/app/structures/node-types.ts
new file mode 100644
index 0000000..e7397c6
--- /dev/null
+++ b/src/app/structures/node-types.ts
@@ -0,0 +1,12 @@
+export const NodeTypeIcons = {
+ branch: 'fa fa-folder',
+ node: 'fa fa-quote-left',
+ norm: 'fa fa-quote-left',
+ page: 'fa fa-sticky-note',
+ db: 'fa fa-database',
+ database_ref: 'fa fa-database',
+ code: 'fa fa-code',
+ code_ref: 'fa fa-code',
+ file_ref: 'fa fa-archive',
+ files: 'fa fa-archive',
+};
diff --git a/src/global.scss b/src/global.scss
index cb3d4a6..5e09cc6 100644
--- a/src/global.scss
+++ b/src/global.scss
@@ -46,6 +46,10 @@
--noded-background-code: #FF006E;
--noded-color-code: white;
--noded-background-code-hover: #FF5CA3;
+
+ --noded-background-files: #0E7B81;
+ --noded-color-files: white;
+ --noded-background-files-hover: #14AFB8;
}
div.picker-wrapper {
@@ -64,3 +68,7 @@ div.picker-wrapper {
//color: #fff;
}
+
+hr {
+ background: darkgray;
+}