ol-ext
Version:
A set of cool extensions for OpenLayers (ol) in node modules structure
704 lines (662 loc) • 20.3 kB
JavaScript
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_Feature>|ol_Collection<ol_Feature>|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: '<div class="ol-buttons"></div>',
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<list.length; i++) {
if (list[i] === feature) {
return i
}
}
}
return -1;
}
/** Find a feature in the current page
* @param {ol_Feature} feature
* @returns {Object}
*/
ol_control_FeatureList.prototype._findInPage = function(feature) {
var list = this._page;
if (list) {
for (var i=0; i<list.length; i++) {
if (list[i].feature === feature) {
return list[i]
}
}
}
return false;
}
/** Collapse the table
* @param {boolean} [b] if no parameters toggle it
*/
ol_control_FeatureList.prototype.collapse = function(b) {
if (b === undefined) b = !this._collapsed;
if (b !== this._collapsed) {
this._collapsed = b;
if (this._collapsed) {
this.element.classList.add('ol-collapsed')
} else {
this.element.classList.remove('ol-collapsed')
}
this.dispatchEvent({
type: 'collapse',
collapsed: this._collapsed
})
if (!this._collapsed) this.refresh();
}
}
/** Is the control collapsed
*/
ol_control_FeatureList.prototype.isCollapsed = function() {
return this._collapsed;
}
/** Set the features list
* @param {ol_source_Vector|ol_Collection<ol_Feature>|Array<ol_Feature>} 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<sort.length; i++) {
var p = sort[i];
var s = this.sortFn(a, b, p);
if (s) return this._sort[p] ? s : -s;
}
return 0;
}.bind(this));
}
// Refresh list
this.refresh(true)
if (!silent) this.dispatchEvent({ type: 'sort' });
}
/** Refresh the list draw + resize use update if features have changed
* @param {boolean} [force]
*/
ol_control_FeatureList.prototype.refresh = function(force) {
if (force) this._curPage = -1;
// Draw Head
this._drawHead();
// Draw list
this._drawPage()
// resize
this.resize();
}
/** Update the list (when features have changed without dispatching event).
*/
ol_control_FeatureList.prototype.update = function() {
this._drawList();
if (this._curSelection && this._curSelection.feature) {
this.select(this._curSelection.feature, true)
}
}
/** Get height of a row
* @return {number}
*/
ol_control_FeatureList.prototype.getRowHeight = function() {
return this._head.getBoundingClientRect().height;
}
/** Pagination draw
* @private
*/
ol_control_FeatureList.prototype._drawPage = function() {
// Page length
var nb = Math.round(this.get('pageLength') / 2);
var nb2 = Math.round(nb/2);
// Get current page on scrollTop
var top = this._list.parentNode.scrollTop;
var h = this.getRowHeight();
var page = Math.round((top / h - nb2) / nb);
if (page*nb > 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<ol_Feature>} [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<string>} 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