From: Tom Hughes Date: Sat, 18 Feb 2017 16:20:20 +0000 (+0000) Subject: Merge branch 'contextmenu' X-Git-Tag: live~4660 X-Git-Url: https://git.openstreetmap.org/rails.git/commitdiff_plain/0bdf29f10bd290cfb6943e6b64a027927ebb8248?hp=af72cb51e935acff9ac0248ca87a0f43d12e999b Merge branch 'contextmenu' --- diff --git a/Vendorfile b/Vendorfile index 361f2c843..304be9e15 100644 --- a/Vendorfile +++ b/Vendorfile @@ -20,6 +20,11 @@ folder 'vendor/assets' do file "images/#{image}", "https://unpkg.com/leaflet@1.0.3/dist/images/#{image}" end + from 'git://github.com/aratcliffe/Leaflet.contextmenu.git', :tag => 'v1.2.1' do + file 'leaflet.contextmenu.js', 'dist/leaflet.contextmenu.js' + file 'leaflet.contextmenu.css', 'dist/leaflet.contextmenu.css' + end + from 'git://github.com/kajic/leaflet-locationfilter.git' do file 'leaflet.locationfilter.css', 'src/locationfilter.css' file 'leaflet.locationfilter.js', 'src/locationfilter.js' diff --git a/app/assets/javascripts/index.js b/app/assets/javascripts/index.js index 9d7122e4d..1ba2fbbde 100644 --- a/app/assets/javascripts/index.js +++ b/app/assets/javascripts/index.js @@ -7,6 +7,8 @@ //= require leaflet.share //= require leaflet.polyline //= require leaflet.query +//= require leaflet.contextmenu +//= require index/contextmenu //= require index/search //= require index/browse //= require index/export @@ -77,7 +79,9 @@ $(document).ready(function () { var map = new L.OSM.Map("map", { zoomControl: false, - layerControl: false + layerControl: false, + contextmenu: true, + contextmenuWidth: 140 }); map.attributionControl.setPrefix(''); @@ -147,6 +151,8 @@ $(document).ready(function () { L.control.scale() .addTo(map); + OSM.initializeContextMenu(map); + if (OSM.STATUS !== 'api_offline' && OSM.STATUS !== 'database_offline') { OSM.initializeNotes(map); if (params.layers.indexOf(map.noteLayer.options.code) >= 0) { diff --git a/app/assets/javascripts/index/contextmenu.js b/app/assets/javascripts/index/contextmenu.js new file mode 100644 index 000000000..1e7251ec6 --- /dev/null +++ b/app/assets/javascripts/index/contextmenu.js @@ -0,0 +1,86 @@ +OSM.initializeContextMenu = function (map) { + map.contextmenu.addItem({ + text: I18n.t("javascripts.context.directions_from"), + callback: function directionsFromHere(e) { + var precision = OSM.zoomPrecision(map.getZoom()), + latlng = e.latlng.wrap(), + lat = latlng.lat.toFixed(precision), + lng = latlng.lng.toFixed(precision); + + OSM.router.route("/directions?" + querystring.stringify({ + route: lat + "," + lng + ";" + $("#route_to").val() + })); + } + }); + + map.contextmenu.addItem({ + text: I18n.t("javascripts.context.directions_to"), + callback: function directionsToHere(e) { + var precision = OSM.zoomPrecision(map.getZoom()), + latlng = e.latlng.wrap(), + lat = latlng.lat.toFixed(precision), + lng = latlng.lng.toFixed(precision); + + OSM.router.route("/directions?" + querystring.stringify({ + route: $("#route_from").val() + ";" + lat + "," + lng + })); + } + }); + + map.contextmenu.addItem({ + text: I18n.t("javascripts.context.add_note"), + callback: function addNoteHere(e) { + var precision = OSM.zoomPrecision(map.getZoom()), + latlng = e.latlng.wrap(), + lat = latlng.lat.toFixed(precision), + lng = latlng.lng.toFixed(precision); + + OSM.router.route("/note/new?lat=" + lat + "&lon=" + lng); + } + }); + + map.contextmenu.addItem({ + text: I18n.t("javascripts.context.show_address"), + callback: function describeLocation(e) { + var precision = OSM.zoomPrecision(map.getZoom()), + latlng = e.latlng.wrap(), + lat = latlng.lat.toFixed(precision), + lng = latlng.lng.toFixed(precision); + + OSM.router.route("/search?query=" + encodeURIComponent(lat + "," + lng)); + } + }); + + map.contextmenu.addItem({ + text: I18n.t("javascripts.context.query_features"), + callback: function queryFeatures(e) { + var precision = OSM.zoomPrecision(map.getZoom()), + latlng = e.latlng.wrap(), + lat = latlng.lat.toFixed(precision), + lng = latlng.lng.toFixed(precision); + + OSM.router.route("/query?lat=" + lat + "&lon=" + lng); + } + }); + + map.contextmenu.addItem({ + text: I18n.t("javascripts.context.centre_map"), + callback: function centreMap(e) { + map.panTo(e.latlng); + } + }); + + map.on("mousedown", function (e) { + if (e.shiftKey) map.contextmenu.disable(); + }).on("mouseup", function () { + map.contextmenu.enable(); + }); + + var updateMenu = function updateMenu () { + map.contextmenu.setDisabled(2, map.getZoom() < 12); + map.contextmenu.setDisabled(4, map.getZoom() < 14); + }; + + map.on("zoomend", updateMenu); + updateMenu(); +}; diff --git a/app/assets/javascripts/index/new_note.js b/app/assets/javascripts/index/new_note.js index 397daa637..53697e65b 100644 --- a/app/assets/javascripts/index/new_note.js +++ b/app/assets/javascripts/index/new_note.js @@ -77,7 +77,9 @@ OSM.NewNote = function(map) { } page.pushstate = page.popstate = function (path) { - OSM.loadSidebarContent(path, page.load); + OSM.loadSidebarContent(path, function () { + page.load(path); + }); }; function newHalo(loc, a) { @@ -97,7 +99,7 @@ OSM.NewNote = function(map) { } } - page.load = function () { + page.load = function (path) { if (addNoteButton.hasClass("disabled")) return; if (addNoteButton.hasClass("active")) return; @@ -105,12 +107,34 @@ OSM.NewNote = function(map) { map.addLayer(noteLayer); - var mapSize = map.getSize(); - var markerPosition; + var params = querystring.parse(path.substring(path.indexOf('?') + 1)); + var markerLatlng; + + if (params.lat && params.lon) { + markerLatlng = L.latLng(params.lat, params.lon); + + var markerPosition = map.latLngToContainerPoint(markerLatlng), + mapSize = map.getSize(), + panBy = L.point(0, 0); + + if (markerPosition.x < 50) { + panBy.x = markerPosition.x - 50; + } else if (markerPosition.x > mapSize.x - 50) { + panBy.x = 50 - mapSize.x + markerPosition.x; + } - markerPosition = [mapSize.x / 2, mapSize.y / 2]; + if (markerPosition.y < 50) { + panBy.y = markerPosition.y - 50; + } else if (markerPosition.y > mapSize.y - 50) { + panBy.y = 50 - mapSize.y + markerPosition.y; + } + + map.panBy(panBy); + } else { + markerLatlng = map.getCenter(); + } - newNote = L.marker(map.containerPointToLatLng(markerPosition), { + newNote = L.marker(markerLatlng, { icon: noteIcons["new"], opacity: 0.9, draggable: true diff --git a/app/assets/stylesheets/leaflet-all.scss b/app/assets/stylesheets/leaflet-all.scss index 10ad2607a..82312e5c2 100644 --- a/app/assets/stylesheets/leaflet-all.scss +++ b/app/assets/stylesheets/leaflet-all.scss @@ -1,6 +1,7 @@ /* *= require leaflet *= require leaflet.locationfilter + *= require leaflet.contextmenu */ /* Override to serve images through the asset pipeline. */ diff --git a/config/locales/en.yml b/config/locales/en.yml index 1867a433f..b2ebffd61 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2305,6 +2305,13 @@ en: nothing_found: No features found error: "Error contacting %{server}: %{error}" timeout: "Timeout contacting %{server}" + context: + directions_from: Directions from here + directions_to: Directions to here + add_note: Add a note here + show_address: Show address + query_features: Query features + centre_map: Centre map here redaction: edit: description: "Description" diff --git a/vendor/assets/leaflet/leaflet.contextmenu.css b/vendor/assets/leaflet/leaflet.contextmenu.css new file mode 100644 index 000000000..0b5e2defc --- /dev/null +++ b/vendor/assets/leaflet/leaflet.contextmenu.css @@ -0,0 +1,54 @@ +.leaflet-contextmenu { + display: none; + box-shadow: 0 1px 7px rgba(0,0,0,0.4); + -webkit-border-radius: 4px; + border-radius: 4px; + padding: 4px 0; + background-color: #fff; + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.leaflet-contextmenu a.leaflet-contextmenu-item { + display: block; + color: #222; + font-size: 12px; + line-height: 20px; + text-decoration: none; + padding: 0 12px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + cursor: default; + outline: none; +} + +.leaflet-contextmenu a.leaflet-contextmenu-item-disabled { + opacity: 0.5; +} + +.leaflet-contextmenu a.leaflet-contextmenu-item.over { + background-color: #f4f4f4; + border-top: 1px solid #f0f0f0; + border-bottom: 1px solid #f0f0f0; +} + +.leaflet-contextmenu a.leaflet-contextmenu-item-disabled.over { + background-color: inherit; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; +} + +.leaflet-contextmenu-icon { + margin: 2px 8px 0 0; + width: 16px; + height: 16px; + float: left; + border: 0; +} + +.leaflet-contextmenu-separator { + border-bottom: 1px solid #ccc; + margin: 5px 0; +} diff --git a/vendor/assets/leaflet/leaflet.contextmenu.js b/vendor/assets/leaflet/leaflet.contextmenu.js new file mode 100644 index 000000000..a9b011d95 --- /dev/null +++ b/vendor/assets/leaflet/leaflet.contextmenu.js @@ -0,0 +1,580 @@ +/* + Leaflet.contextmenu, a context menu for Leaflet. + (c) 2015, Adam Ratcliffe, GeoSmart Maps Limited + + @preserve +*/ + +(function(factory) { + // Packaging/modules magic dance + var L; + if (typeof define === 'function' && define.amd) { + // AMD + define(['leaflet'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + // Node/CommonJS + L = require('leaflet'); + module.exports = factory(L); + } else { + // Browser globals + if (typeof window.L === 'undefined') { + throw new Error('Leaflet must be loaded first'); + } + factory(window.L); + } +})(function(L) { +L.Map.mergeOptions({ + contextmenuItems: [] +}); + +L.Map.ContextMenu = L.Handler.extend({ + _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart', + + statics: { + BASE_CLS: 'leaflet-contextmenu' + }, + + initialize: function (map) { + L.Handler.prototype.initialize.call(this, map); + + this._items = []; + this._visible = false; + + var container = this._container = L.DomUtil.create('div', L.Map.ContextMenu.BASE_CLS, map._container); + container.style.zIndex = 10000; + container.style.position = 'absolute'; + + if (map.options.contextmenuWidth) { + container.style.width = map.options.contextmenuWidth + 'px'; + } + + this._createItems(); + + L.DomEvent + .on(container, 'click', L.DomEvent.stop) + .on(container, 'mousedown', L.DomEvent.stop) + .on(container, 'dblclick', L.DomEvent.stop) + .on(container, 'contextmenu', L.DomEvent.stop); + }, + + addHooks: function () { + var container = this._map.getContainer(); + + L.DomEvent + .on(container, 'mouseleave', this._hide, this) + .on(document, 'keydown', this._onKeyDown, this); + + if (L.Browser.touch) { + L.DomEvent.on(document, this._touchstart, this._hide, this); + } + + this._map.on({ + contextmenu: this._show, + mousedown: this._hide, + movestart: this._hide, + zoomstart: this._hide + }, this); + }, + + removeHooks: function () { + var container = this._map.getContainer(); + + L.DomEvent + .off(container, 'mouseleave', this._hide, this) + .off(document, 'keydown', this._onKeyDown, this); + + if (L.Browser.touch) { + L.DomEvent.off(document, this._touchstart, this._hide, this); + } + + this._map.off({ + contextmenu: this._show, + mousedown: this._hide, + movestart: this._hide, + zoomstart: this._hide + }, this); + }, + + showAt: function (point, data) { + if (point instanceof L.LatLng) { + point = this._map.latLngToContainerPoint(point); + } + this._showAtPoint(point, data); + }, + + hide: function () { + this._hide(); + }, + + addItem: function (options) { + return this.insertItem(options); + }, + + insertItem: function (options, index) { + index = index !== undefined ? index: this._items.length; + + var item = this._createItem(this._container, options, index); + + this._items.push(item); + + this._sizeChanged = true; + + this._map.fire('contextmenu.additem', { + contextmenu: this, + el: item.el, + index: index + }); + + return item.el; + }, + + removeItem: function (item) { + var container = this._container; + + if (!isNaN(item)) { + item = container.children[item]; + } + + if (item) { + this._removeItem(L.Util.stamp(item)); + + this._sizeChanged = true; + + this._map.fire('contextmenu.removeitem', { + contextmenu: this, + el: item + }); + } + }, + + removeAllItems: function () { + var item; + + while (this._container.children.length) { + item = this._container.children[0]; + this._removeItem(L.Util.stamp(item)); + } + }, + + hideAllItems: function () { + var item, i, l; + + for (i = 0, l = this._items.length; i < l; i++) { + item = this._items[i]; + item.el.style.display = 'none'; + } + }, + + showAllItems: function () { + var item, i, l; + + for (i = 0, l = this._items.length; i < l; i++) { + item = this._items[i]; + item.el.style.display = ''; + } + }, + + setDisabled: function (item, disabled) { + var container = this._container, + itemCls = L.Map.ContextMenu.BASE_CLS + '-item'; + + if (!isNaN(item)) { + item = container.children[item]; + } + + if (item && L.DomUtil.hasClass(item, itemCls)) { + if (disabled) { + L.DomUtil.addClass(item, itemCls + '-disabled'); + this._map.fire('contextmenu.disableitem', { + contextmenu: this, + el: item + }); + } else { + L.DomUtil.removeClass(item, itemCls + '-disabled'); + this._map.fire('contextmenu.enableitem', { + contextmenu: this, + el: item + }); + } + } + }, + + isVisible: function () { + return this._visible; + }, + + _createItems: function () { + var itemOptions = this._map.options.contextmenuItems, + item, + i, l; + + for (i = 0, l = itemOptions.length; i < l; i++) { + this._items.push(this._createItem(this._container, itemOptions[i])); + } + }, + + _createItem: function (container, options, index) { + if (options.separator || options === '-') { + return this._createSeparator(container, index); + } + + var itemCls = L.Map.ContextMenu.BASE_CLS + '-item', + cls = options.disabled ? (itemCls + ' ' + itemCls + '-disabled') : itemCls, + el = this._insertElementAt('a', cls, container, index), + callback = this._createEventHandler(el, options.callback, options.context, options.hideOnSelect), + icon = this._getIcon(options), + iconCls = this._getIconCls(options), + html = ''; + + if (icon) { + html = ''; + } else if (iconCls) { + html = ''; + } + + el.innerHTML = html + options.text; + el.href = '#'; + + L.DomEvent + .on(el, 'mouseover', this._onItemMouseOver, this) + .on(el, 'mouseout', this._onItemMouseOut, this) + .on(el, 'mousedown', L.DomEvent.stopPropagation) + .on(el, 'click', callback); + + if (L.Browser.touch) { + L.DomEvent.on(el, this._touchstart, L.DomEvent.stopPropagation); + } + + // Devices without a mouse fire "mouseover" on tap, but never “mouseout" + if (!L.Browser.pointer) { + L.DomEvent.on(el, 'click', this._onItemMouseOut, this); + } + + return { + id: L.Util.stamp(el), + el: el, + callback: callback + }; + }, + + _removeItem: function (id) { + var item, + el, + i, l, callback; + + for (i = 0, l = this._items.length; i < l; i++) { + item = this._items[i]; + + if (item.id === id) { + el = item.el; + callback = item.callback; + + if (callback) { + L.DomEvent + .off(el, 'mouseover', this._onItemMouseOver, this) + .off(el, 'mouseover', this._onItemMouseOut, this) + .off(el, 'mousedown', L.DomEvent.stopPropagation) + .off(el, 'click', callback); + + if (L.Browser.touch) { + L.DomEvent.off(el, this._touchstart, L.DomEvent.stopPropagation); + } + + if (!L.Browser.pointer) { + L.DomEvent.on(el, 'click', this._onItemMouseOut, this); + } + } + + this._container.removeChild(el); + this._items.splice(i, 1); + + return item; + } + } + return null; + }, + + _createSeparator: function (container, index) { + var el = this._insertElementAt('div', L.Map.ContextMenu.BASE_CLS + '-separator', container, index); + + return { + id: L.Util.stamp(el), + el: el + }; + }, + + _createEventHandler: function (el, func, context, hideOnSelect) { + var me = this, + map = this._map, + disabledCls = L.Map.ContextMenu.BASE_CLS + '-item-disabled', + hideOnSelect = (hideOnSelect !== undefined) ? hideOnSelect : true; + + return function (e) { + if (L.DomUtil.hasClass(el, disabledCls)) { + return; + } + + if (hideOnSelect) { + me._hide(); + } + + if (func) { + func.call(context || map, me._showLocation); + } + + me._map.fire('contextmenu:select', { + contextmenu: me, + el: el + }); + }; + }, + + _insertElementAt: function (tagName, className, container, index) { + var refEl, + el = document.createElement(tagName); + + el.className = className; + + if (index !== undefined) { + refEl = container.children[index]; + } + + if (refEl) { + container.insertBefore(el, refEl); + } else { + container.appendChild(el); + } + + return el; + }, + + _show: function (e) { + this._showAtPoint(e.containerPoint, e); + }, + + _showAtPoint: function (pt, data) { + if (this._items.length) { + var map = this._map, + layerPoint = map.containerPointToLayerPoint(pt), + latlng = map.layerPointToLatLng(layerPoint), + event = L.extend(data || {}, {contextmenu: this}); + + this._showLocation = { + latlng: latlng, + layerPoint: layerPoint, + containerPoint: pt + }; + + if (data && data.relatedTarget){ + this._showLocation.relatedTarget = data.relatedTarget; + } + + this._setPosition(pt); + + if (!this._visible) { + this._container.style.display = 'block'; + this._visible = true; + } + + this._map.fire('contextmenu.show', event); + } + }, + + _hide: function () { + if (this._visible) { + this._visible = false; + this._container.style.display = 'none'; + this._map.fire('contextmenu.hide', {contextmenu: this}); + } + }, + + _getIcon: function (options) { + return L.Browser.retina && options.retinaIcon || options.icon; + }, + + _getIconCls: function (options) { + return L.Browser.retina && options.retinaIconCls || options.iconCls; + }, + + _setPosition: function (pt) { + var mapSize = this._map.getSize(), + container = this._container, + containerSize = this._getElementSize(container), + anchor; + + if (this._map.options.contextmenuAnchor) { + anchor = L.point(this._map.options.contextmenuAnchor); + pt = pt.add(anchor); + } + + container._leaflet_pos = pt; + + if (pt.x + containerSize.x > mapSize.x) { + container.style.left = 'auto'; + container.style.right = Math.min(Math.max(mapSize.x - pt.x, 0), mapSize.x - containerSize.x - 1) + 'px'; + } else { + container.style.left = Math.max(pt.x, 0) + 'px'; + container.style.right = 'auto'; + } + + if (pt.y + containerSize.y > mapSize.y) { + container.style.top = 'auto'; + container.style.bottom = Math.min(Math.max(mapSize.y - pt.y, 0), mapSize.y - containerSize.y - 1) + 'px'; + } else { + container.style.top = Math.max(pt.y, 0) + 'px'; + container.style.bottom = 'auto'; + } + }, + + _getElementSize: function (el) { + var size = this._size, + initialDisplay = el.style.display; + + if (!size || this._sizeChanged) { + size = {}; + + el.style.left = '-999999px'; + el.style.right = 'auto'; + el.style.display = 'block'; + + size.x = el.offsetWidth; + size.y = el.offsetHeight; + + el.style.left = 'auto'; + el.style.display = initialDisplay; + + this._sizeChanged = false; + } + + return size; + }, + + _onKeyDown: function (e) { + var key = e.keyCode; + + // If ESC pressed and context menu is visible hide it + if (key === 27) { + this._hide(); + } + }, + + _onItemMouseOver: function (e) { + L.DomUtil.addClass(e.target || e.srcElement, 'over'); + }, + + _onItemMouseOut: function (e) { + L.DomUtil.removeClass(e.target || e.srcElement, 'over'); + } +}); + +L.Map.addInitHook('addHandler', 'contextmenu', L.Map.ContextMenu); +L.Mixin.ContextMenu = { + bindContextMenu: function (options) { + L.setOptions(this, options); + this._initContextMenu(); + + return this; + }, + + unbindContextMenu: function (){ + this.off('contextmenu', this._showContextMenu, this); + + return this; + }, + + addContextMenuItem: function (item) { + this.options.contextmenuItems.push(item); + }, + + removeContextMenuItemWithIndex: function (index) { + var items = []; + for (var i = 0; i < this.options.contextmenuItems.length; i++) { + if (this.options.contextmenuItems[i].index == index){ + items.push(i); + } + } + var elem = items.pop(); + while (elem !== undefined) { + this.options.contextmenuItems.splice(elem,1); + elem = items.pop(); + } + }, + + replaceContextMenuItem: function (item) { + this.removeContextMenuItemWithIndex(item.index); + this.addContextMenuItem(item); + }, + + _initContextMenu: function () { + this._items = []; + + this.on('contextmenu', this._showContextMenu, this); + }, + + _showContextMenu: function (e) { + var itemOptions, + data, pt, i, l; + + if (this._map.contextmenu) { + data = L.extend({relatedTarget: this}, e); + + pt = this._map.mouseEventToContainerPoint(e.originalEvent); + + if (!this.options.contextmenuInheritItems) { + this._map.contextmenu.hideAllItems(); + } + + for (i = 0, l = this.options.contextmenuItems.length; i < l; i++) { + itemOptions = this.options.contextmenuItems[i]; + this._items.push(this._map.contextmenu.insertItem(itemOptions, itemOptions.index)); + } + + this._map.once('contextmenu.hide', this._hideContextMenu, this); + + this._map.contextmenu.showAt(pt, data); + } + }, + + _hideContextMenu: function () { + var i, l; + + for (i = 0, l = this._items.length; i < l; i++) { + this._map.contextmenu.removeItem(this._items[i]); + } + this._items.length = 0; + + if (!this.options.contextmenuInheritItems) { + this._map.contextmenu.showAllItems(); + } + } +}; + +var classes = [L.Marker, L.Path], + defaultOptions = { + contextmenu: false, + contextmenuItems: [], + contextmenuInheritItems: true + }, + cls, i, l; + +for (i = 0, l = classes.length; i < l; i++) { + cls = classes[i]; + + // L.Class should probably provide an empty options hash, as it does not test + // for it here and add if needed + if (!cls.prototype.options) { + cls.prototype.options = defaultOptions; + } else { + cls.mergeOptions(defaultOptions); + } + + cls.addInitHook(function () { + if (this.options.contextmenu) { + this._initContextMenu(); + } + }); + + cls.include(L.Mixin.ContextMenu); +} +return L.Map.ContextMenu; +});