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