github.com/DerekStrickland/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 });