diff --git a/core/includes/form.inc b/core/includes/form.inc index 226d4a6..d7741d1 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -871,6 +871,9 @@ function drupal_process_form($form_id, &$form, &$form_state) { // In case of errors, do not break HTML IDs of other forms. drupal_static_reset('drupal_html_id'); } + elseif (!empty($form['#autosave'])) { + $form['#autosave']['#presave'] = TRUE; + } if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild']) { // Execute form submit handlers. @@ -949,6 +952,13 @@ function drupal_process_form($form_id, &$form, &$form_state) { } } + if (!empty($form['#autosave']['#enabled'])) { + drupal_add_js(array('formAutosave' => array($form['#id'] => array( + 'form' => $form['#id'], + 'enabled' => TRUE, + 'presave' => !empty($form['#autosave']['#presave'])))), 'setting'); + } + // After processing the form, the form builder or a #process callback may // have set $form_state['cache'] to indicate that the form and form state // shall be cached. But the form may only be cached if the 'no_cache' property diff --git a/core/misc/form.autosave.js b/core/misc/form.autosave.js new file mode 100644 index 0000000..1aedf13 --- /dev/null +++ b/core/misc/form.autosave.js @@ -0,0 +1,254 @@ +(function ($) { + +"use strict"; + +/** + * Automatically save the contents of a form + * using localStorage when it is modified. + */ +Drupal.behaviors.formAutoSave = { + attach: function(context, settings) { + + // Handle each form on the page. + for (var formId in settings.formAutosave) { + var $form = $('#' + formId); + + // Get the last time this form was saved to the server and + // make sure it is a number. In the case where changed is + // not set on the form, this will be missing (NaN). + var changed = parseInt($form.find("input[name='changed']").val(), 10); + + // Update the form with values from localStorage if available. + var changedForm = Drupal.form.localUpdate(formId, changed); + + // If the form was updated from localStorage, alert the user. + if (changedForm === true) { + var localEditWarning = $(Drupal.theme('formLocalEditsWarning')); + var clearFormLink = $('' + Drupal.t('Discard previous changes') + ''); + localEditWarning.append(clearFormLink); + clearFormLink.on('click', function() { + $form[0].reset(); + Drupal.form.localClear(formId); + localEditWarning.fadeOut('slow'); + }); + localEditWarning.insertBefore('#' + formId).hide().fadeIn('slow'); + } + + // Make an initial backup if the form requires it, + // for example on preview or on form validation error. + if (settings.formAutosave[formId].presave) { + Drupal.form.localSave($form[0], changed); + } + + // On a formUpdated event, save the form's values to localStorage. + $form.on('change keyup', function() { + Drupal.form.localSave($form[0], changed); + }); + + // Clear the saved form when the user deliberatley + // clicks the overlay close link. + $(document).on('drupalOverlayCloseByLink', function() { + Drupal.form.localClear(formId); + }); + + // Clear form on any submission of data to the server. + $form.on('submit', function() { + Drupal.form.localClear(formId); + }); + }; + } +}; + +/** + * Namespace form functions in the form object. + */ +Drupal.form = {}; + +/** + * Save form values into localStorage. + * + * @param element form + * The form on the page whose values to save. + * @param int|NaN changed + * The last time the form was saved on the server + * or NaN if not specified. + */ +Drupal.form.localSave = function (form, changed) { + + var formId = form.id; + var $form = $(form); + + var serializedForm = { + // Store the last time this form was saved on the server. + _changed: changed + }; + + // Serialize all form text elements. + $form.find(".form-text").each(function() { + serializedForm[this.id] = this.value; + }); + + // Serialize all form textarea elements. + $form.find(".form-textarea").each(function() { + serializedForm[this.id] = this.value; + }); + + // Serialize all form checkbox elements. + $form.find(".form-checkbox").each(function() { + serializedForm[this.id] = this.checked; + }); + + // Serialize all form select elements. + $form.find(".form-select").each(function() { + var selectId = this.id; + var $select = $(this); + serializedForm[selectId] = []; + $select.find(":selected").each(function() { + serializedForm[selectId].push($select.val()); + }); + }); + + // Serialize all form radio elements. + $form.find(".form-radio").each(function() { + serializedForm[this.id] = this.checked; + }); + + var saveString = JSON.stringify(serializedForm); + + // Put all serialized items into localStorage. + localStorage.setItem(Drupal.form.getLocalFormId(formId), saveString); +}; + +/** + * Updates the DOM representation of a form with a serialised + * version. + * + * @param string formId + * The form id to update from local + * @param int|NaN changed + * The form last updated timestamp from the server. + * If the local copy is behind the server then it will + * not be loaded. + * If changed is NaN then the form does not provide this + * so we can only assume local copy is latest. + * @param array[object] formValues + * (optional) A loaded array of form elements + * If not provided, the values will be loaded + * from localStorage. + * + * @return boolean + * TRUE if something was updated otherwise FALSE. + */ +Drupal.form.localUpdate = function(formId, changed, formValues) { + + // If changed is not provided, make it NaN. + changed = typeof changed !== 'undefined' ? changed : Number.NaN; + + // If formValues was not given, try to load from localStorage. + formValues = typeof formValues !== 'undefined' ? formValues : Drupal.form.localLoad(formId); + + if (formValues.length === 0) { + // If there is nothing to update leave here. + return false; + } + + // Check if the local data values for the form are out of date + // compared with the latest from the server. + if ( !isNaN(changed) && (isNaN(formValues._changed) || formValues._changed !== changed) ) { + // The local copy is out of date so do not load it. + return false; + } + + // Remove the _changed data item before loading formValues. + delete formValues._changed; + + // Loop over all form values and apply. + for ( var key in formValues ) { + + // Get the DOM element to set a saved value for. + var el = document.getElementById(key); + + if (el) { + // Get the type of element, its either sum subtype + // of INPUT or a TEXTAREA. + var type = el.tagName === 'INPUT' ? $(el).attr('type') : el.tagName; + + if ( type === 'TEXTAREA' || type === 'text') { + el.value = formValues[key]; + } + else if ( type === 'checkbox' || type === 'radio' ) { + if (el.checked != formValues[key]) { + // We want to physically click this as it might + // invoke other JS events such as displaying + // previously hidden page furniture. + el.click(); + } + } + else if ( type === 'SELECT' ) { + for ( var optionsIndex = 0; optionsIndex < el.options.length; optionsIndex++ ) { + el.options[optionsIndex].selected = $.inArray(el.options[optionsIndex].value, formValues[key]) > -1; + } + } + } + } + + return true; +}; + +/** + * Loads form values from the localStorage + * + * @param string formId + * The id of the form + * + * @return array[object] + * An array of form value objects + */ +Drupal.form.localLoad = function (formId) { + + var serializedForm = localStorage.getItem(Drupal.form.getLocalFormId(formId)); + + if (typeof(serializedForm) !== 'string') { + return []; + } + + return JSON.parse( serializedForm ); +}; + +/** + * Clear a form from localStorage. + * + * @param string formId + * The form id to remove from localStorage. + */ +Drupal.form.localClear = function (formId) { + localStorage.removeItem(Drupal.form.getLocalFormId(formId)); +}; + + +/** + * Get Local Storage ID from Form ID + * + * @param string formId + * The form id to get LocalStorage ID for. + * + * @return string + * A local storage id of the form: + * "Drupal.Form.node.add.article.article-node-form" + */ +Drupal.form.getLocalFormId = function (formId) { + return 'Drupal.Form' + window.location.pathname.replace(/\//g, '.') + formId; +}; + +/** + * Themed warning message to display when a forms + * values from Drupal are overriden with localStorage. + * + * @return string + * The warning message. + */ +Drupal.theme.formLocalEditsWarning = function () { + return '
' + Drupal.t("You have previous changes which have not been saved yet. ") + '
'; +}; + +})(jQuery); diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php index 215ad81..36acd24 100644 --- a/core/modules/node/lib/Drupal/node/NodeFormController.php +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -56,10 +56,17 @@ protected function prepareEntity(EntityInterface $node) { */ public function form(array $form, array &$form_state, EntityInterface $node) { $user_config = config('user.settings'); + + // Let the node form use localstorage javascript mechanism. + $form['#autosave'] = array('#enabled' => TRUE); + // Some special stuff when previewing a node. if (isset($form_state['node_preview'])) { $form['#prefix'] = $form_state['node_preview']; $node->in_preview = TRUE; + + // In preview state, backup the unsaved state of the node on load. + $form['#autosave']['#presave'] = TRUE; } else { unset($node->in_preview); diff --git a/core/modules/overlay/overlay-parent.js b/core/modules/overlay/overlay-parent.js index 1f38f2f..3053088 100644 --- a/core/modules/overlay/overlay-parent.js +++ b/core/modules/overlay/overlay-parent.js @@ -137,7 +137,8 @@ Drupal.overlay.create = function () { .bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRefreshPage')) .bind('drupalOverlayBeforeClose' + eventClass + ' drupalOverlayBeforeLoad' + eventClass + - ' drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerDispatchEvent')); + ' drupalOverlayResize' + eventClass + + ' drupalOverlayCloseByLink' + eventClass, $.proxy(this, 'eventhandlerDispatchEvent')); if ($('.overlay-displace-top, .overlay-displace-bottom').length) { $(document) @@ -569,6 +570,11 @@ Drupal.overlay.eventhandlerOverrideLink = function (event) { // Close the overlay when the link contains the overlay-close class. if ($target.hasClass('overlay-close')) { + // Trigger event informing scripts the overlay was + // deliberatley closed rather than just navigating away. + var linkCloseOverlayEvent = $.Event('drupalOverlayCloseByLink'); + $(document).trigger(linkCloseOverlayEvent); + // Clearing the overlay URL fragment will close the overlay. $.bbq.removeState('overlay'); return; diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 6401361..1d03489 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1204,6 +1204,7 @@ function system_library_info() { 'version' => VERSION, 'js' => array( 'core/misc/form.js' => array('group' => JS_LIBRARY, 'weight' => 1), + 'core/misc/form.autosave.js' => array('group' => JS_LIBRARY, 'weight' => 2), ), 'dependencies' => array( array('system', 'jquery'),