Skip to content

Commit b4400a5

Browse files
committed
Add more plugin examples
1 parent 59a4da1 commit b4400a5

File tree

3 files changed

+288
-3
lines changed

3 files changed

+288
-3
lines changed

resources/js/plugins/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Plugins
2+
3+
Plugins use internal undocumented API and features of Unlayer.
4+
5+
6+
## Events
7+
8+
- `content:added`
9+
- `content:removed`
10+
- `content:modified`
11+
- `row:added`
12+
- `row:removed`
13+
14+
15+
## Example
16+
17+
```php
18+
Unlayer::make('Content', 'design')->config(function (): array {
19+
/** @see https://docs.unlayer.com/docs/email-builder */
20+
return $this->generateUnlayerConfig();
21+
})->plugins([
22+
asset(mix('js/unlayer-editor-plugins/utm.js')->toHtml()),
23+
asset(mix('js/unlayer-editor-plugins/fontsize.js')->toHtml()),
24+
]);
25+
```

resources/js/plugins/example.js renamed to resources/js/plugins/fontsize.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
const defaultFontSizeInPx = 16;
99

1010
/**
11-
* @typedef {UnlayerConfigNode} UnlayerConfigNode
11+
* @typedef {object} UnlayerConfigNode
1212
* @property {string} type
13-
* @property {Object} values
14-
* @property {Object} [changes]
13+
* @property {object} values
14+
* @property {object} [changes]
1515
*/
1616

1717
/**

resources/js/plugins/utm.js

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/**
2+
* Example of a plugin to add UTM parameters to internal links.
3+
*/
4+
(function (unlayer, Nova, originOfInternalLinks) {
5+
unlayer.plugins['utm'] = processNode;
6+
7+
const supportedEventTypes = ['content:added', 'content:modified'];
8+
9+
/**
10+
* @typedef {object} UnlayerNameValueChange
11+
* @property {string} name
12+
* @property {string} value
13+
*/
14+
15+
/**
16+
* @typedef {object} UnlayerSingleChange
17+
* @property {string} name
18+
* @property {object} attrs
19+
* @property {object} values
20+
*/
21+
22+
/**
23+
* @typedef {object} UnlayerChanges
24+
* @property {string} name
25+
* @property {UnlayerSingleChange} value
26+
*/
27+
28+
/**
29+
* @typedef {object} UnlayerConfigNode
30+
* @property {string} type
31+
* @property {object} values
32+
* @property {object} [changes]
33+
*/
34+
35+
/**
36+
* @public
37+
* @param {UnlayerConfigNode} node
38+
* @param {string} eventType
39+
* @param {UnlayerChanges} changes
40+
* @returns {UnlayerConfigNode}
41+
*/
42+
function processNode(node, eventType, changes) {
43+
if (!supportedEventTypes.includes(eventType)) {
44+
return node;
45+
}
46+
47+
if (eventType === 'content:added') {
48+
if (node.type === 'button') {
49+
return processAddedButtonNode(node);
50+
}
51+
52+
if (node.type === 'html') {
53+
return processAddedHtmlNode(node);
54+
}
55+
56+
if (node.type === 'text') {
57+
return processAddedTextNode(node);
58+
}
59+
}
60+
61+
if (eventType === 'content:modified') {
62+
if (node.type === 'button') {
63+
return processModifiedButtonNode(node, changes);
64+
}
65+
66+
if (node.type === 'html') {
67+
return processModifiedHtmlNode(node, changes);
68+
}
69+
70+
if (node.type === 'text') {
71+
return processModifiedTextNode(node, changes);
72+
}
73+
}
74+
75+
return node;
76+
}
77+
78+
/**
79+
* @private
80+
* @param {UnlayerConfigNode} node
81+
* @param {UnlayerChanges} changes
82+
* @returns {UnlayerConfigNode}
83+
*/
84+
function processModifiedButtonNode(node, changes) {
85+
if (changes.name !== 'href') {
86+
return node;
87+
}
88+
89+
/** @type {string} */
90+
const url = changes.value.values.href;
91+
if (!url || !isInternalUrl(url)) {
92+
return node;
93+
}
94+
95+
node.values.href.values.href = addUtmParametersToUrl(url, getUtmParameters());
96+
return node;
97+
}
98+
99+
/**
100+
* @private
101+
* @param {UnlayerConfigNode} node
102+
* @returns {UnlayerConfigNode}
103+
*/
104+
function processAddedHtmlNode(node) {
105+
node.values.text = updateHtmlToAddUtmParametersToInternalLinks(node.values.html, getUtmParameters());
106+
return node;
107+
}
108+
109+
/**
110+
* @private
111+
* @param {UnlayerConfigNode} node
112+
* @param {UnlayerChanges} changes
113+
* @returns {UnlayerConfigNode}
114+
*/
115+
function processModifiedHtmlNode(node, changes) {
116+
if (changes.name !== 'html') {
117+
return node;
118+
}
119+
120+
if (!changes.value.includes(originOfInternalLinks) && !node.values.html.includes(originOfInternalLinks)) {
121+
return node;
122+
}
123+
124+
node.values.html = updateHtmlToAddUtmParametersToInternalLinks(changes.value, getUtmParameters());
125+
return node;
126+
}
127+
128+
/**
129+
* @private
130+
* @param {UnlayerConfigNode} node
131+
* @returns {UnlayerConfigNode}
132+
*/
133+
function processAddedTextNode(node) {
134+
node.values.text = updateHtmlToAddUtmParametersToInternalLinks(node.values.text, getUtmParameters());
135+
return node;
136+
}
137+
138+
/**
139+
* @private
140+
* @param {UnlayerConfigNode} node
141+
* @param {UnlayerNameValueChange} changes
142+
* @returns {UnlayerConfigNode}
143+
*/
144+
function processModifiedTextNode(node, changes) {
145+
if (changes.name !== 'text') {
146+
return node;
147+
}
148+
149+
if (!changes.value.includes(originOfInternalLinks) && !node.values.text.includes(originOfInternalLinks)) {
150+
return node;
151+
}
152+
153+
node.values.text = updateHtmlToAddUtmParametersToInternalLinks(changes.value, getUtmParameters());
154+
return node;
155+
}
156+
157+
/**
158+
* @private
159+
* @param {UnlayerConfigNode} node
160+
* @returns {UnlayerConfigNode}
161+
*/
162+
function processAddedButtonNode(node) {
163+
/** @type {string} */
164+
const url = node.values.href.values.href;
165+
if (!url || !isInternalUrl(url)) {
166+
return node;
167+
}
168+
169+
node.values.href.values.href = addUtmParametersToUrl(url, getUtmParameters());
170+
return node;
171+
}
172+
173+
/**
174+
* @private
175+
* @returns {URLSearchParams}
176+
*/
177+
function getUtmParameters() {
178+
const currentDate = new Date().toISOString().slice(0, 10); // example: "2017-02-01"
179+
180+
const segment = document.querySelector('#segment').value;
181+
if (!segment) {
182+
Nova.app.$toasted.info(
183+
'"Segment" input is empty, can not set "utm_campaign" parameter. Please select a group and edit this link again.',
184+
{type: 'error'}
185+
);
186+
}
187+
188+
const urlSearchParameters = new URLSearchParams();
189+
urlSearchParameters.set('utm_source', 'newsletter');
190+
urlSearchParameters.set('utm_medium', 'email');
191+
urlSearchParameters.set('utm_content', `letter-${currentDate}`);
192+
if (segment) {
193+
urlSearchParameters.set('utm_campaign', segment);
194+
}
195+
196+
return urlSearchParameters;
197+
}
198+
199+
/**
200+
* @private
201+
* @param {string} url
202+
* @param {URLSearchParams} utmParameters
203+
* @returns {string}
204+
*/
205+
function addUtmParametersToUrl(url, utmParameters) {
206+
const urlObject = new URL(url);
207+
let parameters = urlObject.searchParams;
208+
for (const [key, value] of utmParameters) {
209+
parameters.set(key, value);
210+
}
211+
212+
return urlObject.toString();
213+
}
214+
215+
/**
216+
* @private
217+
* @param {string} url
218+
* @returns {boolean}
219+
*/
220+
function isInternalUrl(url) {
221+
return url.startsWith(originOfInternalLinks);
222+
}
223+
224+
/**
225+
* Adds UTM parameters to all internal links.
226+
* At this point for 'utm_content' parameter we use stub value
227+
* that should be updated right before submitting the email
228+
*
229+
* @private
230+
* @param {string} htmlContent - html generated by RTE
231+
* @param {URLSearchParams} utmParameters - An object with UTM tags
232+
* @returns {string} html code with UTM parameters
233+
*/
234+
function updateHtmlToAddUtmParametersToInternalLinks(htmlContent, utmParameters) {
235+
/**
236+
* @param {string} fullMatchedString
237+
* @param {string} url
238+
* @param {string} quoteType
239+
* @returns {string}
240+
*/
241+
function urlReplacer(fullMatchedString, url, quoteType) {
242+
const urlWithUtmParameters = addUtmParametersToUrl(url, utmParameters);
243+
return ` href=${quoteType}${urlWithUtmParameters}${quoteType}`;
244+
}
245+
246+
const internalOriginEscaped = escapeStringForRegExp(originOfInternalLinks);
247+
const regExp = new RegExp(`href=["'](${internalOriginEscaped}.*?)(["'])`, 'giu');
248+
249+
return htmlContent.replace(regExp, urlReplacer);
250+
}
251+
252+
/**
253+
* @private
254+
* @param {string} string
255+
* @returns {string}
256+
*/
257+
function escapeStringForRegExp(string) {
258+
return string.replaceAll(/[$()*+./?[\\\]^{|}]/g, '\\$&'); // $& means the whole matched string
259+
}
260+
})(window.unlayer, window.Nova, location.origin);

0 commit comments

Comments
 (0)