state.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import $ from 'jquery';
  2. import Immutable from 'immutable';
  3. import immutablediff from 'immutablediff';
  4. import '../utils/jquery-utils';
  5. let FormLoadState = {};
  6. const DOMBehaviors = {
  7. attach() {
  8. this.preventUnload();
  9. this.preventClickAway();
  10. },
  11. preventUnload() {
  12. let selector = '[name="task"][value^="save"], [data-delete-action]';
  13. if ($._data(window, 'events') && ($._data(window, 'events').beforeunload || []).filter((event) => event.namespace === '_grav').length) {
  14. return;
  15. }
  16. // Allow some elements to leave the page without native confirmation
  17. $(selector).on('click._grav', function(event) {
  18. $(global).off('beforeunload');
  19. });
  20. // Catch browser uri change / refresh attempt and stop it if the form state is dirty
  21. $(global).on('beforeunload._grav', () => {
  22. if (Instance.equals() === false) {
  23. return 'You have made changes on this page that you have not yet confirmed. If you navigate away from this page you will lose your unsaved changes.';
  24. }
  25. });
  26. },
  27. preventClickAway() {
  28. let selector = 'a[href]:not([href^="#"]):not([target="_blank"]):not([href^="javascript:"])';
  29. if ($._data($(selector).get(0), 'events') && ($._data($(selector).get(0), 'events').click || []).filter((event) => event.namespace === '_grav')) {
  30. return;
  31. }
  32. // Prevent clicking away if the form state is dirty
  33. // instead, display a confirmation before continuing
  34. $(selector).on('click._grav', function(event) {
  35. let isClean = Instance.equals();
  36. if (isClean === null || isClean) { return true; }
  37. event.preventDefault();
  38. let destination = $(this).attr('href');
  39. let modal = $('[data-remodal-id="changes"]');
  40. let lookup = $.remodal.lookup[modal.data('remodal')];
  41. let buttons = $('a.button', modal);
  42. let handler = function(event) {
  43. event.preventDefault();
  44. let action = $(this).data('leave-action');
  45. buttons.off('click', handler);
  46. lookup.close();
  47. if (action === 'continue') {
  48. $(global).off('beforeunload');
  49. global.location.href = destination;
  50. }
  51. };
  52. buttons.on('click', handler);
  53. lookup.open();
  54. });
  55. }
  56. };
  57. export default class FormState {
  58. constructor(options = {
  59. ignore: [],
  60. form_id: 'blueprints'
  61. }) {
  62. this.options = options;
  63. this.refresh();
  64. if (!this.form || !this.fields.length) { return; }
  65. FormLoadState = this.collect();
  66. this.loadState = FormLoadState;
  67. DOMBehaviors.attach();
  68. }
  69. refresh() {
  70. this.form = $(`form#${this.options.form_id}`).filter(':noparents(.remodal)');
  71. this.fields = $(`form#${this.options.form_id} *, [form="${this.options.form_id}"]`).filter(':input:not(.no-form)').filter(':noparents(.remodal)');
  72. return this;
  73. }
  74. collect() {
  75. if (!this.form || !this.fields.length) { return; }
  76. let values = {};
  77. this.refresh().fields.each((index, field) => {
  78. field = $(field);
  79. let name = field.prop('name');
  80. let type = field.prop('type');
  81. let tag = field.prop('tagName').toLowerCase();
  82. let value;
  83. if (name.startsWith('toggleable_') || name === 'data[lang]' || name === 'data[redirect]') {
  84. return;
  85. }
  86. switch (type) {
  87. case 'checkbox':
  88. value = field.is(':checked');
  89. break;
  90. case 'radio':
  91. if (!field.is(':checked')) { return; }
  92. value = field.val();
  93. break;
  94. default:
  95. value = field.val();
  96. }
  97. if (tag === 'select' && value === null) {
  98. value = '';
  99. }
  100. if (Array.isArray(value)) {
  101. value = value.join('|');
  102. }
  103. if (name && !~this.options.ignore.indexOf(name)) {
  104. values[name] = value;
  105. }
  106. });
  107. return Immutable.OrderedMap(values);
  108. }
  109. diff() {
  110. return immutablediff(FormLoadState, this.collect());
  111. }
  112. // When the form doesn't exist or there are no fields, `equals` returns `null`
  113. // for this reason, _NEVER_ check with !Instance.equals(), use Instance.equals() === false
  114. equals() {
  115. if (!this.form || !this.fields.length) { return null; }
  116. return Immutable.is(FormLoadState, this.collect());
  117. }
  118. };
  119. export let Instance = new FormState();
  120. export { DOMBehaviors };