diff --git a/umap/static/umap/css/tooltip.css b/umap/static/umap/css/tooltip.css index 0a5e6d4d..ea289285 100644 --- a/umap/static/umap/css/tooltip.css +++ b/umap/static/umap/css/tooltip.css @@ -59,3 +59,16 @@ .tooltip-accent li:last-of-type { margin-bottom: 0; } + +.umap-tooltip-container.tooltip-right:before { + right: 100%; + top: calc(50% - var(--arrow-size)); + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-right-color: var(--tooltip-color); + border-width: var(--arrow-size); +} diff --git a/umap/static/umap/js/modules/rendering/controls.js b/umap/static/umap/js/modules/rendering/controls.js new file mode 100644 index 00000000..6379b827 --- /dev/null +++ b/umap/static/umap/js/modules/rendering/controls.js @@ -0,0 +1,248 @@ +import { Control } from '../../../vendors/leaflet/leaflet-src.esm.js' +import * as Utils from '../utils.js' +import { translate } from '../i18n.js' + +export const HomeControl = Control.extend({ + options: { + position: 'topleft', + }, + + onAdd: (map) => { + const path = map._umap.getStaticPathFor('home.svg') + const container = Utils.loadTemplate( + `${translate('Home logo')}` + ) + return container + }, +}) + +export const EditControl = Control.extend({ + options: { + position: 'topright', + }, + + onAdd: (map) => { + const template = ` +
+ +
+ ` + const [container, { button }] = Utils.loadTemplateWithRefs(template) + button.addEventListener('click', () => map._umap.enableEdit()) + button.addEventListener('mouseover', () => { + map._umap.tooltip.open({ + content: map._umap.help.displayLabel('TOGGLE_EDIT'), + anchor: button, + position: 'bottom', + delay: 750, + duration: 5000, + }) + }) + return container + }, +}) + +export const MoreControl = Control.extend({ + options: { + position: 'topleft', + }, + + onAdd: function (map) { + const pos = this.getPosition() + const corner = map._controlCorners[pos] + const className = 'umap-more-controls' + const template = ` +
+ +
+ ` + const [container, { button }] = Utils.loadTemplateWithRefs(template) + button.addEventListener('click', () => corner.classList.toggle(className)) + button.addEventListener('mouseover', () => { + const extended = corner.classList.contains(className) + map._umap.tooltip.open({ + content: extended ? translate('Hide controls') : translate('More controls'), + anchor: button, + position: 'right', + delay: 750, + }) + }) + return container + }, +}) + +export const PermanentCreditsControl = Control.extend({ + options: { + position: 'bottomleft', + }, + + onAdd: (map) => { + const container = Utils.loadTemplate( + `
${Utils.toHTML(map.options.permanentCredit)}
` + ) + const background = map.options.permanentCreditBackground ? '#FFFFFFB0' : '' + container.style.backgroundColor = background + return container + }, +}) + +const BaseButton = Control.extend({ + initialize: function (umap, options) { + this._umap = umap + Control.prototype.initialize.call(this, options) + }, + + onAdd: function (map) { + const template = ` +
+ +
+ ` + const [container, { button }] = Utils.loadTemplateWithRefs(template) + button.addEventListener('click', () => this.onClick()) + button.addEventListener('dblclick', (event) => { + event.stopPropagation() + }) + this.afterAdd(container) + return container + }, + + afterAdd: (container) => {}, +}) + +export const DataLayersControl = BaseButton.extend({ + options: { + position: 'topleft', + className: 'umap-control-browse', + title: translate('Open browser'), + }, + + afterAdd: function (container) { + Utils.toggleBadge(container, this._umap.browser?.hasFilters()) + }, + + onClick: function () { + this._umap.openBrowser() + }, +}) + +export const CaptionControl = BaseButton.extend({ + options: { + position: 'topleft', + className: 'umap-control-caption', + title: translate('About'), + }, + + onClick: function () { + this._umap.openCaption() + }, +}) + +export const EmbedControl = BaseButton.extend({ + options: { + position: 'topleft', + title: translate('Share and download'), + className: 'leaflet-control-embed', + }, + + onClick: function () { + this._umap.share.open() + }, +}) + +export const AttributionControl = Control.Attribution.extend({ + options: { + prefix: '', + }, + + _update: function () { + // Layer is no more on the map + if (!this._map) return + Control.Attribution.prototype._update.call(this) + const shortCredit = this._map._umap.getProperty('shortCredit') + const captionMenus = this._map._umap.getProperty('captionMenus') + // Use our own container, so we can hide/show on small screens + const originalCredits = this._container.innerHTML + this._container.innerHTML = '' + const template = ` +
+ ${originalCredits} + — ${Utils.toHTML(shortCredit)} + — ${translate('Open caption')} + — ${translate('Home')} + — ${translate('Powered by uMap')} + +
+ ` + const [container, { short, caption, home, site }] = + Utils.loadTemplateWithRefs(template) + caption.addEventListener('click', () => this._map._umap.openCaption()) + this._container.appendChild(container) + short.hidden = !shortCredit + caption.hidden = !captionMenus + site.hidden = !captionMenus + home.hidden = this._map._umap.isEmbed || !captionMenus + }, +}) + +/* Used in edit mode to define the default tilelayer */ +export const TileLayerChooser = BaseButton.extend({ + options: { + position: 'topleft', + }, + + onClick: function () { + this.openSwitcher({ edit: true }) + }, + + openSwitcher: function (options = {}) { + const template = ` +
+

${translate('Change tilelayers')}

+ +
+ ` + const [container, { tileContainer }] = Utils.loadTemplateWithRefs(template) + this.buildList(tileContainer, options) + const panel = options.edit ? this._umap.editPanel : this._umap.panel + panel.open({ content: container, highlight: 'tilelayers' }) + }, + + buildList: function (container, options) { + this._umap._leafletMap.eachTileLayer((tilelayer) => { + const browserIsHttps = window.location.protocol === 'https:' + const tileLayerIsHttp = tilelayer.options.url_template.indexOf('http:') === 0 + if (browserIsHttps && tileLayerIsHttp) return + container.appendChild(this.addTileLayerElement(tilelayer, options)) + }) + }, + + addTileLayerElement: function (tilelayer, options) { + const selectedClass = this._umap._leafletMap.hasLayer(tilelayer) ? 'selected' : '' + const src = Utils.template( + tilelayer.options.url_template, + this._umap._leafletMap.options.demoTileInfos + ) + const template = ` +
  • + +
    ${tilelayer.options.name}
    +
  • + ` + const li = Utils.loadTemplate(template) + li.addEventListener('click', () => { + const oldTileLayer = this._umap.properties.tilelayer + this._umap._leafletMap.selectTileLayer(tilelayer) + this._umap._leafletMap._controls.tilelayers.setLayers() + if (options?.edit) { + this._umap.properties.tilelayer = tilelayer.toJSON() + this._umap.sync.update( + 'properties.tilelayer', + this._umap.properties.tilelayer, + oldTileLayer + ) + } + }) + return li + }, +}) diff --git a/umap/static/umap/js/modules/rendering/map.js b/umap/static/umap/js/modules/rendering/map.js index b5f5d5ae..599068f7 100644 --- a/umap/static/umap/js/modules/rendering/map.js +++ b/umap/static/umap/js/modules/rendering/map.js @@ -11,6 +11,17 @@ import { import { uMapAlert as Alert } from '../../components/alerts/alert.js' import DropControl from '../drop.js' import { translate } from '../i18n.js' +import { + AttributionControl, + CaptionControl, + DataLayersControl, + EmbedControl, + EditControl, + HomeControl, + MoreControl, + PermanentCreditsControl, + TileLayerChooser, +} from './controls.js' import * as Utils from '../utils.js' import * as Icon from './icon.js' @@ -40,15 +51,15 @@ const ControlsMixin = { this._controls = {} if (this._umap.hasEditMode() && !this.options.noControl) { - new U.EditControl(this).addTo(this) + new EditControl(this).addTo(this) } - this._controls.home = new U.HomeControl(this._umap) + this._controls.home = new HomeControl(this._umap) this._controls.zoom = new Control.Zoom({ zoomInTitle: translate('Zoom in'), zoomOutTitle: translate('Zoom out'), }) - this._controls.datalayers = new U.DataLayersControl(this._umap) - this._controls.caption = new U.CaptionControl(this._umap) + this._controls.datalayers = new DataLayersControl(this._umap) + this._controls.caption = new CaptionControl(this._umap) this._controls.locate = new U.Locate(this, { strings: { title: translate('Center map on your location'), @@ -69,8 +80,8 @@ const ControlsMixin = { }, }) this._controls.search = new U.SearchControl() - this._controls.embed = new Control.Embed(this._umap) - this._controls.tilelayersChooser = new U.TileLayerChooser(this) + this._controls.embed = new EmbedControl(this._umap) + this._controls.tilelayersChooser = new TileLayerChooser(this._umap) this._controls.editinosm = new Control.EditInOSM({ position: 'topleft', widgetOptions: { @@ -80,9 +91,9 @@ const ControlsMixin = { }, }) this._controls.measure = new L.MeasureControl().initHandler(this) - this._controls.more = new U.MoreControls() + this._controls.more = new MoreControl() this._controls.scale = L.control.scale() - this._controls.permanentCredit = new U.PermanentCreditsControl(this) + this._controls.permanentCredit = new PermanentCreditsControl(this) this._umap.drop = new DropControl(this._umap, this, this._container) this._controls.tilelayers = new U.TileLayerControl(this) }, @@ -93,7 +104,7 @@ const ControlsMixin = { } if (this.options.noControl) return - this._controls.attribution = new U.AttributionControl().addTo(this) + this._controls.attribution = new AttributionControl().addTo(this) if (this.options.miniMap) { this.whenReady(function () { if (this.selectedTilelayer) { diff --git a/umap/static/umap/js/modules/ui/base.js b/umap/static/umap/js/modules/ui/base.js index b6479b37..9ff710a7 100644 --- a/umap/static/umap/js/modules/ui/base.js +++ b/umap/static/umap/js/modules/ui/base.js @@ -4,6 +4,8 @@ export class Positioned { this.anchorTop(anchor) } else if (anchor && position === 'bottom') { this.anchorBottom(anchor) + } else if (anchor && position === 'right') { + this.anchorRight(anchor) } else { this.anchorAbsolute() } @@ -12,6 +14,7 @@ export class Positioned { toggleClassPosition(position) { this.container.classList.toggle('tooltip-bottom', position === 'bottom') this.container.classList.toggle('tooltip-top', position === 'top') + this.container.classList.toggle('tooltip-right', position === 'right') } anchorTop(el) { @@ -33,6 +36,16 @@ export class Positioned { }) } + anchorRight(el) { + this.toggleClassPosition('right') + const coords = this.getPosition(el) + console.log(coords) + this.setPosition({ + left: coords.right + 11, + top: coords.top, + }) + } + anchorAbsolute() { const left = this.parent.offsetLeft + diff --git a/umap/static/umap/js/modules/umap.js b/umap/static/umap/js/modules/umap.js index 980ac1b8..e7a98085 100644 --- a/umap/static/umap/js/modules/umap.js +++ b/umap/static/umap/js/modules/umap.js @@ -107,7 +107,7 @@ export default class Umap { if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema) // Do not display in an iframe. - if (window.self !== window.top) { + if (this.isEmbed) { this.properties.homeControl = false } @@ -258,6 +258,10 @@ export default class Umap { } } + get isEmbed() { + return window.self !== window.top + } + setPropertiesFromQueryString() { const asBoolean = (key) => { const value = this.searchParams.get(key) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 8c302a5a..903502f8 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1,183 +1,3 @@ -U.HomeControl = L.Control.extend({ - options: { - position: 'topleft', - }, - - onAdd: (map) => { - const path = map._umap.getStaticPathFor('home.svg') - const container = U.Utils.loadTemplate( - `${L._('Home logo')}` - ) - return container - }, -}) - -U.EditControl = L.Control.extend({ - options: { - position: 'topright', - }, - - onAdd: function (map) { - const container = L.DomUtil.create('div', 'edit-enable') - const enableEditing = L.DomUtil.createButton( - '', - container, - L._('Edit'), - map._umap.enableEdit, - map._umap - ) - L.DomEvent.on( - enableEditing, - 'mouseover', - () => { - map._umap.tooltip.open({ - content: map._umap.help.displayLabel('TOGGLE_EDIT'), - anchor: enableEditing, - position: 'bottom', - delay: 750, - duration: 5000, - }) - }, - this - ) - - return container - }, -}) - -U.MoreControls = L.Control.extend({ - options: { - position: 'topleft', - }, - - onAdd: function () { - const container = L.DomUtil.create('div', 'umap-control-text') - const moreButton = L.DomUtil.createButton( - 'umap-control-more', - container, - L._('More controls'), - this.toggle, - this - ) - const lessButton = L.DomUtil.createButton( - 'umap-control-less', - container, - L._('Hide controls'), - this.toggle, - this - ) - return container - }, - - toggle: function () { - const pos = this.getPosition() - const corner = this._map._controlCorners[pos] - const className = 'umap-more-controls' - if (L.DomUtil.hasClass(corner, className)) L.DomUtil.removeClass(corner, className) - else L.DomUtil.addClass(corner, className) - }, -}) - -U.PermanentCreditsControl = L.Control.extend({ - options: { - position: 'bottomleft', - }, - - initialize: function (map, options) { - this.map = map - L.Control.prototype.initialize.call(this, options) - }, - - onAdd: function () { - this.paragraphContainer = L.DomUtil.create( - 'div', - 'umap-permanent-credits-container text' - ) - this.setCredits() - this.setBackground() - return this.paragraphContainer - }, - - setCredits: function () { - this.paragraphContainer.innerHTML = U.Utils.toHTML(this.map.options.permanentCredit) - }, - - setBackground: function () { - if (this.map.options.permanentCreditBackground) { - this.paragraphContainer.style.backgroundColor = '#FFFFFFB0' - } else { - this.paragraphContainer.style.backgroundColor = '' - } - }, -}) - -L.Control.Button = L.Control.extend({ - initialize: function (umap, options) { - this._umap = umap - L.Control.prototype.initialize.call(this, options) - }, - - getClassName: function () { - return this.options.className - }, - - onAdd: function (map) { - const container = L.DomUtil.create('div', `${this.getClassName()} umap-control`) - const button = L.DomUtil.createButton( - '', - container, - this.options.title, - this.onClick, - this - ) - L.DomEvent.on(button, 'dblclick', L.DomEvent.stopPropagation) - this.afterAdd(container) - return container - }, - - afterAdd: (container) => {}, -}) - -U.DataLayersControl = L.Control.Button.extend({ - options: { - position: 'topleft', - className: 'umap-control-browse', - title: L._('Open browser'), - }, - - afterAdd: function (container) { - U.Utils.toggleBadge(container, this._umap.browser?.hasFilters()) - }, - - onClick: function () { - this._umap.openBrowser() - }, -}) - -U.CaptionControl = L.Control.Button.extend({ - options: { - position: 'topleft', - className: 'umap-control-caption', - title: L._('About'), - }, - - onClick: function () { - this._umap.openCaption() - }, -}) - -L.Control.Embed = L.Control.Button.extend({ - options: { - position: 'topleft', - title: L._('Share and download'), - className: 'leaflet-control-embed umap-control', - }, - - onClick: function () { - this._umap.share.open() - }, -}) - /* Used in view mode to define the current tilelayer */ U.TileLayerControl = L.Control.IconLayers.extend({ initialize: function (map, options) { @@ -238,128 +58,6 @@ U.TileLayerControl = L.Control.IconLayers.extend({ }, }) -/* Used in edit mode to define the default tilelayer */ -U.TileLayerChooser = L.Control.extend({ - options: { - position: 'topleft', - }, - - initialize: function (map, options = {}) { - this.map = map - L.Control.prototype.initialize.call(this, options) - }, - - onAdd: function () { - const container = L.DomUtil.create('div', 'leaflet-control-tilelayers umap-control') - const changeMapBackgroundButton = L.DomUtil.createButton( - '', - container, - L._('Change map background'), - this.openSwitcher, - this - ) - L.DomEvent.on(changeMapBackgroundButton, 'dblclick', L.DomEvent.stopPropagation) - return container - }, - - openSwitcher: function (options = {}) { - const container = L.DomUtil.create('div', 'umap-edit-tilelayers') - L.DomUtil.createTitle(container, L._('Change tilelayers'), 'icon-tilelayer') - this._tilelayers_container = L.DomUtil.create('ul', '', container) - this.buildList(options) - const panel = options.edit ? this.map._umap.editPanel : this.map._umap.panel - panel.open({ content: container, highlight: 'tilelayers' }) - }, - - buildList: function (options) { - this.map.eachTileLayer(function (tilelayer) { - if ( - window.location.protocol === 'https:' && - tilelayer.options.url_template.indexOf('http:') === 0 - ) - return - this.addTileLayerElement(tilelayer, options) - }, this) - }, - - addTileLayerElement: function (tilelayer, options) { - const selectedClass = this.map.hasLayer(tilelayer) ? 'selected' : '' - const el = L.DomUtil.create('li', selectedClass, this._tilelayers_container) - const img = L.DomUtil.create('img', '', el) - const name = L.DomUtil.create('div', '', el) - img.src = U.Utils.template( - tilelayer.options.url_template, - this.map.options.demoTileInfos - ) - img.loading = 'lazy' - name.textContent = tilelayer.options.name - L.DomEvent.on( - el, - 'click', - () => { - const oldTileLayer = this.map._umap.properties.tilelayer - this.map.selectTileLayer(tilelayer) - this.map._controls.tilelayers.setLayers() - if (options?.edit) { - this.map._umap.properties.tilelayer = tilelayer.toJSON() - this.map._umap.isDirty = true - this.map._umap.sync.update( - 'properties.tilelayer', - this.map._umap.properties.tilelayer, - oldTileLayer - ) - } - }, - this - ) - }, -}) - -U.AttributionControl = L.Control.Attribution.extend({ - options: { - prefix: '', - }, - - _update: function () { - // Layer is no more on the map - if (!this._map) return - L.Control.Attribution.prototype._update.call(this) - // Use our own container, so we can hide/show on small screens - const credits = this._container.innerHTML - this._container.innerHTML = '' - const container = L.DomUtil.create('div', 'attribution-container', this._container) - container.innerHTML = credits - const shortCredit = this._map._umap.getProperty('shortCredit') - const captionMenus = this._map._umap.getProperty('captionMenus') - if (shortCredit) { - L.DomUtil.element({ - tagName: 'span', - parent: container, - safeHTML: ` — ${U.Utils.toHTML(shortCredit)}`, - }) - } - if (captionMenus) { - const link = L.DomUtil.add('a', '', container, ` — ${L._('Open caption')}`) - L.DomEvent.on(link, 'click', L.DomEvent.stop) - .on(link, 'click', () => this._map._umap.openCaption()) - .on(link, 'dblclick', L.DomEvent.stop) - } - if (window.top === window.self && captionMenus) { - // We are not in iframe mode - L.DomUtil.createLink('', container, ` — ${L._('Home')}`, '/') - } - if (captionMenus) { - L.DomUtil.createLink( - '', - container, - ` — ${L._('Powered by uMap')}`, - 'https://umap-project.org/' - ) - } - L.DomUtil.createLink('attribution-toggle', this._container, '') - }, -}) - /* * Take control over L.Control.Locate to be able to * call start() before adding the control (and thus the button) to the map. diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 0f6a3f4d..77b89a51 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -96,27 +96,20 @@ html[dir="rtl"] .leaflet-tooltip-pane > * { background-color: white; min-height: initial; } -.leaflet-control.display-on-more, -.umap-control-less { +.leaflet-control.display-on-more { display: none; } -.umap-control-more, -.umap-control-less { +.umap-control-more { background-image: url('./img/24-white.svg'); background-position: -72px -402px; - text-indent: -9999px; margin-bottom: 0; } -.umap-control-less { +.umap-more-controls .umap-control-more { background-position: -108px -402px; } -.umap-more-controls .display-on-more, -.umap-more-controls .umap-control-less { +.umap-more-controls .display-on-more { display: block; } -.umap-more-controls .umap-control-more { - display: none; -} .leaflet-control-embed [type="button"] { background-position: 0 -180px; } diff --git a/umap/tests/integration/test_querystring.py b/umap/tests/integration/test_querystring.py index 0694a335..75ed705d 100644 --- a/umap/tests/integration/test_querystring.py +++ b/umap/tests/integration/test_querystring.py @@ -63,5 +63,5 @@ def test_zoom_control(map, live_server, datalayer, page): expect(control).to_be_visible() page.goto(f"{live_server.url}{map.get_absolute_url()}?zoomControl=null") expect(control).to_be_hidden() - page.get_by_title("More controls").click() + page.locator(".umap-control-more").click() expect(control).to_be_visible()