FieldDecorationView.es6.js 10 KB

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