Chris@0: /** Chris@0: * @file Chris@0: * A Backbone View that decorates the in-place edited element. Chris@0: */ Chris@0: Chris@4: (function($, Backbone, Drupal) { Chris@4: Drupal.quickedit.FieldDecorationView = Backbone.View.extend( Chris@4: /** @lends Drupal.quickedit.FieldDecorationView# */ { Chris@4: /** Chris@4: * @type {null} Chris@4: */ Chris@4: _widthAttributeIsEmpty: null, Chris@0: Chris@4: /** Chris@4: * @type {object} Chris@4: */ Chris@4: events: { Chris@4: 'mouseenter.quickedit': 'onMouseEnter', Chris@4: 'mouseleave.quickedit': 'onMouseLeave', Chris@4: click: 'onClick', Chris@4: 'tabIn.quickedit': 'onMouseEnter', Chris@4: 'tabOut.quickedit': 'onMouseLeave', Chris@4: }, Chris@0: Chris@4: /** Chris@4: * @constructs Chris@4: * Chris@4: * @augments Backbone.View Chris@4: * Chris@4: * @param {object} options Chris@4: * An object with the following keys: Chris@4: * @param {Drupal.quickedit.EditorView} options.editorView Chris@4: * The editor object view. Chris@4: */ Chris@4: initialize(options) { Chris@4: this.editorView = options.editorView; Chris@0: Chris@4: this.listenTo(this.model, 'change:state', this.stateChange); Chris@4: this.listenTo( Chris@4: this.model, Chris@4: 'change:isChanged change:inTempStore', Chris@4: this.renderChanged, Chris@4: ); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * @inheritdoc Chris@4: */ Chris@4: remove() { Chris@4: // The el property is the field, which should not be removed. Remove the Chris@4: // pointer to it, then call Backbone.View.prototype.remove(). Chris@4: this.setElement(); Chris@4: Backbone.View.prototype.remove.call(this); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Determines the actions to take given a change of state. Chris@4: * Chris@4: * @param {Drupal.quickedit.FieldModel} model Chris@4: * The `FieldModel` model. Chris@4: * @param {string} state Chris@4: * The state of the associated field. One of Chris@4: * {@link Drupal.quickedit.FieldModel.states}. Chris@4: */ Chris@4: stateChange(model, state) { Chris@4: const from = model.previous('state'); Chris@4: const to = state; Chris@4: switch (to) { Chris@4: case 'inactive': Chris@4: this.undecorate(); Chris@4: break; Chris@0: Chris@4: case 'candidate': Chris@4: this.decorate(); Chris@4: if (from !== 'inactive') { Chris@4: this.stopHighlight(); Chris@4: if (from !== 'highlighted') { Chris@4: this.model.set('isChanged', false); Chris@4: this.stopEdit(); Chris@4: } Chris@4: } Chris@4: this._unpad(); Chris@4: break; Chris@0: Chris@4: case 'highlighted': Chris@4: this.startHighlight(); Chris@4: break; Chris@4: Chris@4: case 'activating': Chris@4: // NOTE: this state is not used by every editor! It's only used by Chris@4: // those that need to interact with the server. Chris@4: this.prepareEdit(); Chris@4: break; Chris@4: Chris@4: case 'active': Chris@4: if (from !== 'activating') { Chris@4: this.prepareEdit(); Chris@0: } Chris@4: if (this.editorView.getQuickEditUISettings().padding) { Chris@4: this._pad(); Chris@4: } Chris@4: break; Chris@0: Chris@4: case 'changed': Chris@4: this.model.set('isChanged', true); Chris@4: break; Chris@0: Chris@4: case 'saving': Chris@4: break; Chris@0: Chris@4: case 'saved': Chris@4: break; Chris@0: Chris@4: case 'invalid': Chris@4: break; Chris@4: } Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Adds a class to the edited element that indicates whether the field has Chris@4: * been changed by the user (i.e. locally) or the field has already been Chris@4: * changed and stored before by the user (i.e. remotely, stored in Chris@4: * PrivateTempStore). Chris@4: */ Chris@4: renderChanged() { Chris@4: this.$el.toggleClass( Chris@4: 'quickedit-changed', Chris@4: this.model.get('isChanged') || this.model.get('inTempStore'), Chris@4: ); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Starts hover; transitions to 'highlight' state. Chris@4: * Chris@4: * @param {jQuery.Event} event Chris@4: * The mouse event. Chris@4: */ Chris@4: onMouseEnter(event) { Chris@4: const that = this; Chris@4: that.model.set('state', 'highlighted'); Chris@4: event.stopPropagation(); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Stops hover; transitions to 'candidate' state. Chris@4: * Chris@4: * @param {jQuery.Event} event Chris@4: * The mouse event. Chris@4: */ Chris@4: onMouseLeave(event) { Chris@4: const that = this; Chris@4: that.model.set('state', 'candidate', { reason: 'mouseleave' }); Chris@4: event.stopPropagation(); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Transition to 'activating' stage. Chris@4: * Chris@4: * @param {jQuery.Event} event Chris@4: * The click event. Chris@4: */ Chris@4: onClick(event) { Chris@4: this.model.set('state', 'activating'); Chris@4: event.preventDefault(); Chris@4: event.stopPropagation(); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Adds classes used to indicate an elements editable state. Chris@4: */ Chris@4: decorate() { Chris@4: this.$el.addClass('quickedit-candidate quickedit-editable'); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Removes classes used to indicate an elements editable state. Chris@4: */ Chris@4: undecorate() { Chris@4: this.$el.removeClass( Chris@4: 'quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing', Chris@4: ); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Adds that class that indicates that an element is highlighted. Chris@4: */ Chris@4: startHighlight() { Chris@4: // Animations. Chris@4: const that = this; Chris@4: // Use a timeout to grab the next available animation frame. Chris@4: that.$el.addClass('quickedit-highlighted'); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Removes the class that indicates that an element is highlighted. Chris@4: */ Chris@4: stopHighlight() { Chris@4: this.$el.removeClass('quickedit-highlighted'); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Removes the class that indicates that an element as editable. Chris@4: */ Chris@4: prepareEdit() { Chris@4: this.$el.addClass('quickedit-editing'); Chris@0: Chris@4: // Allow the field to be styled differently while editing in a pop-up Chris@4: // in-place editor. Chris@4: if (this.editorView.getQuickEditUISettings().popup) { Chris@4: this.$el.addClass('quickedit-editor-is-popup'); Chris@4: } Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Removes the class that indicates that an element is being edited. Chris@4: * Chris@4: * Reapplies the class that indicates that a candidate editable element is Chris@4: * again available to be edited. Chris@4: */ Chris@4: stopEdit() { Chris@4: this.$el.removeClass('quickedit-highlighted quickedit-editing'); Chris@0: Chris@4: // Done editing in a pop-up in-place editor; remove the class. Chris@4: if (this.editorView.getQuickEditUISettings().popup) { Chris@4: this.$el.removeClass('quickedit-editor-is-popup'); Chris@4: } Chris@0: Chris@4: // Make the other editors show up again. Chris@4: $('.quickedit-candidate').addClass('quickedit-editable'); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Adds padding around the editable element to make it pop visually. Chris@4: */ Chris@4: _pad() { Chris@4: // Early return if the element has already been padded. Chris@4: if (this.$el.data('quickedit-padded')) { Chris@4: return; Chris@4: } Chris@4: const self = this; Chris@0: Chris@4: // Add 5px padding for readability. This means we'll freeze the current Chris@4: // width and *then* add 5px padding, hence ensuring the padding is added Chris@4: // "on the outside". Chris@4: // 1) Freeze the width (if it's not already set); don't use animations. Chris@4: if (this.$el[0].style.width === '') { Chris@4: this._widthAttributeIsEmpty = true; Chris@4: this.$el Chris@4: .addClass('quickedit-animate-disable-width') Chris@4: .css('width', this.$el.width()); Chris@4: } Chris@0: Chris@4: // 2) Add padding; use animations. Chris@4: const posProp = this._getPositionProperties(this.$el); Chris@4: setTimeout(() => { Chris@4: // Re-enable width animations (padding changes affect width too!). Chris@4: self.$el.removeClass('quickedit-animate-disable-width'); Chris@0: Chris@4: // Pad the editable. Chris@4: self.$el Chris@4: .css({ Chris@4: position: 'relative', Chris@4: top: `${posProp.top - 5}px`, Chris@4: left: `${posProp.left - 5}px`, Chris@4: 'padding-top': `${posProp['padding-top'] + 5}px`, Chris@4: 'padding-left': `${posProp['padding-left'] + 5}px`, Chris@4: 'padding-right': `${posProp['padding-right'] + 5}px`, Chris@4: 'padding-bottom': `${posProp['padding-bottom'] + 5}px`, Chris@4: 'margin-bottom': `${posProp['margin-bottom'] - 10}px`, Chris@4: }) Chris@4: .data('quickedit-padded', true); Chris@4: }, 0); Chris@4: }, Chris@0: Chris@4: /** Chris@4: * Removes the padding around the element being edited when editing ceases. Chris@4: */ Chris@4: _unpad() { Chris@4: // Early return if the element has not been padded. Chris@4: if (!this.$el.data('quickedit-padded')) { Chris@4: return; Chris@4: } Chris@4: const self = this; Chris@0: Chris@4: // 1) Set the empty width again. Chris@4: if (this._widthAttributeIsEmpty) { Chris@4: this.$el.addClass('quickedit-animate-disable-width').css('width', ''); Chris@4: } Chris@0: Chris@4: // 2) Remove padding; use animations (these will run simultaneously with) Chris@4: // the fading out of the toolbar as its gets removed). Chris@4: const posProp = this._getPositionProperties(this.$el); Chris@4: setTimeout(() => { Chris@4: // Re-enable width animations (padding changes affect width too!). Chris@4: self.$el.removeClass('quickedit-animate-disable-width'); Chris@0: Chris@4: // Unpad the editable. Chris@4: self.$el.css({ Chris@0: position: 'relative', Chris@0: top: `${posProp.top + 5}px`, Chris@0: left: `${posProp.left + 5}px`, Chris@0: 'padding-top': `${posProp['padding-top'] - 5}px`, Chris@0: 'padding-left': `${posProp['padding-left'] - 5}px`, Chris@0: 'padding-right': `${posProp['padding-right'] - 5}px`, Chris@0: 'padding-bottom': `${posProp['padding-bottom'] - 5}px`, Chris@0: 'margin-bottom': `${posProp['margin-bottom'] + 10}px`, Chris@0: }); Chris@4: }, 0); Chris@4: // Remove the marker that indicates that this field has padding. This is Chris@4: // done outside the timed out function above so that we don't get numerous Chris@4: // queued functions that will remove padding before the data marker has Chris@4: // been removed. Chris@4: this.$el.removeData('quickedit-padded'); Chris@4: }, Chris@4: Chris@4: /** Chris@4: * Gets the top and left properties of an element. Chris@4: * Chris@4: * Convert extraneous values and information into numbers ready for Chris@4: * subtraction. Chris@4: * Chris@4: * @param {jQuery} $e Chris@4: * The element to get position properties from. Chris@4: * Chris@4: * @return {object} Chris@4: * An object containing css values for the needed properties. Chris@4: */ Chris@4: _getPositionProperties($e) { Chris@4: let p; Chris@4: const r = {}; Chris@4: const props = [ Chris@4: 'top', Chris@4: 'left', Chris@4: 'bottom', Chris@4: 'right', Chris@4: 'padding-top', Chris@4: 'padding-left', Chris@4: 'padding-right', Chris@4: 'padding-bottom', Chris@4: 'margin-bottom', Chris@4: ]; Chris@4: Chris@4: const propCount = props.length; Chris@4: for (let i = 0; i < propCount; i++) { Chris@4: p = props[i]; Chris@4: r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10); Chris@4: } Chris@4: return r; Chris@4: }, Chris@4: Chris@4: /** Chris@4: * Replaces blank or 'auto' CSS `position: ` values with "0px". Chris@4: * Chris@4: * @param {string} [pos] Chris@4: * The value for a CSS position declaration. Chris@4: * Chris@4: * @return {string} Chris@4: * A CSS value that is valid for `position`. Chris@4: */ Chris@4: _replaceBlankPosition(pos) { Chris@4: if (pos === 'auto' || !pos) { Chris@4: pos = '0px'; Chris@4: } Chris@4: return pos; Chris@4: }, Chris@0: }, Chris@4: ); Chris@4: })(jQuery, Backbone, Drupal);