From 0ce8693bf2e255c5d02ae31af9415eb6d78b1fa6 Mon Sep 17 00:00:00 2001 From: Stefan Forsgren <33832077+steff-o@users.noreply.github.com> Date: Thu, 12 Jan 2023 12:22:02 +0100 Subject: [PATCH] feature: editor api (#1625) * Added api to editor * Cleaned up comments * Change drawtools on layer change Co-authored-by: Stefan Forsgren --- src/controls/editor.js | 43 ++++++++- src/controls/editor/drawtools.js | 16 ++++ src/controls/editor/edithandler.js | 128 +++++++++++++++++++++++++-- src/controls/editor/editorlayers.js | 14 ++- src/controls/editor/editortoolbar.js | 27 ++++-- src/dropdown.js | 19 ++++ 6 files changed, 231 insertions(+), 16 deletions(-) diff --git a/src/controls/editor.js b/src/controls/editor.js index 020d25603..d84131aef 100644 --- a/src/controls/editor.js +++ b/src/controls/editor.js @@ -1,5 +1,6 @@ import { Component, Button, dom } from '../ui'; import editorToolbar from './editor/editortoolbar'; +import EditHandler from './editor/edithandler'; const Editor = function Editor(options = {}) { const { @@ -10,12 +11,19 @@ const Editor = function Editor(options = {}) { let editorButton; let target; let viewer; + let isVisible = isActive; + + /** The handler were all state is kept */ + let editHandler; const toggleState = function toggleState() { const detail = { name: 'editor', active: editorButton.getState() === 'initial' }; + // 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; viewer.dispatch('toggleClickInteraction', detail); }; @@ -27,19 +35,41 @@ const Editor = function Editor(options = {}) { editorToolbar.toggleToolbar(false); }; + async function createFeature(layerName, geometry = null) { + const feature = await editHandler.createFeature(layerName, geometry); + return feature; + } + + async function deleteFeature(featureId, layerName) { + await editHandler.deleteFeature(featureId, layerName); + } + + function editFeatureAttributes(featureId, layerName) { + editHandler.editAttributesDialog(featureId, layerName); + } + return Component({ name: 'editor', onAdd(evt) { viewer = evt.target; target = `${viewer.getMain().getMapTools().getId()}`; const editableLayers = viewer.getLayersByProperty('editable', true, true); + const editableFeatureLayers = editableLayers.filter(l => !viewer.getLayer(l).get('isTable')); const currentLayer = options.defaultLayer || editableLayers[0]; const toolbarOptions = Object.assign({}, options, { + autoSave, + currentLayer, + editableLayers: editableFeatureLayers + }); + const handlerOptions = Object.assign({}, options, { autoForm, autoSave, currentLayer, - editableLayers + editableLayers, + isActive }); + editHandler = EditHandler(handlerOptions, viewer); + viewer.on('toggleClickInteraction', (detail) => { if (detail.name === 'editor' && detail.active) { editorButton.dispatch('change', { state: 'active' }); @@ -78,6 +108,17 @@ const Editor = function Editor(options = {}) { name: 'editor', active: isActive }); + }, + createFeature, + editFeatureAttributes, + deleteFeature, + changeActiveLayer: (layerName) => { + // Only need to actually cahne 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); + } + editorToolbar.changeActiveLayer(layerName); } }); }; diff --git a/src/controls/editor/drawtools.js b/src/controls/editor/drawtools.js index 1e488f3eb..65d75fef1 100644 --- a/src/controls/editor/drawtools.js +++ b/src/controls/editor/drawtools.js @@ -155,6 +155,22 @@ const drawToolsSelector = function drawToolsSelector(tools, defaultLayer, v) { } init(); + + return { + /** + * Call this to update available tools when layer has changed. No need to call if layer changed using GUI, as that is done by an event. + * @param {any} layerName + */ + updateTools: (layerName) => { + currentLayer = layerName; + // If not visible we don't actually have to change the tools now + if (active) { + setActive(false); + setDrawTools(currentLayer); + setActive(true); + } + } + }; }; export default drawToolsSelector; diff --git a/src/controls/editor/edithandler.js b/src/controls/editor/edithandler.js index be4545e06..4d0a4c1fb 100644 --- a/src/controls/editor/edithandler.js +++ b/src/controls/editor/edithandler.js @@ -56,6 +56,9 @@ let breadcrumbs = []; let autoCreatedFeature = false; 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 + // the exption that it can not determine if it is visble before interactions are set, i.e. it can't be used to determine if interactions should be set. + // Right now it does not matter as it is not used anywhere critical. if (modify === undefined || select === undefined) { return false; } @@ -80,10 +83,10 @@ function setActive(editType) { select.setActive(false); break; default: - draw.setActive(false); - hasDraw = false; + if (draw) draw.setActive(false); if (modify) modify.setActive(true); - select.setActive(true); + if (select) select.setActive(true); + hasDraw = false; break; } } @@ -526,11 +529,21 @@ function setInteractions(drawType) { } } +function closeAllModals() { + // Close all modals first to get rid of tags in DOM + if (modal) modal.closeModal(); + modal = null; + breadcrumbs.forEach(br => { + if (br.modal) br.modal.closeModal(); + }); + 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. - breadcrumbs = []; + closeAllModals(); currentLayer = layerName; setAllowedOperations(); setInteractions(); @@ -689,8 +702,15 @@ function onModalClosed() { attributes = lastBread.attributes; // State is restored, now show parent modal instead and refresh as the title attribute might have changed - modal.show(); - refreshRelatedTablesForm(lastBread.feature); + if (modal) { + modal.show(); + } + if (lastBread.feature) { + refreshRelatedTablesForm(lastBread.feature); + } + } else { + // last modal to be closed. Set to null so we can check if there is an modal. + modal = null; } } @@ -729,7 +749,9 @@ function onAttributesAbort(features) { abortBtnEl.addEventListener('click', (e) => { abortBtnEl.blur(); features.forEach((feature) => { - deleteFeature(feature, editLayers[currentLayer]).then(() => select.getFeatures().clear()); + deleteFeature(feature, viewer.getLayer(currentLayer)).then(() => { + if (select) select.getFeatures().clear(); + }); }); modal.closeModal(); // The modal does not fire close event when it is closed externally @@ -1303,6 +1325,82 @@ async function onAddChild(e) { } } +/** + * Opens the attribute editor dialog for a feature. The dialog excutes asynchronously and never returns anything. + * @param {any} feature + * @param {any} layer + */ +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. + // 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) { + const newBreadcrumb = { + layerName: currentLayer, + title, + attributes + }; + breadcrumbs.push(newBreadcrumb); + title = layer.get('title'); + attributes = layer.get('attributes'); + // Don't call setEditLayer, as that would change tools which requires that editor is active, + // and if it is a table it would probably crash on somehing geometry related. + currentLayer = layerName; + } + editAttributes(feature); +} + +/** + * Creates a new feature and adds it to a layer. Default values are set. If autosave is set, it returns when + * the feature has been saved and thus will have a permanent database Id. If not autosave it returns immediately (async of course) and + * the id will be a temporary Guid that can be used until the feature is saved, then it will be replaced. Keeping a reference to the feature + * itself will still work. + * @param {any} layerName Name of layer to add a feature to + * @param {any} geometry A geomtry to add to the feature that will be created + * @returns {Feature} the newly created feature + */ +async function createFeatureApi(layerName, geometry = null) { + const editLayer = editLayers[layerName]; + if (!editLayer) { + throw new Error('Ej redigerbart lager'); + } + const newfeature = new Feature(); + if (geometry) { + if (geometry.getType() !== editLayer.get('geometryType')) { + throw new Error('Kan inte lägga till en geometri av den typen i det lagret'); + } + newfeature.setGeometryName(editLayer.get('geometryName')); + newfeature.setGeometry(geometry); + } + await addFeatureToLayer(newfeature, layerName); + if (autoForm) { + autoCreatedFeature = true; + editAttributesDialogApi(newfeature.getId(), layerName); + } + return newfeature; +} + +async function deleteFeatureApi(featureId, layerName) { + const feature = viewer.getLayer(layerName).getSource().getFeatureById(featureId); + const layer = viewer.getLayer(layerName); + await deleteFeature(feature, layer); +} + +function setActiveLayerApi(layerName) { + const layer = editLayers[layerName]; + if (!layer || layer.get('isTable')) { + // Can't set tables as active in editor as the editor can't handle them. They are in list though, as they may + // be edited through api + throw new Error(`Layer ${layerName} är inte redigerbart`); + } + setEditLayer(layerName); +} /** * Eventhandler called from relatedTableForm when delete button is pressed * @param { any } e Event containing layers and features necessary @@ -1311,6 +1409,12 @@ function onDeleteChild(e) { deleteFeature(e.detail.feature, e.detail.layer).then(() => refreshRelatedTablesForm(e.detail.parentFeature)); } +/** + * 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. + * @param {any} options + * @param {any} v The viewer object + */ export default function editHandler(options, v) { viewer = v; featureInfo = viewer.getControlByName('featureInfo'); @@ -1331,6 +1435,8 @@ export default function editHandler(options, v) { autoSave = options.autoSave; autoForm = options.autoForm; validateOnDraw = options.validateOnDraw; + + // Listen to DOM events from menus and forms document.addEventListener('toggleEdit', onToggleEdit); document.addEventListener('changeEdit', onChangeEdit); document.addEventListener('editorShapes', onChangeShape); @@ -1338,4 +1444,12 @@ export default function editHandler(options, v) { document.addEventListener(dispatcher.EDIT_CHILD_EVENT, onEditChild); document.addEventListener(dispatcher.ADD_CHILD_EVENT, onAddChild); document.addEventListener(dispatcher.DELETE_CHILD_EVENT, onDeleteChild); + + return { + // These functions are called from Editor Component, possibly from its Api so change these calls with caution. + createFeature: createFeatureApi, + editAttributesDialog: editAttributesDialogApi, + deleteFeature: deleteFeatureApi, + setActiveLayer: setActiveLayerApi + }; } diff --git a/src/controls/editor/editorlayers.js b/src/controls/editor/editorlayers.js index 6935db5dc..0071885a7 100644 --- a/src/controls/editor/editorlayers.js +++ b/src/controls/editor/editorlayers.js @@ -5,6 +5,7 @@ import utils from '../../utils'; const createElement = utils.createElement; let viewer; +let dropdown; export default function editorLayers(editableLayers, v, optOptions = {}) { viewer = v; @@ -35,7 +36,7 @@ export default function editorLayers(editableLayers, v, optOptions = {}) { }); const { body: popoverHTML } = new DOMParser().parseFromString(popover, 'text/html'); document.getElementById('o-editor-layers').insertAdjacentElement('afterend', popoverHTML); - dropDown(options.target, options.selectOptions, { + dropdown = dropDown(options.target, options.selectOptions, { dataAttribute: 'layer', active: options.activeLayer }); @@ -83,6 +84,17 @@ export default function editorLayers(editableLayers, v, optOptions = {}) { document.addEventListener('changeEdit', onChangeEdit); } + /** + * Updates layer selection list to reflect the current setting + * @param {any} layerName + */ + function changeLayer(layerName) { + dropdown.select(layerName); + } + render(renderOptions); addListener(target); + return { + changeLayer + }; } diff --git a/src/controls/editor/editortoolbar.js b/src/controls/editor/editortoolbar.js index 31552b879..dccbb29c6 100644 --- a/src/controls/editor/editortoolbar.js +++ b/src/controls/editor/editortoolbar.js @@ -1,6 +1,5 @@ import editortemplate from './editortemplate'; import dispatcher from './editdispatcher'; -import editHandler from './edithandler'; import editorLayers from './editorlayers'; import drawTools from './drawtools'; @@ -14,6 +13,8 @@ let $editDelete; let $editLayers; let $editSave; let viewer; +let layerSelector; +let drawToolsSelector; function render() { const { body: editortemplateHTML } = new DOMParser().parseFromString(editortemplate, 'text/html'); @@ -152,6 +153,11 @@ function toggleSave(e) { } } +function changeLayerInternal(layer) { + currentLayer = layer; + setAllowedTools(); +} + /** * Called when toggleEdit event is raised * @param {any} e Custom event @@ -161,8 +167,7 @@ function onToggleEdit(e) { // If the event contains a currentLayer, the currentLayer has either changed // or the editor toolbar is activated and should display the last edited layer or default if first time if (tool === 'edit' && e.detail.currentLayer) { - currentLayer = e.detail.currentLayer; - setAllowedTools(); + changeLayerInternal(e.detail.currentLayer); } e.stopPropagation(); } @@ -172,7 +177,6 @@ function init(options, v) { editableLayers = options.editableLayers; // Keep a reference to viewer. Used later. viewer = v; - editHandler(options, v); render(); // Hide layers choice button if only 1 layer in editable if (editableLayers.length < 2) { @@ -182,10 +186,10 @@ function init(options, v) { if (options.autoSave) { $editSave.classList.add('o-hidden'); } - editorLayers(editableLayers, v, { + layerSelector = editorLayers(editableLayers, v, { activeLayer: currentLayer }); - drawTools(options.drawTools, currentLayer, v); + drawToolsSelector = drawTools(options.drawTools, currentLayer, v); document.addEventListener('enableInteraction', onEnableInteraction); document.addEventListener('changeEdit', onChangeEdit); @@ -202,6 +206,15 @@ function init(options, v) { export default (function exportInit() { return { init, - toggleToolbar + toggleToolbar, + /** + * Updates layer selection list to reflect the current setting + * @param {any} layerName + */ + changeActiveLayer: (layerName) => { + changeLayerInternal(layerName); + layerSelector.changeLayer(layerName); + drawToolsSelector.updateTools(layerName); + } }; }()); diff --git a/src/dropdown.js b/src/dropdown.js index 5cefd1d0d..1b488eb19 100644 --- a/src/dropdown.js +++ b/src/dropdown.js @@ -56,6 +56,25 @@ export default function dropDown(target, items, options) { }); } + /** + * Marks the provided value as selected. Does NOT fire the changeDropdown event as it is assumed that caller controls this control and + * already knows what to do. + * @param {any} value + */ + function select(value) { + const optionslist = targetEl.getElementsByTagName('ul').item(0).getElementsByTagName('li'); + const length = optionslist.length; + for (let ix = 0; ix < length; ix += 1) { + if (optionslist.item(ix).attributes[dataAttribute].value === value) { + toggleActive(optionslist.item(ix)); + } + } + } + render(); addListener(); + + return { + select + }; }