]>
Commit | Line | Data |
---|---|---|
1378c0d3 | 1 | import { firstValueFrom } from 'rxjs' |
2539932e C |
2 | import { ComponentRef, Injectable } from '@angular/core' |
3 | import { MarkdownService } from '@app/core' | |
4 | import { | |
63042139 | 5 | ButtonMarkupData, |
2539932e | 6 | ChannelMiniatureMarkupData, |
f7894f09 | 7 | ContainerMarkupData, |
2539932e C |
8 | EmbedMarkupData, |
9 | PlaylistMiniatureMarkupData, | |
10 | VideoMiniatureMarkupData, | |
11 | VideosListMarkupData | |
12 | } from '@shared/models' | |
2539932e | 13 | import { DynamicElementService } from './dynamic-element.service' |
8ee25e17 C |
14 | import { |
15 | ButtonMarkupComponent, | |
16 | ChannelMiniatureMarkupComponent, | |
17 | EmbedMarkupComponent, | |
18 | PlaylistMiniatureMarkupComponent, | |
19 | VideoMiniatureMarkupComponent, | |
20 | VideosListMarkupComponent | |
21 | } from './peertube-custom-tags' | |
0ca454e3 | 22 | import { CustomMarkupComponent } from './peertube-custom-tags/shared' |
42b40636 | 23 | import { logger } from '@root-helpers/logger' |
2539932e | 24 | |
0ca454e3 | 25 | type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<CustomMarkupComponent> |
f7894f09 | 26 | type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement |
2539932e C |
27 | |
28 | @Injectable() | |
29 | export 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) { | |
61 | const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags()) | |
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 | } |