import ol_geom_Geometry from 'ol/geom/Geometry.js' import ol_control_Control from 'ol/control/Control.js' import ol_ext_element from '../util/element.js'; /** Feature list control * * @constructor * @extends {ol_control_Control} * @fires select * @fires dblclick * @fires collapse * @fires resize * @fires sort * @param {Object=} options * @param {number} [options.title] table title * @param {Element} [options.target] to display the control outside the map * @param {string} [options.className] use `ol-bottom` to scroll at bottom (default top) * @param {boolean} [options.collapsed=true] collapse the list on start, default true * @param {Array|ol_Collection|ol_source_Vector} [features] a set of feature to display. If provided as Source or Collection the features will stay in sync. * @param {number} [options.pageLength=100] number of row to display in the table (page optimzation) */ var ol_control_FeatureList = class olcontrolFeatureList extends ol_control_Control { constructor(options) { options = options || {}; // Control element var element = ol_ext_element.create('DIV', { className: ((options.className || '') + ' ol-feature-list').trim() }) if (!options.target) { element.classList.add('ol-unselectable'); element.classList.add('ol-control'); element.dataset.control = 1; } super({ element: element, target: options.target }); // List of features / sort this._listFeatures = []; // Current columns this._columns = []; // Bottom scroll this._bottomScroll = element.classList.contains('ol-bottom'); // Page lengeh this.set('pageLength', options.pageLength || 100); // Enable sort this._canSort = []; this._sort = {}; // Button if (!options.target) { ol_ext_element.create('BUTTON', { type: 'button', click: function() { this.collapse() }.bind(this), parent: element }) } // List var content = ol_ext_element.create('DIV', { className: 'ol-content', parent: element }); // Menu header this._menu = ol_ext_element.create('DIV', { className: 'ol-header', html: '
', parent: content }) ol_ext_element.create('P', { text: options.title || '', parent: this._menu }) this.addButton({ className: 'ol-closebox', type: 'button', click: function() { this.collapse() }.bind(this), parent: this._menu }) // Sizer var sizer = ol_ext_element.create('DIV', { className: 'ol-sizer', parent: content }) sizer.addEventListener('mousedown', this._dragSizer.bind(this)); sizer.addEventListener('touchstart', this._dragSizer.bind(this)); // List array var list = this._list = ol_ext_element.create('TABLE', { className: 'ol-list', parent: ol_ext_element.create('DIV', { className: 'ol-scroll-container', tabindex: -1, parent: content }) }); list.parentNode.addEventListener('scroll', this._drawPage.bind(this)) this._head = ol_ext_element.create('THEAD', { parent: list }) this._listbody = ol_ext_element.create('TBODY', { parent: list }) // Collapse this.collapse(options.collapsed !== false); // Source this.setFeatures(options.features) } /** * Set the map instance the control is associated with * and add its controls associated to this map. * @param {ol_Map} map The map instance. */ setMap(map) { super.setMap(map); map.addEventListener('change:size', this.resize.bind(this)); this.resize(); } }; /** Scroll resize at botton * @param {boolean} b */ ol_control_FeatureList.prototype.setBottomScroll = function(b) { this._bottomScroll = !!b if (this._bottomScroll) { this.element.classList.add('ol-bottom') } else { this.element.classList.remove('ol-bottom') } } /** Scroll to an element / feature * @param {ol_Feature|string} feature a featue or `select` to scroll at the selected row */ ol_control_FeatureList.prototype.scrollTo = function(feature) { if (feature === 'select') { if (this._curSelection) return this.scrollTo(this._curSelection.feature); else return false; } else { var i = this._findFeatureIndex(feature) if (i >= 0) { var scrollDiv = this._list.parentNode; scrollDiv.scrollTo(scrollDiv.scrollLeft, (i+1.5)*this.getRowHeight() - scrollDiv.getBoundingClientRect().height/2) var fpage = this._findInPage(feature) return { feature: feature, tr: fpage ? fpage.tr : ol_ext_element.create('TR') } } } return false; } /** Find the feature index in the list * @param {ol_Feature} feature * @returns {Object} * @private */ ol_control_FeatureList.prototype._findFeatureIndex = function(feature) { var list = this._listFeatures; if (list) { for (var i=0; i|Array} source a vector source or a collection of features to list */ ol_control_FeatureList.prototype.setFeatures = function(source) { if (!this._drawbind) { this._drawbind = this._drawList.bind(this) } // Remove listeners if (this._source && this._source.un) { this._source.un(['loadend', 'addfeature', 'removefeature', 'add', 'remove'], this._drawbind) } // New source this._source = source || []; this._drawList(); // Refresh on change if (this._source.on) { this._source.on(['loadend', 'addfeature', 'removefeature', 'add', 'remove'], this._drawbind) } } /** Sort list by properties * @param {string} prop property name * @param {string} [sort] 'up' or 'down', default remove sort * */ ol_control_FeatureList.prototype.sortBy = function(prop, sort) { if (!sort) { delete this._sort[prop] } else { this._sort[prop] = (sort !== 'down'); } this.sort(true); this.dispatchEvent({ type: 'sort', property: prop, sort: sort }); } /** Get sorted properties list * @return Object */ ol_control_FeatureList.prototype.getSort = function() { return this._sort; } /** Reset all sorts */ ol_control_FeatureList.prototype.resetSort = function() { this._sort = {}; this.sort(); } /** Enable sort list by properties in the header * @param {...string} propName */ ol_control_FeatureList.prototype.enableSort = function() { this._canSort = [...arguments]; Object.keys(this._sort).forEach(function(s) { if (this._canSort.indexOf(s) < 0) { delete this._sort[s]; } }.bind(this)) this.sort(); } /** Draw the list * @param {boolean} delay to prevent to many redraw * @private */ ol_control_FeatureList.prototype._drawList = function(delay) { if (delay) { clearTimeout(this._drawout); this._drawout = setTimeout(this._drawList.bind(this), 100); return; } var features = this._source.getFeatures ? this._source.getFeatures() : this._source; this._columns = this.getColumns(features); // Head this._drawHead() // List of features this._listFeatures = []; features.forEach(function (f) { this._listFeatures.push(f) }.bind(this)) // Sort this.sort(); } /** Update a feature line in the table (attribute changed) * @param {ol_Feature} feature * @returns {boolean} */ ol_control_FeatureList.prototype.updateFeature = function(feature) { var pfeature = this._findInPage(feature) if (pfeature) { this._updateFeature(pfeature.feature, pfeature.tr) return true; } return false; } /** Update a feature line in the table * @param {ol_Feature} feature * @param {Element} tr * @private */ ol_control_FeatureList.prototype._updateFeature = function(f, tr) { tr.innerHTML = ''; this._columns.forEach(function(c) { var p = this.formatProperty(f, c); ol_ext_element.create('TD', { text: p.val, html: p.html, className: p.type, 'data-prop': c, parent: tr }) }.bind(this)) // Selected ? if (this._curSelection && this._curSelection.feature === f) { this._curSelection.tr = tr; tr.classList.add('ol-selected') } } /** Get the list element * @param {ol.Feature} f * @returns {Element} */ ol_control_FeatureList.prototype._getLi = function(f) { var tr = ol_ext_element.create('TR', { on: { click: function (e) { var td = e.target.closest('TD'); this.dispatchEvent({ type: 'select', property: td.dataset.prop, feature: f, row: e.target.closest('TR'), col: td, originalEvent: e }) this.select(f, true) }.bind(this), dblclick: function(e) { var td = e.target.closest('TD'); this.dispatchEvent({ type: 'dblclick', property: td.dataset.prop, feature: f, row: e.target.closest('TR'), col: td, originalEvent: e }) }.bind(this) } }) this._updateFeature(f, tr) return tr; } /** Draw columns heads * @private */ ol_control_FeatureList.prototype._drawHead = function() { var columns = this._columns; this._head.innerHTML = ''; var tr = ol_ext_element.create('TR', { parent: this._head }) columns.forEach(function(c) { var td = ol_ext_element.create('TD', { html: ol_ext_element.create('P', { text: c }), parent: tr }) if (this._canSort.length && this._canSort.indexOf(c) >= 0) { td.classList.add('sort') var b = ol_ext_element.create('BUTTON', { className: 'sort' + (this._sort[c]===true ? ' sortup' : this._sort[c]===false ? ' sortdown' : ''), click: function() { var sort; if (b.classList.contains('sortup')) { sort = 'down'; } else if (!b.classList.contains('sortdown')) { sort = 'up'; } this.sortBy(c, sort); }.bind(this), parent: td }) } }.bind(this)) } /** Sort features in the table * @param {boolean} [silent] sort without triggering an event */ ol_control_FeatureList.prototype.sort = function(silent) { var sort = Object.keys(this._sort) if (sort.length) { this._listFeatures.sort(function(a, b) { for (let i=0; i this._listFeatures.length) page = 0; // No changes if (page === this._curPage) return; // Draw page this._curPage = page; this._listbody.innerHTML = ''; // Fist row to preserve space this._listbody.appendChild(ol_ext_element.create('TR', { style: { height: Math.max(0, (page*nb - nb2)*h) + 'px' } })) // Page list var nmax = Math.min(this._listFeatures.length, (page+1)*nb + nb2) this._page = []; for (var i = Math.max(0, page*nb - nb2); i < nmax; i++) { // this._listbody.appendChild(this._listFeatures[i].tr) var tr = this._getLi(this._listFeatures[i]) this._page.push({ feature: this._listFeatures[i], tr: tr }) if (this._curSelection && this._listFeatures[i] === this._curSelection.feature) { this._curSelection.tr = tr; tr.classList.add('ol-selected') } this._listbody.appendChild(tr) } // Last row to preserve space this._listbody.appendChild(ol_ext_element.create('TR', { style: { height: Math.max(0, (this._listFeatures.length - nmax) * h) + 'px' } })) // force focus on list if (this.get('focus') !== false) this._listbody.focus(); } /** A sort function to compare 2 properties * @param {ol_Feature} f1 * @param {ol_Feature} f2 * @param {string} prop property name to sort at * @return number -1: v1 < v2, 1: v1 > v2, 0: v1 = v2 * @api */ ol_control_FeatureList.prototype.sortFn = function(f1, f2, p) { var v1 = f1.get(p) || ''; var v2 = f2.get(p) || ''; if (v1 < v2) return -1; if (v1 > v2) return 1; return 0; } /** Format feature property * @param {ol_Feature} feature, * @param {string} prop property name * @api */ ol_control_FeatureList.prototype.formatProperty = function(feature, prop) { var p = feature.get(prop); return { val: p === undefined ? '-' : p, type: typeof(p) } } /** Get the list of columns * @param {Array} [features] a list of features to retrieve columns (if none, returns columns defined by setColumns) */ ol_control_FeatureList.prototype.getColumns = function(features) { var columns = this.columns || []; if (!columns.length && features) { var col = {} features.forEach(function (f) { // Get properties but geom Object.keys(f.getProperties()).forEach(function(p) { if (!(f.get(p) instanceof ol_geom_Geometry)) { col[p] = true; } }) }) columns = Object.keys(col); } return columns; } /** Set the list of columns to display * @param {Array} columns */ ol_control_FeatureList.prototype.setColumns = function(columns) { this.columns = columns || []; this._drawList(); if (this._curSelection && this._curSelection.feature) { this.select(this._curSelection.feature, true) } } /** Dragging sizer * @private */ ol_control_FeatureList.prototype._dragSizer = function(e) { var self = this; if (!this._movefn) { this._movefn = this._dragSizer.bind(this); } e.stopPropagation(); e.preventDefault(); switch (e.type) { case 'touchcancel': case 'touchend': case 'mouseup': { self._pageY = false; ["mouseup", "mousemove", "touchend", "touchcancel", "touchmove"] .forEach(function (eventName) { document.removeEventListener(eventName, self._movefn); }); break; } case 'mousedown': case 'touchstart': { self._contentHeight = self._list.parentNode.getBoundingClientRect().height; self._pageY = e.pageY || (e.touches && e.touches.length && e.touches[0].pageY) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY); ["mouseup", "mousemove", "touchend", "touchcancel", "touchmove"] .forEach(function (eventName) { document.addEventListener(eventName, self._movefn); }); } // fallthrough case 'mousemove': case 'touchmove': { if (self.pageY !== false) { var pageY = e.pageY || (e.touches && e.touches.length && e.touches[0].pageY) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY); if (this._bottomScroll) { self.resize(self._contentHeight + pageY - self._pageY); } else { self.resize(self._contentHeight - pageY + self._pageY); } } break; } default: break; } } /** Resize the control to the map * @param {number} [height] the table height (if in a map sticks to the viewport height) */ ol_control_FeatureList.prototype.resize = function(height) { if (!this.getMap()) return; var h0 = this._list.parentNode.getBoundingClientRect().height // Set new height if (height !== undefined) { this._list.parentNode.style.height = height + 'px'; } // Prevent getting out of the map if (this.element.dataset.control) { var h = this.getMap().getTargetElement().getBoundingClientRect().height - this._head.getBoundingClientRect().height; this._list.parentNode.style.maxHeight = Math.min(h, this._list.getBoundingClientRect().height) + 'px'; } // Has changed? if (h0 !== this._list.parentNode.getBoundingClientRect().height) { this.dispatchEvent({ type: 'resize', height: this._list.parentNode.getBoundingClientRect().height }) } } /** Get the current selection in the list * @returns {ol_Feature} */ ol_control_FeatureList.prototype.getSelection = function() { if (this._curSelection) { return this._curSelection.feature || null; } return null; } /** Select a feature in the list * @param {ol_Feature} [feature] if none remove selection * @param {boolean} [noScroll=false] prevent scrolling to the selected row */ ol_control_FeatureList.prototype.select = function(feature, noScroll) { // Remove previous if (this._curSelection) { this._curSelection.tr.classList.remove('ol-selected'); } this._curSelection = null; // New selection if (feature) { if (noScroll) { var f = this._findInPage(feature) this._curSelection = { feature: feature, tr: f ? f.tr : ol_ext_element.create('TR') }; } else { this._curSelection = this.scrollTo(feature) } } if (this._curSelection) { this._curSelection.tr.classList.add('ol-selected'); } } /** Add a button to the list header menu * @param {Object} options * @param {string} className * @param {string} [title] * @param {string} [html] * @param {function} click on click function * @returns {Element} */ ol_control_FeatureList.prototype.addButton = function(options) { return ol_ext_element.create('BUTTON', { className: options.className, title: options.title || '', html: options.html || '', click: options.click, parent: this._menu.querySelector('.ol-buttons') }) } export default ol_control_FeatureList