]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/app/shared/shared-custom-markup/custom-markup.service.ts
Fix custom markup
[github/Chocobozzz/PeerTube.git] / client / src / app / shared / shared-custom-markup / custom-markup.service.ts
CommitLineData
1378c0d3 1import { firstValueFrom } from 'rxjs'
2539932e
C
2import { ComponentRef, Injectable } from '@angular/core'
3import { MarkdownService } from '@app/core'
4import {
63042139 5 ButtonMarkupData,
2539932e 6 ChannelMiniatureMarkupData,
f7894f09 7 ContainerMarkupData,
2539932e
C
8 EmbedMarkupData,
9 PlaylistMiniatureMarkupData,
10 VideoMiniatureMarkupData,
11 VideosListMarkupData
12} from '@shared/models'
2539932e 13import { DynamicElementService } from './dynamic-element.service'
8ee25e17
C
14import {
15 ButtonMarkupComponent,
16 ChannelMiniatureMarkupComponent,
17 EmbedMarkupComponent,
18 PlaylistMiniatureMarkupComponent,
19 VideoMiniatureMarkupComponent,
20 VideosListMarkupComponent
21} from './peertube-custom-tags'
0ca454e3 22import { CustomMarkupComponent } from './peertube-custom-tags/shared'
42b40636 23import { logger } from '@root-helpers/logger'
2539932e 24
0ca454e3 25type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<CustomMarkupComponent>
f7894f09 26type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement
2539932e
C
27
28@Injectable()
29export class CustomMarkupService {
f7894f09 30 private angularBuilders: { [ selector: string ]: AngularBuilderFunction } = {
63042139 31 'peertube-button': el => this.buttonBuilder(el),
2539932e
C
32 'peertube-video-embed': el => this.embedBuilder(el, 'video'),
33 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
34 'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
35 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
36 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
37 'peertube-videos-list': el => this.videosListBuilder(el)
38 }
39
f7894f09
C
40 private htmlBuilders: { [ selector: string ]: HTMLBuilderFunction } = {
41 'peertube-container': el => this.containerBuilder(el)
42 }
43
8ee25e17
C
44 private customMarkdownRenderer: (text: string) => Promise<HTMLElement>
45
2539932e
C
46 constructor (
47 private dynamicElementService: DynamicElementService,
48 private markdown: MarkdownService
8ee25e17 49 ) {
0ca454e3
C
50 this.customMarkdownRenderer = (text: string) => {
51 return this.buildElement(text)
52 .then(({ rootElement }) => rootElement)
53 }
8ee25e17
C
54 }
55
56 getCustomMarkdownRenderer () {
57 return this.customMarkdownRenderer
58 }
2539932e
C
59
60 async buildElement (text: string) {
0e45e336 61 const html = await this.markdown.customPageMarkdownToHTML({ markdown: text, additionalAllowedTags: this.getSupportedTags() })
2539932e
C
62
63 const rootElement = document.createElement('div')
64 rootElement.innerHTML = html
65
f7894f09
C
66 for (const selector of Object.keys(this.htmlBuilders)) {
67 rootElement.querySelectorAll(selector)
3da38d6e
C
68 .forEach((e: HTMLElement) => {
69 try {
70 const element = this.execHTMLBuilder(selector, e)
71 // Insert as first child
72 e.insertBefore(element, e.firstChild)
73 } catch (err) {
42b40636 74 logger.error(`Cannot inject component ${selector}`, err)
3da38d6e
C
75 }
76 })
f7894f09
C
77 }
78
0ca454e3
C
79 const loadedPromises: Promise<boolean>[] = []
80
f7894f09 81 for (const selector of Object.keys(this.angularBuilders)) {
2539932e
C
82 rootElement.querySelectorAll(selector)
83 .forEach((e: HTMLElement) => {
84 try {
f7894f09 85 const component = this.execAngularBuilder(selector, e)
2539932e 86
0ca454e3 87 if (component.instance.loaded) {
1378c0d3 88 const p = firstValueFrom(component.instance.loaded)
0ca454e3
C
89 loadedPromises.push(p)
90 }
91
2539932e
C
92 this.dynamicElementService.injectElement(e, component)
93 } catch (err) {
42b40636 94 logger.error(`Cannot inject component ${selector}`, err)
2539932e
C
95 }
96 })
97 }
98
0ca454e3 99 return { rootElement, componentsLoaded: Promise.all(loadedPromises) }
2539932e
C
100 }
101
102 private getSupportedTags () {
f7894f09
C
103 return Object.keys(this.angularBuilders)
104 .concat(Object.keys(this.htmlBuilders))
2539932e
C
105 }
106
f7894f09
C
107 private execHTMLBuilder (selector: string, el: HTMLElement) {
108 return this.htmlBuilders[selector](el)
109 }
110
111 private execAngularBuilder (selector: string, el: HTMLElement) {
112 return this.angularBuilders[selector](el)
2539932e
C
113 }
114
115 private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
116 const data = el.dataset as EmbedMarkupData
117 const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
118
119 this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
120
121 return component
122 }
123
2539932e
C
124 private playlistMiniatureBuilder (el: HTMLElement) {
125 const data = el.dataset as PlaylistMiniatureMarkupData
126 const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
127
128 this.dynamicElementService.setModel(component, { uuid: data.uuid })
129
130 return component
131 }
132
133 private channelMiniatureBuilder (el: HTMLElement) {
134 const data = el.dataset as ChannelMiniatureMarkupData
135 const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
136
61cbafc1
C
137 const model = {
138 name: data.name,
139 displayLatestVideo: this.buildBoolean(data.displayLatestVideo) ?? true,
140 displayDescription: this.buildBoolean(data.displayDescription) ?? true
141 }
142
143 this.dynamicElementService.setModel(component, model)
2539932e
C
144
145 return component
146 }
147
63042139
C
148 private buttonBuilder (el: HTMLElement) {
149 const data = el.dataset as ButtonMarkupData
150 const component = this.dynamicElementService.createElement(ButtonMarkupComponent)
151
152 const model = {
4ead40e7 153 theme: data.theme ?? 'primary',
63042139
C
154 href: data.href,
155 label: data.label,
4ead40e7 156 blankTarget: this.buildBoolean(data.blankTarget) ?? false
63042139
C
157 }
158 this.dynamicElementService.setModel(component, model)
159
160 return component
161 }
162
9105634f
C
163 private videoMiniatureBuilder (el: HTMLElement) {
164 const data = el.dataset as VideoMiniatureMarkupData
165 const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
166
167 const model = {
168 uuid: data.uuid,
169 onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false
170 }
171
172 this.dynamicElementService.setModel(component, model)
173
174 return component
175 }
176
2539932e
C
177 private videosListBuilder (el: HTMLElement) {
178 const data = el.dataset as VideosListMarkupData
179 const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
180
181 const model = {
9105634f 182 onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false,
5d6395af
C
183 maxRows: this.buildNumber(data.maxRows) ?? -1,
184
9105634f 185 sort: data.sort || '-publishedAt',
5d6395af
C
186 count: this.buildNumber(data.count) || 10,
187
9105634f
C
188 categoryOneOf: this.buildArrayNumber(data.categoryOneOf) ?? [],
189 languageOneOf: this.buildArrayString(data.languageOneOf) ?? [],
5d6395af 190
674d903b
C
191 accountHandle: data.accountHandle || undefined,
192 channelHandle: data.channelHandle || undefined,
193
ff4de383
C
194 isLive: this.buildBoolean(data.isLive),
195
2760b454 196 isLocal: this.buildBoolean(data.onlyLocal) ? true : undefined
2539932e
C
197 }
198
199 this.dynamicElementService.setModel(component, model)
200
201 return component
202 }
203
f7894f09
C
204 private containerBuilder (el: HTMLElement) {
205 const data = el.dataset as ContainerMarkupData
206
5d6395af
C
207 // Move inner HTML in the new element we'll create
208 const content = el.innerHTML
209 el.innerHTML = ''
210
f7894f09 211 const root = document.createElement('div')
5d6395af 212 root.innerHTML = content
61cbafc1
C
213
214 const layoutClass = data.layout
215 ? 'layout-' + data.layout
5d6395af 216 : 'layout-column'
61cbafc1
C
217
218 root.classList.add('peertube-container', layoutClass)
f7894f09 219
0dce48c1
C
220 root.style.justifyContent = data.justifyContent || 'space-between'
221
f7894f09
C
222 if (data.width) {
223 root.setAttribute('width', data.width)
224 }
225
5d6395af
C
226 if (data.title || data.description) {
227 const headerElement = document.createElement('div')
228 headerElement.classList.add('header')
229
230 if (data.title) {
231 const titleElement = document.createElement('h4')
232 titleElement.innerText = data.title
233 headerElement.appendChild(titleElement)
234 }
235
236 if (data.description) {
237 const descriptionElement = document.createElement('div')
238 descriptionElement.innerText = data.description
239 headerElement.append(descriptionElement)
240 }
f7894f09 241
5d6395af 242 root.insertBefore(headerElement, root.firstChild)
f7894f09
C
243 }
244
245 return root
246 }
247
2539932e
C
248 private buildNumber (value: string) {
249 if (!value) return undefined
250
251 return parseInt(value, 10)
252 }
253
63042139
C
254 private buildBoolean (value: string) {
255 if (value === 'true') return true
256 if (value === 'false') return false
257
258 return undefined
259 }
260
2539932e
C
261 private buildArrayNumber (value: string) {
262 if (!value) return undefined
263
264 return value.split(',').map(v => parseInt(v, 10))
265 }
266
267 private buildArrayString (value: string) {
268 if (!value) return undefined
269
270 return value.split(',')
271 }
272}