annotate core/modules/quickedit/js/views/FieldDecorationView.es6.js @ 19:fa3358dc1485 tip

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