editor-refactor #18
@ -1,8 +1,9 @@
|
|||||||
import PageRecord from '../../structures/PageRecord';
|
import PageRecord from '../../structures/PageRecord';
|
||||||
|
import HostRecord from '../../structures/HostRecord';
|
||||||
|
|
||||||
export abstract class EditorNodeContract {
|
export abstract class EditorNodeContract {
|
||||||
protected pageRec!: PageRecord;
|
protected pageRec!: PageRecord;
|
||||||
protected nodeRec!: any; // TODO
|
protected nodeRec!: HostRecord;
|
||||||
protected initialValue: any;
|
protected initialValue: any;
|
||||||
|
|
||||||
get page() {
|
get page() {
|
||||||
@ -13,11 +14,20 @@ export abstract class EditorNodeContract {
|
|||||||
this.pageRec = page;
|
this.pageRec = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get node() {
|
||||||
|
return this.nodeRec;
|
||||||
|
}
|
||||||
|
|
||||||
|
set node(node: HostRecord) {
|
||||||
|
this.nodeRec = node;
|
||||||
|
}
|
||||||
|
|
||||||
get identifier() {
|
get identifier() {
|
||||||
return this.nodeRec.UUID;
|
return this.nodeRec.UUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract isDirty(): boolean | Promise<boolean>;
|
public abstract isDirty(): boolean | Promise<boolean>;
|
||||||
|
public abstract writeChangesToNode(): void | Promise<void>;
|
||||||
|
|
||||||
public needsSave(): boolean | Promise<boolean> {
|
public needsSave(): boolean | Promise<boolean> {
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
.editable-base {
|
.editable-base {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: aliceblue; // TODO temporary
|
|
||||||
|
&.focused {
|
||||||
|
background: aliceblue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-base {
|
.toolbar-base {
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
import {Component, HostListener, ViewChild} from '@angular/core';
|
import {Component, HostListener, Input, OnInit, ViewChild} from '@angular/core';
|
||||||
import {EditorNodeContract} from '../EditorNode.contract';
|
import {EditorNodeContract} from '../EditorNode.contract';
|
||||||
|
import {EditorService} from '../../../service/editor.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'editor-norm',
|
selector: 'editor-norm',
|
||||||
templateUrl: './norm.component.html',
|
templateUrl: './norm.component.html',
|
||||||
styleUrls: ['./norm.component.scss'],
|
styleUrls: ['./norm.component.scss'],
|
||||||
})
|
})
|
||||||
export class NormComponent extends EditorNodeContract {
|
export class NormComponent extends EditorNodeContract implements OnInit {
|
||||||
@ViewChild('editable') editable;
|
@ViewChild('editable') editable;
|
||||||
|
@Input() nodeId: string;
|
||||||
|
|
||||||
public isFocused = false;
|
public isFocused = false;
|
||||||
public initialValue = 'Content editable now...';
|
public initialValue = 'Click to edit...';
|
||||||
public contents = '';
|
public contents = '';
|
||||||
private dirtyOverride = false;
|
private dirtyOverride = false;
|
||||||
|
|
||||||
constructor() {
|
constructor(
|
||||||
|
public readonly editorService: EditorService,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
console.log('norm compt', this);
|
|
||||||
this.contents = this.initialValue;
|
this.contents = this.initialValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +27,18 @@ export class NormComponent extends EditorNodeContract {
|
|||||||
return this.dirtyOverride || this.contents !== this.initialValue;
|
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(() => {
|
||||||
|
this.initialValue = this.node.Value.Value;
|
||||||
|
this.contents = this.initialValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onFocusIn(event: MouseEvent) {
|
onFocusIn(event: MouseEvent) {
|
||||||
this.isFocused = true;
|
this.isFocused = true;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title #titleBar>
|
<ion-title #titleBar>
|
||||||
<ion-input
|
<ion-input
|
||||||
[(ngModel)]="pageName"
|
[(ngModel)]="editorService.mutablePageName"
|
||||||
placeholder="Click to edit page name..."
|
placeholder="Click to edit page name..."
|
||||||
class="title-input"
|
class="title-input"
|
||||||
></ion-input>
|
></ion-input>
|
||||||
@ -17,10 +17,21 @@
|
|||||||
<ion-content>
|
<ion-content>
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<div class="editor-root ion-padding">
|
<div class="editor-root ion-padding">
|
||||||
<div class="host-container" style="display: flex;">
|
<div
|
||||||
<editor-norm style="flex: 1;"></editor-norm>
|
class="host-container"
|
||||||
|
style="display: flex;"
|
||||||
|
*ngFor="let node of editorService.immutableNodes"
|
||||||
|
>
|
||||||
|
<ng-container *ngIf="node.isNorm()">
|
||||||
|
<editor-norm style="flex: 1;" [nodeId]="node.UUID"></editor-norm>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div class="editor-root ion-padding">-->
|
||||||
|
<!-- <div class="host-container" style="display: flex;">-->
|
||||||
|
<!-- <editor-norm style="flex: 1;"></editor-norm>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Component, Host, Input, OnInit, ViewChild, ViewChildren} from '@angular/core';
|
import {Component, Host, HostListener, Input, OnInit, ViewChild, ViewChildren} from '@angular/core';
|
||||||
import HostRecord from '../../structures/HostRecord';
|
import HostRecord from '../../structures/HostRecord';
|
||||||
import PageRecord from '../../structures/PageRecord';
|
import PageRecord from '../../structures/PageRecord';
|
||||||
import {PageService} from '../../service/page.service';
|
import {PageService} from '../../service/page.service';
|
||||||
@ -6,6 +6,7 @@ import {ActivatedRoute, Router} from '@angular/router';
|
|||||||
import {LoadingController, PopoverController} from '@ionic/angular';
|
import {LoadingController, PopoverController} from '@ionic/angular';
|
||||||
import {NodePickerComponent} from '../../components/editor/node-picker/node-picker.component';
|
import {NodePickerComponent} from '../../components/editor/node-picker/node-picker.component';
|
||||||
import {HostOptionsComponent} from '../../components/editor/host-options/host-options.component';
|
import {HostOptionsComponent} from '../../components/editor/host-options/host-options.component';
|
||||||
|
import {EditorService} from '../../service/editor.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-editor',
|
selector: 'app-editor',
|
||||||
@ -23,6 +24,7 @@ export class EditorPage implements OnInit {
|
|||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected loader: LoadingController,
|
protected loader: LoadingController,
|
||||||
|
public readonly editorService: EditorService,
|
||||||
) {
|
) {
|
||||||
this.route.params.subscribe(params => {
|
this.route.params.subscribe(params => {
|
||||||
this.pageId = params.id;
|
this.pageId = params.id;
|
||||||
@ -31,6 +33,20 @@ export class EditorPage implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {}
|
ngOnInit() {}
|
||||||
|
|
||||||
|
ionViewDidEnter() {
|
||||||
|
if ( this.pageId ) {
|
||||||
|
this.editorService.startEditing(this.pageId);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keydown.control.s', ['$event'])
|
||||||
|
onManualSave(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.editorService.save();
|
||||||
|
}
|
||||||
|
|
||||||
// buttonIsVisible(index) {
|
// buttonIsVisible(index) {
|
||||||
// return this.visibleButtons.includes(index);
|
// return this.visibleButtons.includes(index);
|
||||||
// }
|
// }
|
||||||
|
197
src/app/service/editor.service.ts
Normal file
197
src/app/service/editor.service.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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[] = [];
|
||||||
|
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
console.log('editor service', this);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
console.log('editing', this.currentPage);
|
||||||
|
console.log('nodes', this.currentNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveNodesAsPage(page: PageRecord, nodes: HostRecord[]) {
|
||||||
|
await 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(); // TODO load in returned data!!
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -7,10 +7,16 @@ export default class HostRecord {
|
|||||||
|
|
||||||
public CreatedAt: string;
|
public CreatedAt: string;
|
||||||
public PageId: string;
|
public PageId: string;
|
||||||
public UUID: string;
|
private privUUID: string;
|
||||||
public UpdatedAt: string;
|
public UpdatedAt: string;
|
||||||
public Value: any;
|
public Value: any;
|
||||||
|
|
||||||
|
public get UUID(): string {
|
||||||
|
return this.privUUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set UUID(val: string) {}
|
||||||
|
|
||||||
constructor(value = '') {
|
constructor(value = '') {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
@ -21,11 +27,11 @@ export default class HostRecord {
|
|||||||
|
|
||||||
load(data: any) {
|
load(data: any) {
|
||||||
this.type = data.Type;
|
this.type = data.Type;
|
||||||
|
this.privUUID = data.UUID;
|
||||||
|
|
||||||
[
|
[
|
||||||
'CreatedAt',
|
'CreatedAt',
|
||||||
'PageId',
|
'PageId',
|
||||||
'UUID',
|
|
||||||
'UpdatedAt',
|
'UpdatedAt',
|
||||||
'Value',
|
'Value',
|
||||||
].forEach(field => {
|
].forEach(field => {
|
||||||
|
Loading…
Reference in New Issue
Block a user