Introduce @TrackLink
shorthand for generating tracking links.
The default `{{ TrackLink "https://listmonk.app" }}` template function is clumsy to write and does breaks WYSIWYG editors and HTML syntax highlighting because of the quotes. The new syntax doesn't break HTML and is easier to write. Eg: `<a href="https://listmonk.app@TrackLink">Link</a>` - Introduce @TrackLink shorthand. - Add first-class support for tracking links in the WYSIWYG (TinyMCE) editor by introducing an on/off checkbox on the link dialog. - Improve default dummy campaign content to highlight this.
This commit is contained in:
parent
d3f543cb15
commit
d86438bde9
18 changed files with 139 additions and 10 deletions
|
@ -136,7 +136,13 @@ func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempo
|
|||
"Welcome to listmonk",
|
||||
"No Reply <noreply@yoursite.com>",
|
||||
`<h3>Hi {{ .Subscriber.FirstName }}!</h3>
|
||||
This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.`,
|
||||
<p>This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.</p>
|
||||
<p>Here is a <a href="https://listmonk.app@TrackLink">tracked link</a>.</p>
|
||||
<p>Use the link icon in the editor toolbar or when writing raw HTML or Markdown,
|
||||
simply suffix @TrackLink to the end of a URL to turn it into a tracking link. Example:</p>
|
||||
<pre><a href="https:/‌/listmonk.app@TrackLink"></a></pre>
|
||||
<p>For help, refer to the <a href="https://listmonk.app/docs">documentation</a>.</p>
|
||||
`,
|
||||
nil,
|
||||
"richtext",
|
||||
nil,
|
||||
|
|
|
@ -225,6 +225,17 @@ body.is-noscroll {
|
|||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tox-track-link {
|
||||
display: block !important;
|
||||
cursor: pointer !important;
|
||||
|
||||
margin: 5px 0 10px 0 !important;
|
||||
input {
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.plain-editor textarea {
|
||||
height: 65vh;
|
||||
}
|
||||
|
|
|
@ -148,6 +148,7 @@ export default {
|
|||
isReady: false,
|
||||
isRichtextReady: false,
|
||||
richtextConf: {},
|
||||
isTrackLink: false,
|
||||
form: {
|
||||
body: '',
|
||||
format: this.contentType,
|
||||
|
@ -174,7 +175,18 @@ export default {
|
|||
const { lang } = this.serverConfig;
|
||||
|
||||
this.richtextConf = {
|
||||
init_instance_callback: () => { this.isReady = true; },
|
||||
urlconverter_callback: this.onEditorURLConvert,
|
||||
|
||||
setup: (editor) => {
|
||||
editor.on('init', () => {
|
||||
this.onEditorDialogOpen(editor);
|
||||
});
|
||||
},
|
||||
|
||||
min_height: 500,
|
||||
entity_encoding: 'raw',
|
||||
convert_urls: true,
|
||||
plugins: [
|
||||
'autoresize', 'autolink', 'charmap', 'code', 'emoticons', 'fullscreen', 'help',
|
||||
'hr', 'image', 'imagetools', 'link', 'lists', 'paste', 'searchreplace',
|
||||
|
@ -194,15 +206,14 @@ export default {
|
|||
table, td { border-color: #ccc;}
|
||||
`,
|
||||
|
||||
language: LANGS[lang] || null,
|
||||
language_url: LANGS[lang] ? `${uris.static}/tinymce/lang/${LANGS[lang]}.js` : null,
|
||||
|
||||
file_picker_types: 'image',
|
||||
file_picker_callback: (callback) => {
|
||||
this.isMediaVisible = true;
|
||||
this.runTinyMceImageCallback = callback;
|
||||
},
|
||||
init_instance_callback: () => { this.isReady = true; },
|
||||
|
||||
language: LANGS[lang] || null,
|
||||
language_url: LANGS[lang] ? `${uris.static}/tinymce/lang/${LANGS[lang]}.js` : null,
|
||||
};
|
||||
|
||||
this.isRichtextReady = true;
|
||||
|
@ -258,6 +269,72 @@ export default {
|
|||
);
|
||||
},
|
||||
|
||||
onEditorURLConvert(url) {
|
||||
let u = url;
|
||||
if (this.isTrackLink) {
|
||||
u = `${u}@TrackLink`;
|
||||
}
|
||||
|
||||
this.isTrackLink = false;
|
||||
return u;
|
||||
},
|
||||
|
||||
onEditorDialogOpen(editor) {
|
||||
const ed = editor;
|
||||
const oldEd = ed.windowManager.open;
|
||||
const self = this;
|
||||
|
||||
ed.windowManager.open = (t, r) => {
|
||||
const isOK = t.initialData && 'url' in t.initialData && 'anchor' in t.initialData;
|
||||
|
||||
// Not the link modal.
|
||||
if (!isOK) {
|
||||
return oldEd.apply(this, [t, r]);
|
||||
}
|
||||
|
||||
// If an existing link is being edited, check for the tracking flag `@TrackLink` at the end
|
||||
// of the url. Remove that from the URL and instead check the checkbox.
|
||||
let checked = false;
|
||||
if (!t.initialData.link !== '') {
|
||||
const t2 = t;
|
||||
const url = t2.initialData.url.value.replace(/@TrackLink$/, '');
|
||||
|
||||
if (t2.initialData.url.value !== url) {
|
||||
t2.initialData.url.value = url;
|
||||
checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the modal.
|
||||
const modal = oldEd.apply(this, [t, r]);
|
||||
|
||||
// Is it the link dialog?
|
||||
if (isOK) {
|
||||
// Insert tracking checkbox.
|
||||
const c = document.createElement('input');
|
||||
c.setAttribute('type', 'checkbox');
|
||||
|
||||
if (checked) {
|
||||
c.setAttribute('checked', checked);
|
||||
}
|
||||
|
||||
// Store the checkbox's state in the Vue instance to pick up from
|
||||
// the TinyMCE link conversion callback.
|
||||
c.onchange = (e) => {
|
||||
self.isTrackLink = e.target.checked;
|
||||
};
|
||||
|
||||
const l = document.createElement('label');
|
||||
l.appendChild(c);
|
||||
l.appendChild(document.createTextNode('Track link?'));
|
||||
l.classList.add('tox-label', 'tox-track-link');
|
||||
|
||||
document.querySelector('.tox-form__controls-h-stack .tox-control-wrap').appendChild(l);
|
||||
}
|
||||
return modal;
|
||||
};
|
||||
},
|
||||
|
||||
onEditorChange() {
|
||||
if (!this.isReady) {
|
||||
return;
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "E-maily",
|
||||
"campaigns.testSent": "Testovací zpráva odeslána",
|
||||
"campaigns.timestamps": "Časová razítka",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Pohledy",
|
||||
"dashboard.campaignViews": "Pohledy na kampaň",
|
||||
"dashboard.linkClicks": "Klepnutí na odkaz",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "E-Mails",
|
||||
"campaigns.testSent": "Testnachricht gesendet",
|
||||
"campaigns.timestamps": "Zeitstempel",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Ansichten",
|
||||
"dashboard.campaignViews": "Kampagnenansichten",
|
||||
"dashboard.linkClicks": "Linkklicks",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "E-mails",
|
||||
"campaigns.testSent": "Test message sent",
|
||||
"campaigns.timestamps": "Timestamps",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Views",
|
||||
"dashboard.campaignViews": "Campaign views",
|
||||
"dashboard.linkClicks": "Link clicks",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "Correos electrónicos",
|
||||
"campaigns.testSent": "Mensaje de prueba enviado",
|
||||
"campaigns.timestamps": "Marca de timepo",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Vistas",
|
||||
"dashboard.campaignViews": "Vista de campañas",
|
||||
"dashboard.linkClicks": "Vinculos cliqueados",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "Emails de test",
|
||||
"campaigns.testSent": "Message de test envoyé",
|
||||
"campaigns.timestamps": "Horodatages",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Vues",
|
||||
"dashboard.campaignViews": "vues de campagne",
|
||||
"dashboard.linkClicks": "clics sur liens",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "Emails di prova",
|
||||
"campaigns.testSent": "Messaggio di prova inviato",
|
||||
"campaigns.timestamps": "Marcatura temporale ",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Visualizzazioni",
|
||||
"dashboard.campaignViews": "Visualizzazioni della campagna",
|
||||
"dashboard.linkClicks": "Clic sui link",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "ഈ-മെയിലുകൾ",
|
||||
"campaigns.testSent": "ടെസ്റ്റ് സന്ദേശം അയച്ചു",
|
||||
"campaigns.timestamps": "സമയം",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "കാഴ്ചകൾ",
|
||||
"dashboard.campaignViews": "ക്യാമ്പേയ്ൻ കാഴ്ചകൾ",
|
||||
"dashboard.linkClicks": "കണ്ണിയിലെ ക്ലിക്കുകൾ",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "E-maile",
|
||||
"campaigns.testSent": "Wiadomość testowa wysłana",
|
||||
"campaigns.timestamps": "Sygnatury czasowe",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Wyświetlenia",
|
||||
"dashboard.campaignViews": "Wyświetlenia kampanii",
|
||||
"dashboard.linkClicks": "Kliknięcia linków",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "E-mails",
|
||||
"campaigns.testSent": "Mensagem de teste enviada",
|
||||
"campaigns.timestamps": "Data e hora",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Visualizações",
|
||||
"dashboard.campaignViews": "Visualizações da campanha",
|
||||
"dashboard.linkClicks": "Links clicados",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "E-mails",
|
||||
"campaigns.testSent": "Mensagem de teste enviada",
|
||||
"campaigns.timestamps": "Carimbo de hora",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Visualizações",
|
||||
"dashboard.campaignViews": "Vista de campanhas",
|
||||
"dashboard.linkClicks": "Cliques nos links",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "Emailuri",
|
||||
"campaigns.testSent": "Mesaju de test a fost trimis",
|
||||
"campaigns.timestamps": "Marcaje de timp",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Vizualizări",
|
||||
"dashboard.campaignViews": "Vizualizări ale campaniei",
|
||||
"dashboard.linkClicks": "Clickuri pe link",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "E-mails",
|
||||
"campaigns.testSent": "Тестовое сообщение отправлено",
|
||||
"campaigns.timestamps": "Метки времени",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Просмотры",
|
||||
"dashboard.campaignViews": "Просмотров компании",
|
||||
"dashboard.linkClicks": "Кликов по ссылкам",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"campaigns.testEmails": "E-postalar",
|
||||
"campaigns.testSent": "Test mesajı gönderildi",
|
||||
"campaigns.timestamps": "Zaman etiketi",
|
||||
"campaigns.trackLink": "Track link",
|
||||
"campaigns.views": "Görüntülenme",
|
||||
"dashboard.campaignViews": "Kampanya görüntülenme Sayısı",
|
||||
"dashboard.linkClicks": "Linklerin tıklanması",
|
||||
|
|
|
@ -80,15 +80,23 @@ type regTplFunc struct {
|
|||
replace string
|
||||
}
|
||||
|
||||
// Regular expression for matching {{ Track "http://link.com" }} in the template
|
||||
// and substituting it with {{ Track "http://link.com" .Campaign.UUID .Subscriber.UUID }}
|
||||
// before compilation. This string gimmick is to make linking easier for users.
|
||||
var regTplFuncs = []regTplFunc{
|
||||
regTplFunc{
|
||||
// Convert the shorthand https://google.com@TrackLink to {{ TrackLink ... }}.
|
||||
// This is for WYSIWYG editors that encode and break quotes {{ "" }} when inserted
|
||||
// inside <a href="{{ TrackLink "https://these-quotes-break" }}>.
|
||||
{
|
||||
regExp: regexp.MustCompile(`(https?://.+?)@TrackLink`),
|
||||
replace: `{{ TrackLink "$1" . }}`,
|
||||
},
|
||||
|
||||
// Regular expression for matching {{ TrackLink "http://link.com" }} in the template
|
||||
// and substituting it with {{ Track "http://link.com" . }} (the dot context)
|
||||
// before compilation. This is to make linking easier for users.
|
||||
{
|
||||
regExp: regexp.MustCompile("{{(\\s+)?TrackLink\\s+?(\"|`)(.+?)(\"|`)(\\s+)?}}"),
|
||||
replace: `{{ TrackLink "$3" . }}`,
|
||||
},
|
||||
regTplFunc{
|
||||
{
|
||||
regExp: regexp.MustCompile(`{{(\s+)?(TrackView|UnsubscribeURL|OptinURL|MessageURL)(\s+)?}}`),
|
||||
replace: `{{ $2 . }}`,
|
||||
},
|
||||
|
|
|
@ -15,6 +15,20 @@
|
|||
color: #444;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f4f4f4f4;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
table td {
|
||||
border-color: #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
background-color: #fff;
|
||||
padding: 30px;
|
||||
|
|
Loading…
Reference in a new issue