Например, у вас на сайте сделана публикация постов фронтенд с помощью форм jetformbuilder. И допустим есть поле с текстом, то при перезагрузки страницы весь текст исчезнет. Пользователь ваш сайт и эту форму проклянёт. Особенно на телефоне можно случайно смахнуть и перезагрузить страницу.
У jetformbuilder есть официальный плагин аддон Save Form Progress, но он очень громоздкий и платный.
Поэтому мы добавим чуточку кода и сделаем своё автосохранение.
Зайдите в файлы сайта, в папку mu-plugins и добавьте jfb-save-progress-mu.php
<?php
/**
* Plugin Name: MU — JFB Save Progress (Lite)
* Description: Включает автосохранение JetFormBuilder только на страницах с шорткодом [jfb_save_progress].
* Version: 3.2.0
*/
if ( ! defined( 'WPINC' ) ) { die; }
/**
* Шорткод [jfb_save_progress]
* - Ничего не выводит на странице, кроме загрузчика JS.
* - JS грузится только если шорткод присутствует.
* - Повторные вызовы шорткода не дублируют загрузку.
*/
add_shortcode( 'jfb_save_progress', function () {
$src = esc_url( plugin_dir_url( __FILE__ ) . 'save-lite.js?v=3.2.0' );
ob_start(); ?>
<script>
(function(){
// грузим только один раз
if (window.JFB_SP_LOADED) return;
window.JFB_SP_LOADED = true;
window.JFB_SP = window.JFB_SP || {};
window.JFB_SP.enabled = true;
var s = document.createElement('script');
s.src = '<?php echo $src; ?>';
s.defer = true;
document.head.appendChild(s);
})();
</script>
<?php
return ob_get_clean();
} );И ещё файл save-lite.js
(function () {
'use strict';
// --- CONFIG: максимальный возраст сохранения в миллисекундах ---
// 1 час
var MAX_AGE_MS = 1 * 60 * 60 * 1000;
// --- Utils ---
function ready(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn, { once: true });
} else {
fn();
}
}
function getFormId(form) {
return form?.dataset?.formId || (form.querySelector('input[name="form_id"]') || {}).value || '';
}
function storageKey(formId) {
return 'jfb_sp:' + String(formId || 'unknown');
}
function clearFlagKey(formId) {
return 'jfb_sp:clear-on-load:' + String(formId || 'unknown');
}
function isSavable(el) {
if (!el || !el.name) return false;
if (el.disabled) return false;
if (el.closest && el.closest('[data-jfb-repeater-template="true"]')) return false;
const t = (el.type || '').toLowerCase();
const tag = (el.tagName || '').toLowerCase();
if (t === 'password' || t === 'file' || t === 'submit' || t === 'button' || t === 'image' || t === 'reset') return false;
if (tag === 'button') return false;
return true;
}
function debounce(fn, ms) {
let t; return function () { clearTimeout(t); t = setTimeout(() => fn.apply(this, arguments), ms); };
}
// --- WYSIWYG support ---
function syncTinyMCEIntoTextareas(form) {
if (!window.tinymce || !window.tinymce.editors) return;
try {
window.tinymce.editors.forEach(function (ed) {
if (!ed || !ed.id) return;
const textarea = document.getElementById(ed.id);
if (!textarea || !form.contains(textarea)) return;
const html = ed.getContent({ format: 'html' });
if (typeof html === 'string' && textarea.value !== html) {
textarea.value = html;
}
});
} catch (e) { /* ignore */ }
}
function syncContentEditableIntoHidden(form) {
const editors = form.querySelectorAll('[contenteditable="true"], .ql-editor');
editors.forEach(function (ed) {
const host = ed.closest('.jet-form-builder__field, .jet-form-builder-wysiwyg-field, .field, .jet-form-builder-row') || ed.parentElement;
if (!host) return;
const target = host.querySelector('textarea[name], input[type="hidden"][name]');
if (!target) return;
const html = ed.innerHTML;
if (typeof html === 'string' && target.value !== html) {
target.value = html;
}
});
}
function syncWysiwyg(form) {
syncTinyMCEIntoTextareas(form);
syncContentEditableIntoHidden(form);
}
// --- Collect/Restore ---
function collect(form) {
syncWysiwyg(form);
const data = {};
const els = form.querySelectorAll('input, textarea, select');
els.forEach(function (el) {
if (!isSavable(el)) return;
const name = el.name;
const type = (el.type || '').toLowerCase();
const tag = (el.tagName || '').toLowerCase();
if (type === 'checkbox' || type === 'radio') {
const group = form.querySelectorAll('input[name="' + CSS.escape(name) + '"]');
data[name] = Array.from(group).filter(i => i.checked).map(i => i.value);
} else if (tag === 'select') {
if (el.multiple) {
data[name] = Array.from(el.selectedOptions).map(o => o.value);
} else {
data[name] = el.value;
}
} else {
data[name] = el.value;
}
});
return data;
}
function restore(form, savedWrapped) {
if (!savedWrapped || typeof savedWrapped !== 'object') return;
// Проверим таймстамп, если установлен MAX_AGE_MS > 0
if (MAX_AGE_MS > 0 && savedWrapped.ts && (Date.now() - savedWrapped.ts) > MAX_AGE_MS) {
// старые данные — удалим и не восстанавливаем
try { localStorage.removeItem(storageKey(getFormId(form))); } catch (e) { /* ignore */ }
return;
}
var saved = savedWrapped.v || savedWrapped; // поддержка старого формата (без обёртки)
if (!saved || typeof saved !== 'object') return;
Object.keys(saved).forEach(function (name) {
const val = saved[name];
const nodes = form.querySelectorAll('[name="' + CSS.escape(name) + '"]');
if (!nodes.length) return;
const first = nodes[0];
const type = (first.type || '').toLowerCase();
const tag = (first.tagName || '').toLowerCase();
if (type === 'checkbox' || type === 'radio') {
const arr = Array.isArray(val) ? val : [val];
nodes.forEach(function (n) {
const shouldCheck = arr.includes(n.value);
if (n.checked !== shouldCheck) {
n.checked = shouldCheck;
n.dispatchEvent(new Event('input', { bubbles: true }));
n.dispatchEvent(new Event('change', { bubbles: true }));
}
});
} else if (tag === 'select') {
if (first.multiple && Array.isArray(val)) {
const set = new Set(val);
nodes.forEach(function (n) {
Array.from(n.options).forEach(o => { o.selected = set.has(o.value); });
n.dispatchEvent(new Event('input', { bubbles: true }));
n.dispatchEvent(new Event('change', { bubbles: true }));
});
} else {
nodes.forEach(function (n) {
if (n.value !== String(val)) {
n.value = String(val);
n.dispatchEvent(new Event('input', { bubbles: true }));
n.dispatchEvent(new Event('change', { bubbles: true }));
}
});
}
} else {
nodes.forEach(function (n) {
if (!isSavable(n)) return;
const v = (typeof val === 'string' || typeof val === 'number') ? String(val) : '';
if (n.value !== v) {
n.value = v;
n.dispatchEvent(new Event('input', { bubbles: true }));
n.dispatchEvent(new Event('change', { bubbles: true }));
}
});
}
});
// WYSIWYG: вернуть контент в active editors
if (window.tinymce && window.tinymce.editors) {
try {
window.tinymce.editors.forEach(function (ed) {
const textarea = document.getElementById(ed.id);
if (!textarea || !form.contains(textarea)) return;
const html = textarea.value || '';
if (ed.getContent({ format: 'html' }) !== html) {
ed.setContent(html);
}
});
} catch (e) { /* ignore */ }
}
}
// --- Attach per form ---
function attach(form) {
const formId = getFormId(form);
const key = storageKey(formId);
const clearKey = clearFlagKey(formId);
try {
if (sessionStorage.getItem(clearKey)) {
localStorage.removeItem(key);
sessionStorage.removeItem(clearKey);
}
} catch (e) { /* ignore */ }
try {
const raw = localStorage.getItem(key);
if (raw) {
try {
const parsed = JSON.parse(raw);
restore(form, parsed);
} catch (e) {
try { restore(form, JSON.parse(raw)); } catch (ee) { /* ignore */ }
}
}
} catch (e) { /* ignore */ }
const saveNow = () => {
try {
var payload = { ts: Date.now(), v: collect(form) };
localStorage.setItem(key, JSON.stringify(payload));
} catch (e) { /* quota/deny */ }
};
const saveDebounced = debounce(saveNow, 150);
form.addEventListener('input', e => { if (isSavable(e.target)) saveDebounced(); }, true);
form.addEventListener('change', e => { if (isSavable(e.target)) saveDebounced(); }, true);
// TinyMCE handlers
if (window.tinymce && window.tinymce.editors) {
try {
window.tinymce.editors.forEach(function (ed) {
const textarea = document.getElementById(ed.id);
if (!textarea || !form.contains(textarea)) return;
const handler = () => saveDebounced();
ed.on('change keyup input setcontent undo redo', handler);
});
if (typeof window.tinymce.on === 'function') {
window.tinymce.on('AddEditor', function (e) {
const ed = e.editor;
const textarea = document.getElementById(ed.id);
if (!textarea || !form.contains(textarea)) return;
const handler = () => saveDebounced();
ed.on('change keyup input setcontent undo redo', handler);
});
}
} catch (e) { /* ignore */ }
}
const editableHandler = (e) => {
if (!(e.target && (e.target.isContentEditable || (e.target.closest && e.target.closest('[contenteditable="true"], .ql-editor'))))) return;
syncContentEditableIntoHidden(form);
saveDebounced();
};
form.addEventListener('input', editableHandler, true);
form.addEventListener('keyup', editableHandler, true);
function clearNowAndFlag() {
try {
localStorage.removeItem(key);
sessionStorage.setItem(clearKey, '1');
} catch (e) { /* ignore */ }
}
form.addEventListener('reset', function () { clearNowAndFlag(); }, true);
const container = form.closest('.jet-form-builder') || form.parentElement || form;
const successSelectors = [
'.jet-form-builder__success-state',
'.jet-form-builder-message--success',
'.jet-form-builder__message.success',
'.jet-form-builder-message[data-type="success"]',
'[data-jfb-success="true"]',
'.jet-form-builder-success-state',
'.jet-form-builder__submit-success',
'.jet-form-builder-message--green'
];
const mo = new MutationObserver(() => {
const found = successSelectors.some(sel => container.querySelector(sel));
if (found) {
clearNowAndFlag();
mo.disconnect();
}
});
mo.observe(container, { childList: true, subtree: true });
document.addEventListener('jet-form-builder/ajax/on-success', function () { clearNowAndFlag(); });
document.addEventListener('jet-form-builder/form-submit-success', function () { clearNowAndFlag(); });
if (window.JetPlugins && window.JetPlugins.hooks && typeof window.JetPlugins.hooks.addAction === 'function') {
try {
const hooks = window.JetPlugins.hooks;
hooks.addAction('jet.fb.request.success', 'save-lite', function () { clearNowAndFlag(); });
hooks.addAction('jet.fb.form.success', 'save-lite', function () { clearNowAndFlag(); });
} catch (e) { /* ignore */ }
}
if (window.jQuery) {
try {
const $ = window.jQuery;
$(document).on('ajaxSuccess', function (_e, _xhr, settings) {
const u = (settings && (settings.url || '')) + '&' + (settings && (settings.data || ''));
if (typeof u === 'string' && /jet_form_builder|action=jet_form_builder_submit|\/jet-form-builder\//i.test(u)) {
clearNowAndFlag();
}
});
} catch (e) { /* ignore */ }
}
window.addEventListener('jfb:clear-storage', function (ev) {
const id = String(ev.detail?.formId || '');
if (!formId || id === String(formId)) {
clearNowAndFlag();
}
});
window.addEventListener('beforeunload', function () {
// ничего дополнительно не делаем
});
}
function clearOnLoadSweep() {
try {
const keys = Object.keys(sessionStorage);
keys.forEach(function (k) {
if (k.indexOf('jfb_sp:clear-on-load:') === 0) {
const formId = k.split('jfb_sp:clear-on-load:')[1] || '';
const lkey = storageKey(formId);
try { localStorage.removeItem(lkey); } catch (e) {}
sessionStorage.removeItem(k);
}
});
} catch (e) { /* ignore */ }
}
ready(function () {
clearOnLoadSweep();
const forms = document.querySelectorAll('form.jet-form-builder');
forms.forEach(function (f) { attach(f); });
});
})();Вставьте шорткод под формой, где нужно автосохранение [ jfb_save_progress ]
Поля очистятся после успешной отправки.
Важно! Нужно в настройках формы, отправку формы Submit Type поставить AJAX, иначе после отправки форма не очистится.
Автосохранение я сделала 60 минут. Это никак не влияет на ваш сайт/сервер, всё сохраняется временно в браузере.
Также если у вас несколько форм на одной странице можно чуть доработать код и сделать автосохранение для конкретных форм по id. Но я думаю это очень маловеротяно.
Вот в целом и всё.
Кстати реальный кейс, где это может пригодиться.
Есть сайт, где можно разместить небольшую историю о своей собаке. Если с телефона заполнять форму, то когда выбираешь 3-5 фото в медиа галерею (а на телефоне у всех большие фотки от 8-12мб), то сайт как бы немного подвисает и страница может сама перезагрузиться. В целом на телефоне легко случайно обновить страницу. Поэтому лучше (нужно 😅) сделать автосохранение.
Также, если Вы делаете публикацию постов с фронтенд:
как сделать первое фото в галерее миниатюрой поста
как удалять медиа при удалении поста