1 import truncate from 'lodash-es/truncate'
2 import { Subject } from 'rxjs'
3 import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
4 import { ViewportScroller } from '@angular/common'
5 import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'
6 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
7 import { SafeHtml } from '@angular/platform-browser'
8 import { MarkdownService, ScreenService } from '@app/core'
9 import { Video } from '@shared/models'
12 selector: 'my-markdown-textarea',
13 templateUrl: './markdown-textarea.component.html',
14 styleUrls: [ './markdown-textarea.component.scss' ],
17 provide: NG_VALUE_ACCESSOR,
18 useExisting: forwardRef(() => MarkdownTextareaComponent),
24 export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
27 @Input() classes: string[] | { [klass: string]: any[] | any } = []
29 @Input() textareaMaxWidth = '100%'
30 @Input() textareaHeight = '150px'
32 @Input() truncate: number
34 @Input() markdownType: 'text' | 'enhanced' = 'text'
35 @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement>
37 @Input() markdownVideo: Video
39 @Input() name = 'description'
41 @ViewChild('textarea') textareaElement: ElementRef
42 @ViewChild('previewElement') previewElement: ElementRef
44 truncatedPreviewHTML: SafeHtml | string = ''
45 previewHTML: SafeHtml | string = ''
50 maximizeInText = $localize`Maximize editor`
51 maximizeOutText = $localize`Exit maximized editor`
53 private contentChanged = new Subject<string>()
54 private scrollPosition: [number, number]
57 private viewportScroller: ViewportScroller,
58 private screenService: ScreenService,
59 private markdownService: MarkdownService
66 distinctUntilChanged()
68 .subscribe(() => this.updatePreviews())
70 this.contentChanged.next(this.content)
73 propagateChange = (_: any) => { /* empty */ }
75 writeValue (description: string) {
76 this.content = description
78 this.contentChanged.next(this.content)
81 registerOnChange (fn: (_: any) => void) {
82 this.propagateChange = fn
85 registerOnTouched () {
90 this.propagateChange(this.content)
92 this.contentChanged.next(this.content)
96 this.isMaximized = !this.isMaximized
98 // Make sure textarea have the focus
99 // Except on touchscreens devices, the virtual keyboard may move up and hide the textarea in maximized mode
100 if (!this.screenService.isInTouchScreen()) {
101 this.textareaElement.nativeElement.focus()
104 // Make sure the window has no scrollbars
105 if (!this.isMaximized) {
106 this.unlockBodyScroll()
108 this.lockBodyScroll()
112 setDisabledState (isDisabled: boolean) {
113 this.disabled = isDisabled
116 private lockBodyScroll () {
117 this.scrollPosition = this.viewportScroller.getScrollPosition()
118 document.getElementById('content').classList.add('lock-scroll')
121 private unlockBodyScroll () {
122 document.getElementById('content').classList.remove('lock-scroll')
123 this.viewportScroller.scrollToPosition(this.scrollPosition)
126 private async updatePreviews () {
127 if (this.content === null || this.content === undefined) return
129 this.truncatedPreviewHTML = await this.markdownRender(truncate(this.content, { length: this.truncate }))
130 this.previewHTML = await this.markdownRender(this.content)
133 private async markdownRender (text: string) {
136 if (this.customMarkdownRenderer) {
137 const result = await this.customMarkdownRenderer(text)
139 if (result instanceof HTMLElement) {
140 const wrapperElement = this.previewElement.nativeElement as HTMLElement
141 wrapperElement.innerHTML = ''
142 wrapperElement.appendChild(result)
147 } else if (this.markdownType === 'text') {
148 html = await this.markdownService.textMarkdownToHTML(text)
150 html = await this.markdownService.enhancedMarkdownToHTML(text)
153 if (this.markdownVideo) {
154 html = this.markdownService.processVideoTimestamps(this.markdownVideo.shortUUID, html)