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(); @Output() newHostRequested = new EventEmitter(); @Output() destroyHostRequested = new EventEmitter(); @Output() saveHostRequested = new EventEmitter(); @ViewChild('hostContainer') hostContainer: ElementRef; @ViewChildren('liItems') liItems; public listLines: Array = []; 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); } }