Merge pull request 'editor-refactor' (#18) from editor-refactor into master
Reviewed-on: #18
This commit is contained in:
commit
8ca9b736eb
15
package-lock.json
generated
15
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -29,4 +29,10 @@
|
||||
color: var(--noded-background-code);
|
||||
}
|
||||
}
|
||||
|
||||
&.files {
|
||||
.tree-node-icon {
|
||||
color: var(--noded-background-files);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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 {}
|
||||
|
@ -7,7 +7,7 @@
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-toolbar>
|
||||
<div class="ed-wrapper" style="width: 100%; height: 480px;">
|
||||
<div class="ed-wrapper" style="width: 100%; height: 540px;">
|
||||
<ngx-monaco-editor style="width: 100%; height: 100%;"
|
||||
[options]="editorOptions"
|
||||
[(ngModel)]="editorValue"
|
||||
@ -15,10 +15,4 @@
|
||||
#theEditor
|
||||
></ngx-monaco-editor>
|
||||
</div>
|
||||
<ion-toolbar *ngIf="!readonly">
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="onDropClick()"><ion-icon name="alert" color="danger"></ion-icon> Drop Editor</ion-button>
|
||||
<ion-button (click)="onSaveClick()"><ion-icon name="save" [color]="dirty ? 'warning' : 'success'"></ion-icon> Save Changes</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</div>
|
||||
|
@ -1,26 +1,30 @@
|
||||
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'],
|
||||
})
|
||||
export class CodeComponent implements OnInit {
|
||||
@Input() readonly = false;
|
||||
@Input() hostRecord: HostRecord;
|
||||
@Output() hostRecordChange = new EventEmitter<HostRecord>();
|
||||
@Output() requestParentSave = new EventEmitter<CodeComponent>();
|
||||
@Output() requestParentDelete = new EventEmitter<CodeComponent>();
|
||||
@ViewChild('theEditor') theEditor;
|
||||
|
||||
export class CodeComponent extends EditorNodeContract implements OnInit {
|
||||
@Input() nodeId: string;
|
||||
public dirty = false;
|
||||
public pendingSetup = true;
|
||||
protected dbRecord: any = {};
|
||||
protected codeRefId!: string;
|
||||
|
||||
public editorOptions = {
|
||||
language: 'javascript',
|
||||
uri: v4(),
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
public editorValue = '';
|
||||
public get readonly() {
|
||||
return !this.node || !this.editorService.canEdit();
|
||||
}
|
||||
|
||||
public languageOptions: Array<string> = [
|
||||
'ABAP',
|
||||
@ -87,123 +91,125 @@ export class CodeComponent implements OnInit {
|
||||
'XML',
|
||||
'YAML',
|
||||
];
|
||||
|
||||
public editorOptions = {
|
||||
language: 'javascript',
|
||||
uri: v4(),
|
||||
readOnly: this.readonly,
|
||||
};
|
||||
public editorValue = '';
|
||||
protected hadLoad = false;
|
||||
|
||||
constructor(
|
||||
protected api: ApiService,
|
||||
protected loader: LoadingController,
|
||||
protected alerts: AlertController,
|
||||
) { }
|
||||
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;
|
||||
public isDirty(): boolean | Promise<boolean> {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
public needsSave(): boolean | Promise<boolean> {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
public writeChangesToNode(): void | Promise<void> {
|
||||
this.node.Value.Mode = 'code';
|
||||
this.node.Value.Value = this.codeRefId;
|
||||
this.node.value = this.codeRefId;
|
||||
}
|
||||
|
||||
public needsLoad(): boolean | Promise<boolean> {
|
||||
return this.node && !this.hadLoad;
|
||||
}
|
||||
|
||||
public performLoad(): void | Promise<void> {
|
||||
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);
|
||||
loader.dismiss();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getInitObservable(): Observable<any> {
|
||||
return new Observable<any>(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();
|
||||
this.hadLoad = true;
|
||||
res();
|
||||
},
|
||||
error: rej,
|
||||
});
|
||||
} else {
|
||||
this.api.get(`/code/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}`).subscribe(res => {
|
||||
this.dbRecord = res.data;
|
||||
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.pendingSetup = false;
|
||||
sub.next(true);
|
||||
sub.complete();
|
||||
this.codeRefId = this.node.Value.Value;
|
||||
this.editorOptions.readOnly = this.readonly;
|
||||
this.onSelectChange(false);
|
||||
this.hadLoad = true;
|
||||
res();
|
||||
},
|
||||
error: rej,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.pendingSetup = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSaveClick() {
|
||||
if ( this.readonly ) {
|
||||
public performSave(): void | Promise<void> {
|
||||
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.hostRecord.PageId}/${this.hostRecord.UUID}/set/${this.hostRecord.Value.Value}`, this.dbRecord)
|
||||
.subscribe(res => {
|
||||
this.dbRecord = res.data;
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async onDropClick() {
|
||||
if ( this.readonly ) {
|
||||
return;
|
||||
public performDelete(): void | Promise<void> {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
ngOnInit() {
|
||||
this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
|
||||
this.editorOptions.readOnly = !this.editorService.canEdit();
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
public onEditorModelChange($event) {
|
||||
if ( this.editorValue !== this.dbRecord.code ) {
|
||||
this.dirty = true;
|
||||
this.editorService.triggerSave();
|
||||
}
|
||||
}
|
||||
|
||||
onSelectChange(updateDbRecord = true) {
|
||||
public onSelectChange(updateDbRecord = true) {
|
||||
if ( updateDbRecord ) {
|
||||
this.dbRecord.Language = this.editorOptions.language;
|
||||
this.editorService.triggerSave();
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
this.editorOptions = {...this.editorOptions};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
<div class="database-wrapper">
|
||||
<ion-toolbar>
|
||||
<ion-input
|
||||
[readonly]="readonly"
|
||||
[(ngModel)]="dbName"
|
||||
(ionChange)="onCellValueChanged()"
|
||||
style="font-size: 15pt;"
|
||||
></ion-input>
|
||||
<ion-toolbar *ngIf="!readonly">
|
||||
<ion-buttons>
|
||||
<ion-buttons *ngIf="!readonly">
|
||||
<ion-button (click)="onManageColumns()"><ion-icon name="build" color="primary"></ion-icon> Manage Columns</ion-button>
|
||||
<ion-button (click)="onInsertRow()"><ion-icon name="add-circle" color="success"></ion-icon> Insert Row</ion-button>
|
||||
<ion-button (click)="onRemoveRow()" [disabled]="lastClickRow < 0"><ion-icon name="remove-circle" color="danger"></ion-icon> Delete Row</ion-button>
|
||||
<ion-button (click)="onSyncRecords()"><ion-icon name="save" [color]="dirty ? 'warning' : 'success'"></ion-icon> Sync Records</ion-button>
|
||||
<ion-button (click)="onDropDatabase()"><ion-icon name="alert" color="danger"></ion-icon> Drop Database</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
<div class="grid-wrapper">
|
||||
|
@ -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<HostRecord>();
|
||||
@Output() requestParentSave = new EventEmitter<DatabaseComponent>();
|
||||
@Output() requestParentDelete = new EventEmitter<DatabaseComponent>();
|
||||
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<boolean> {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
public needsSave(): boolean | Promise<boolean> {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
public needsLoad(): boolean | Promise<boolean> {
|
||||
return this.node && this.pendingSetup;
|
||||
}
|
||||
|
||||
public writeChangesToNode(): void | Promise<void> {
|
||||
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();
|
||||
this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
// });
|
||||
// }, 100);
|
||||
// });
|
||||
}
|
||||
|
||||
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,83 +133,12 @@ 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<any> {
|
||||
return new Observable<any>(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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onSyncRecords() {
|
||||
if ( this.readonly ) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
this.agGridElement.api.setRowData(this.rowData);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getColumnLoadObservable(): Observable<any> {
|
||||
return new Observable<any>(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setColumns(data): Observable<any> {
|
||||
return new Observable<any>(sub => {
|
||||
setColumns(data) {
|
||||
this.columnDefs = data.map(x => {
|
||||
x.editable = !this.readonly;
|
||||
|
||||
@ -235,48 +166,118 @@ export class DatabaseComponent implements OnInit {
|
||||
x.editable = false;
|
||||
}
|
||||
|
||||
console.log({x});
|
||||
return x;
|
||||
});
|
||||
|
||||
this.agGridElement.api.setColumnDefs(this.columnDefs);
|
||||
this.dirty = true;
|
||||
this.editorService.triggerSave();
|
||||
}
|
||||
|
||||
sub.next();
|
||||
sub.complete();
|
||||
public async performLoad(): Promise<void> {
|
||||
if ( !this.node.Value ) {
|
||||
this.node.Value = {};
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
|
||||
getInitObservable(): Observable<any> {
|
||||
return new Observable<any>(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();
|
||||
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,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.pendingSetup = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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.pendingSetup = false;
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
public performDelete(): void | Promise<void> {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async performSave(): Promise<void> {
|
||||
// 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,
|
||||
});
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
<input style="margin-top: 10px;" type="file" id="file" name="uploaded_file">
|
||||
<input type="hidden" name="redirectTo" [value]="getReturnUrl()">
|
||||
<ion-button (click)="onSubmitClick()" type="submit" fill="outline" class="ion-margin-start">Upload</ion-button>
|
||||
<ion-button (click)="onDestroyClick()" type="submit" fill="outline" class="ion-margin-start" color="danger">Drop Files</ion-button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="existing-uploads">
|
||||
|
@ -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<HostRecord>();
|
||||
@Output() requestParentSave = new EventEmitter<FilesComponent>();
|
||||
@Output() requestParentDelete = new EventEmitter<FilesComponent>();
|
||||
export class FilesComponent extends EditorNodeContract implements OnInit {
|
||||
@Input() nodeId: string;
|
||||
@ViewChild('uploadForm') uploadForm: ElementRef;
|
||||
// @Input() readonly = false;
|
||||
// @Input() hostRecord: HostRecord;
|
||||
// @Output() hostRecordChange = new EventEmitter<HostRecord>();
|
||||
// @Output() requestParentSave = new EventEmitter<FilesComponent>();
|
||||
// @Output() requestParentDelete = new EventEmitter<FilesComponent>();
|
||||
|
||||
public fileRecords: Array<any> = [];
|
||||
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<boolean> {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
public writeChangesToNode(): void | Promise<void> {
|
||||
this.node.Value.Mode = 'files';
|
||||
}
|
||||
|
||||
public needsLoad(): boolean | Promise<boolean> {
|
||||
return this.node && this.pendingSetup;
|
||||
}
|
||||
|
||||
public async performLoad(): Promise<void> {
|
||||
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<void> {
|
||||
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<any> {
|
||||
return new Observable<any>(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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
<ion-list>
|
||||
<ion-item *ngFor="let menuItem of menuItems; let i = index" button (click)="onSelect(menuItems[i].value)">
|
||||
<ion-icon slot="start" [name]="menuItems[i].icon"></ion-icon>
|
||||
<ion-item *ngFor="let menuItem of menuItems; let i = index" button (click)="onSelect($event, menuItems[i].value)">
|
||||
<i *ngIf="menuItems[i].icon" class="fa" slot="start" [ngClass]="menuItems[i].icon"></i>
|
||||
<div *ngIf="menuItems[i].icons" slot="start">
|
||||
<i *ngFor="let icon of menuItems[i].icons" class="fa" slot="start" [ngClass]="icon" style="margin-right: 5px;"></i>
|
||||
</div>
|
||||
<ion-label>{{ menuItems[i].name }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
<ng-container>
|
||||
<div
|
||||
*ngIf="!page.isViewOnly() && ( record.type === 'paragraph'
|
||||
|| record.type === 'header1'
|
||||
|| record.type === 'header2'
|
||||
|| record.type === 'header3'
|
||||
|| record.type === 'header4'
|
||||
|| record.type === 'block_code'
|
||||
|| record.type === 'click_link' )"
|
||||
class="host-host ion-padding"
|
||||
contenteditable="true"
|
||||
(keyup)="onKeyUp($event)"
|
||||
(blur)="record.value=hostContainer.innerHTML"
|
||||
(dblclick)="onHostDblClick()"
|
||||
#hostContainer
|
||||
[ngClass]="{'paragraph': record.type === 'paragraph', 'header1': record.type === 'header1', 'header2': record.type === 'header2', 'header3': record.type === 'header3', 'header4': record.type === 'header4', 'block_code': record.type === 'block_code', 'click_link': record.type === 'click_link'}"
|
||||
[innerHTML]="record.value.replace('\n', '<br>')"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="page.isViewOnly() && ( record.type === 'paragraph'
|
||||
|| record.type === 'header1'
|
||||
|| record.type === 'header2'
|
||||
|| record.type === 'header3'
|
||||
|| record.type === 'header4'
|
||||
|| record.type === 'block_code'
|
||||
|| record.type === 'click_link' )"
|
||||
(click)="onHostDblClick()"
|
||||
class="host-host ion-padding"
|
||||
#hostContainer
|
||||
[ngClass]="{'paragraph': record.type === 'paragraph', 'header1': record.type === 'header1', 'header2': record.type === 'header2', 'header3': record.type === 'header3', 'header4': record.type === 'header4', 'block_code': record.type === 'block_code', 'click_link': record.type === 'click_link'}"
|
||||
[innerHTML]="record.value.replace('\n', '<br>')"
|
||||
></div>
|
||||
<ul
|
||||
*ngIf="record.type === 'ul' && !page.isViewOnly()"
|
||||
class="host-host ion-padding"
|
||||
>
|
||||
<li
|
||||
#liItems
|
||||
contenteditable="true"
|
||||
(keyup)="onUlKeyUp($event, i)"
|
||||
(keydown)="onUlKeyDown($event, i)"
|
||||
*ngFor="let line of listLines; let i = index"
|
||||
[innerHTML]="listLines[i]"
|
||||
></li>
|
||||
</ul>
|
||||
<ul
|
||||
*ngIf="record.type === 'ul' && page.isViewOnly()"
|
||||
class="host-host ion-padding"
|
||||
>
|
||||
<li
|
||||
#liItems
|
||||
*ngFor="let line of listLines; let i = index"
|
||||
[innerHTML]="listLines[i]"
|
||||
></li>
|
||||
</ul>
|
||||
<div *ngIf="record.type === 'page_sep'" class="hr-wrapper">
|
||||
<hr>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="record.type === 'database_ref'"
|
||||
class="db-wrapper"
|
||||
>
|
||||
<editor-database
|
||||
[readonly]="page.isViewOnly()"
|
||||
[hostRecord]="record"
|
||||
(hostRecordChange)="onRecordChange($event)"
|
||||
(requestParentSave)="onRequestParentSave($event)"
|
||||
(requestParentDelete)="onRequestDelete($event)"
|
||||
></editor-database>
|
||||
</div>
|
||||
<div class="code-wrapper" *ngIf="record.type === 'code_ref'">
|
||||
<editor-code
|
||||
[readonly]="page.isViewOnly()"
|
||||
[hostRecord]="record"
|
||||
(hostRecordChange)="onRecordChange($event)"
|
||||
(requestParentSave)="onRequestParentSave($event)"
|
||||
(requestParentDelete)="onRequestDelete($event)"
|
||||
></editor-code>
|
||||
</div>
|
||||
<div class="files-wrapper" *ngIf="record.type === 'file_ref'">
|
||||
<editor-files
|
||||
[readonly]="page.isViewOnly()"
|
||||
[hostRecord]="record"
|
||||
(hostRecordChange)="onRecordChange($event)"
|
||||
(requestParentSave)="onRequestParentSave($event)"
|
||||
(requestParentDelete)="onRequestDelete($event)"
|
||||
></editor-files>
|
||||
</div>
|
||||
</ng-container>
|
@ -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;
|
||||
}
|
@ -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<HostRecord>();
|
||||
@Output() newHostRequested = new EventEmitter<HostComponent>();
|
||||
@Output() destroyHostRequested = new EventEmitter<HostComponent>();
|
||||
@Output() saveHostRequested = new EventEmitter<HostComponent>();
|
||||
@ViewChild('hostContainer') hostContainer: ElementRef;
|
||||
@ViewChildren('liItems') liItems;
|
||||
|
||||
public listLines: Array<string> = [];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,50 +1,18 @@
|
||||
<ion-list>
|
||||
<ion-item button (click)="onSelect('paragraph')">
|
||||
<ion-icon slot="start" name="menu"></ion-icon>
|
||||
<ion-item button (click)="onSelect('paragraph')" class="node">
|
||||
<i class="fa" slot="start" [ngClass]="typeIcons.node"></i>
|
||||
<ion-label>Paragraph</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('header1')">
|
||||
<ion-icon slot="start" name="alert"></ion-icon>
|
||||
<ion-label>Heading 1</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('header2')">
|
||||
<ion-icon slot="start" name="alert"></ion-icon>
|
||||
<ion-label>Heading 2</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('header3')">
|
||||
<ion-icon slot="start" name="alert"></ion-icon>
|
||||
<ion-label>Heading 3</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('header4')">
|
||||
<ion-icon slot="start" name="alert"></ion-icon>
|
||||
<ion-label>Heading 4</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('ul')">
|
||||
<ion-icon slot="start" name="list"></ion-icon>
|
||||
<ion-label>Unordered List</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('block_code')">
|
||||
<ion-icon slot="start" name="information"></ion-icon>
|
||||
<ion-label>Monospace Block</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('click_link')">
|
||||
<ion-icon slot="start" name="link"></ion-icon>
|
||||
<ion-label>Hyperlink</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('database_ref')">
|
||||
<ion-icon slot="start" name="analytics"></ion-icon>
|
||||
<ion-item button (click)="onSelect('database_ref')" class="db">
|
||||
<i class="fa" slot="start" [ngClass]="typeIcons.db"></i>
|
||||
<ion-label>Database</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('code_ref')">
|
||||
<ion-icon slot="start" name="code"></ion-icon>
|
||||
<ion-item button (click)="onSelect('code_ref')" class="code">
|
||||
<i class="fa" slot="start" [ngClass]="typeIcons.code"></i>
|
||||
<ion-label>Code Editor</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('file_ref')">
|
||||
<ion-icon slot="start" name="document"></ion-icon>
|
||||
<ion-item button (click)="onSelect('file_ref')" class="files">
|
||||
<i class="fa" slot="start" [ngClass]="typeIcons.files"></i>
|
||||
<ion-label>Upload Files</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('page_sep')">
|
||||
<ion-icon slot="start" name="remove"></ion-icon>
|
||||
<ion-label>Horizontal Row</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
) { }
|
||||
|
45
src/app/components/nodes/EditorNode.contract.ts
Normal file
45
src/app/components/nodes/EditorNode.contract.ts
Normal file
@ -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<boolean>;
|
||||
public abstract writeChangesToNode(): void | Promise<void>;
|
||||
|
||||
public needsSave(): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public needsLoad(): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public performSave(): void | Promise<void> {}
|
||||
|
||||
public performLoad(): void | Promise<void> {}
|
||||
|
||||
public performDelete(): void | Promise<void> {}
|
||||
}
|
78
src/app/components/nodes/norm/norm.component.html
Normal file
78
src/app/components/nodes/norm/norm.component.html
Normal file
@ -0,0 +1,78 @@
|
||||
<div class="container"
|
||||
(focusin)="onFocusIn($event)"
|
||||
(focusout)="onFocusIn($event)">
|
||||
<div class="toolbar-base" *ngIf="isFocused">
|
||||
<button class="toolbar-button" title="Bold" (click)="documentCommand('bold')">
|
||||
<i class="icon fa fa-bold"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Italic" (click)="documentCommand('italic')">
|
||||
<i class="icon fa fa-italic"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Underline" (click)="documentCommand('underline')">
|
||||
<i class="icon fa fa-underline"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Strikethrough" (click)="documentCommand('strikeThrough')">
|
||||
<i class="icon fa fa-strikethrough"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<button class="toolbar-button" title="Align Left" (click)="documentCommand('justifyLeft')">
|
||||
<i class="icon fa fa-align-left"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Align Center" (click)="documentCommand('justifyCenter')">
|
||||
<i class="icon fa fa-align-center"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Align Right" (click)="documentCommand('justifyRight')">
|
||||
<i class="icon fa fa-align-right"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<button class="toolbar-button" title="Undo" (click)="documentCommand('undo')">
|
||||
<i class="icon fa fa-undo"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Redo" (click)="documentCommand('redo')">
|
||||
<i class="icon fa fa-redo"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<button class="toolbar-button" title="Make text bigger" (click)="documentCommand('increaseFontSize')">
|
||||
<i class="icon fa fa-font"></i>
|
||||
<i class="icon fa fa-long-arrow-alt-up"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Make text smaller" (click)="documentCommand('decreaseFontSize')">
|
||||
<i class="icon fa fa-font"></i>
|
||||
<i class="icon fa fa-long-arrow-alt-down"></i>
|
||||
</button>
|
||||
|
||||
<button class="toolbar-button" title="Make text superscript" (click)="documentCommand('superscript')">
|
||||
<i class="icon fa fa-superscript"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Make text subscript" (click)="documentCommand('subscript')">
|
||||
<i class="icon fa fa-subscript"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<button class="toolbar-button" title="Insert unordered list" (click)="documentCommand('insertUnorderedList')">
|
||||
<i class="icon fa fa-list-ul"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Insert ordered list" (click)="documentCommand('insertOrderedList')">
|
||||
<i class="icon fa fa-list-ol"></i>
|
||||
</button>
|
||||
<button class="toolbar-button" title="Insert horizontal rule" (click)="documentCommand('insertHorizontalRule')">
|
||||
―
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="editable-base"
|
||||
[ngClass]="isFocused ? 'focused' : ''"
|
||||
contenteditable
|
||||
appDomChange
|
||||
[innerHTML]="initialValue"
|
||||
#editable
|
||||
(domChange)="onContentsChanged($event)"
|
||||
></div>
|
||||
</div>
|
36
src/app/components/nodes/norm/norm.component.scss
Normal file
36
src/app/components/nodes/norm/norm.component.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
110
src/app/components/nodes/norm/norm.component.ts
Normal file
110
src/app/components/nodes/norm/norm.component.ts
Normal file
@ -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<boolean> {
|
||||
return this.dirtyOverride || this.contents !== this.initialValue;
|
||||
}
|
||||
|
||||
public writeChangesToNode(): void | Promise<void> {
|
||||
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');
|
||||
}
|
||||
}
|
@ -36,6 +36,12 @@
|
||||
color: var(--noded-background-code);
|
||||
}
|
||||
}
|
||||
|
||||
&.files {
|
||||
.search-icon {
|
||||
color: var(--noded-background-files);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-assoc {
|
||||
|
@ -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<SearchResult[]> = new BehaviorSubject<SearchResult[]>([]);
|
||||
|
||||
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,
|
||||
|
9
src/app/directives/directives.module.ts
Normal file
9
src/app/directives/directives.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {DomChangeDirective} from './dom-change.directive';
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
exports: [DomChangeDirective],
|
||||
declarations: [DomChangeDirective],
|
||||
})
|
||||
export class DirectivesModule {}
|
30
src/app/directives/dom-change.directive.ts
Normal file
30
src/app/directives/dom-change.directive.ts
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -1,46 +1,55 @@
|
||||
<ng-container>
|
||||
<ion-header (keydown)="onEditorKeydown($event)">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
<ion-title #titleBar>
|
||||
<div contenteditable="true"> {{ pageRecord.Name }} </div>
|
||||
<ion-input
|
||||
[(ngModel)]="editorService.mutablePageName"
|
||||
placeholder="Click to edit page name..."
|
||||
class="title-input"
|
||||
></ion-input>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<button class="save-button" (click)="editorService.triggerSave()" title="Manually save this note">
|
||||
<i *ngIf="!(editorService.isSaving || editorService.willSave)" class="fa fa-check-circle"></i>
|
||||
{{ (editorService.isSaving || editorService.willSave) ? 'Saving...' : 'Saved!' }}
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content (keydown)="onEditorKeydown($event)">
|
||||
<ion-content>
|
||||
<ng-container>
|
||||
<div class="editor-root ion-padding">
|
||||
<div
|
||||
*ngFor="let record of hostRecords; let i = index"
|
||||
class="host-container" style="display: flex;"
|
||||
(mouseenter)="makeVisible(i)"
|
||||
(mouseleave)="makeInvisible(i)"
|
||||
class="host-container"
|
||||
style="display: flex; margin-bottom: 20px;"
|
||||
*ngFor="let node of editorService.immutableNodes"
|
||||
>
|
||||
<ion-button fill="invisible" color="primary" (click)="onOptionsClick($event, i)" *ngIf="pageRecord.level !== 'view'">
|
||||
<ion-icon
|
||||
name="options"
|
||||
color="medium"
|
||||
[ngClass]="{'invisible': !buttonIsVisible(i)}"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<editor-host
|
||||
style="width: 100%;"
|
||||
#editorHosts
|
||||
[page]="pageRecord"
|
||||
[record]="hostRecords[i]"
|
||||
(recordChange)="onHostRecordChange($event, i)"
|
||||
(newHostRequested)="onNewHostRequested($event)"
|
||||
(destroyHostRequested)="onDestroyHostRequested($event)"
|
||||
(saveHostRequested)="onSaveClick()">
|
||||
</editor-host>
|
||||
<div class="host-icons">
|
||||
<i class="type-icon fa" [ngClass]="typeIcons[(node.isNorm() ? 'node' : node.type)] + ' ' + (node.isNorm() ? 'node' : node.type)"></i>
|
||||
<button (click)="onOptionsClick($event, node)">
|
||||
<i class="fa fa-ellipsis-v" title="Node options"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="node.isNorm()">
|
||||
<editor-norm style="flex: 1;" [nodeId]="node.UUID"></editor-norm>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="node.type === 'database_ref'">
|
||||
<editor-database style="flex: 1;" [nodeId]="node.UUID"></editor-database>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="node.type === 'code_ref'">
|
||||
<editor-code style="flex: 1;" [nodeId]="node.UUID"></editor-code>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="node.type === 'file_ref'">
|
||||
<editor-files style="flex: 1;" [nodeId]="node.UUID"></editor-files>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="editor-buttons" style="margin-bottom: 50px;" *ngIf="pageRecord.level !== 'view'">
|
||||
<ion-button (click)="onAddClick($event)" class="ion-padding ion-margin-start" fill="outline" color="medium">Add Node</ion-button>
|
||||
<ion-button (click)="onSaveClick()" class="ion-padding" fill="outline" color="medium">Save</ion-button>
|
||||
<button class="host-add-button" (click)="onAddClick($event)">
|
||||
<i class="fa fa-plus"></i> Add Node
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<HostRecord> = [new HostRecord('Click to edit page...')];
|
||||
public pageRecord: PageRecord = new PageRecord();
|
||||
public pageId: string;
|
||||
public visibleButtons: Array<number> = [];
|
||||
// @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<HostRecord>) => {
|
||||
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<HostRecord>) => {
|
||||
// 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();
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
333
src/app/service/editor.service.ts
Normal file
333
src/app/service/editor.service.ts
Normal file
@ -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<boolean> = new BehaviorSubject<boolean>(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<HostRecord[]> {
|
||||
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<HostRecord> {
|
||||
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<PageRecord> {
|
||||
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<HostRecord[]> {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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 => {
|
||||
|
12
src/app/structures/node-types.ts
Normal file
12
src/app/structures/node-types.ts
Normal file
@ -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',
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user