Add suppport for UL; fix file uploader redirects

This commit is contained in:
garrettmills 2020-02-10 23:55:59 -06:00
parent 665fdc91a8
commit 6a7618f971
11 changed files with 257 additions and 82 deletions

View File

@ -13,6 +13,22 @@ import { ComponentsModule } from './components/components.module';
import { TreeModule } from 'angular-tree-component'; import { TreeModule } from 'angular-tree-component';
import {AgGridModule} from 'ag-grid-angular'; import {AgGridModule} from 'ag-grid-angular';
import {MonacoEditorModule} from 'ngx-monaco-editor'; import {MonacoEditorModule} from 'ngx-monaco-editor';
import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
/**
* This function is used internal to get a string instance of the `<base href="" />` value from `index.html`.
* This is an exported function, instead of a private function or inline lambda, to prevent this error:
*
* `Error encountered resolving symbol values statically.`
* `Function calls are not supported.`
* `Consider replacing the function or lambda with a reference to an exported function.`
*
* @param platformLocation an Angular service used to interact with a browser's URL
* @return a string instance of the `<base href="" />` value from `index.html`
*/
export function getBaseHref(platformLocation: PlatformLocation): string {
return platformLocation.getBaseHrefFromDOM();
}
@NgModule({ @NgModule({
declarations: [AppComponent], declarations: [AppComponent],
@ -30,7 +46,12 @@ import {MonacoEditorModule} from 'ngx-monaco-editor';
providers: [ providers: [
StatusBar, StatusBar,
SplashScreen, SplashScreen,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy } { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: APP_BASE_HREF,
useFactory: getBaseHref,
deps: [PlatformLocation]
}
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -2,6 +2,7 @@
<div class="new-uploads" style="margin: 20px;"> <div class="new-uploads" style="margin: 20px;">
<form #uploadForm [action]="getApiSubmit()" enctype="multipart/form-data" method="post"> <form #uploadForm [action]="getApiSubmit()" enctype="multipart/form-data" method="post">
<input style="margin-top: 10px;" type="file" id="file" name="uploaded_file"> <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)="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> <ion-button (click)="onDestroyClick()" type="submit" fill="outline" class="ion-margin-start" color="danger">Drop Files</ion-button>
</form> </form>

View File

@ -1,4 +1,5 @@
div.files-wrapper { div.files-wrapper {
border: 2px solid #8c8c8c; border: 2px solid #8c8c8c;
border-radius: 3px; border-radius: 3px;
margin-top: 15px;
} }

View File

@ -1,8 +1,9 @@
import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; import {Component, ElementRef, EventEmitter, Inject, Input, OnInit, Output, ViewChild} from '@angular/core';
import HostRecord from '../../../structures/HostRecord'; import HostRecord from '../../../structures/HostRecord';
import {ApiService} from '../../../service/api.service'; import {ApiService} from '../../../service/api.service';
import {AlertController} from '@ionic/angular'; import {AlertController} from '@ionic/angular';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import { APP_BASE_HREF } from '@angular/common';
@Component({ @Component({
selector: 'editor-files', selector: 'editor-files',
@ -23,6 +24,7 @@ export class FilesComponent implements OnInit {
constructor( constructor(
protected api: ApiService, protected api: ApiService,
protected alerts: AlertController, protected alerts: AlertController,
@Inject(APP_BASE_HREF) private baseHref: string
) { } ) { }
ngOnInit() { ngOnInit() {
@ -39,6 +41,10 @@ export class FilesComponent implements OnInit {
this.uploadForm.nativeElement.submit(); this.uploadForm.nativeElement.submit();
} }
getReturnUrl() {
return `${this.baseHref}editor;id=${this.hostRecord.PageId}`;
}
downloadFile(fileRecord) { downloadFile(fileRecord) {
// tslint:disable-next-line:max-line-length // 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.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}/${fileRecord._id}`), '_blank');

View File

@ -15,14 +15,17 @@
class="host-host ion-padding" class="host-host ion-padding"
> >
<li <li
contenteditable="true"
*ngFor="let line of listLines; let i = index"
#liItems #liItems
(keyup)="onLIKeyUp($event, i)" contenteditable="true"
(blur)="listLines[i]=liItems.innerHTML" (keyup)="onUlKeyUp($event, i)"
(keydown)="onUlKeyDown($event, i)"
*ngFor="let line of listLines; let i = index"
[innerHTML]="listLines[i]" [innerHTML]="listLines[i]"
></li> ></li>
</ul> </ul>
<div *ngIf="record.type === 'page_sep'" class="hr-wrapper">
<hr>
</div>
<div <div
*ngIf="record.type === 'database_ref'" *ngIf="record.type === 'database_ref'"
class="db-wrapper" class="db-wrapper"

View File

@ -33,3 +33,32 @@
text-decoration: underline; 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;
}

View File

@ -1,4 +1,4 @@
import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild, ViewChildren} from '@angular/core'; import {Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild, ViewChildren} from '@angular/core';
import HostRecord from '../../../structures/HostRecord'; import HostRecord from '../../../structures/HostRecord';
@Component({ @Component({
@ -19,7 +19,18 @@ export class HostComponent implements OnInit {
constructor() { } constructor() { }
ngOnInit() {} 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) { onRecordChange($event) {
this.recordChange.emit($event); this.recordChange.emit($event);
@ -27,13 +38,13 @@ export class HostComponent implements OnInit {
onKeyUp($event) { onKeyUp($event) {
const innerText = this.hostContainer.nativeElement.innerText.trim() const innerText = this.hostContainer.nativeElement.innerText.trim()
if ( $event.code === 'Enter' if ( $event.code === 'Enter' && this.record.isNorm() && !$event.shiftKey
&& ( this.record.type !== 'block_code' && ( this.record.type !== 'block_code'
|| (innerText.endsWith('```') && (innerText.match(/`/g) || []).length >= 6) || (innerText.endsWith('```') && (innerText.match(/`/g) || []).length >= 6) // TODO don't add new if cursor in block
) )
) { ) {
this.newHostRequested.emit(this);
this.hostContainer.nativeElement.innerText = this.hostContainer.nativeElement.innerText.trim(); this.hostContainer.nativeElement.innerText = this.hostContainer.nativeElement.innerText.trim();
this.newHostRequested.emit(this);
} else if ( $event.code === 'Backspace' && !this.hostContainer.nativeElement.innerText.trim() ) { } else if ( $event.code === 'Backspace' && !this.hostContainer.nativeElement.innerText.trim() ) {
this.destroyHostRequested.emit(this); this.destroyHostRequested.emit(this);
} }
@ -50,60 +61,119 @@ export class HostComponent implements OnInit {
this.record.type = 'block_code'; this.record.type = 'block_code';
} else if ( innerText.startsWith('http') ) { } else if ( innerText.startsWith('http') ) {
this.record.type = 'click_link'; this.record.type = 'click_link';
} else if ( false && innerText.startsWith('-') || innerText.startsWith(' -') ) { } else if ( innerText === '---' ) {
// this.record.type = 'ul'; this.record.type = 'page_sep';
} else if ( innerText.startsWith('-') || innerText.startsWith(' -') ) {
this.record.type = 'ul';
this.listLines = [this.record.value]; this.listLines = [this.record.value];
setTimeout(() => { setTimeout(() => {
const item = this.liItems.toArray()[0].nativeElement; this.focusStart(this.liItems.toArray()[0].nativeElement);
const s = window.getSelection();
const r = document.createRange();
r.setStart(item, 0);
r.setEnd(item, 0);
s.removeAllRanges();
s.addRange(r);
}, 0); }, 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.innerText ? elem.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) { onRequestDelete($event) {
this.destroyHostRequested.emit(this); this.destroyHostRequested.emit(this);
} }
onLIKeyUp($event, i) {
if ( $event.code === 'Enter' ) {
/*const newListLines = [];
this.liItems.forEach((li, index) => {
newListLines.push(li.nativeElement.innerText.trim());
if ( index === i ) {
newListLines.push('');
}
});
this.listLines = newListLines;*/
// this.listLines[i] = this.liItems[i].innerText.trim()
// const newLines = []
// this.listLines.forEach((rec, x) => {
// newLines.push(rec.trim());
// if ( i === x ) {
// newLines.push('');
// }
// })
// this.listLines = newLines;
// setTimeout(() => {
// const item = this.liItems.toArray()[i + 1].nativeElement;
// const s = window.getSelection();
// const r = document.createRange();
// r.setStart(item, 0);
// r.setEnd(item, 0);
// s.removeAllRanges();
// s.addRange(r);
// }, 10);
}
}
onRequestParentSave($event) { onRequestParentSave($event) {
this.saveHostRequested.emit(this); this.saveHostRequested.emit(this);
} }
@ -114,4 +184,48 @@ export class HostComponent implements OnInit {
} }
} }
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);
}
} }

View File

@ -19,6 +19,10 @@
<ion-icon slot="start" name="alert"></ion-icon> <ion-icon slot="start" name="alert"></ion-icon>
<ion-label>Heading 4</ion-label> <ion-label>Heading 4</ion-label>
</ion-item> </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-item button (click)="onSelect('block_code')">
<ion-icon slot="start" name="information"></ion-icon> <ion-icon slot="start" name="information"></ion-icon>
<ion-label>Monospace Block</ion-label> <ion-label>Monospace Block</ion-label>
@ -39,4 +43,8 @@
<ion-icon slot="start" name="document"></ion-icon> <ion-icon slot="start" name="document"></ion-icon>
<ion-label>Upload Files</ion-label> <ion-label>Upload Files</ion-label>
</ion-item> </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> </ion-list>

View File

@ -16,7 +16,7 @@
<ion-content> <ion-content>
<ng-container> <ng-container>
<div class="editor-root ion-padding"> <div class="editor-root ion-padding" style="padding: 30px 80px;">
<div class="host-container ion-padding"> <div class="host-container ion-padding">
<editor-host #editorHosts *ngFor="let record of hostRecords; let i = index" [record]="hostRecords[i]" (recordChange)="onHostRecordChange($event, i)" <editor-host #editorHosts *ngFor="let record of hostRecords; let i = index" [record]="hostRecords[i]" (recordChange)="onHostRecordChange($event, i)"
(newHostRequested)="onNewHostRequested($event)" (destroyHostRequested)="onDestroyHostRequested($event)" (newHostRequested)="onNewHostRequested($event)" (destroyHostRequested)="onDestroyHostRequested($event)"
@ -24,7 +24,7 @@
</editor-host> </editor-host>
</div> </div>
</div> </div>
<div class="editor-buttons"> <div class="editor-buttons" style="margin-bottom: 50px;">
<ion-button (click)="onAddClick($event)" class="ion-padding ion-margin-start" fill="outline" color="medium">Add Node</ion-button> <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> <ion-button (click)="onSaveClick()" class="ion-padding" fill="outline" color="medium">Save</ion-button>
</div> </div>

View File

@ -62,16 +62,15 @@ export class EditorPage implements OnInit {
const hostRec = new HostRecord(defValue); const hostRec = new HostRecord(defValue);
hostRec.type = arg.data; hostRec.type = arg.data;
hostRec.PageId = this.pageRecord.UUID; hostRec.PageId = this.pageRecord.UUID;
if ( hostRec.type === 'ul' ) {
hostRec.value = JSON.stringify([{value: '', indentationLevel: 0}]);
}
this.hostRecords.push(hostRec); this.hostRecords.push(hostRec);
if ( hostRec.isNorm() ) { if ( hostRec.isNorm() ) {
setTimeout(() => { setTimeout(() => {
const host = this.editorHosts.toArray().reverse()[0].hostContainer.nativeElement; this.editorHosts.toArray().reverse()[0].takeFocus();
const s = window.getSelection();
const r = document.createRange();
r.setStart(host, defValue.length);
r.setEnd(host, defValue.length);
s.removeAllRanges();
s.addRange(r);
}, 0); }, 0);
} else { } else {
this.onSaveClick(); this.onSaveClick();
@ -96,6 +95,8 @@ export class EditorPage implements OnInit {
return '```'; return '```';
} else if ( type === 'click_link' ) { } else if ( type === 'click_link' ) {
return 'https://'; return 'https://';
} else if ( type === 'page_sep' ) {
return '---';
} else { } else {
return ''; return '';
} }
@ -115,15 +116,8 @@ export class EditorPage implements OnInit {
this.hostRecords = newHosts; this.hostRecords = newHosts;
setTimeout(() => { setTimeout(() => {
const host = this.editorHosts.toArray()[insertAfter + 1].hostContainer.nativeElement; this.editorHosts.toArray()[insertAfter + 1].takeFocus();
const s = window.getSelection();
const r = document.createRange();
r.setStart(host, 0);
r.setEnd(host, 0);
s.removeAllRanges();
s.addRange(r);
}, 0); }, 0);
} }
onDestroyHostRequested($event) { onDestroyHostRequested($event) {
@ -148,14 +142,10 @@ export class EditorPage implements OnInit {
focusIndex = removedIndex - 1; focusIndex = removedIndex - 1;
} }
console.log({removedIndex, focusIndex, edHArr: this.editorHosts.toArray()});
if ( focusIndex >= 0 ) { if ( focusIndex >= 0 ) {
const host = this.editorHosts.toArray()[focusIndex].hostContainer.nativeElement; this.editorHosts.toArray()[focusIndex].takeFocus(false);
const s = window.getSelection();
const r = document.createRange();
r.setStart(host, 0);
r.setEnd(host, 0);
s.removeAllRanges();
s.addRange(r);
} }
}, 0); }, 0);
} }
@ -167,7 +157,6 @@ export class EditorPage implements OnInit {
index = i; index = i;
} }
}); });
return index; return index;
} }

View File

@ -1,6 +1,9 @@
export default class HostRecord { export default class HostRecord {
public value = ''; public value = '';
public type: 'paragraph'|'header1'|'header2'|'header3'|'header4'|'block_code'|'click_link'|'database_ref'|'ul'|'code_ref'|'file_ref' = 'paragraph'; public type: 'paragraph'
|'header1'|'header2'|'header3'|'header4'
|'block_code'|'click_link'|'database_ref'
|'ul'|'code_ref'|'file_ref'|'page_sep' = 'paragraph';
public CreatedAt: string; public CreatedAt: string;
public PageId: string; public PageId: string;
@ -13,7 +16,7 @@ export default class HostRecord {
} }
public isNorm() { public isNorm() {
return ['paragraph', 'header1', 'header2', 'header3', 'header4', 'block_code', 'click_link'].includes(this.type); return ['paragraph', 'header1', 'header2', 'header3', 'header4', 'block_code', 'click_link', 'page_sep'].includes(this.type);
} }
load(data: any) { load(data: any) {