diff --git a/umap/models.py b/umap/models.py index 399d033c..5c92e481 100644 --- a/umap/models.py +++ b/umap/models.py @@ -551,6 +551,7 @@ class DataLayer(NamedModel): if self.old_id: metadata["old_id"] = self.old_id metadata["id"] = self.pk + metadata["rank"] = self.rank metadata["permissions"] = {"edit_status": self.edit_status} metadata["editMode"] = "advanced" if self.can_edit(request) else "disabled" metadata["_referenceVersion"] = self.reference_version diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index 5f7d14e6..c5262413 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -113,7 +113,7 @@ export default class Browser { } onFormChange() { - this._umap.eachBrowsableDataLayer((datalayer) => { + this._umap.datalayers.browsable().map((datalayer) => { datalayer.resetLayer(true) this.updateDatalayer(datalayer) if (this._umap.fullPanel?.isOpen()) datalayer.tableEdit() @@ -136,7 +136,7 @@ export default class Browser { onMoveEnd() { if (!this.isOpen()) return const isListDynamic = this.options.inBbox - this._umap.eachBrowsableDataLayer((datalayer) => { + this._umap.datalayers.browsable().map((datalayer) => { if (!isListDynamic && !datalayer.hasDynamicData()) return this.updateDatalayer(datalayer) }) @@ -145,7 +145,7 @@ export default class Browser { update() { if (!this.isOpen()) return this.dataContainer.innerHTML = '' - this._umap.eachBrowsableDataLayer((datalayer) => { + this._umap.datalayers.browsable().map((datalayer) => { this.addDataLayer(datalayer, this.dataContainer) }) } @@ -254,10 +254,10 @@ export default class Browser { // If at least one layer is shown, hide it // otherwise show all let allHidden = true - this._umap.eachBrowsableDataLayer((datalayer) => { + this._umap.datalayers.browsable().map((datalayer) => { if (datalayer.isVisible()) allHidden = false }) - this._umap.eachBrowsableDataLayer((datalayer) => { + this._umap.datalayers.browsable().map((datalayer) => { datalayer._forcedVisibility = true if (allHidden) { datalayer.show() diff --git a/umap/static/umap/js/modules/caption.js b/umap/static/umap/js/modules/caption.js index 1ec7c218..285ae95d 100644 --- a/umap/static/umap/js/modules/caption.js +++ b/umap/static/umap/js/modules/caption.js @@ -68,9 +68,11 @@ export default class Caption extends Utils.WithTemplate { this.elements.description.hidden = true } this.elements.datalayersContainer.innerHTML = '' - this._umap.eachDataLayerReverse((datalayer) => - this.addDataLayer(datalayer, this.elements.datalayersContainer) - ) + this._umap.datalayers + .reverse() + .map((datalayer) => + this.addDataLayer(datalayer, this.elements.datalayersContainer) + ) this.addCredits() if (this._umap.properties.created_at) { const created_at = translate('created at {date}', { @@ -85,7 +87,7 @@ export default class Caption extends Utils.WithTemplate { } this._umap.panel.open({ content: this.element }).then(() => { // Create the legend when the panel is actually on the DOM - this._umap.eachDataLayerReverse((datalayer) => datalayer.renderLegend()) + this._umap.datalayers.reverse().map((datalayer) => datalayer.renderLegend()) this._umap.propagate() }) } diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index c3280df1..9cc31283 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -64,6 +64,9 @@ export class DataLayer { this.setOptions(data) this.pane.dataset.id = this.id + if (this.options.rank === undefined) { + this.options.rank = this._umap.datalayers.count() + } if (!Utils.isObject(this.options.remoteData)) { this.options.remoteData = {} @@ -153,6 +156,9 @@ export class DataLayer { case 'remote-data': this.fetchRemoteData() break + case 'datalayer-rank': + this._umap.reorderDataLayers() + break } } } @@ -249,16 +255,9 @@ export class DataLayer { if (!error) { this._umap.modifiedAt = response.headers.get('last-modified') this.setReferenceVersion({ response, sync: false }) - // FIXME: for now the _umap_options property is set dynamically from backend - // And thus it's not in the geojson file in the server - // So do not let all options to be reset - // Fix is a proper migration so all datalayers settings are - // in DB, and we remove it from geojson flat files. - if (geojson._umap_options) { - geojson._umap_options.editMode = this.options.editMode - } + delete geojson._umap_options // In case of maps pre 1.0 still around - if (geojson._storage) geojson._storage.editMode = this.options.editMode + delete geojson._storage await this.fromUmapGeoJSON(geojson) this.backupOptions() this._loading = false @@ -287,7 +286,6 @@ export class DataLayer { async fromUmapGeoJSON(geojson) { if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat - geojson._umap_options.id = this.id if (geojson._umap_options) this.setOptions(geojson._umap_options) if (this.isRemoteLayer()) { await this.fetchRemoteData() @@ -297,6 +295,7 @@ export class DataLayer { } clear() { + // TODO do not startBatch for remoteData layer this.sync.startBatch() for (const feature of Object.values(this._features)) { feature.del() @@ -395,12 +394,7 @@ export class DataLayer { } connectToMap() { - if (!this._umap.datalayers[this.id]) { - this._umap.datalayers[this.id] = this - } - if (!this._umap.datalayersIndex.includes(this)) { - this._umap.datalayersIndex.push(this) - } + this._umap.datalayers.add(this) this._umap.onDataLayersChanged() } @@ -644,11 +638,19 @@ export class DataLayer { del(sync = true) { const oldValue = Utils.CopyJSON(this.umapGeoJSON()) - this.erase() + // TODO merge datalayer del and features del in same + // batch + this.clear() if (sync) { this.isDeleted = true this.sync.delete(oldValue) } + this.hide() + this.parentPane.removeChild(this.pane) + this._umap.onDataLayersChanged() + this.layer.onDelete(this._leafletMap) + this.propagateDelete() + this._leaflet_events_bk = this._leaflet_events } empty() { @@ -666,17 +668,6 @@ export class DataLayer { return datalayer } - erase() { - this.hide() - this._umap.datalayersIndex.splice(this.getRank(), 1) - this.parentPane.removeChild(this.pane) - this._umap.onDataLayersChanged() - this.layer.onDelete(this._leafletMap) - this.propagateDelete() - this._leaflet_events_bk = this._leaflet_events - this.clear() - } - redraw() { if (!this.isVisible()) return this.eachFeature((feature) => feature.redraw()) @@ -1091,23 +1082,11 @@ export class DataLayer { } getPreviousBrowsable() { - let id = this.getRank() - let next - const index = this._umap.datalayersIndex - while (((id = index[++id] ? id : 0), (next = index[id]))) { - if (next === this || next.canBrowse()) break - } - return next + return this._umap.datalayers.prev(this) } getNextBrowsable() { - let id = this.getRank() - let prev - const index = this._umap.datalayersIndex - while (((id = index[--id] ? id : index.length - 1), (prev = index[id]))) { - if (prev === this || prev.canBrowse()) break - } - return prev + return this._umap.datalayers.next(this) } umapGeoJSON() { @@ -1118,8 +1097,8 @@ export class DataLayer { } } - getRank() { - return this._umap.datalayersIndex.indexOf(this) + getDOMOrder() { + return Array.from(this.parentPane.children).indexOf(this.pane) } isReadOnly() { @@ -1141,6 +1120,12 @@ export class DataLayer { } } + prepareOptions() { + const options = Utils.CopyJSON(this.options) + delete options.permissions + return JSON.stringify(options) + } + async save() { if (this.isDeleted) return await this.saveDelete() if (!this.isRemoteLayer() && !this.isLoaded()) return @@ -1148,8 +1133,8 @@ export class DataLayer { const formData = new FormData() formData.append('name', this.options.name) formData.append('display_on_load', !!this.options.displayOnLoad) - formData.append('rank', this.getRank()) - formData.append('settings', JSON.stringify(this.options)) + formData.append('rank', this.options.rank) + formData.append('settings', this.prepareOptions()) // Filename support is shaky, don't do it for now. const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' }) formData.append('geojson', blob) diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js index e41a7e3d..ee75d742 100644 --- a/umap/static/umap/js/modules/facets.js +++ b/umap/static/umap/js/modules/facets.js @@ -24,7 +24,7 @@ export default class Facets { this.selected[name] = selected } - this._umap.eachBrowsableDataLayer((datalayer) => { + this._umap.datalayers.browsable().map((datalayer) => { datalayer.eachFeature((feature) => { for (const name of names) { let value = feature.properties[name] diff --git a/umap/static/umap/js/modules/form/fields.js b/umap/static/umap/js/modules/form/fields.js index c3a23bd4..8ba21a9d 100644 --- a/umap/static/umap/js/modules/form/fields.js +++ b/umap/static/umap/js/modules/form/fields.js @@ -560,7 +560,7 @@ Fields.SlideshowDelay = class extends Fields.IntSelect { Fields.DataLayerSwitcher = class extends Fields.Select { getOptions() { const options = [] - this.builder._umap.eachDataLayerReverse((datalayer) => { + this.builder._umap.datalayers.reverse().map((datalayer) => { if ( datalayer.isLoaded() && !datalayer.isDataReadOnly() && diff --git a/umap/static/umap/js/modules/importer.js b/umap/static/umap/js/modules/importer.js index f2738f08..b16b898c 100644 --- a/umap/static/umap/js/modules/importer.js +++ b/umap/static/umap/js/modules/importer.js @@ -243,7 +243,7 @@ export default class Importer extends Utils.WithTemplate { this.raw = null const layerSelect = this.qs('[name="layer-id"]') layerSelect.innerHTML = '' - this._umap.eachDataLayerReverse((datalayer) => { + this._umap.datalayers.reverse().map((datalayer) => { if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) { DomUtil.element({ tagName: 'option', diff --git a/umap/static/umap/js/modules/managers.js b/umap/static/umap/js/modules/managers.js new file mode 100644 index 00000000..00a06606 --- /dev/null +++ b/umap/static/umap/js/modules/managers.js @@ -0,0 +1,46 @@ +export class DataLayerManager extends Object { + add(datalayer) { + this[datalayer.id] = datalayer + } + active() { + return Object.values(this) + .filter((datalayer) => !datalayer.isDeleted) + .sort((a, b) => a.options.rank > b.options.rank) + } + reverse() { + return this.active().reverse() + } + count() { + return this.active().length + } + find(func) { + for (const datalayer of this.reverse()) { + if (func.call(datalayer, datalayer)) { + return datalayer + } + } + } + filter(func) { + return this.active().filter(func) + } + visible() { + return this.filter((datalayer) => datalayer.isVisible()) + } + browsable() { + return this.reverse().filter((datalayer) => datalayer.allowBrowse()) + } + prev(datalayer) { + const browsable = this.browsable() + const current = browsable.indexOf(datalayer) + const prev = browsable[current - 1] || browsable[browsable.length - 1] + if (!prev.canBrowse()) return this.prev(prev) + return prev + } + next(datalayer) { + const browsable = this.browsable() + const current = browsable.indexOf(datalayer) + const next = browsable[current + 1] || browsable[0] + if (!next.canBrowse()) return this.next(next) + return next + } +} diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index 04b0b0bd..fad5c2dd 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -159,7 +159,7 @@ export class MapPermissions { `
` ) container.appendChild(fieldset) - this._umap.eachDataLayer((datalayer) => { + this._umap.datalayers.active().map((datalayer) => { datalayer.permissions.edit(fieldset) }) } diff --git a/umap/static/umap/js/modules/rendering/map.js b/umap/static/umap/js/modules/rendering/map.js index 599068f7..5d5e5007 100644 --- a/umap/static/umap/js/modules/rendering/map.js +++ b/umap/static/umap/js/modules/rendering/map.js @@ -327,7 +327,7 @@ export const LeafletMap = BaseMap.extend({ } else if (this.options.defaultView === 'latest') { this._umap.onceDataLoaded(() => { if (!this._umap.hasData()) return - const datalayer = this._umap.firstVisibleDatalayer() + const datalayer = this._umap.datalayers.visible()[0] let feature if (datalayer) { const feature = datalayer.getFeatureByIndex(-1) diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js index 0509d7af..8e77b799 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -10,7 +10,7 @@ import { translate } from './i18n.js' * - `type`: The type of the data * - `impacts`: A list of impacts than happen when this property is updated, among * 'ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data', - * 'background' 'sync'. + * 'background', 'sync', 'datalayer-rank'. * * - Extra keys are being passed to the FormBuilder automatically. */ @@ -436,6 +436,10 @@ export const SCHEMA = { ], default: 'Default', }, + rank: { + type: Number, + impacts: ['datalayer-rank'], + }, remoteData: { type: Object, impacts: ['remote-data'], diff --git a/umap/static/umap/js/modules/share.js b/umap/static/umap/js/modules/share.js index 20965032..6c45fa88 100644 --- a/umap/static/umap/js/modules/share.js +++ b/umap/static/umap/js/modules/share.js @@ -204,8 +204,8 @@ class IframeExporter { delete this.queryString.feature } if (this.options.keepCurrentDatalayers) { - this._umap.eachDataLayer((datalayer) => { - if (datalayer.isVisible() && datalayer.createdOnServer) { + this._umap.datalayers.visible().map((datalayer) => { + if (datalayer.createdOnServer) { datalayers.push(datalayer.id) } }) diff --git a/umap/static/umap/js/modules/slideshow.js b/umap/static/umap/js/modules/slideshow.js index c00b60a7..4326b448 100644 --- a/umap/static/umap/js/modules/slideshow.js +++ b/umap/static/umap/js/modules/slideshow.js @@ -66,7 +66,7 @@ export default class Slideshow extends WithTemplate { } defaultDatalayer() { - return this._umap.findDataLayer((d) => d.canBrowse()) + return this._umap.datalayers.find((d) => d.canBrowse()) } startSpinner() { diff --git a/umap/static/umap/js/modules/sync/engine.js b/umap/static/umap/js/modules/sync/engine.js index f117b259..513d82e6 100644 --- a/umap/static/umap/js/modules/sync/engine.js +++ b/umap/static/umap/js/modules/sync/engine.js @@ -207,22 +207,35 @@ export class SyncEngine { this._send(operation) } - async save() { - const needSave = new Map() + _getDirtyObjects() { + const dirty = new Map() if (!this._umap.id) { // There is no operation for first map save - needSave.set(this._umap, []) + dirty.set(this._umap, []) + } + const addDirtyObject = (operation) => { + const updater = this._getUpdater(operation.subject) + const obj = updater.getStoredObject(operation.metadata) + if (!dirty.has(obj)) { + dirty.set(obj, []) + } + dirty.get(obj).push(operation) } for (const operation of this._operations.sorted()) { if (operation.dirty) { - const updater = this._getUpdater(operation.subject) - const obj = updater.getStoredObject(operation.metadata) - if (!needSave.has(obj)) { - needSave.set(obj, []) + addDirtyObject(operation) + if (operation.verb === 'batch') { + for (const op of operation.operations) { + addDirtyObject(op) + } } - needSave.get(obj).push(operation) } } + return dirty + } + + async save() { + const needSave = this._getDirtyObjects() for (const [obj, operations] of needSave.entries()) { const ok = await obj.save() if (!ok) return false diff --git a/umap/static/umap/js/modules/ui/bar.js b/umap/static/umap/js/modules/ui/bar.js index d75ddb42..dbed49d8 100644 --- a/umap/static/umap/js/modules/ui/bar.js +++ b/umap/static/umap/js/modules/ui/bar.js @@ -207,7 +207,7 @@ export class BottomBar extends WithTemplate { const select = this.elements.layers const selected = select.options[select.selectedIndex].value if (!selected) return - this._umap.eachDataLayer((datalayer) => { + this._umap.datalayers.active().map((datalayer) => { datalayer.toggle(datalayer.id === selected) }) }) @@ -228,7 +228,7 @@ export class BottomBar extends WithTemplate { buildDataLayerSwitcher() { this.elements.layers.innerHTML = '' - const datalayers = this._umap.datalayersIndex.filter((d) => d.options.inCaption) + const datalayers = this._umap.datalayers.filter((d) => d.options.inCaption) if (datalayers.length < 2) { this.elements.layers.hidden = true } else { diff --git a/umap/static/umap/js/modules/umap.js b/umap/static/umap/js/modules/umap.js index f8a233eb..238c8b99 100644 --- a/umap/static/umap/js/modules/umap.js +++ b/umap/static/umap/js/modules/umap.js @@ -33,6 +33,7 @@ import { EditPanel, FullPanel, Panel } from './ui/panel.js' import Tooltip from './ui/tooltip.js' import URLs from './urls.js' import * as Utils from './utils.js' +import { DataLayerManager } from './managers.js' export default class Umap { constructor(element, geojson) { @@ -166,8 +167,7 @@ export default class Umap { } // Global storage for retrieving datalayers and features. - this.datalayers = {} // All datalayers, including deleted. - this.datalayersIndex = [] // Datalayers actually on the map and ordered. + this.datalayers = new DataLayerManager() this.featuresIndex = {} this.formatter = new Formatter(this) @@ -217,7 +217,6 @@ export default class Umap { } window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null - this.backup() } get isDirty() { @@ -616,7 +615,7 @@ export default class Umap { this.datalayersLoaded = true this.fire('datalayersloaded') const toLoad = [] - for (const datalayer of this.datalayersIndex) { + for (const datalayer of this.datalayers.active()) { if (datalayer.showAtLoad()) toLoad.push(() => datalayer.show()) } while (toLoad.length) { @@ -630,7 +629,7 @@ export default class Umap { createDataLayer(options = {}, sync = true) { options.name = - options.name || `${translate('Layer')} ${this.datalayersIndex.length + 1}` + options.name || `${translate('Layer')} ${this.datalayers.count() + 1}` const datalayer = new DataLayer(this, this._leafletMap, options) if (sync !== false) { @@ -651,19 +650,21 @@ export default class Umap { } reindexDataLayers() { - this.eachDataLayer((datalayer) => datalayer.reindex()) + this.datalayers.active().map((datalayer) => datalayer.reindex()) this.onDataLayersChanged() } - indexDatalayers() { - const panes = this._leafletMap.getPane('overlayPane') - - this.datalayersIndex = [] - for (const pane of panes.children) { - if (!pane.dataset || !pane.dataset.id) continue - this.datalayersIndex.push(this.datalayers[pane.dataset.id]) + reorderDataLayers() { + const parent = this._leafletMap.getPane('overlayPane') + const datalayers = Object.values(this.datalayers) + .filter((datalayer) => !datalayer._isDeleted) + .sort( + (datalayer1, datalayer2) => datalayer1.options.rank > datalayer2.options.rank + ) + for (const datalayer of datalayers) { + const child = parent.querySelector(`[data-id="${datalayer.id}"]`) + parent.appendChild(child) } - this.onDataLayersChanged() } onceDatalayersLoaded(callback, context) { @@ -694,7 +695,6 @@ export default class Umap { async saveAll() { if (!this.isDirty) return if (this._defaultExtent) this._setCenterAndZoom() - this.backup() const status = await this.sync.save() if (!status) return // Do a blind render for now, as we are not sure what could @@ -714,24 +714,6 @@ export default class Umap { return this.properties.name || translate('Untitled map') } - backup() { - this.backupProperties() - this._datalayersIndex_bk = [].concat(this.datalayersIndex) - } - - backupProperties() { - this._backupProperties = Object.assign({}, this.properties) - this._backupProperties.tilelayer = Object.assign({}, this.properties.tilelayer) - this._backupProperties.limitBounds = Object.assign({}, this.properties.limitBounds) - this._backupProperties.permissions = Object.assign({}, this.permissions.properties) - } - - resetProperties() { - this.properties = Object.assign({}, this._backupProperties) - this.properties.tilelayer = Object.assign({}, this._backupProperties.tilelayer) - this.permissions.properties = Object.assign({}, this._backupProperties.permissions) - } - setProperties(newProperties) { for (const key of Object.keys(SCHEMA)) { if (newProperties[key] !== undefined) { @@ -744,24 +726,24 @@ export default class Umap { } hasData() { - for (const datalayer of this.datalayersIndex) { + for (const datalayer of this.datalayers.active()) { if (datalayer.hasData()) return true } } hasLayers() { - return Boolean(this.datalayersIndex.length) + return Boolean(this.datalayers.count()) } allProperties() { - return [].concat(...this.datalayersIndex.map((dl) => dl.allProperties())) + return [].concat(...this.datalayers.active().map((dl) => dl.allProperties())) } sortedValues(property) { return [] - .concat(...this.datalayersIndex.map((dl) => dl.sortedValues(property))) + .concat(...this.datalayers.active().map((dl) => dl.sortedValues(property))) .filter((val, idx, arr) => arr.indexOf(val) === idx) - .sort(U.Utils.naturalSort) + .sort(Utils.naturalSort) } editCaption() { @@ -1278,7 +1260,7 @@ export default class Umap { toGeoJSON() { let features = [] - this.eachDataLayer((datalayer) => { + this.datalayers.active().map((datalayer) => { if (datalayer.isVisible()) { features = features.concat(datalayer.featuresToGeoJSON()) } @@ -1354,13 +1336,20 @@ export default class Umap { if (fields.includes('properties.rules')) { this.rules.load() } - this.eachVisibleDataLayer((datalayer) => { + this.datalayers.visible().map((datalayer) => { datalayer.redraw() }) break case 'datalayer-index': this.reindexDataLayers() break + case 'datalayer-rank': + // When drag'n'dropping datalayers, + // this get called once per datalayers. + // (and same for undo/redo of the action) + // TODO: call only once + this.reorderDataLayers() + break case 'background': this._leafletMap.initTileLayers() break @@ -1449,7 +1438,7 @@ export default class Umap { ) { return datalayer } - datalayer = this.findDataLayer((datalayer) => { + datalayer = this.datalayers.find((datalayer) => { if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) { fallback = datalayer if (datalayer.isVisible()) return true @@ -1464,49 +1453,20 @@ export default class Umap { return this.createDirtyDataLayer() } - findDataLayer(method, context) { - for (let i = this.datalayersIndex.length - 1; i >= 0; i--) { - if (method.call(context, this.datalayersIndex[i])) { - return this.datalayersIndex[i] - } - } - } - - eachDataLayer(method, context) { - for (let i = 0; i < this.datalayersIndex.length; i++) { - method.call(context, this.datalayersIndex[i]) - } - } - - eachDataLayerReverse(method, context, filter) { - for (let i = this.datalayersIndex.length - 1; i >= 0; i--) { - if (filter && !filter.call(context, this.datalayersIndex[i])) continue - method.call(context, this.datalayersIndex[i]) - } - } - - eachBrowsableDataLayer(method, context) { - this.eachDataLayerReverse(method, context, (d) => d.allowBrowse()) - } - - eachVisibleDataLayer(method, context) { - this.eachDataLayerReverse(method, context, (d) => d.isVisible()) - } - - eachFeature(callback, context) { - this.eachBrowsableDataLayer((datalayer) => { - if (datalayer.isVisible()) datalayer.eachFeature(callback, context) + eachFeature(callback) { + this.datalayers.browsable().map((datalayer) => { + if (datalayer.isVisible()) datalayer.eachFeature(callback) }) } removeDataLayers() { - this.eachDataLayerReverse((datalayer) => { + this.datalayers.active().map((datalayer) => { datalayer.del() }) } emptyDataLayers() { - this.eachDataLayerReverse((datalayer) => { + this.datalayers.active().map((datalayer) => { datalayer.empty() }) } @@ -1520,7 +1480,7 @@ export default class Umap { ` const [container, { ul }] = Utils.loadTemplateWithRefs(template) - this.eachDataLayerReverse((datalayer) => { + this.datalayers.reverse().map((datalayer) => { const row = Utils.loadTemplate( `