diff --git a/src/controls/editor.js b/src/controls/editor.js index a38775741..feda1e806 100644 --- a/src/controls/editor.js +++ b/src/controls/editor.js @@ -14,6 +14,9 @@ const Editor = function Editor(options = {}) { let viewer; let isVisible = isActive; let toolbarVisible = false; + /** Keeps track of the last selected item in featureinfo. We need to use our own variable for this + * in order to determine if editor got activated directly from featureinfo or some other tool has been active between. */ + let lastSelectedItem; /** The handler were all state is kept */ let editHandler; @@ -26,6 +29,16 @@ const Editor = function Editor(options = {}) { // There are some serious event dependencies between viewer, editor, edithandler, editortoolbar, editorlayers, dropdown and editorbutton, // which makes it almost impossible to do things in correct order. isVisible = detail.active; + // Actually, if we're going visible + if (isVisible) { + if (lastSelectedItem && lastSelectedItem.getLayer().get('editable') && !lastSelectedItem.getLayer().get('isTable')) { + // Set a preselected feature. No use to set layer in handler as toolbar keeps state that will override a change of layer in handler anyway + // when editor toolbar becomes visible. + editHandler.preselectFeature(lastSelectedItem.getFeature()); + // Have to set layer in toolbar instead of handler. + editorToolbar.changeActiveLayer(lastSelectedItem.getLayer().get('name')); + } + } viewer.dispatch('toggleClickInteraction', detail); }; @@ -76,10 +89,39 @@ const Editor = function Editor(options = {}) { }); editHandler = EditHandler(handlerOptions, viewer); + // Set up eventhandler for when featureinfo selects (or highlights) a feature + const featureInfo = viewer.getFeatureinfo(); + featureInfo.on('changeselection', detail => { + if (isVisible) { + // This can only happen if featureInfo.ShowFeatureInfo, featureInfo.showInfow or featureInfo.render is called + // from api, as featureInfo component can not be active when editor is so the user can not click in the map to select anything. + if (detail && detail.getLayer().get('editable') && !detail.getLayer().get('isTable')) { + // Set a preselected feature. + editHandler.preselectFeature(detail.getFeature()); + // Actually change active layer. + editHandler.setActiveLayer(detail.getLayer().get('name')); + // Have to update state in toolbar as well. + editorToolbar.changeActiveLayer(detail.getLayer().get('name')); + // Clear featureinfo to close the popup which otherwise would just be annoying + featureInfo.clear(); + } + } else { + lastSelectedItem = detail; + } + }); + + // Set up eventhandler for when featureinfo clears selection + // Event is sent from featureinfo when popup etc is closed or cleared, but not when tool changes + featureInfo.on('clearselection', () => { + lastSelectedItem = null; + }); + viewer.on('toggleClickInteraction', (detail) => { if (detail.name === 'editor' && detail.active) { editorButton.dispatch('change', { state: 'active' }); } else { + // Someone else got active. Ditch the last selected item as we don't go directly from featureinfo to edit + lastSelectedItem = null; editorButton.dispatch('change', { state: 'initial' }); } }); @@ -131,7 +173,7 @@ const Editor = function Editor(options = {}) { editFeatureAttributes, deleteFeature, changeActiveLayer: (layerName) => { - // Only need to actually cahne layer if editor is active. Otherwise state is just set in toolbar and will + // Only need to actually change layer if editor is active. Otherwise state is just set in toolbar and will // activate set layer when toggled visible if (isVisible) { editHandler.setActiveLayer(layerName); diff --git a/src/controls/editor/edithandler.js b/src/controls/editor/edithandler.js index 6053bc58c..b9bfc6f63 100644 --- a/src/controls/editor/edithandler.js +++ b/src/controls/editor/edithandler.js @@ -56,6 +56,7 @@ let allowEditGeometry; let breadcrumbs = []; let autoCreatedFeature = false; let infowindowCmp = false; +let preselectedFeature; function isActive() { // FIXME: this only happens at startup as they are set to null on closing. If checking for null/falsley/not truely it could work as isVisible with @@ -571,6 +572,12 @@ function setInteractions(drawType) { } }); } + + if (preselectedFeature) { + select.getFeatures().push(preselectedFeature); + } + // Clear it so we won't get stuck on this feature. This makes it unnecessary to clear it anywhere else. + preselectedFeature = null; if (allowEditGeometry) { modify = new Modify({ features: select.getFeatures() @@ -592,6 +599,7 @@ function setInteractions(drawType) { // If snap should be active then add snap internactions for all snap layers hasSnap = editLayer.get('snap'); if (hasSnap) { + // FIXME: selection will almost certainly be empty as featureInfo is cleared const selectionSource = featureInfo.getSelectionLayer().getSource(); const snapSources = editLayer.get('snapLayers') ? getSnapSources(editLayer.get('snapLayers')) : [editLayer.get('source')]; snapSources.push(selectionSource); @@ -599,20 +607,25 @@ function setInteractions(drawType) { } } +/** Closes all modals and resets breadcrumbs */ function closeAllModals() { - // Close all modals first to get rid of tags in DOM + // Close all modals before resetting breadcrumbs to get rid of tags in DOM if (modal) modal.closeModal(); modal = null; breadcrumbs.forEach(br => { if (br.modal) br.modal.closeModal(); }); + if (breadcrumbs.length > 0) { + currentLayer = breadcrumbs[0].layerName; + title = breadcrumbs[0].title; + attributes = breadcrumbs[0].attributes; + } breadcrumbs = []; } function setEditLayer(layerName) { - // It is not possible to actually change layer while having breadcrubs as all modals must be closed, which will - // pop off all breadcrumbs. - // But just in case something changes, reset the breadcrumbs when a new layer is edited. + // Close all modals first and restore state. This can only happen if calling using api, as + // the modal prevents user from clicking in the map conrol closeAllModals(); currentLayer = layerName; setAllowedOperations(); @@ -1388,10 +1401,8 @@ function editAttributesDialogApi(featureId, layerName = null) { const layer = viewer.getLayer(layerName); const feature = layer.getSource().getFeatureById(featureId); // Hijack the current layer for a while. If there's a modal visible it is closed (without saving) as editAttributes can not handle - // multiple dialogs for the same layer so to be safe we always close. Technically the user can not - // call this function when a modal is visible, as they can't click anywhere. + // multiple dialogs for the same layer so to be safe we always close. // Restoring currentLayer is performed in onModalClosed(), as we can't await the modal. - // Close all modals and eat all breadcrumbs closeAllModals(); // If editing in another layer, add a breadcrumb to restore layer when modal is closed. if (layerName && layerName !== currentLayer) { @@ -1463,6 +1474,15 @@ function onDeleteChild(e) { deleteFeature(e.detail.feature, e.detail.layer).then(() => refreshRelatedTablesForm(e.detail.parentFeature)); } +/** + * Sets a feature that will be active for editing when editor is activated. When the edit session starts, the feature's layer must + * be active and that state is kept in the editor toolbar and sent through an event, so you better update toolbar as well. + * @param {any} feature + */ +function preselectFeature(feature) { + preselectedFeature = feature; +} + /** * Creates the handler. It is used as sort of a singelton, but in theory there could be many handlers. * It communicates with the editor toolbar and forms using DOM events, which makes it messy to have more than one instance as they would use the same events. @@ -1508,6 +1528,7 @@ export default function editHandler(options, v) { createFeature: createFeatureApi, editAttributesDialog: editAttributesDialogApi, deleteFeature: deleteFeatureApi, - setActiveLayer: setActiveLayerApi + setActiveLayer: setActiveLayerApi, + preselectFeature }; } diff --git a/src/featureinfo.js b/src/featureinfo.js index 6c58e51c4..65b84aab7 100644 --- a/src/featureinfo.js +++ b/src/featureinfo.js @@ -17,7 +17,6 @@ import getAttributes, { getContent, featureinfotemplates } from './getattributes import relatedtables from './utils/relatedtables'; const styleTypes = StyleTypes(); -let selectionLayer; const Featureinfo = function Featureinfo(options = {}) { const { @@ -32,6 +31,7 @@ const Featureinfo = function Featureinfo(options = {}) { autoplay = false } = options; + let selectionLayer; let identifyTarget; let overlay; let items; @@ -47,6 +47,47 @@ const Featureinfo = function Featureinfo(options = {}) { const savedFeature = savedPin || savedSelection || undefined; const uiOutput = 'infowindow' in options ? options.infowindow : 'overlay'; + /** Dispatches a clearselectionevent. Should be emitted when window is closed or cleared but not when featureinfo is closed as a result of tool change. */ + const dispatchClearSelection = function dispatchClearSelection() { + component.dispatch('clearselection', null); + }; + + /** Eventhandler for Selectionmanager's clear event. */ + function onSelectionManagerClear() { + // Not much do to, just dispatch event as our own. + dispatchClearSelection(); + } + + /** + * Clears selection in all possible infowindows (overlay, sidebar and infoWindow) and closes the windows + * @param {any} supressEvent Set to true when closing as a result of tool change to supress sending clearselection event + */ + const clear = function clear(supressEvent) { + selectionLayer.clear(); + // Sidebar is static and always present. + sidebar.setVisibility(false); + // check needed for when sidebar or overlay are selected. + if (overlay) { + viewer.removeOverlays(overlay); + } + if (selectionManager) { + // clearSelection will fire an cleared event, but we don't want our handler to emit a clear event as we are the one closing, + // so we stop listening for a while. + selectionManager.un('cleared', onSelectionManagerClear); + // This actually closes infowindow as infowindow closes automatically when selection is empty. + selectionManager.clearSelection(); + selectionManager.on('cleared', onSelectionManagerClear); + } + if (!supressEvent) { + dispatchClearSelection(); + } + }; + + /** Callback called from overlay and sidebar when they are closed by their close buttons */ + function onInfoClosed() { + clear(false); + } + function setUIoutput(v) { switch (uiOutput) { case 'infowindow': @@ -54,7 +95,7 @@ const Featureinfo = function Featureinfo(options = {}) { break; case 'sidebar': - sidebar.init(v); + sidebar.init(v, { closeCb: onInfoClosed }); identifyTarget = 'sidebar'; break; @@ -64,16 +105,6 @@ const Featureinfo = function Featureinfo(options = {}) { } } - const clear = function clear() { - selectionLayer.clear(); - // check needed for when sidebar or overlay are selected. - if (selectionManager) selectionManager.clearSelection(); - sidebar.setVisibility(false); - if (overlay) { - viewer.removeOverlays(overlay); - } - }; - // FIXME: overly complex. Don't think layer can be a string anymore const getTitle = function getTitle(item) { let featureinfoTitle; @@ -379,14 +410,14 @@ const Featureinfo = function Featureinfo(options = {}) { const map = viewer.getMap(); items = identifyItems; - clear(); + clear(false); // FIXME: variable is overwritten in next row let content = items.map((i) => i.content).join(''); content = '
'; switch (target) { case 'overlay': { - popup = Popup(`#${viewer.getId()}`); + popup = Popup(`#${viewer.getId()}`, { closeCb: onInfoClosed }); popup.setContent({ content, title: getTitle(items[0]) @@ -607,10 +638,10 @@ const Featureinfo = function Featureinfo(options = {}) { const serverResult = data || []; const result = serverResult.concat(clientResult); if (result.length > 0) { - selectionLayer.clear(); + selectionLayer.clear(false); render(result, identifyTarget, evt.coordinate); } else if (selectionLayer.getFeatures().length > 0 || (identifyTarget === 'infowindow' && selectionManager.getNumberOfSelectedItems() > 0)) { - clear(); + clear(false); } else if (pinning) { const resolution = map.getView().getResolution(); sidebar.setVisibility(false); @@ -631,7 +662,7 @@ const Featureinfo = function Featureinfo(options = {}) { if (state) { map.on(clickEvent, onClick); } else { - clear(); + clear(true); map.un(clickEvent, onClick); } }; @@ -655,9 +686,12 @@ const Featureinfo = function Featureinfo(options = {}) { // Re dispatch selectionmanager's event as our own if (selectionManager) { selectionManager.on('highlight', evt => dispatchToggleFeatureEvent(evt)); + selectionManager.on('cleared', onSelectionManagerClear); } map.on(clickEvent, onClick); viewer.on('toggleClickInteraction', (detail) => { + // This line of beauty makes feature info active if explicitly set active of another control yields active state. + // which effectively makes this the default tool. if ((detail.name === 'featureinfo' && detail.active) || (detail.name !== 'featureinfo' && !detail.active)) { setActive(true); } else { diff --git a/src/popup.js b/src/popup.js index 3ba36d1b2..1f3896050 100644 --- a/src/popup.js +++ b/src/popup.js @@ -1,5 +1,4 @@ import { dom } from './ui'; -import Featureinfo from './featureinfo'; function render(target) { const pop = `
@@ -58,27 +57,46 @@ function setContent(config) { } } -function closePopup() { +/** + * Closes the window and optionally calls a callback set at init + * @param {any} cb + */ +function closePopupInternal(cb) { setVisibility(false); - Featureinfo().clear(); + if (cb) { + cb(); + } } -function bindUIActions() { - const closeel = document.querySelector('#o-popup .o-popup #o-close-button'); - closeel.addEventListener('click', (evt) => { - closePopup(); - evt.preventDefault(); - }); -} +/** + * Creates a new popup and adds it to the dom. + * @param {any} target id of parent DOM object + * @param {Object} opts options. + * @param {function} opts.closeCb Function without parameters to be called when popup is closed from close button. + */ +export default function popup(target, opts = {}) { + const { + closeCb + } = opts; + + function bindUIActions() { + const closeel = document.querySelector('#o-popup .o-popup #o-close-button'); + closeel.addEventListener('click', (evt) => { + closePopupInternal(closeCb); + evt.preventDefault(); + }); + } -export default function popup(target) { render(target); bindUIActions(); + return { getEl, setVisibility, setTitle, setContent, - closePopup + closePopup: () => { + closePopupInternal(closeCb); + } }; } diff --git a/src/selectionmanager.js b/src/selectionmanager.js index d2c277785..feb57f0d5 100644 --- a/src/selectionmanager.js +++ b/src/selectionmanager.js @@ -149,6 +149,7 @@ const Selectionmanager = function Selectionmanager(options = {}) { function clearSelection() { selectedItems.clear(); + component.dispatch('cleared', null); } function featureStyler(feature) { diff --git a/src/sidebar.js b/src/sidebar.js index 319b49f91..9069508fa 100644 --- a/src/sidebar.js +++ b/src/sidebar.js @@ -1,5 +1,9 @@ import { dom } from './ui'; -import Featureinfo from './featureinfo'; + +/* + * Will be imported as sort of "static" as it does not contain any creatish function + * There can be only one sidebar in an entire page (or iframe) + */ function setVisibility(visible) { const sideEl = document.getElementById('o-sidebar'); @@ -10,14 +14,20 @@ function setVisibility(visible) { } } -function closeSidebar() { +/** + * Closes the sidebar and optionally calls a callback. + * @param {any} viewer + */ +function closeSidebar(cb) { setVisibility(false); - Featureinfo().clear(); + if (cb) { + cb(); + } } -function bindUIActions() { +function bindUIActions(closeCb) { document.querySelector('#o-sidebar .o-sidebar #o-close-button').addEventListener('click', (evt) => { - closeSidebar(); + closeSidebar(closeCb); evt.preventDefault(); }); } @@ -46,7 +56,16 @@ function setContent(config) { } } -function init(viewer) { +/** + * Creates a new popup and adds it to the dom. + * @param {any} viewer the viewer object to attach to + * @param {Object} opts options. + * @param {function} opts.closeCb Function without parameters to be called when popup is closed from close button. + */ +function init(viewer, opts = {}) { + const { + closeCb + } = opts; const mapId = viewer.getId(); const el = `
@@ -65,7 +84,7 @@ function init(viewer) {
`; document.getElementById(mapId).appendChild(dom.html(el)); - bindUIActions(); + bindUIActions(closeCb); } export default {