Автосохранение в форме jetformbuilder

Например, у вас на сайте сделана публикация постов фронтенд с помощью форм 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мб), то сайт как бы немного подвисает и страница может сама перезагрузиться. В целом на телефоне легко случайно обновить страницу. Поэтому лучше (нужно 😅) сделать автосохранение.


Также, если Вы делаете публикацию постов с фронтенд:
как сделать первое фото в галерее миниатюрой поста
как удалять медиа при удалении поста

Picture of Автор: Александра

Автор: Александра

@avsalexandra
Занимаюсь натуральным питанием собак и кошек BARF. Wordpress для души ☺️

Crocoblock
Elementor
Gutenberg
Jetengine
Jetformbuilder
profile builder
Woocommerce
Wordpress
WYSIWYG
Лейка
#автосохранение
#доменная почта
#рассылка
#бейдж
#благотворительность
#заказ ожидает
#подарок
#подчёркивание
#публикация постов
#видео
#пожертвования
#мультивыбор
#роли
#drag and drop
#изображения товаров
#подписки
#распродажа
#личный кабинет
#пагинация
#alt text
#галерея товара
#аватар
#возврат
#видео товара
#купон
#отменить заказ
Комментарии:

Добавить комментарий