]>
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' |
2539932e | 23 | |
0ca454e3 | 24 | type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<CustomMarkupComponent> |
f7894f09 | 25 | type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement |
2539932e C |
26 | |
27 | @Injectable() | |
28 | export 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 | } |