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