Skip to content

Commit c6ae6ae

Browse files
authored
Merge pull request #1831 from codidact/0valt/caching
Remove draft deletion via AJAX before post submit
2 parents ff3ec40 + 554d93c commit c6ae6ae

4 files changed

Lines changed: 134 additions & 106 deletions

File tree

app/assets/javascripts/posts.js

Lines changed: 118 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
const IGNORE_UNSUPPORTED = [Comment, HTMLBodyElement];
33

44
$(() => {
5-
DOMPurify.addHook("uponSanitizeAttribute", (node, event) => {
6-
const rowspan = node.getAttribute("rowspan");
7-
const colspan = node.getAttribute("colspan");
5+
DOMPurify.addHook('uponSanitizeAttribute', (node, event) => {
6+
const rowspan = node.getAttribute('rowspan');
7+
const colspan = node.getAttribute('colspan');
88

99
if (rowspan && Number.isNaN(+rowspan)) {
1010
event.keepAttr = false;
@@ -29,7 +29,7 @@ $(() => {
2929
*/
3030
const stringInsert = (str, idx, insert) => str.slice(0, idx) + insert + str.slice(idx);
3131

32-
const placeholder = "![Uploading, please wait...]()";
32+
const placeholder = '![Uploading, please wait...]()';
3333

3434
$uploadForm.find('input[type="file"]').on('change', async (evt) => {
3535
/** @type {HTMLInputElement} */
@@ -39,7 +39,7 @@ $(() => {
3939

4040
postField.value = stringInsert(postText, cursorPos, placeholder);
4141

42-
$uploadForm.trigger('submit')
42+
$uploadForm.trigger('submit');
4343
});
4444

4545
QPixel.DOM?.watchClass('#markdown-image-upload.is-active', (target) => {
@@ -67,7 +67,8 @@ $(() => {
6767

6868
if (!isUploadModalOpened) {
6969
QPixel.createNotification('danger', `Can't upload files with size more than 2MB`);
70-
} else {
70+
}
71+
else {
7172
$tgt.find('.js-max-size').addClass('has-color-red-700 error-shake');
7273
setTimeout(() => {
7374
$tgt.find('.js-max-size').removeClass('error-shake');
@@ -82,7 +83,7 @@ $(() => {
8283

8384
const resp = await fetch($tgt.attr('action'), {
8485
method: $tgt.attr('method'),
85-
body: new FormData(/** @type {HTMLFormElement} */ ($tgt[0]))
86+
body: new FormData(/** @type {HTMLFormElement} */ ($tgt[0])),
8687
});
8788

8889
const data = await resp.json();
@@ -104,7 +105,7 @@ $(() => {
104105
$postField.val(postText.replace(placeholder, `![Image_alt_text](${data.link})`));
105106
$tgt.parents('.modal').removeClass('is-active');
106107

107-
$postFields.trigger('change')
108+
$postFields.trigger('change');
108109
});
109110

110111
$uploadForm.on('ajax:failure', async (evt, data) => {
@@ -168,13 +169,20 @@ $(() => {
168169
* @returns {Promise<boolean>}
169170
*/
170171
const deleteDraft = async ($field) => {
171-
const data = await QPixel.deleteDraft();
172+
// TODO: extra error handling should be moved to QPixel#deleteDraft
173+
try {
174+
const data = await QPixel.deleteDraft();
172175

173-
return QPixel.handleJSONResponse(data, () => {
174-
flashDraftStatus($field, 'draft deleted');
175-
removeDraftLoadedNotice();
176-
});
177-
}
176+
return QPixel.handleJSONResponse(data, () => {
177+
flashDraftStatus($field, 'draft deleted');
178+
removeDraftLoadedNotice();
179+
});
180+
}
181+
catch (error) {
182+
QPixel.createNotification('danger', `Failed to delete post draft`);
183+
return false;
184+
}
185+
};
178186

179187
/**
180188
* Helper for getting draft-related elements from a given event target
@@ -204,7 +212,7 @@ $(() => {
204212

205213
const $licenseField = $form.find('.js-license-select');
206214
const $excerptField = $form.find('.js-tag-excerpt');
207-
215+
208216
const $tagsField = $form.find('#post_tags_cache');
209217
const $titleField = $form.find('#post_title');
210218
const $commentField = $form.find('#edit_comment');
@@ -224,7 +232,7 @@ $(() => {
224232
comment: commentText,
225233
excerpt: excerptText,
226234
license: license,
227-
tags: Array.isArray(tags) ? tags: [],
235+
tags: Array.isArray(tags) ? tags : [],
228236
tag_name: tagName,
229237
title: titleText,
230238
};
@@ -269,7 +277,7 @@ $(() => {
269277
const eventData = /** @type {ClipboardEvent} */ (evt.originalEvent);
270278
if (eventData.clipboardData.files.length > 0) {
271279
// must be called to prevent raw file name to be inserted after the placeholder
272-
evt.preventDefault()
280+
evt.preventDefault();
273281

274282
/** @type {JQuery<HTMLInputElement>} */
275283
const $fileInput = $uploadForm.find('input[type="file"]');
@@ -278,53 +286,79 @@ $(() => {
278286
}
279287
});
280288

281-
$postFields.on('focus keyup paste change markdown', (() => {
289+
const onPostFieldChange = (() => {
290+
/** @type {string | null} */
282291
let previous = null;
292+
293+
/**
294+
* @param {JQuery.EventBase} evt
295+
*/
283296
return (evt) => {
284297
const $tgt = $(evt.target);
285298
const text = $(evt.target).val();
299+
286300
// Don't bother re-rendering if nothing's changed
287-
if (text === previous) { return; }
301+
if (text === previous) {
302+
return;
303+
}
304+
288305
previous = text;
306+
289307
if (!window.converter) {
290308
window.converter = window.markdownit({
291309
html: true,
292310
breaks: false,
293-
linkify: true
311+
linkify: true,
294312
});
295313
window.converter.use(window.markdownitFootnote);
296314
window.converter.use(window.latexEscape);
297315
}
316+
298317
window.setTimeout(() => {
299318
const converter = window.converter;
300319
const unsafe_html = converter.render(text);
301320
const html = DOMPurify.sanitize(unsafe_html, {
302321
ALLOWED_TAGS: QPixel.ALLOWED_POST_TAGS,
303-
ALLOWED_ATTR: QPixel.ALLOWED_POST_ATTRS
322+
ALLOWED_ATTR: QPixel.ALLOWED_POST_ATTRS,
304323
});
305324

306-
const removedElements = [...new Set(DOMPurify.removed
307-
.filter((entry) => entry.element && !IGNORE_UNSUPPORTED.some((ctor) => entry.element instanceof ctor))
308-
.map((entry) => entry.element.localName))];
309-
310-
const removedAttributes = [...new Set(DOMPurify.removed
311-
.filter((entry) => entry.attribute)
312-
.map((entry) => [
313-
entry.attribute.name + (entry.attribute.value ? `='${entry.attribute.value}'` : ''),
314-
entry.from.localName
315-
]))]
316-
317-
$tgt.parents('form')
325+
const removedElements = [
326+
...new Set(
327+
DOMPurify.removed
328+
.filter((entry) => entry.element && !IGNORE_UNSUPPORTED.some((ctor) => entry.element instanceof ctor))
329+
.map((entry) => entry.element.localName),
330+
),
331+
];
332+
333+
const removedAttributes = [
334+
...new Set(
335+
DOMPurify.removed
336+
.filter((entry) => entry.attribute)
337+
.map((entry) => [
338+
entry.attribute.name + (entry.attribute.value ? `='${entry.attribute.value}'` : ''),
339+
entry.from.localName,
340+
]),
341+
),
342+
];
343+
344+
$tgt
345+
.parents('form')
318346
.find('.rejected-elements')
319347
.toggleClass('hide', removedElements.length === 0 && removedAttributes.length === 0)
320348
.find('ul')
321349
.empty()
322350
.append(
323351
removedElements.map((name) => $(`<li><code>&lt;${name}&gt;</code></li>`)),
324-
removedAttributes.map(([attr, elName]) => $(`<li><code>${attr}</code> (in <code>&lt;${elName}&gt;</code>)</li>`)));
352+
removedAttributes.map(([attr, elName]) =>
353+
$(`<li><code>${attr}</code> (in <code>&lt;${elName}&gt;</code>)</li>`),
354+
),
355+
);
325356

326357
$tgt.parents('.form-group').siblings('.post-preview').html(html);
327-
$tgt.parents('form').find('.js-post-html[name="__html"]').val(html + '<!-- g: js, mdit -->');
358+
$tgt
359+
.parents('form')
360+
.find('.js-post-html[name="__html"]')
361+
.val(html + '<!-- g: js, mdit -->');
328362
}, 0);
329363

330364
if (featureTimeout) {
@@ -340,79 +374,60 @@ $(() => {
340374
}
341375
}, 1000);
342376
};
343-
})()).trigger('markdown');
377+
})();
378+
379+
$postFields.on('focus keyup paste change markdown', onPostFieldChange).trigger('markdown');
344380

345381
$postFields.parents('form').on('submit', async (ev) => {
346382
const $tgt = $(ev.target);
347383
const $field = $tgt.find('.js-post-field');
348384

349-
const draftDeleted = $tgt.attr('data-draft-deleted') === 'true';
350385
const isValidated = $tgt.attr('data-validated') === 'true';
351386

352-
if (draftDeleted && isValidated) {
387+
if (isValidated) {
353388
return;
354389
}
355390

356391
ev.preventDefault();
357392

358-
// Draft handling
359-
if (!draftDeleted) {
360-
const status = await deleteDraft($field);
361-
362-
if (status) {
363-
$tgt.attr('data-draft-deleted', 'true');
364-
365-
if (isValidated) {
366-
$tgt.submit();
367-
}
368-
}
369-
else {
370-
QPixel.createNotification('danger', `Failed to delete post draft. (${status})`);
371-
}
393+
const text = $field.val()?.toString();
394+
const validated = QPixel.validatePost(text);
395+
if (validated[0] === true) {
396+
$tgt.attr('data-validated', 'true');
397+
$tgt.submit();
372398
}
373-
374-
375-
// Validation
376-
if (!isValidated) {
377-
const text = $field.val()?.toString();
378-
const validated = QPixel.validatePost(text);
379-
if (validated[0] === true) {
380-
$tgt.attr('data-validated', 'true');
381-
$tgt.submit();
399+
else {
400+
const warnings = validated[1].filter((x) => x['type'] === 'warning');
401+
const errors = validated[1].filter((x) => x['type'] === 'error');
402+
403+
if (warnings.length > 0) {
404+
const $warningBox = $(`<div class="notice is-warning"></div>`);
405+
const $warningList = $(`<ul></ul>`);
406+
warnings.forEach((w) => {
407+
$warningList.append(`<li>${w['message']}</li>`);
408+
});
409+
$warningBox.append($warningList);
410+
$tgt.find('input[type="submit"]').before($warningBox);
382411
}
383-
else {
384-
const warnings = validated[1].filter((x) => x['type'] === 'warning');
385-
const errors = validated[1].filter((x) => x['type'] === 'error');
386-
387-
if (warnings.length > 0) {
388-
const $warningBox = $(`<div class="notice is-warning"></div>`);
389-
const $warningList = $(`<ul></ul>`);
390-
warnings.forEach((w) => {
391-
$warningList.append(`<li>${w['message']}</li>`);
392-
});
393-
$warningBox.append($warningList);
394-
$tgt.find('input[type="submit"]').before($warningBox);
395-
}
396412

397-
if (errors.length > 0) {
398-
const $errorBox = $(`<div class="notice is-danger"></div>`);
399-
const $errorList = $(`<ul></ul>`);
400-
errors.forEach((e) => {
401-
$errorList.append(`<li>${e['message']}</li>`);
402-
});
403-
$errorBox.append($errorList);
404-
$tgt.find('input[type="submit"]').before($errorBox);
405-
}
406-
407-
if (warnings.length > 0 && errors.length === 0) {
408-
$tgt.attr('data-validated', 'true');
409-
}
413+
if (errors.length > 0) {
414+
const $errorBox = $(`<div class="notice is-danger"></div>`);
415+
const $errorList = $(`<ul></ul>`);
416+
errors.forEach((e) => {
417+
$errorList.append(`<li>${e['message']}</li>`);
418+
});
419+
$errorBox.append($errorList);
420+
$tgt.find('input[type="submit"]').before($errorBox);
410421
}
411422

412-
setTimeout(() => {
413-
$tgt.find('input[type="submit"]').attr('disabled', null);
414-
}, 1000);
423+
if (warnings.length > 0 && errors.length === 0) {
424+
$tgt.attr('data-validated', 'true');
425+
}
415426
}
427+
428+
setTimeout(() => {
429+
$tgt.find('input[type="submit"]').attr('disabled', null);
430+
}, 1000);
416431
});
417432

418433
$('.js-draft-loaded').each((_i, e) => {
@@ -423,36 +438,34 @@ $(() => {
423438
});
424439

425440
const setCopyButtonState = ($button, state) => {
426-
const isSuccess = state === "success";
427-
const buttonClass = isSuccess ? "is-green" : "is-danger";
428-
const iconClass = isSuccess ? "fa-check" : "fa-times";
441+
const isSuccess = state === 'success';
442+
const buttonClass = isSuccess ? 'is-green' : 'is-danger';
443+
const iconClass = isSuccess ? 'fa-check' : 'fa-times';
429444

430-
const $icon = $button.find(".fa");
445+
const $icon = $button.find('.fa');
431446

432-
$icon.removeClass("fa-copy");
447+
$icon.removeClass('fa-copy');
433448
$icon.addClass(iconClass);
434449
$button.addClass(buttonClass);
435450

436451
setTimeout(() => {
437452
$icon.removeClass(iconClass);
438453
$button.removeClass(buttonClass);
439-
$icon.addClass("fa-copy");
454+
$icon.addClass('fa-copy');
440455
}, 1e3);
441456
};
442457

443-
$(".js-permalink-trigger").removeAttr("hidden");
458+
$('.js-permalink-trigger').removeAttr('hidden');
444459

445-
$(".js-permalink-copy").on("click", async (ev) => {
460+
$('.js-permalink-copy').on('click', async (ev) => {
446461
ev.preventDefault();
447462

448463
const $tgt = $(ev.target);
449464

450-
const $button = $tgt.hasClass("js-permalink-copy")
451-
? $tgt
452-
: $tgt.parents(".js-permalink-copy");
465+
const $button = $tgt.hasClass('js-permalink-copy') ? $tgt : $tgt.parents('.js-permalink-copy');
453466

454-
const postId = $button.data("post-id");
455-
const linkType = $button.data("link-type");
467+
const postId = $button.data('post-id');
468+
const linkType = $button.data('link-type');
456469

457470
if (!postId || !linkType) {
458471
return;
@@ -468,10 +481,10 @@ $(() => {
468481

469482
try {
470483
await navigator.clipboard.writeText(url);
471-
setCopyButtonState($button, "success");
484+
setCopyButtonState($button, 'success');
472485
}
473486
catch (_e) {
474-
setCopyButtonState($button, "error");
487+
setCopyButtonState($button, 'error');
475488
}
476489
});
477490

app/views/layouts/_head.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<% if SiteSetting['DonationsEnabled'] && (SiteSetting['LoadStripeEverywhere'] || controller_name == 'donations') %>
3131
<%= javascript_include_tag "https://js.stripe.com/v3/" %>
3232
<% end %>
33-
<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/dompurify@2.2.9/dist/purify.min.js" %>
33+
<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/dompurify@2.5.8/dist/purify.min.js" %>
3434
<%= javascript_include_tag "/assets/community/#{@community.host.split('.')[0]}.js" %>
3535
<%= javascript_include_tag 'application' %>
3636
<script src="https://cdn.jsdelivr.net/npm/@codidact/co-design@latest/js/co-design.js" defer></script>

0 commit comments

Comments
 (0)