Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: editor api #1625

Merged
merged 3 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion src/controls/editor.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
};

Expand All @@ -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' });
Expand Down Expand Up @@ -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);
}
});
};
Expand Down
16 changes: 16 additions & 0 deletions src/controls/editor/drawtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
128 changes: 121 additions & 7 deletions src/controls/editor/edithandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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');
Expand All @@ -1331,11 +1435,21 @@ 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);
document.addEventListener('customDrawEnd', onCustomDrawEnd);
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
};
}
14 changes: 13 additions & 1 deletion src/controls/editor/editorlayers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
});
Expand Down Expand Up @@ -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
};
}
Loading