diff --git a/addon/components/paper-grid-list.hbs b/addon/components/paper-grid-list.hbs index 2282181b1..b753abdcc 100644 --- a/addon/components/paper-grid-list.hbs +++ b/addon/components/paper-grid-list.hbs @@ -1,3 +1,5 @@ -{{yield (hash - tile=(component "paper-grid-tile") -)}} + + {{yield (hash + tile=(component "paper-grid-tile" parent=this) + )}} + \ No newline at end of file diff --git a/addon/components/paper-grid-list.js b/addon/components/paper-grid-list.js index 823a387be..49b944082 100644 --- a/addon/components/paper-grid-list.js +++ b/addon/components/paper-grid-list.js @@ -1,16 +1,11 @@ -/* eslint-disable ember/classic-decorator-no-classic-methods, ember/no-classic-components, ember/no-computed-properties-in-native-classes, ember/no-get */ /** * @module ember-paper */ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; import { inject as service } from '@ember/service'; - -import { tagName } from '@ember-decorators/component'; -import Component from '@ember/component'; -import { computed } from '@ember/object'; -import { bind, debounce } from '@ember/runloop'; -import { ParentMixin } from 'ember-composability-tools'; import gridLayout from '../utils/grid-layout'; -import { invokeAction } from 'ember-paper/utils/invoke-action'; const mediaRegex = /(^|\s)((?:print-)|(?:[a-z]{2}-){1,2})?(\d+)(?!\S)/g; const rowHeightRegex = @@ -38,77 +33,161 @@ const applyStyles = (el, styles) => { } }; -@tagName('md-grid-list') -export default class PaperGridList extends Component.extend(ParentMixin) { +/** + * A responsive grid list component that arranges child tiles in a configurable grid layout + * + * @class PaperGridList + * @extends Component + * @arg {string} class + * @arg {string} cols + * @arg {string} gutter + * @arg {string} rowHeight + */ +export default class PaperGridList extends Component { + /** + * Service containing media query breakpoints and constants + */ @service constants; - get tiles() { - return this.childComponents; + /** + * Set of child grid tile components + * @type {Set} + */ + @tracked children; + /** + * Currently active media query breakpoints + * @type {Array} + */ + @tracked currentMedia; + /** + * RAF ID for debouncing media query updates + * @type {number} + */ + @tracked debounceUpdateCurrentMedia; + /** + * RAF ID for debouncing grid updates + * @type {number} + */ + @tracked debounceUpdateGrid; + /** + * Reference to the component's DOM element + * @type {HTMLElement} + */ + @tracked element; + /** + * Map of media query listener instances + * @type {Object} + */ + @tracked listenerList = {}; + /** + * Map of media query change handler functions + * @type {Object} + */ + @tracked listeners = {}; + /** + * Map of active media query states + * @type {Object} + */ + @tracked media = {}; + /** + * Number of rows in the grid + * @type {number} + */ + @tracked rowCount; + + constructor() { + super(...arguments); + + this.children = new Set(); } - didInsertElement() { - super.didInsertElement(...arguments); + @action didInsertNode(element) { + this.element = element; this._installMediaListener(); } - didUpdate() { - super.didUpdate(...arguments); + @action didUpdateNode() { + if (this.debounceUpdateGrid) { + window.cancelAnimationFrame(this.debounceUpdateGrid); + } - // Debounces until the next run loop - debounce(this, this.updateGrid, 0); + // Debounce until the next frame + this.debounceUpdateGrid = window.requestAnimationFrame( + this.updateGrid.bind(this) + ); } - willDestroyElement() { - super.willDestroyElement(...arguments); + willDestroy() { + super.willDestroy(...arguments); this._uninstallMediaListener(); } + /** + * Registers a child tile component + * @param {PaperGridTile} tile - The tile component to register + */ + @action registerChild(tile) { + this.children.add(tile); + } + + /** + * Unregisters a child tile component + * @param {PaperGridTile} tile - The tile component to unregister + */ + @action unregisterChild(tile) { + this.children.delete(tile); + } + // Sets up a listener for each media query _installMediaListener() { - for (let mediaName in this.get('constants.MEDIA')) { - let query = this.get('constants.MEDIA')[mediaName] || media(mediaName); + for (let mediaName in this.constants.MEDIA) { + let query = this.constants.MEDIA[mediaName] || media(mediaName); let mediaList = window.matchMedia(query); let listenerName = mediaListenerName(mediaName); // Sets mediaList to a property so removeListener can access it - this.set(`${listenerName}List`, mediaList); + this.listenerList[`${listenerName}List`] = mediaList; // Creates a function based on mediaName so that removeListener can remove it. - this.set( - listenerName, - bind(this, (result) => { - this._mediaDidChange(mediaName, result.matches); - }) - ); + let onchange = (result) => { + this._mediaDidChange(mediaName, result.matches); + }; + this.listeners[listenerName] = onchange.bind(this); // Trigger initial grid calculations this._mediaDidChange(mediaName, mediaList.matches); - mediaList.addListener(this[listenerName]); + mediaList.addListener(this.listeners[listenerName]); } } _uninstallMediaListener() { - for (let mediaName in this.get('constants.MEDIA')) { + for (let mediaName in this.constants.MEDIA) { let listenerName = mediaListenerName(mediaName); - let mediaList = this.get(`${listenerName}List`); - mediaList.removeListener(this[listenerName]); + let mediaList = this.listenerList[`${listenerName}List`]; + if (mediaList) { + mediaList.removeListener(this.listeners[listenerName]); + } } } _mediaDidChange(mediaName, matches) { - this.set(mediaName, matches); + this.media[mediaName] = matches; // Debounces until the next run loop - debounce(this, this._updateCurrentMedia, 0); + if (this.debounceUpdateCurrentMedia) { + window.cancelAnimationFrame(this.debounceUpdateCurrentMedia); + } + this.debounceUpdateCurrentMedia = window.requestAnimationFrame( + this._updateCurrentMedia.bind(this) + ); } _updateCurrentMedia() { - let mediaPriorities = this.get('constants.MEDIA_PRIORITY'); - let currentMedia = mediaPriorities.filter((mediaName) => - this.get(mediaName) + let mediaPriorities = this.constants.MEDIA_PRIORITY; + this.currentMedia = mediaPriorities.filter( + (mediaName) => this.media[mediaName] ); - this.set('currentMedia', currentMedia); this.updateGrid(); } @@ -116,8 +195,10 @@ export default class PaperGridList extends Component.extend(ParentMixin) { updateGrid() { applyStyles(this.element, this._gridStyle()); - this.tiles.forEach((tile) => tile.updateTile()); - invokeAction(this, 'onUpdate'); + this.children.forEach((tile) => tile.updateTile()); + if (this.args.onUpdate) { + this.args.onUpdate(); + } } _gridStyle() { @@ -171,26 +252,28 @@ export default class PaperGridList extends Component.extend(ParentMixin) { // Calculates tile positions _setTileLayout() { - let tiles = this.orderedTiles(); + let tiles = this.orderedTiles; let layoutInfo = gridLayout(this.currentCols, tiles); - tiles.forEach((tile, i) => tile.set('position', layoutInfo.positions[i])); + tiles.forEach((tile, i) => { + tile.position = layoutInfo.positions[i]; + }); - this.set('rowCount', layoutInfo.rowCount); + this.rowCount = layoutInfo.rowCount; } - // Sorts tiles by their order in the dom - orderedTiles() { + /** + * Returns child tiles sorted by DOM order + * @type {Array} + */ + get orderedTiles() { // Convert NodeList to native javascript array, to be able to use indexOf. let domTiles = Array.prototype.slice.call( this.element.querySelectorAll('md-grid-tile') ); - return this.tiles.sort((a, b) => { - return domTiles.indexOf(a.get('element')) > - domTiles.indexOf(b.get('element')) - ? 1 - : -1; + return Array.from(this.children).sort((a, b) => { + return domTiles.indexOf(a.element) > domTiles.indexOf(b.element) ? 1 : -1; }); } @@ -218,36 +301,51 @@ export default class PaperGridList extends Component.extend(ParentMixin) { return sizes.base; } - @computed('cols') + /** + * Returns the parsed responsive column sizes + * @type {Object} + */ get colsMedia() { - let sizes = this._extractResponsiveSizes(this.cols); + let sizes = this._extractResponsiveSizes(this.args.cols); if (Object.keys(sizes).length === 0) { throw new Error('md-grid-list: No valid cols found'); } return sizes; } - @computed('colsMedia', 'currentMedia.[]') + /** + * Returns the current number of columns based on active media queries + * @type {number} + */ get currentCols() { return this._getAttributeForMedia(this.colsMedia, this.currentMedia) || 1; } - @computed('gutter') + /** + * Returns the parsed responsive gutter sizes + * @type {Object} + */ get gutterMedia() { - return this._extractResponsiveSizes(this.gutter, rowHeightRegex); + return this._extractResponsiveSizes(this.args.gutter, rowHeightRegex); } - @computed('gutterMedia', 'currentMedia.[]') + /** + * Returns the current gutter size based on active media queries + * @type {string} + */ get currentGutter() { return this._applyDefaultUnit( this._getAttributeForMedia(this.gutterMedia, this.currentMedia) || 1 ); } - @computed('rowHeight') + /** + * Returns the parsed responsive row heights + * @type {Object} + */ get rowHeightMedia() { let rowHeights = this._extractResponsiveSizes( - this.rowHeight, + this.args.rowHeight, rowHeightRegex ); if (Object.keys(rowHeights).length === 0) { @@ -256,15 +354,29 @@ export default class PaperGridList extends Component.extend(ParentMixin) { return rowHeights; } - @computed('rowHeightMedia', 'currentMedia.[]') + /** + * Returns the calculated row height based on the current media query. + * @returns {string} + */ + get rowHeight() { + return this._getAttributeForMedia(this.rowHeightMedia, this.currentMedia); + } + + /** + * Current row height mode ('fixed', 'ratio', or 'fit') + * @type {string} + */ + get currentRowMode() { + return this._getRowMode(this.rowHeight); + } + + /** + * Returns the current row height based on the row mode. + * @type {string|number|undefined} + */ get currentRowHeight() { - let rowHeight = this._getAttributeForMedia( - this.rowHeightMedia, - this.currentMedia - ); - // eslint-disable-next-line ember/no-side-effects - this.set('currentRowMode', this._getRowMode(rowHeight)); - switch (this._getRowMode(rowHeight)) { + let rowHeight = this.rowHeight; + switch (this.currentRowMode) { case 'fixed': { return this._applyDefaultUnit(rowHeight); }