You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
frontend/src/app/components/editor/host/host.component.ts

234 lines
7.7 KiB

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);
}
}