import './types/types'; class HtmlViewer { /** * The json content that will be converted to html. */ jsonContent: Array /** * The converted html content. */ html?: string /** * The plain text of the content */ private plainText: string = "" /** * Word per minute */ private wpm: number = 250 /** * The reading time */ private readingTime: ReadingTime = { minutes: 0, wordsCount: 0 } /** * The editorjs viewer options */ private options: EditorJsOptions = { withDefaultStyle: false, codeTheme: "dark" } /** * Initialize instance from HtmlViewer class. * * @param jsonContent */ constructor(jsonContent: Array, options?: EditorJsOptions) { this.jsonContent = jsonContent; if(options && typeof options == 'object') { if(options.withDefaultStyle) { this.options.withDefaultStyle = options.withDefaultStyle; } if(options.codeTheme) { this.options.codeTheme = options.codeTheme; } } this.parser(); } /** * Parser of json list items in editorJs content. * */ parser() { let result: string = "
"; this.jsonContent.map((jsonItem, index) => { result+= `
`; switch(jsonItem.type) { case "paragraph": result+= this.parseParagraph(jsonItem); break; case "text": result+= this.parseParagraph(jsonItem); break; case "header": result+= this.parseHeader(jsonItem); break; case "table": result+= this.parseTable(jsonItem); break; case "image": result+= this.parseImage(jsonItem); break; case "quote": result+= this.parseQuote(jsonItem); break; case "list": result+= this.parseList(jsonItem); break; case "link": result+= this.parseLink(jsonItem); break; case "delimiter": result+= this.parseDelimiter(jsonItem); break; case "checklist": result+= this.parseChecklist(jsonItem); break; case "warning": result+= this.parseWarning(jsonItem); break; case "code": result+= this.parseCode(jsonItem); break; case "embed": result+= this.parseEmbed(jsonItem); break; case "personality": result+= this.parsePersonality(jsonItem); break; case "attaches": result+= this.parseAttaches(jsonItem); break; } result+= '
'; }); result+= '
'; result = result.replace(/(\r\n|\n|\r|\t)/gm, ""); this.html = result; this.calculateReadingTime(); } /** * Render the html content in specific element * * @param selector */ render(selector: string): void { const element: Element|null = document.querySelector(selector); if(element != null) { if(this.html) { element.innerHTML = this.html; HtmlViewer.applyHandlers(); } else { console.error('The html content is empty !'); } } else { console.error('The element is not found !'); } } /** * Parse paragraph item type to html. * * @param jsonItem */ parseParagraph(jsonItem: ParagraphElement): string { const data = jsonItem.data; if(data.text.length == 0) return '
'; this.addPlainText(data.text); return `

${data.text}

`; } /** * Parse header item type to html. * * @param jsonItem */ parseHeader(jsonItem: HeaderElement): string { const data = jsonItem.data; const level: String = (data.level)? data.level : "1"; this.addPlainText(data.text); const headerClass = (this.options.withDefaultStyle)? `class="h${level}"` : ''; return `${data.text}`; } /** * Parse table item type with it's rows and columns to html. * * @param jsonItem */ parseTable(jsonItem: TableElement): string { const data = jsonItem.data; let rows = data.content; let table: string = ''; if(data.withHeadings) { let firstRow = data.content[0]; let heading = ''; firstRow.map((col, index) => { heading+= ``; this.addPlainText(col); }); heading+= ''; table+= heading; rows = rows.slice(1, rows.length); } table+= ''; rows.map((row) => { table+= ''; row.map((col) => { table+= ``; this.addPlainText(col); }); table+= ''; }); table+= ''; table+= '
${col}
${col}
'; return table; } /** * Parse image item type with it's styles and caption to html. * * @param jsonItem */ parseImage(jsonItem: ImageElement): string { const data = jsonItem.data; let imageLayout = `
`+ ''; imageLayout+= `Image`; if(data.caption) { imageLayout+= `

${data.caption}

`; } imageLayout+= "
"; return imageLayout; } /** * Parse image item type with it's styles and caption to html. * * @param jsonItem */ parseQuote(jsonItem: QuoteElement): string { const data = jsonItem.data; let alignment = data.alignment || 'left'; const beginIcon = ` `; const endIcon = ` `; let quote = `

${beginIcon} ${data.text} ${endIcon}

${data.caption}
`; this.addPlainText(`${data.text} ${data.caption}`); return quote; } /** * Parse list element to html. * * @param jsonItem */ parseList(jsonItem: ListElement): string { const data = jsonItem.data; const listClass = (this.options.withDefaultStyle)? 'class="list"' : ''; let beginList = `${data.style == 'ordered'? `
    ` : `
      `}`; let endList = `${data.style == 'ordered'? '
' : ''}`; let list = beginList; let plainText = ""; function renderListItem(item: string|NestedListItem): string { let listItem = ''; if(typeof(item) == 'string') { listItem+= `
  • ${item}
  • `; plainText+= " " + item; } else if('content' in item) { listItem+= `
  • ${item.content}`; if(item.items && item.items.length > 0) { listItem+= `${beginList}`; item.items.map((nestedItem: NestedListItem) => { listItem+= renderListItem(nestedItem); }); listItem+= `${endList}`; } listItem+= `
  • `; plainText+= " " + item.content; } return listItem; } data.items.map((item: string|NestedListItem) => { list+= renderListItem(item); }); list+= endList; this.addPlainText(plainText); return list; } /** * Parse preview link element to html. * * @param jsonItem */ parseLink(jsonItem: LinkElement): string { const data = jsonItem.data; let linkLayoutStyle = "display: flex;"+ "justify-content: space-between;"+ "align-items: center;"+ "background: white;"+ "padding: 11px;"+ "border: 1px solid #f5f5f5;"+ "border-radius: 8px;"+ "box-shadow: 1px 1px 2px #e5e5e5;"+ "text-decoration: none;"+ "color: black;"; let linkLayout = ``; linkLayout+= '
    '; linkLayout+= `

    ${data.meta.title}

    `; linkLayout+= (data.meta.description)? `

    ${data.meta.description}

    ` : ''; linkLayout+= `${data.link}`; linkLayout+= '
    '; if(data.meta.image.url) { linkLayout+= '
    '; linkLayout+= `Image`; linkLayout+= '
    '; } linkLayout+= '
    '; this.addPlainText(`${data.meta.title} ${data.meta.description || ''}`); return linkLayout; } /** * get delimiter html content. * */ parseDelimiter(jsonItem: EditorJsElement): string { return "
    * * *
    "; } /** * Parse check list items with checked property. * */ parseChecklist(jsonItem: CheckListElement): string { const data = jsonItem.data; let plainText = ""; let checkList = '
    '; data.items.forEach((item) => { checkList+= '
    '; checkList+= ` ${ (item.checked == true)? '' : '' } `; checkList+= `

    ${item.text}

    `; checkList+= '
    '; plainText+= ` ${item.text}`; }); checkList+= '
    '; this.addPlainText(plainText); return checkList; } /** * Parse warning item. * */ parseWarning(jsonItem: WarningElement): string { const data = jsonItem.data; let warning = '
    '; warning+= `

    👉 ${data.title}

    `; warning+= `

    ${data.message}

    `; warning+= '
    ' this.addPlainText(`${data.title} ${data.message}`); return warning; } /** * Parse code element to html. * * @param jsonItem */ parseCode(jsonItem: CodeElement): string { const data = jsonItem.data; let code = `
    `; let copyBtn = ''; code+= copyBtn; const codeContent = data.code.replace(/\n/gi, '
    '); code+= `
    ${data.language || 'code'}
    `; code+= `
    ${codeContent}
    `; code+= '
    '; this.addPlainText(data.code); return code; } /** * Parse embed link to html. * * @param jsonItem */ parseEmbed(jsonItem: EmbedElement): string { const data = jsonItem.data; let embedLayout = '
    '; embedLayout+= ``; if(data.caption) { embedLayout+= `

    ${data.caption}

    `; this.addPlainText(data.caption); } embedLayout+= '
    '; return embedLayout; } /** * Parse personality card to html. * * @param jsonItem */ parsePersonality(jsonItem: PersonalityElement): string { const data = jsonItem.data; let personality = `
    `; personality+= `

    ${data.name}

    ${(data.description)? `

    ${data.description}

    `:''} ${(data.link)? `${data.link}`:''}
    `; if(data.photo) { personality+= `` } personality+= '
    '; this.addPlainText(`${data.name} ${data.description}`); return personality; } /** * Parse attchments to html * * @param jsonItem */ parseAttaches(jsonItem: AttachesElement): string { const data = jsonItem.data; let attachment = ``+ `${data.file.extension}`+ `
    `+ `

    ${data.file.name}

    `+ `${(data.file.size)? `${data.file.size} MiB` : ''}`+ `
    `+ 'Show'+ `
    `; return attachment; } /** * Apply some handlers to let features work correctly. * */ static applyHandlers() { if(typeof document == 'undefined') { throw new Error('document is not defined, you can\'t call the applyHandlers function from server side.'); } this.registerCopyHandler(); this.registerScaleImageHandler(); } /** * Register copy handler, to copy code text in code feature. * */ private static registerCopyHandler(): void { const copyBtns = document.querySelectorAll('.ede .copy-code-btn'); copyBtns.forEach((btn, index) => { btn.addEventListener('click', function() { const value = btn.parentElement?.querySelector('.code-value')?.innerHTML; if(value) { navigator.clipboard.writeText(value); } const svg = btn.querySelector('svg'); if(svg) { svg.style.display = "none"; } let copiedNote = document.createElement('span'); copiedNote.className = "copied-note"; copiedNote.innerHTML = 'copied 👍'; btn.append(copiedNote); setTimeout(() => { if(svg) { svg.style.display = "inline-block"; copiedNote.remove(); } }, 1500) }); }); } /** * Register scale image handler. * */ private static registerScaleImageHandler(): void { const scaleBtns = document.querySelectorAll('.ede .scale-image-btn'); scaleBtns.forEach((btn, index) => { btn.addEventListener('click', function() { if(btn.parentElement!.classList.contains('scaled')) { btn.innerHTML = ''; btn.parentElement!.classList.remove('scaled'); } else { btn.parentElement!.classList.add('scaled'); btn.innerHTML = ''; } }); }); } /** * Add the plain text */ private addPlainText(text: String): void { this.plainText+= ` ${text}`; } /** * Calculate the reading time based on words count * and word per minute */ private calculateReadingTime(): void { const wordsCount: number = this.plainText.split(' ').length; if(wordsCount > 0) { const minutes: number = Math.ceil(wordsCount / this.wpm); this.readingTime.wordsCount = wordsCount; this.readingTime.minutes = minutes; } } /** * Reading time getter * */ public getReadingTime(): ReadingTime { return this.readingTime; } public toString = (): String|undefined => { return this.html; } } export default HtmlViewer;