diff --git a/src/app/components/nodes/EditorNode.contract.ts b/src/app/components/nodes/EditorNode.contract.ts index b854218..6835d36 100644 --- a/src/app/components/nodes/EditorNode.contract.ts +++ b/src/app/components/nodes/EditorNode.contract.ts @@ -1,8 +1,9 @@ import PageRecord from '../../structures/PageRecord'; +import HostRecord from '../../structures/HostRecord'; export abstract class EditorNodeContract { protected pageRec!: PageRecord; - protected nodeRec!: any; // TODO + protected nodeRec!: HostRecord; protected initialValue: any; get page() { @@ -13,11 +14,20 @@ export abstract class EditorNodeContract { this.pageRec = page; } + get node() { + return this.nodeRec; + } + + set node(node: HostRecord) { + this.nodeRec = node; + } + get identifier() { return this.nodeRec.UUID; } public abstract isDirty(): boolean | Promise; + public abstract writeChangesToNode(): void | Promise; public needsSave(): boolean | Promise { return false; diff --git a/src/app/components/nodes/norm/norm.component.scss b/src/app/components/nodes/norm/norm.component.scss index eea858f..9f6fafb 100644 --- a/src/app/components/nodes/norm/norm.component.scss +++ b/src/app/components/nodes/norm/norm.component.scss @@ -1,6 +1,9 @@ .editable-base { padding: 20px; - background: aliceblue; // TODO temporary + + &.focused { + background: aliceblue; + } } .toolbar-base { diff --git a/src/app/components/nodes/norm/norm.component.ts b/src/app/components/nodes/norm/norm.component.ts index 772f16a..2eafb13 100644 --- a/src/app/components/nodes/norm/norm.component.ts +++ b/src/app/components/nodes/norm/norm.component.ts @@ -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 {EditorService} from '../../../service/editor.service'; @Component({ selector: 'editor-norm', templateUrl: './norm.component.html', styleUrls: ['./norm.component.scss'], }) -export class NormComponent extends EditorNodeContract { +export class NormComponent extends EditorNodeContract implements OnInit { @ViewChild('editable') editable; + @Input() nodeId: string; + public isFocused = false; - public initialValue = 'Content editable now...'; + public initialValue = 'Click to edit...'; public contents = ''; private dirtyOverride = false; - constructor() { + constructor( + public readonly editorService: EditorService, + ) { super(); - console.log('norm compt', this); this.contents = this.initialValue; } @@ -23,6 +27,18 @@ export class NormComponent extends EditorNodeContract { return this.dirtyOverride || this.contents !== this.initialValue; } + public writeChangesToNode(): void | Promise { + this.nodeRec.value = this.contents; + this.initialValue = this.contents; + } + + ngOnInit() { + this.editorService.registerNodeEditor(this.nodeId, this).then(() => { + this.initialValue = this.node.Value.Value; + this.contents = this.initialValue; + }); + } + onFocusIn(event: MouseEvent) { this.isFocused = true; } diff --git a/src/app/pages/editor/editor.page.html b/src/app/pages/editor/editor.page.html index fdea70a..9a27a3b 100644 --- a/src/app/pages/editor/editor.page.html +++ b/src/app/pages/editor/editor.page.html @@ -6,7 +6,7 @@ @@ -17,10 +17,21 @@
-
- +
+ + +
+ + + + + diff --git a/src/app/pages/editor/editor.page.ts b/src/app/pages/editor/editor.page.ts index 25c9d1b..617a12a 100644 --- a/src/app/pages/editor/editor.page.ts +++ b/src/app/pages/editor/editor.page.ts @@ -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 PageRecord from '../../structures/PageRecord'; import {PageService} from '../../service/page.service'; @@ -6,6 +6,7 @@ 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'; @Component({ selector: 'app-editor', @@ -23,6 +24,7 @@ export class EditorPage implements OnInit { protected route: ActivatedRoute, protected router: Router, protected loader: LoadingController, + public readonly editorService: EditorService, ) { this.route.params.subscribe(params => { this.pageId = params.id; @@ -31,6 +33,20 @@ export class EditorPage implements OnInit { 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) { // return this.visibleButtons.includes(index); // } diff --git a/src/app/service/editor.service.ts b/src/app/service/editor.service.ts new file mode 100644 index 0000000..80f3b47 --- /dev/null +++ b/src/app/service/editor.service.ts @@ -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 = new BehaviorSubject(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 { + return new Promise((res, rej) => { + this.api.get(`/page/${pageId}`).subscribe({ + next: result => { + res(new PageRecord(result.data)); + }, + error: rej, + }); + }); + } + + async loadNodes(pageId: string): Promise { + return new Promise((res, rej) => { + this.api.get(`/page/${pageId}/nodes`).subscribe({ + next: result => { + res(result.data.map(rec => { + const host = new HostRecord(rec.Value.Value); + host.load(rec); + return host; + })); + }, + error: rej, + }); + }); + } +} diff --git a/src/app/structures/HostRecord.ts b/src/app/structures/HostRecord.ts index 3fecb07..c6b082c 100644 --- a/src/app/structures/HostRecord.ts +++ b/src/app/structures/HostRecord.ts @@ -7,10 +7,16 @@ export default class HostRecord { 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 +27,11 @@ export default class HostRecord { load(data: any) { this.type = data.Type; + this.privUUID = data.UUID; [ 'CreatedAt', 'PageId', - 'UUID', 'UpdatedAt', 'Value', ].forEach(field => {