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