github.com/outbrain/consul@v1.4.5/ui-v2/app/components/tabular-collection.js (about)

     1  import Component from 'ember-collection/components/ember-collection';
     2  import needsRevalidate from 'ember-collection/utils/needs-revalidate';
     3  import identity from 'ember-collection/utils/identity';
     4  import Grid from 'ember-collection/layouts/grid';
     5  import SlotsMixin from 'block-slots';
     6  import WithResizing from 'consul-ui/mixins/with-resizing';
     7  import style from 'ember-computed-style';
     8  import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
     9  import sibling from 'consul-ui/utils/dom/sibling';
    10  import closest from 'consul-ui/utils/dom/closest';
    11  import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor';
    12  const clickFirstAnchor = clickFirstAnchorFactory(closest);
    13  
    14  import { computed, get, set } from '@ember/object';
    15  /**
    16   * Heavily extended `ember-collection` component
    17   * This adds support for z-index calculations to enable
    18   * Popup menus to pop over either rows above or below
    19   * the popup.
    20   * Additionally adds calculations for figuring out what the height
    21   * of the tabular component should be depending on the other elements
    22   * in the page.
    23   * Currently everything is here together for clarity, but to be split up
    24   * in the future
    25   */
    26  
    27  // ember doesn't like you using `$` hence `$$`
    28  const $$ = qsaFactory();
    29  // need to copy Cell in wholesale as there is no way to import it
    30  // there is no change made to `Cell` here, its only here as its
    31  // private in `ember-collection`
    32  // TODO: separate both Cell and ZIndexedGrid out
    33  class Cell {
    34    constructor(key, item, index, style) {
    35      this.key = key;
    36      this.hidden = false;
    37      this.item = item;
    38      this.index = index;
    39      this.style = style;
    40    }
    41  }
    42  // this is an amount of rows in the table NOT items
    43  // unlikely to have 10000 DOM rows ever :)
    44  const maxZIndex = 10000;
    45  // Adds z-index styling to the default Grid
    46  class ZIndexedGrid extends Grid {
    47    formatItemStyle(index, w, h, checked) {
    48      let style = super.formatItemStyle(index, w, h);
    49      // count backwards from maxZIndex
    50      let zIndex = maxZIndex - index;
    51      // apart from the row that contains an opened dropdown menu
    52      // this one should be highest z-index, so use max plus 1
    53      if (checked == index) {
    54        zIndex = maxZIndex + 1;
    55      }
    56      style += 'z-index: ' + zIndex;
    57      return style;
    58    }
    59  }
    60  /**
    61   * The tabular-collection can contain 'actions' the UI for which
    62   * uses dropdown 'action groups', so a group of different actions.
    63   * State makes use of native HTML state using radiogroups
    64   * to ensure that only a single dropdown can be open at one time.
    65   * Therefore we listen to change events to do anything extra when
    66   * a dropdown is opened (the change function is bound to the instance of
    67   * the `tabular-component` on init, hoisted here for visibility)
    68   *
    69   * The extra functionality we have here is to detect whether the opened
    70   * dropdown menu would be cut off or not if it 'dropped down'.
    71   * If it would be cut off we use CSS to 'drop it up' instead.
    72   * We also set this row to have the max z-index here, and mark this
    73   * row as the 'checked row' for when a scroll/grid re-calculation is
    74   * performed
    75   */
    76  const change = function(e) {
    77    if (e instanceof MouseEvent) {
    78      return;
    79    }
    80    // TODO: Why am I getting a jQuery event here?!
    81    if (e instanceof Event) {
    82      const value = e.currentTarget.value;
    83      if (value != get(this, 'checked')) {
    84        set(this, 'checked', value);
    85        // 'actions_close' would mean that all menus have been closed
    86        // therefore we don't need to calculate
    87        if (e.currentTarget.getAttribute('id') !== 'actions_close') {
    88          const $tr = closest('tr', e.currentTarget);
    89          const $group = sibling(e.currentTarget, 'ul');
    90          const $footer = [...$$('footer[role="contentinfo"]')][0];
    91          const groupRect = $group.getBoundingClientRect();
    92          const footerRect = $footer.getBoundingClientRect();
    93          const groupBottom = groupRect.top + $group.clientHeight;
    94          const footerTop = footerRect.top;
    95          if (groupBottom > footerTop) {
    96            $group.classList.add('above');
    97          } else {
    98            $group.classList.remove('above');
    99          }
   100          $tr.style.zIndex = maxZIndex + 1;
   101        }
   102      } else {
   103        set(this, 'checked', null);
   104      }
   105    } else if (e.detail && e.detail.index) {
   106      if (e.detail.confirming) {
   107        this.confirming.push(e.detail.index);
   108      } else {
   109        const pos = this.confirming.indexOf(e.detail.index);
   110        if (pos !== -1) {
   111          this.confirming.splice(pos, 1);
   112        }
   113      }
   114    }
   115  };
   116  export default Component.extend(SlotsMixin, WithResizing, {
   117    tagName: 'table',
   118    classNames: ['dom-recycling'],
   119    attributeBindings: ['style'],
   120    width: 1150,
   121    height: 500,
   122    style: style('getStyle'),
   123    checked: null,
   124    hasCaption: false,
   125    init: function() {
   126      this._super(...arguments);
   127      this.change = change.bind(this);
   128      this.confirming = [];
   129      // TODO: The row height should auto calculate properly from the CSS
   130      this['cell-layout'] = new ZIndexedGrid(get(this, 'width'), 50);
   131    },
   132    getStyle: computed('height', function() {
   133      return {
   134        height: get(this, 'height'),
   135      };
   136    }),
   137    resize: function(e) {
   138      const $tbody = this.element;
   139      const $appContent = [...$$('main > div')][0];
   140      if ($appContent) {
   141        const border = 1;
   142        const rect = $tbody.getBoundingClientRect();
   143        const $footer = [...$$('footer[role="contentinfo"]')][0];
   144        const space = rect.top + $footer.clientHeight + border;
   145        const height = e.detail.height - space;
   146        this.set('height', Math.max(0, height));
   147        // TODO: The row height should auto calculate properly from the CSS
   148        this['cell-layout'] = new ZIndexedGrid($appContent.clientWidth, 50);
   149        this.updateItems();
   150        this.updateScrollPosition();
   151      }
   152    },
   153    willRender: function() {
   154      this._super(...arguments);
   155      set(this, 'hasCaption', this._isRegistered('caption'));
   156      set(this, 'hasActions', this._isRegistered('actions'));
   157    },
   158    // `ember-collection` bug workaround
   159    // https://github.com/emberjs/ember-collection/issues/138
   160    _needsRevalidate: function() {
   161      if (this.isDestroyed || this.isDestroying) {
   162        return;
   163      }
   164      if (this._isGlimmer2()) {
   165        this.rerender();
   166      } else {
   167        needsRevalidate(this);
   168      }
   169    },
   170    // need to overwrite this completely so I can pass through the checked index
   171    // unfortunately the nicest way I could think to do this is to copy this in wholesale
   172    // to add an extra argument for `formatItemStyle` in 3 places
   173    // tradeoff between changing as little code as possible in the original code
   174    updateCells: function() {
   175      if (!this._items) {
   176        return;
   177      }
   178      const numItems = get(this._items, 'length');
   179      if (this._cellLayout.length !== numItems) {
   180        this._cellLayout.length = numItems;
   181      }
   182  
   183      var priorMap = this._cellMap;
   184      var cellMap = Object.create(null);
   185  
   186      var index = this._cellLayout.indexAt(
   187        this._scrollLeft,
   188        this._scrollTop,
   189        this._clientWidth,
   190        this._clientHeight
   191      );
   192      var count = this._cellLayout.count(
   193        this._scrollLeft,
   194        this._scrollTop,
   195        this._clientWidth,
   196        this._clientHeight
   197      );
   198      var items = this._items;
   199      var bufferBefore = Math.min(index, this._buffer);
   200      index -= bufferBefore;
   201      count += bufferBefore;
   202      count = Math.min(count + this._buffer, get(items, 'length') - index);
   203      var i, style, itemIndex, itemKey, cell;
   204  
   205      var newItems = [];
   206  
   207      for (i = 0; i < count; i++) {
   208        itemIndex = index + i;
   209        itemKey = identity(items.objectAt(itemIndex));
   210        if (priorMap) {
   211          cell = priorMap[itemKey];
   212        }
   213        if (cell) {
   214          // additional `checked` argument
   215          style = this._cellLayout.formatItemStyle(
   216            itemIndex,
   217            this._clientWidth,
   218            this._clientHeight,
   219            this.checked
   220          );
   221          set(cell, 'style', style);
   222          set(cell, 'hidden', false);
   223          set(cell, 'key', itemKey);
   224          cellMap[itemKey] = cell;
   225        } else {
   226          newItems.push(itemIndex);
   227        }
   228      }
   229  
   230      for (i = 0; i < this._cells.length; i++) {
   231        cell = this._cells[i];
   232        if (!cellMap[cell.key]) {
   233          if (newItems.length) {
   234            itemIndex = newItems.pop();
   235            let item = items.objectAt(itemIndex);
   236            itemKey = identity(item);
   237            // additional `checked` argument
   238            style = this._cellLayout.formatItemStyle(
   239              itemIndex,
   240              this._clientWidth,
   241              this._clientHeight,
   242              this.checked
   243            );
   244            set(cell, 'style', style);
   245            set(cell, 'key', itemKey);
   246            set(cell, 'index', itemIndex);
   247            set(cell, 'item', item);
   248            set(cell, 'hidden', false);
   249            cellMap[itemKey] = cell;
   250          } else {
   251            set(cell, 'hidden', true);
   252            set(cell, 'style', 'height: 0; display: none;');
   253          }
   254        }
   255      }
   256  
   257      for (i = 0; i < newItems.length; i++) {
   258        itemIndex = newItems[i];
   259        let item = items.objectAt(itemIndex);
   260        itemKey = identity(item);
   261        // additional `checked` argument
   262        style = this._cellLayout.formatItemStyle(
   263          itemIndex,
   264          this._clientWidth,
   265          this._clientHeight,
   266          this.checked
   267        );
   268        cell = new Cell(itemKey, item, itemIndex, style);
   269        cellMap[itemKey] = cell;
   270        this._cells.pushObject(cell);
   271      }
   272      this._cellMap = cellMap;
   273    },
   274    actions: {
   275      click: function(e) {
   276        return clickFirstAnchor(e);
   277      },
   278    },
   279  });