| /* |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import { Component, DoCheck, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
| import { DatasetType, Note } from '@zeppelin/sdk'; |
| |
| interface TocResult { |
| paragraphId: string; |
| resultData: string; |
| resultType: DatasetType; |
| } |
| |
| interface TocRow { |
| paragraphId: string; |
| level: number; |
| title: string; |
| } |
| |
| @Component({ |
| selector: 'zeppelin-note-toc', |
| templateUrl: './note-toc.component.html', |
| styleUrls: ['./note-toc.component.less'] |
| }) |
| export class NoteTocComponent implements OnInit, DoCheck { |
| @Input() note!: Exclude<Note['note'], undefined>; |
| @Output() readonly scrollToParagraph = new EventEmitter<string>(); |
| Arr = Array; |
| rows: TocRow[] = []; |
| oldNote!: Exclude<Note['note'], undefined>; |
| |
| onRowClick(id: string) { |
| this.scrollToParagraph.emit(id); |
| } |
| |
| getResults(note: Exclude<Note['note'], undefined>): TocResult[] { |
| const results = note.paragraphs.reduce((allResults: TocResult[], paragraph) => { |
| const newResults: TocResult[] = []; |
| if (paragraph.results && paragraph.results.msg) { |
| paragraph.results.msg.forEach(result => |
| newResults.push({ |
| paragraphId: paragraph.id, |
| resultData: result.data, |
| resultType: result.type |
| }) |
| ); |
| } |
| return [...allResults, ...newResults]; |
| }, []); |
| return results.filter(result => result.resultType === DatasetType.HTML); |
| } |
| |
| unpackNodes(element: Element) { |
| element.querySelectorAll('*').forEach(subElements => (subElements.outerHTML = subElements.innerHTML)); |
| return element.innerHTML; |
| } |
| |
| computeRows() { |
| const htmlResults = this.getResults(this.note); |
| const rows: TocRow[] = htmlResults.reduce((allRows: TocRow[], result) => { |
| const parser = new DOMParser(); |
| const resultDOM = parser.parseFromString(result.resultData, 'text/html'); |
| const headings = Array.from(resultDOM.querySelectorAll('h1, h2, h3, h4, h5, h6')); |
| const newRows = headings.map(heading => ({ |
| level: parseInt(heading.nodeName[1], 10), |
| title: this.unpackNodes(heading), |
| paragraphId: result.paragraphId |
| })); |
| return [...allRows, ...newRows]; |
| }, []); |
| const levelsSet: Set<number> = new Set(); |
| rows.forEach(row => levelsSet.add(row.level)); |
| const levels = Array.from(levelsSet).sort(); |
| const headingLevelToTocLevelMap: Record<number, number> = {}; |
| levels.forEach((level, index) => (headingLevelToTocLevelMap[level] = index + 1)); |
| this.rows = rows.map(heading => ({ ...heading, level: headingLevelToTocLevelMap[heading.level] })); |
| } |
| |
| shouldRecomputeRows() { |
| if (this.note.id !== this.oldNote.id) { |
| return true; |
| } |
| const oldResult = this.getResults(this.oldNote); |
| return this.getResults(this.note).reduce( |
| (hasParagraphUpdated, result, resultIndex) => |
| hasParagraphUpdated || !oldResult[resultIndex] || result.resultData !== oldResult[resultIndex].resultData, |
| false |
| ); |
| } |
| |
| ngOnInit() { |
| this.computeRows(); |
| this.oldNote = this.note; |
| } |
| |
| ngDoCheck() { |
| if (this.shouldRecomputeRows()) { |
| this.computeRows(); |
| this.oldNote = this.note; |
| } |
| } |
| } |