FieldDecorationView.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. /**
  2. * @file
  3. * A Backbone View that decorates the in-place edited element.
  4. */
  5. (function ($, Backbone, Drupal) {
  6. 'use strict';
  7. Drupal.quickedit.FieldDecorationView = Backbone.View.extend(/** @lends Drupal.quickedit.FieldDecorationView# */{
  8. /**
  9. * @type {null}
  10. */
  11. _widthAttributeIsEmpty: null,
  12. /**
  13. * @type {object}
  14. */
  15. events: {
  16. 'mouseenter.quickedit': 'onMouseEnter',
  17. 'mouseleave.quickedit': 'onMouseLeave',
  18. 'click': 'onClick',
  19. 'tabIn.quickedit': 'onMouseEnter',
  20. 'tabOut.quickedit': 'onMouseLeave'
  21. },
  22. /**
  23. * @constructs
  24. *
  25. * @augments Backbone.View
  26. *
  27. * @param {object} options
  28. * An object with the following keys:
  29. * @param {Drupal.quickedit.EditorView} options.editorView
  30. * The editor object view.
  31. */
  32. initialize: function (options) {
  33. this.editorView = options.editorView;
  34. this.listenTo(this.model, 'change:state', this.stateChange);
  35. this.listenTo(this.model, 'change:isChanged change:inTempStore', this.renderChanged);
  36. },
  37. /**
  38. * @inheritdoc
  39. */
  40. remove: function () {
  41. // The el property is the field, which should not be removed. Remove the
  42. // pointer to it, then call Backbone.View.prototype.remove().
  43. this.setElement();
  44. Backbone.View.prototype.remove.call(this);
  45. },
  46. /**
  47. * Determines the actions to take given a change of state.
  48. *
  49. * @param {Drupal.quickedit.FieldModel} model
  50. * The `FieldModel` model.
  51. * @param {string} state
  52. * The state of the associated field. One of
  53. * {@link Drupal.quickedit.FieldModel.states}.
  54. */
  55. stateChange: function (model, state) {
  56. var from = model.previous('state');
  57. var to = state;
  58. switch (to) {
  59. case 'inactive':
  60. this.undecorate();
  61. break;
  62. case 'candidate':
  63. this.decorate();
  64. if (from !== 'inactive') {
  65. this.stopHighlight();
  66. if (from !== 'highlighted') {
  67. this.model.set('isChanged', false);
  68. this.stopEdit();
  69. }
  70. }
  71. this._unpad();
  72. break;
  73. case 'highlighted':
  74. this.startHighlight();
  75. break;
  76. case 'activating':
  77. // NOTE: this state is not used by every editor! It's only used by
  78. // those that need to interact with the server.
  79. this.prepareEdit();
  80. break;
  81. case 'active':
  82. if (from !== 'activating') {
  83. this.prepareEdit();
  84. }
  85. if (this.editorView.getQuickEditUISettings().padding) {
  86. this._pad();
  87. }
  88. break;
  89. case 'changed':
  90. this.model.set('isChanged', true);
  91. break;
  92. case 'saving':
  93. break;
  94. case 'saved':
  95. break;
  96. case 'invalid':
  97. break;
  98. }
  99. },
  100. /**
  101. * Adds a class to the edited element that indicates whether the field has
  102. * been changed by the user (i.e. locally) or the field has already been
  103. * changed and stored before by the user (i.e. remotely, stored in
  104. * PrivateTempStore).
  105. */
  106. renderChanged: function () {
  107. this.$el.toggleClass('quickedit-changed', this.model.get('isChanged') || this.model.get('inTempStore'));
  108. },
  109. /**
  110. * Starts hover; transitions to 'highlight' state.
  111. *
  112. * @param {jQuery.Event} event
  113. * The mouse event.
  114. */
  115. onMouseEnter: function (event) {
  116. var that = this;
  117. that.model.set('state', 'highlighted');
  118. event.stopPropagation();
  119. },
  120. /**
  121. * Stops hover; transitions to 'candidate' state.
  122. *
  123. * @param {jQuery.Event} event
  124. * The mouse event.
  125. */
  126. onMouseLeave: function (event) {
  127. var that = this;
  128. that.model.set('state', 'candidate', {reason: 'mouseleave'});
  129. event.stopPropagation();
  130. },
  131. /**
  132. * Transition to 'activating' stage.
  133. *
  134. * @param {jQuery.Event} event
  135. * The click event.
  136. */
  137. onClick: function (event) {
  138. this.model.set('state', 'activating');
  139. event.preventDefault();
  140. event.stopPropagation();
  141. },
  142. /**
  143. * Adds classes used to indicate an elements editable state.
  144. */
  145. decorate: function () {
  146. this.$el.addClass('quickedit-candidate quickedit-editable');
  147. },
  148. /**
  149. * Removes classes used to indicate an elements editable state.
  150. */
  151. undecorate: function () {
  152. this.$el.removeClass('quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing');
  153. },
  154. /**
  155. * Adds that class that indicates that an element is highlighted.
  156. */
  157. startHighlight: function () {
  158. // Animations.
  159. var that = this;
  160. // Use a timeout to grab the next available animation frame.
  161. that.$el.addClass('quickedit-highlighted');
  162. },
  163. /**
  164. * Removes the class that indicates that an element is highlighted.
  165. */
  166. stopHighlight: function () {
  167. this.$el.removeClass('quickedit-highlighted');
  168. },
  169. /**
  170. * Removes the class that indicates that an element as editable.
  171. */
  172. prepareEdit: function () {
  173. this.$el.addClass('quickedit-editing');
  174. // Allow the field to be styled differently while editing in a pop-up
  175. // in-place editor.
  176. if (this.editorView.getQuickEditUISettings().popup) {
  177. this.$el.addClass('quickedit-editor-is-popup');
  178. }
  179. },
  180. /**
  181. * Removes the class that indicates that an element is being edited.
  182. *
  183. * Reapplies the class that indicates that a candidate editable element is
  184. * again available to be edited.
  185. */
  186. stopEdit: function () {
  187. this.$el.removeClass('quickedit-highlighted quickedit-editing');
  188. // Done editing in a pop-up in-place editor; remove the class.
  189. if (this.editorView.getQuickEditUISettings().popup) {
  190. this.$el.removeClass('quickedit-editor-is-popup');
  191. }
  192. // Make the other editors show up again.
  193. $('.quickedit-candidate').addClass('quickedit-editable');
  194. },
  195. /**
  196. * Adds padding around the editable element to make it pop visually.
  197. */
  198. _pad: function () {
  199. // Early return if the element has already been padded.
  200. if (this.$el.data('quickedit-padded')) {
  201. return;
  202. }
  203. var self = this;
  204. // Add 5px padding for readability. This means we'll freeze the current
  205. // width and *then* add 5px padding, hence ensuring the padding is added
  206. // "on the outside".
  207. // 1) Freeze the width (if it's not already set); don't use animations.
  208. if (this.$el[0].style.width === '') {
  209. this._widthAttributeIsEmpty = true;
  210. this.$el
  211. .addClass('quickedit-animate-disable-width')
  212. .css('width', this.$el.width());
  213. }
  214. // 2) Add padding; use animations.
  215. var posProp = this._getPositionProperties(this.$el);
  216. setTimeout(function () {
  217. // Re-enable width animations (padding changes affect width too!).
  218. self.$el.removeClass('quickedit-animate-disable-width');
  219. // Pad the editable.
  220. self.$el
  221. .css({
  222. 'position': 'relative',
  223. 'top': posProp.top - 5 + 'px',
  224. 'left': posProp.left - 5 + 'px',
  225. 'padding-top': posProp['padding-top'] + 5 + 'px',
  226. 'padding-left': posProp['padding-left'] + 5 + 'px',
  227. 'padding-right': posProp['padding-right'] + 5 + 'px',
  228. 'padding-bottom': posProp['padding-bottom'] + 5 + 'px',
  229. 'margin-bottom': posProp['margin-bottom'] - 10 + 'px'
  230. })
  231. .data('quickedit-padded', true);
  232. }, 0);
  233. },
  234. /**
  235. * Removes the padding around the element being edited when editing ceases.
  236. */
  237. _unpad: function () {
  238. // Early return if the element has not been padded.
  239. if (!this.$el.data('quickedit-padded')) {
  240. return;
  241. }
  242. var self = this;
  243. // 1) Set the empty width again.
  244. if (this._widthAttributeIsEmpty) {
  245. this.$el
  246. .addClass('quickedit-animate-disable-width')
  247. .css('width', '');
  248. }
  249. // 2) Remove padding; use animations (these will run simultaneously with)
  250. // the fading out of the toolbar as its gets removed).
  251. var posProp = this._getPositionProperties(this.$el);
  252. setTimeout(function () {
  253. // Re-enable width animations (padding changes affect width too!).
  254. self.$el.removeClass('quickedit-animate-disable-width');
  255. // Unpad the editable.
  256. self.$el
  257. .css({
  258. 'position': 'relative',
  259. 'top': posProp.top + 5 + 'px',
  260. 'left': posProp.left + 5 + 'px',
  261. 'padding-top': posProp['padding-top'] - 5 + 'px',
  262. 'padding-left': posProp['padding-left'] - 5 + 'px',
  263. 'padding-right': posProp['padding-right'] - 5 + 'px',
  264. 'padding-bottom': posProp['padding-bottom'] - 5 + 'px',
  265. 'margin-bottom': posProp['margin-bottom'] + 10 + 'px'
  266. });
  267. }, 0);
  268. // Remove the marker that indicates that this field has padding. This is
  269. // done outside the timed out function above so that we don't get numerous
  270. // queued functions that will remove padding before the data marker has
  271. // been removed.
  272. this.$el.removeData('quickedit-padded');
  273. },
  274. /**
  275. * Gets the top and left properties of an element.
  276. *
  277. * Convert extraneous values and information into numbers ready for
  278. * subtraction.
  279. *
  280. * @param {jQuery} $e
  281. * The element to get position properties from.
  282. *
  283. * @return {object}
  284. * An object containing css values for the needed properties.
  285. */
  286. _getPositionProperties: function ($e) {
  287. var p;
  288. var r = {};
  289. var props = [
  290. 'top', 'left', 'bottom', 'right',
  291. 'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
  292. 'margin-bottom'
  293. ];
  294. var propCount = props.length;
  295. for (var i = 0; i < propCount; i++) {
  296. p = props[i];
  297. r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10);
  298. }
  299. return r;
  300. },
  301. /**
  302. * Replaces blank or 'auto' CSS `position: <value>` values with "0px".
  303. *
  304. * @param {string} [pos]
  305. * The value for a CSS position declaration.
  306. *
  307. * @return {string}
  308. * A CSS value that is valid for `position`.
  309. */
  310. _replaceBlankPosition: function (pos) {
  311. if (pos === 'auto' || !pos) {
  312. pos = '0px';
  313. }
  314. return pos;
  315. }
  316. });
  317. })(jQuery, Backbone, Drupal);