diff --git a/src/layer.js b/src/layer.js index 2ce4c1f50..dd16c5e2b 100644 --- a/src/layer.js +++ b/src/layer.js @@ -6,7 +6,7 @@ import { createLayerControlHTML } from './mapml/elementSupport/layers/createLaye export class BaseLayerElement extends HTMLElement { static get observedAttributes() { - return ['src', 'label', 'checked', 'hidden', 'opacity']; + return ['src', 'label', 'checked', 'hidden', 'opacity', 'media']; } /* jshint ignore:start */ #hasConnected; @@ -53,6 +53,13 @@ export class BaseLayerElement extends HTMLElement { } } + get media() { + return this.getAttribute('media'); + } + set media(val) { + this.setAttribute('media', val); + } + get opacity() { // use ?? since 0 is falsy, || would return rhs in that case return +(this._opacity ?? this.getAttribute('opacity')); @@ -114,17 +121,59 @@ export class BaseLayerElement extends HTMLElement { this._onAdd(); } } + break; + case 'media': + if (oldValue !== newValue) { + this._registerMediaQuery(newValue); + } + break; } } } + _registerMediaQuery(mq) { + if (!this._changeHandler) { + this._changeHandler = () => { + this._onRemove(); + if (this._mql.matches) { + this._onAdd(); + } + // set the disabled 'read-only' attribute indirectly, via _validateDisabled + this._validateDisabled(); + }; + } + + if (mq) { + // a new media query is being established + let map = this.getMapEl(); + if (!map) return; + // Remove listener from the old media query (if it exists) + if (this._mql) { + this._mql.removeEventListener('change', this._changeHandler); + } + + this._mql = map.matchMedia(mq); + this._changeHandler(); + this._mql.addEventListener('change', this._changeHandler); + } else if (this._mql) { + // the media attribute removed or query set to '' + this._mql.removeEventListener('change', this._changeHandler); + delete this._mql; + // effectively, no / empty media attribute matches, do what changeHandler does + this._onRemove(); + this._onAdd(); + this._validateDisabled(); + } + } + getMapEl() { + return Util.getClosest(this, 'mapml-viewer,map[is=web-map]'); + } constructor() { // Always call super first in constructor super(); // this._opacity is used to record the current opacity value (with or without updates), // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 this._opacity = this.opacity || 1.0; - this._renderingMapContent = M.options.contentPreference; this.attachShadow({ mode: 'open' }); } disconnectedCallback() { @@ -132,6 +181,13 @@ export class BaseLayerElement extends HTMLElement { // removed from the map and the layer control if (this.hasAttribute('data-moving')) return; this._onRemove(); + + if (this._mql) { + if (this._changeHandler) { + this._mql.removeEventListener('change', this._changeHandler); + } + delete this._mql; + } } _onRemove() { @@ -141,13 +197,6 @@ export class BaseLayerElement extends HTMLElement { let l = this._layer, lc = this._layerControl, lchtml = this._layerControlHTML; - // remove properties of layer involved in whenReady() logic - delete this._layer; - delete this._layerControl; - delete this._layerControlHTML; - delete this._fetchError; - this.shadowRoot.innerHTML = ''; - if (this.src) this.innerHTML = ''; if (l) { l.off(); @@ -158,8 +207,16 @@ export class BaseLayerElement extends HTMLElement { } if (lc && !this.hidden) { + // lc.removeLayer depends on this._layerControlHTML, can't delete it until after lc.removeLayer(l); } + // remove properties of layer involved in whenReady() logic + delete this._layer; + delete this._layerControl; + delete this._layerControlHTML; + delete this._fetchError; + this.shadowRoot.innerHTML = ''; + if (this.src) this.innerHTML = ''; } connectedCallback() { @@ -170,11 +227,17 @@ export class BaseLayerElement extends HTMLElement { this._createLayerControlHTML = createLayerControlHTML.bind(this); const doConnected = this._onAdd.bind(this); const doRemove = this._onRemove.bind(this); + const registerMediaQuery = this._registerMediaQuery.bind(this); + let mq = this.media; this.parentElement .whenReady() .then(() => { doRemove(); - doConnected(); + if (mq) { + registerMediaQuery(mq); + } else { + doConnected(); + } }) .catch((error) => { throw new Error('Map never became ready: ' + error); @@ -189,20 +252,11 @@ export class BaseLayerElement extends HTMLElement { e.stopPropagation(); // if user changes the style in layer control if (e.detail) { - this._renderingMapContent = e.detail._renderingMapContent; this.src = e.detail.src; } }, { once: true } ); - this.addEventListener( - 'zoomchangesrc', - function (e) { - e.stopPropagation(); - this.src = e.detail.href; - }, - { once: true } - ); let base = this.baseURI ? this.baseURI : document.baseURI; const headers = new Headers(); headers.append('Accept', 'text/mapml'); @@ -240,7 +294,6 @@ export class BaseLayerElement extends HTMLElement { .then(() => { // may throw: this.selectAlternateOrChangeProjection(); - this.checkForPreferredContent(); }) .then(() => { this._layer = mapMLLayer(new URL(this.src, base).href, this, { @@ -278,7 +331,6 @@ export class BaseLayerElement extends HTMLElement { .then(() => { // may throw: this.selectAlternateOrChangeProjection(); - this.checkForPreferredContent(); }) .then(() => { this._layer = mapMLLayer(null, this, { @@ -317,13 +369,6 @@ export class BaseLayerElement extends HTMLElement { ); this.parentElement.projection = e.cause.mapprojection; } - } else if (e.message === 'findmatchingpreferredcontent') { - if (e.cause.href) { - console.log( - 'Changing layer to matching preferred content at: ' + e.cause.href - ); - this.src = e.cause.href; - } } else if (e.message === 'Failed to fetch') { // cut short whenReady with the _fetchError property this._fetchError = true; @@ -372,23 +417,6 @@ export class BaseLayerElement extends HTMLElement { } } - checkForPreferredContent() { - let mapml = this.src ? this.shadowRoot : this; - let availablePreferMapContents = mapml.querySelector( - `map-link[rel="style"][media="prefers-map-content=${this._renderingMapContent}"][href]` - ); - if (availablePreferMapContents) { - // resolve href - let url = new URL( - availablePreferMapContents.getAttribute('href'), - availablePreferMapContents.getBase() - ).href; - throw new Error('findmatchingpreferredcontent', { - cause: { href: url } - }); - } - } - copyRemoteContentToShadowRoot(mapml) { let shadowRoot = this.shadowRoot; // get the map-meta[name=projection/cs/extent/zoom] from map-head of remote mapml, attach them to the shadowroot @@ -454,12 +482,12 @@ export class BaseLayerElement extends HTMLElement { }; const _addStylesheetLink = (mapLink) => { this.whenReady().then(() => { - this._layer.appendStyleLink(mapLink); + this._layer.renderStyles(mapLink); }); }; const _addStyleElement = (mapStyle) => { this.whenReady().then(() => { - this._layer.appendStyleElement(mapStyle); + this._layer.renderStyles(mapStyle); }); }; const _addExtentElement = (mapExtent) => { @@ -610,8 +638,13 @@ export class BaseLayerElement extends HTMLElement { setTimeout(() => { let layer = this._layer, map = layer?._map; + // if there's a media query in play, check it early + if (this._mql && !this._mql.matches) { + this.setAttribute('disabled', ''); + this.disabled = true; + return; + } if (map) { - this._validateLayerZoom({ zoom: map.getZoom() }); // prerequisite: no inline and remote mapml elements exists at the same time const mapExtents = this.src ? this.shadowRoot.querySelectorAll('map-extent') @@ -664,35 +697,6 @@ export class BaseLayerElement extends HTMLElement { } }, 0); } - _validateLayerZoom(e) { - // get the min and max zooms from all extents - let toZoom = e.zoom; - let min = this.extent.zoom.minZoom; - let max = this.extent.zoom.maxZoom; - let inLink = this.src - ? this.shadowRoot.querySelector('map-link[rel=zoomin]') - : this.querySelector('map-link[rel=zoomin]'), - outLink = this.src - ? this.shadowRoot.querySelector('map-link[rel=zoomout]') - : this.querySelector('map-link[rel=zoomout]'); - let targetURL; - if (!(min <= toZoom && toZoom <= max)) { - if (inLink && toZoom > max) { - targetURL = inLink.href; - } else if (outLink && toZoom < min) { - targetURL = outLink.href; - } - if (targetURL) { - this.dispatchEvent( - new CustomEvent('zoomchangesrc', { - detail: { - href: targetURL - } - }) - ); - } - } - } // disable/italicize layer control elements based on the map-layer.disabled property toggleLayerControlDisabled() { let input = this._layerControlCheckbox, diff --git a/src/map-extent.js b/src/map-extent.js index b94b34271..cdc1f0395 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -311,12 +311,12 @@ export class HTMLExtentElement extends HTMLElement { }; const _addStylesheetLink = (mapLink) => { this.whenReady().then(() => { - this._extentLayer.appendStyleLink(mapLink); + this._extentLayer.renderStyles(mapLink); }); }; const _addStyleElement = (mapStyle) => { this.whenReady().then(() => { - this._extentLayer.appendStyleElement(mapStyle); + this._extentLayer.renderStyles(mapStyle); }); }; for (let i = 0; i < elementsGroup.length; ++i) { @@ -431,7 +431,7 @@ export class HTMLExtentElement extends HTMLElement { _handleChange() { // add _extentLayer to map if map-extent is checked, otherwise remove it - if (this.checked && !this.disabled) { + if (this.checked && !this.disabled && this.parentLayer._layer) { // can be added to mapmllayer layerGroup no matter map-layer is checked or not this._extentLayer.addTo(this.parentLayer._layer); this._extentLayer.setZIndex( diff --git a/src/map-link.js b/src/map-link.js index bcc1fa7d6..600b2f423 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -252,9 +252,9 @@ export class HTMLLinkElement extends HTMLElement { break; case 'disabled': if (typeof newValue === 'string') { - this.disableLink(); + this._disableLink(); } else { - this.enableLink(); + this._enableLink(); } break; } @@ -277,6 +277,9 @@ export class HTMLLinkElement extends HTMLElement { case 'image': case 'features': case 'query': + // because we skip the attributeChangedCallback for initialization, + // respect the disabled attribute which can be set by the author prior + // to initialization if (!this.disabled) { this._initTemplateVars(); await this._createTemplatedLink(); @@ -296,7 +299,9 @@ export class HTMLLinkElement extends HTMLElement { //this._createLegendLink(); break; case 'stylesheet': - this._createStylesheetLink(); + if (!this.disabled) { + this._createStylesheetLink(); + } break; case 'alternate': this._createAlternateLink(); // add media attribute @@ -305,7 +310,10 @@ export class HTMLLinkElement extends HTMLElement { // this._createLicenseLink(); break; } - this._registerMediaQuery(this.media); + // the media attribute uses / overrides the disabled attribute to enable or + // disable the link, so at this point the #hasConnected must be true so + // that the disabled attributeChangedCallback can have its desired side effect + await this._registerMediaQuery(this.media); // create the type of templated leaflet layer appropriate to the rel value // image/map/features = templated(Image/Feature), tile=templatedTile, // this._tempatedTileLayer = Util.templatedTile(pane: this.extentElement._leafletLayer._container) @@ -323,7 +331,7 @@ export class HTMLLinkElement extends HTMLElement { break; } } - disableLink() { + _disableLink() { switch (this.rel.toLowerCase()) { case 'tile': case 'image': @@ -355,24 +363,22 @@ export class HTMLLinkElement extends HTMLElement { break; } } - async enableLink() { + async _enableLink() { switch (this.rel.toLowerCase()) { case 'tile': case 'image': case 'features': case 'query': - if (!this.disabled) { - this._initTemplateVars(); - await this._createTemplatedLink(); - this.getLayerEl()._validateDisabled(); - } + this._initTemplateVars(); + await this._createTemplatedLink(); + this.getLayerEl()._validateDisabled(); break; case 'stylesheet': this._createStylesheetLink(); break; } } - _registerMediaQuery(mq) { + async _registerMediaQuery(mq) { if (!this._changeHandler) { // Define and bind the change handler once this._changeHandler = () => { @@ -383,6 +389,9 @@ export class HTMLLinkElement extends HTMLElement { if (mq) { let map = this.getMapEl(); if (!map) return; + // have to wait until map has an extent i.e. is ready, because the + // matchMedia function below relies on it for map related queries + await map.whenReady(); // Remove listener from the old media query (if it exists) if (this._mql) { @@ -397,6 +406,8 @@ export class HTMLLinkElement extends HTMLElement { // Clean up the existing listener this._mql.removeEventListener('change', this._changeHandler); delete this._mql; + // unlike map-layer.disabled, map-link.disabled is an observed attribute + this.disabled = false; } } _createAlternateLink(mapml) { @@ -441,17 +452,17 @@ export class HTMLLinkElement extends HTMLElement { copyAttributes(this, this.link); if (this._stylesheetHost._layer) { - this._stylesheetHost._layer.appendStyleLink(this); + this._stylesheetHost._layer.renderStyles(this); } else if (this._stylesheetHost._templatedLayer) { - this._stylesheetHost._templatedLayer.appendStyleLink(this); + this._stylesheetHost._templatedLayer.renderStyles(this); } else if (this._stylesheetHost._extentLayer) { - this._stylesheetHost._extentLayer.appendStyleLink(this); + this._stylesheetHost._extentLayer.renderStyles(this); } } function copyAttributes(source, target) { return Array.from(source.attributes).forEach((attribute) => { - if (attribute.nodeName !== 'href') + if (attribute.nodeName !== 'href' && attribute.nodeName !== 'media') target.setAttribute(attribute.nodeName, attribute.nodeValue); }); } @@ -989,8 +1000,7 @@ export class HTMLLinkElement extends HTMLElement { layerEl.dispatchEvent( new CustomEvent('changestyle', { detail: { - src: e.target.getAttribute('data-href'), - preference: this.media['prefers-map-content'] + src: e.target.getAttribute('data-href') } }) ); diff --git a/src/map-style.js b/src/map-style.js index 1aa21d35f..5aff36f10 100644 --- a/src/map-style.js +++ b/src/map-style.js @@ -1,43 +1,91 @@ +import { Util } from './mapml/utils/Util.js'; export class HTMLStyleElement extends HTMLElement { static get observedAttributes() { - return; + return ['media']; } + /* jshint ignore:start */ + #hasConnected; + /* jshint ignore:end */ + get media() { + return this.getAttribute('media'); + } + set media(val) { + this.setAttribute('media', val); + } + async attributeChangedCallback(name, oldValue, newValue) { + if (this.#hasConnected /* jshint ignore:line */) { + switch (name) { + case 'media': + if (oldValue !== newValue) { + await this._registerMediaQuery(newValue); + } + break; + } + } + } + async _registerMediaQuery(mq) { + if (!this._changeHandler) { + // Define and bind the change handler once + this._changeHandler = () => { + this._disconnect(); + if (this._mql.matches) { + this._connect(); + } + }; + } + + if (mq) { + let map = this.getMapEl(); + if (!map) return; + // have to wait until map has an extent i.e. is ready, because the + // matchMedia function below relies on it for map related queries + await map.whenReady(); - attributeChangedCallback(name, oldValue, newValue) {} + // Remove listener from the old media query (if it exists) + if (this._mql) { + this._mql.removeEventListener('change', this._changeHandler); + } + + // Set up the new media query and listener + this._mql = map.matchMedia(mq); + this._changeHandler(); // Initial evaluation + this._mql.addEventListener('change', this._changeHandler); + } else if (this._mql) { + // Clean up the existing listener + this._mql.removeEventListener('change', this._changeHandler); + delete this._mql; + this._disconnect(); + this._connect(); + } + } + getMapEl() { + return Util.getClosest(this, 'mapml-viewer,map[is=web-map]'); + } constructor() { // Always call super first in constructor super(); } - connectedCallback() { - // if the parent element is a map-link, the stylesheet should - // be created as part of a templated layer processing i.e. on moveend / when connected - // and the generated + +
+