FieldDecorationView.es6.js 11 KB

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