2021-08-17 11:27:47 +02:00
|
|
|
import { firstValueFrom } from 'rxjs'
|
2021-05-27 15:59:55 +02:00
|
|
|
import { ComponentRef, Injectable } from '@angular/core'
|
|
|
|
import { MarkdownService } from '@app/core'
|
2023-05-23 11:15:00 +02:00
|
|
|
import { logger } from '@root-helpers/logger'
|
2021-05-27 15:59:55 +02:00
|
|
|
import {
|
2021-05-28 15:23:17 +02:00
|
|
|
ButtonMarkupData,
|
2021-05-27 15:59:55 +02:00
|
|
|
ChannelMiniatureMarkupData,
|
2021-06-09 09:19:36 +02:00
|
|
|
ContainerMarkupData,
|
2021-05-27 15:59:55 +02:00
|
|
|
EmbedMarkupData,
|
|
|
|
PlaylistMiniatureMarkupData,
|
|
|
|
VideoMiniatureMarkupData,
|
|
|
|
VideosListMarkupData
|
|
|
|
} from '@shared/models'
|
|
|
|
import { DynamicElementService } from './dynamic-element.service'
|
2021-05-31 11:33:49 +02:00
|
|
|
import {
|
|
|
|
ButtonMarkupComponent,
|
|
|
|
ChannelMiniatureMarkupComponent,
|
|
|
|
EmbedMarkupComponent,
|
|
|
|
PlaylistMiniatureMarkupComponent,
|
|
|
|
VideoMiniatureMarkupComponent,
|
|
|
|
VideosListMarkupComponent
|
|
|
|
} from './peertube-custom-tags'
|
2021-06-29 16:16:12 +02:00
|
|
|
import { CustomMarkupComponent } from './peertube-custom-tags/shared'
|
2021-05-27 15:59:55 +02:00
|
|
|
|
2021-06-29 16:16:12 +02:00
|
|
|
type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<CustomMarkupComponent>
|
2021-06-09 09:19:36 +02:00
|
|
|
type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement
|
2021-05-27 15:59:55 +02:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class CustomMarkupService {
|
2021-06-09 09:19:36 +02:00
|
|
|
private angularBuilders: { [ selector: string ]: AngularBuilderFunction } = {
|
2021-05-28 15:23:17 +02:00
|
|
|
'peertube-button': el => this.buttonBuilder(el),
|
2021-05-27 15:59:55 +02:00
|
|
|
'peertube-video-embed': el => this.embedBuilder(el, 'video'),
|
|
|
|
'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
|
|
|
|
'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
|
|
|
|
'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
|
|
|
|
'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
|
|
|
|
'peertube-videos-list': el => this.videosListBuilder(el)
|
|
|
|
}
|
|
|
|
|
2021-06-09 09:19:36 +02:00
|
|
|
private htmlBuilders: { [ selector: string ]: HTMLBuilderFunction } = {
|
|
|
|
'peertube-container': el => this.containerBuilder(el)
|
|
|
|
}
|
|
|
|
|
2021-05-31 11:33:49 +02:00
|
|
|
private customMarkdownRenderer: (text: string) => Promise<HTMLElement>
|
|
|
|
|
2021-05-27 15:59:55 +02:00
|
|
|
constructor (
|
|
|
|
private dynamicElementService: DynamicElementService,
|
|
|
|
private markdown: MarkdownService
|
2021-05-31 11:33:49 +02:00
|
|
|
) {
|
2021-06-29 16:16:12 +02:00
|
|
|
this.customMarkdownRenderer = (text: string) => {
|
|
|
|
return this.buildElement(text)
|
|
|
|
.then(({ rootElement }) => rootElement)
|
|
|
|
}
|
2021-05-31 11:33:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
getCustomMarkdownRenderer () {
|
|
|
|
return this.customMarkdownRenderer
|
|
|
|
}
|
2021-05-27 15:59:55 +02:00
|
|
|
|
|
|
|
async buildElement (text: string) {
|
2022-11-14 10:47:39 +01:00
|
|
|
const html = await this.markdown.customPageMarkdownToHTML({ markdown: text, additionalAllowedTags: this.getSupportedTags() })
|
2021-05-27 15:59:55 +02:00
|
|
|
|
|
|
|
const rootElement = document.createElement('div')
|
|
|
|
rootElement.innerHTML = html
|
|
|
|
|
2021-06-09 09:19:36 +02:00
|
|
|
for (const selector of Object.keys(this.htmlBuilders)) {
|
|
|
|
rootElement.querySelectorAll(selector)
|
2021-08-02 15:29:09 +02:00
|
|
|
.forEach((e: HTMLElement) => {
|
|
|
|
try {
|
|
|
|
const element = this.execHTMLBuilder(selector, e)
|
|
|
|
// Insert as first child
|
|
|
|
e.insertBefore(element, e.firstChild)
|
|
|
|
} catch (err) {
|
2022-07-15 15:30:14 +02:00
|
|
|
logger.error(`Cannot inject component ${selector}`, err)
|
2021-08-02 15:29:09 +02:00
|
|
|
}
|
|
|
|
})
|
2021-06-09 09:19:36 +02:00
|
|
|
}
|
|
|
|
|
2021-06-29 16:16:12 +02:00
|
|
|
const loadedPromises: Promise<boolean>[] = []
|
|
|
|
|
2021-06-09 09:19:36 +02:00
|
|
|
for (const selector of Object.keys(this.angularBuilders)) {
|
2021-05-27 15:59:55 +02:00
|
|
|
rootElement.querySelectorAll(selector)
|
|
|
|
.forEach((e: HTMLElement) => {
|
|
|
|
try {
|
2021-06-09 09:19:36 +02:00
|
|
|
const component = this.execAngularBuilder(selector, e)
|
2021-05-27 15:59:55 +02:00
|
|
|
|
2021-06-29 16:16:12 +02:00
|
|
|
if (component.instance.loaded) {
|
2021-08-17 11:27:47 +02:00
|
|
|
const p = firstValueFrom(component.instance.loaded)
|
2021-06-29 16:16:12 +02:00
|
|
|
loadedPromises.push(p)
|
|
|
|
}
|
|
|
|
|
2021-05-27 15:59:55 +02:00
|
|
|
this.dynamicElementService.injectElement(e, component)
|
|
|
|
} catch (err) {
|
2022-07-15 15:30:14 +02:00
|
|
|
logger.error(`Cannot inject component ${selector}`, err)
|
2021-05-27 15:59:55 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-06-29 16:16:12 +02:00
|
|
|
return { rootElement, componentsLoaded: Promise.all(loadedPromises) }
|
2021-05-27 15:59:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private getSupportedTags () {
|
2021-06-09 09:19:36 +02:00
|
|
|
return Object.keys(this.angularBuilders)
|
|
|
|
.concat(Object.keys(this.htmlBuilders))
|
2021-05-27 15:59:55 +02:00
|
|
|
}
|
|
|
|
|
2021-06-09 09:19:36 +02:00
|
|
|
private execHTMLBuilder (selector: string, el: HTMLElement) {
|
|
|
|
return this.htmlBuilders[selector](el)
|
|
|
|
}
|
|
|
|
|
|
|
|
private execAngularBuilder (selector: string, el: HTMLElement) {
|
|
|
|
return this.angularBuilders[selector](el)
|
2021-05-27 15:59:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
|
|
|
|
const data = el.dataset as EmbedMarkupData
|
|
|
|
const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
|
|
|
|
|
|
|
|
this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
|
|
|
|
|
|
|
|
return component
|
|
|
|
}
|
|
|
|
|
|
|
|
private playlistMiniatureBuilder (el: HTMLElement) {
|
|
|
|
const data = el.dataset as PlaylistMiniatureMarkupData
|
|
|
|
const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
|
|
|
|
|
|
|
|
this.dynamicElementService.setModel(component, { uuid: data.uuid })
|
|
|
|
|
|
|
|
return component
|
|
|
|
}
|
|
|
|
|
|
|
|
private channelMiniatureBuilder (el: HTMLElement) {
|
|
|
|
const data = el.dataset as ChannelMiniatureMarkupData
|
|
|
|
const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
|
|
|
|
|
2021-06-09 10:31:27 +02:00
|
|
|
const model = {
|
|
|
|
name: data.name,
|
|
|
|
displayLatestVideo: this.buildBoolean(data.displayLatestVideo) ?? true,
|
|
|
|
displayDescription: this.buildBoolean(data.displayDescription) ?? true
|
|
|
|
}
|
|
|
|
|
|
|
|
this.dynamicElementService.setModel(component, model)
|
2021-05-27 15:59:55 +02:00
|
|
|
|
|
|
|
return component
|
|
|
|
}
|
|
|
|
|
2021-05-28 15:23:17 +02:00
|
|
|
private buttonBuilder (el: HTMLElement) {
|
|
|
|
const data = el.dataset as ButtonMarkupData
|
|
|
|
const component = this.dynamicElementService.createElement(ButtonMarkupComponent)
|
|
|
|
|
|
|
|
const model = {
|
2021-06-09 13:34:40 +02:00
|
|
|
theme: data.theme ?? 'primary',
|
2021-05-28 15:23:17 +02:00
|
|
|
href: data.href,
|
|
|
|
label: data.label,
|
2021-06-09 13:34:40 +02:00
|
|
|
blankTarget: this.buildBoolean(data.blankTarget) ?? false
|
2021-05-28 15:23:17 +02:00
|
|
|
}
|
|
|
|
this.dynamicElementService.setModel(component, model)
|
|
|
|
|
|
|
|
return component
|
|
|
|
}
|
|
|
|
|
2021-06-09 09:32:47 +02:00
|
|
|
private videoMiniatureBuilder (el: HTMLElement) {
|
|
|
|
const data = el.dataset as VideoMiniatureMarkupData
|
|
|
|
const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
|
|
|
|
|
|
|
|
const model = {
|
|
|
|
uuid: data.uuid,
|
|
|
|
onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false
|
|
|
|
}
|
|
|
|
|
|
|
|
this.dynamicElementService.setModel(component, model)
|
|
|
|
|
|
|
|
return component
|
|
|
|
}
|
|
|
|
|
2021-05-27 15:59:55 +02:00
|
|
|
private videosListBuilder (el: HTMLElement) {
|
|
|
|
const data = el.dataset as VideosListMarkupData
|
|
|
|
const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
|
|
|
|
|
|
|
|
const model = {
|
2021-06-09 09:32:47 +02:00
|
|
|
onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false,
|
2021-06-09 10:59:20 +02:00
|
|
|
maxRows: this.buildNumber(data.maxRows) ?? -1,
|
|
|
|
|
2021-06-09 09:32:47 +02:00
|
|
|
sort: data.sort || '-publishedAt',
|
2021-06-09 10:59:20 +02:00
|
|
|
count: this.buildNumber(data.count) || 10,
|
|
|
|
|
2021-06-09 09:32:47 +02:00
|
|
|
categoryOneOf: this.buildArrayNumber(data.categoryOneOf) ?? [],
|
|
|
|
languageOneOf: this.buildArrayString(data.languageOneOf) ?? [],
|
2021-06-09 10:59:20 +02:00
|
|
|
|
2021-07-01 17:41:03 +02:00
|
|
|
accountHandle: data.accountHandle || undefined,
|
|
|
|
channelHandle: data.channelHandle || undefined,
|
|
|
|
|
2021-08-02 16:50:56 +02:00
|
|
|
isLive: this.buildBoolean(data.isLive),
|
|
|
|
|
2021-10-27 14:37:04 +02:00
|
|
|
isLocal: this.buildBoolean(data.onlyLocal) ? true : undefined
|
2021-05-27 15:59:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
this.dynamicElementService.setModel(component, model)
|
|
|
|
|
|
|
|
return component
|
|
|
|
}
|
|
|
|
|
2021-06-09 09:19:36 +02:00
|
|
|
private containerBuilder (el: HTMLElement) {
|
|
|
|
const data = el.dataset as ContainerMarkupData
|
|
|
|
|
2021-06-09 10:59:20 +02:00
|
|
|
// Move inner HTML in the new element we'll create
|
|
|
|
const content = el.innerHTML
|
|
|
|
el.innerHTML = ''
|
|
|
|
|
2021-06-09 09:19:36 +02:00
|
|
|
const root = document.createElement('div')
|
2021-06-09 10:59:20 +02:00
|
|
|
root.innerHTML = content
|
2021-06-09 10:31:27 +02:00
|
|
|
|
|
|
|
const layoutClass = data.layout
|
|
|
|
? 'layout-' + data.layout
|
2021-06-09 10:59:20 +02:00
|
|
|
: 'layout-column'
|
2021-06-09 10:31:27 +02:00
|
|
|
|
|
|
|
root.classList.add('peertube-container', layoutClass)
|
2021-06-09 09:19:36 +02:00
|
|
|
|
2021-08-26 13:44:54 +02:00
|
|
|
root.style.justifyContent = data.justifyContent || 'space-between'
|
|
|
|
|
2021-06-09 09:19:36 +02:00
|
|
|
if (data.width) {
|
|
|
|
root.setAttribute('width', data.width)
|
|
|
|
}
|
|
|
|
|
2021-06-09 10:59:20 +02:00
|
|
|
if (data.title || data.description) {
|
|
|
|
const headerElement = document.createElement('div')
|
|
|
|
headerElement.classList.add('header')
|
|
|
|
|
|
|
|
if (data.title) {
|
|
|
|
const titleElement = document.createElement('h4')
|
|
|
|
titleElement.innerText = data.title
|
|
|
|
headerElement.appendChild(titleElement)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (data.description) {
|
|
|
|
const descriptionElement = document.createElement('div')
|
|
|
|
descriptionElement.innerText = data.description
|
|
|
|
headerElement.append(descriptionElement)
|
|
|
|
}
|
2021-06-09 09:19:36 +02:00
|
|
|
|
2021-06-09 10:59:20 +02:00
|
|
|
root.insertBefore(headerElement, root.firstChild)
|
2021-06-09 09:19:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return root
|
|
|
|
}
|
|
|
|
|
2021-05-27 15:59:55 +02:00
|
|
|
private buildNumber (value: string) {
|
|
|
|
if (!value) return undefined
|
|
|
|
|
|
|
|
return parseInt(value, 10)
|
|
|
|
}
|
|
|
|
|
2021-05-28 15:23:17 +02:00
|
|
|
private buildBoolean (value: string) {
|
|
|
|
if (value === 'true') return true
|
|
|
|
if (value === 'false') return false
|
|
|
|
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
2021-05-27 15:59:55 +02:00
|
|
|
private buildArrayNumber (value: string) {
|
|
|
|
if (!value) return undefined
|
|
|
|
|
|
|
|
return value.split(',').map(v => parseInt(v, 10))
|
|
|
|
}
|
|
|
|
|
|
|
|
private buildArrayString (value: string) {
|
|
|
|
if (!value) return undefined
|
|
|
|
|
|
|
|
return value.split(',')
|
|
|
|
}
|
|
|
|
}
|