Skip to content

Commit 75839e8

Browse files
committed
feat: add finishOnEnter option and fix Escape key behavior
Adds a new global option `finishOnEnter` that allows users to finish drawing shapes (Line, Polygon) by pressing Enter when enough vertices are placed. Also fixes Escape key handling to prevent default browser behavior (blue focus ring) when exiting modes. Usage: map.pm.setGlobalOptions({ exitModeOnEscape: true, // Exit any mode with Escape key finishOnEnter: true, // Finish drawing with Enter key }); - finishOnEnter works for Line (2+ vertices) and Polygon (3+ vertices) - Escape key now prevents default browser behavior when active - Both options are disabled by default for backward compatibility - Includes TypeScript definitions - Includes comprehensive Cypress E2E tests Related to #1586
1 parent 0ac61d5 commit 75839e8

File tree

4 files changed

+314
-5
lines changed

4 files changed

+314
-5
lines changed

cypress/e2e/finishOnEnter.js

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
describe('Exit Mode on Escape Key - Default Option', () => {
2+
it('should have exitModeOnEscape disabled by default', () => {
3+
// No beforeEach runs here, so map has default options
4+
cy.window().then(({ map }) => {
5+
const options = map.pm.getGlobalOptions();
6+
expect(options.exitModeOnEscape).to.equal(false);
7+
});
8+
});
9+
10+
it('should NOT exit draw mode when exitModeOnEscape is disabled by default', () => {
11+
cy.toolbarButton('polygon').click();
12+
13+
// Verify draw mode is enabled
14+
cy.window().then(({ map }) => {
15+
expect(map.pm.globalDrawModeEnabled()).to.equal(true);
16+
});
17+
18+
// Press Escape key
19+
cy.get('body').type('{esc}');
20+
21+
// Verify draw mode is STILL enabled (default is false)
22+
cy.window().then(({ map }) => {
23+
expect(map.pm.globalDrawModeEnabled()).to.equal(true);
24+
});
25+
});
26+
});
27+
28+
describe('Finish Drawing on Enter Key', () => {
29+
const mapSelector = '#map';
30+
31+
beforeEach(() => {
32+
cy.window().then(({ map }) => {
33+
map.pm.setGlobalOptions({
34+
finishOnEnter: true,
35+
});
36+
});
37+
});
38+
39+
describe('Polygon', () => {
40+
it('should finish polygon drawing when Enter is pressed with 3+ vertices', () => {
41+
// Disable snapping to ensure vertices don't snap to each other
42+
cy.window().then(({ map }) => {
43+
map.pm.setGlobalOptions({
44+
finishOnEnter: true,
45+
snappable: false,
46+
});
47+
});
48+
49+
cy.toolbarButton('polygon').click();
50+
51+
// Draw 3 vertices (spread out to avoid any snapping/closing issues)
52+
cy.get(mapSelector).click(150, 150);
53+
cy.get(mapSelector).click(150, 350);
54+
cy.get(mapSelector).click(350, 350);
55+
56+
// Verify draw mode is enabled and we have 3 vertices
57+
cy.window().then(({ map }) => {
58+
expect(map.pm.globalDrawModeEnabled()).to.equal(true);
59+
// Polygon uses coords.length directly, not flattened
60+
const coords = map.pm.Draw.Polygon._layer.getLatLngs();
61+
expect(coords.length).to.equal(3);
62+
});
63+
64+
// Press Enter key to finish
65+
cy.get('body').type('{enter}');
66+
67+
// Verify a polygon was created
68+
cy.window().then(({ map }) => {
69+
const layers = map.pm.getGeomanDrawLayers();
70+
expect(layers.length).to.equal(1);
71+
});
72+
});
73+
74+
it('should NOT finish polygon drawing when Enter is pressed with less than 3 vertices', () => {
75+
// Disable snapping to ensure vertices don't snap to each other
76+
cy.window().then(({ map }) => {
77+
map.pm.setGlobalOptions({
78+
finishOnEnter: true,
79+
snappable: false,
80+
});
81+
});
82+
83+
cy.toolbarButton('polygon').click();
84+
85+
// Draw only 2 vertices
86+
cy.get(mapSelector).click(150, 150);
87+
cy.get(mapSelector).click(150, 350);
88+
89+
// Verify we have only 2 vertices
90+
cy.window().then(({ map }) => {
91+
// Polygon uses coords.length directly, not flattened
92+
const coords = map.pm.Draw.Polygon._layer.getLatLngs();
93+
expect(coords.length).to.equal(2);
94+
});
95+
96+
// Press Enter key
97+
cy.get('body').type('{enter}');
98+
99+
// Verify no polygon was created and draw mode is still enabled
100+
cy.window().then(({ map }) => {
101+
expect(map.pm.globalDrawModeEnabled()).to.equal(true);
102+
const layers = map.pm.getGeomanDrawLayers();
103+
expect(layers.length).to.equal(0);
104+
});
105+
});
106+
});
107+
108+
describe('Line', () => {
109+
it('should finish line drawing when Enter is pressed with 2+ vertices', () => {
110+
cy.toolbarButton('polyline').click();
111+
112+
// Draw 2 vertices
113+
cy.get(mapSelector).click(200, 200);
114+
cy.get(mapSelector).click(200, 300);
115+
116+
// Verify draw mode is enabled
117+
cy.window().then(({ map }) => {
118+
expect(map.pm.globalDrawModeEnabled()).to.equal(true);
119+
});
120+
121+
// Press Enter key to finish
122+
cy.get('body').type('{enter}');
123+
124+
// Verify a line was created
125+
cy.window().then(({ map }) => {
126+
const layers = map.pm.getGeomanDrawLayers();
127+
expect(layers.length).to.equal(1);
128+
});
129+
});
130+
131+
it('should NOT finish line drawing when Enter is pressed with less than 2 vertices', () => {
132+
cy.toolbarButton('polyline').click();
133+
134+
// Draw only 1 vertex
135+
cy.get(mapSelector).click(200, 200);
136+
137+
// Press Enter key
138+
cy.get('body').type('{enter}');
139+
140+
// Verify no line was created and draw mode is still enabled
141+
cy.window().then(({ map }) => {
142+
expect(map.pm.globalDrawModeEnabled()).to.equal(true);
143+
const layers = map.pm.getGeomanDrawLayers();
144+
expect(layers.length).to.equal(0);
145+
});
146+
});
147+
});
148+
149+
describe('Marker', () => {
150+
it('should NOT finish marker on Enter (markers are single-click)', () => {
151+
cy.toolbarButton('marker').click();
152+
153+
// Don't place any marker yet
154+
// Press Enter key
155+
cy.get('body').type('{enter}');
156+
157+
// Verify draw mode is still enabled and no marker was created
158+
cy.window().then(({ map }) => {
159+
expect(map.pm.globalDrawModeEnabled()).to.equal(true);
160+
const layers = map.pm.getGeomanDrawLayers();
161+
expect(layers.length).to.equal(0);
162+
});
163+
});
164+
});
165+
166+
describe('finishOnEnter option disabled', () => {
167+
it('should NOT finish drawing when finishOnEnter is false', () => {
168+
// Disable finishOnEnter and snapping
169+
cy.window().then(({ map }) => {
170+
map.pm.setGlobalOptions({
171+
finishOnEnter: false,
172+
snappable: false,
173+
});
174+
});
175+
176+
cy.toolbarButton('polygon').click();
177+
178+
// Draw 3 vertices (spread out to avoid any snapping/closing issues)
179+
cy.get(mapSelector).click(150, 150);
180+
cy.get(mapSelector).click(150, 350);
181+
cy.get(mapSelector).click(350, 350);
182+
183+
// Verify we have 3 vertices
184+
cy.window().then(({ map }) => {
185+
// Polygon uses coords.length directly, not flattened
186+
const coords = map.pm.Draw.Polygon._layer.getLatLngs();
187+
expect(coords.length).to.equal(3);
188+
});
189+
190+
// Press Enter key
191+
cy.get('body').type('{enter}');
192+
193+
// Verify draw mode is STILL enabled and no polygon was created
194+
cy.window().then(({ map }) => {
195+
expect(map.pm.globalDrawModeEnabled()).to.equal(true);
196+
const layers = map.pm.getGeomanDrawLayers();
197+
expect(layers.length).to.equal(0);
198+
});
199+
});
200+
});
201+
});
202+
203+
describe('Finish Drawing on Enter Key - Default Option', () => {
204+
it('should have finishOnEnter disabled by default', () => {
205+
cy.window().then(({ map }) => {
206+
const options = map.pm.getGlobalOptions();
207+
expect(options.finishOnEnter).to.equal(false);
208+
});
209+
});
210+
});

leaflet-geoman.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,9 @@ declare module 'leaflet' {
12461246

12471247
/** Enable exiting active modes (draw, edit, drag, rotate, remove, cut) by pressing the Escape key. Default: false */
12481248
exitModeOnEscape?: boolean;
1249+
1250+
/** Enable finishing drawing shapes (Line, Polygon, Cut) by pressing the Enter key when enough vertices are placed. Default: false */
1251+
finishOnEnter?: boolean;
12491252
}
12501253

12511254
interface PMDrawMap {

src/js/L.PM.Map.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const Map = L.Class.extend({
4141
},
4242
draggable: true,
4343
exitModeOnEscape: false,
44+
finishOnEnter: false,
4445
};
4546

4647
this.Keyboard._initKeyListener(map);

src/js/Mixins/Keyboard.js

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,31 @@ const createKeyboardMixins = () => ({
99
// clean up global listeners when current map instance is destroyed
1010
map.once('unload', this._unbindKeyListenerEvents, this);
1111
},
12-
_handleEscapeKey() {
12+
_handleEscapeKey(e) {
1313
const pm = this.map.pm;
1414
const globalOptions = pm.getGlobalOptions();
1515

1616
// Only handle Escape if the option is enabled
1717
if (!globalOptions.exitModeOnEscape) {
18-
return;
18+
return false;
1919
}
2020

21+
// Check if any mode is active
22+
const hasActiveMode =
23+
pm.globalDrawModeEnabled() ||
24+
pm.globalEditModeEnabled() ||
25+
pm.globalDragModeEnabled() ||
26+
pm.globalRemovalModeEnabled() ||
27+
pm.globalRotateModeEnabled() ||
28+
pm.globalCutModeEnabled();
29+
30+
if (!hasActiveMode) {
31+
return false;
32+
}
33+
34+
// Prevent default browser behavior (focus ring, etc.)
35+
e.preventDefault();
36+
2137
// Disable all active modes
2238
// 1. Disable draw mode if active
2339
if (pm.globalDrawModeEnabled()) {
@@ -48,6 +64,78 @@ const createKeyboardMixins = () => ({
4864
if (pm.globalCutModeEnabled()) {
4965
pm.disableGlobalCutMode();
5066
}
67+
68+
return true;
69+
},
70+
_handleEnterKey(e) {
71+
const pm = this.map.pm;
72+
const globalOptions = pm.getGlobalOptions();
73+
74+
// Only handle Enter if the option is enabled
75+
if (!globalOptions.finishOnEnter) {
76+
return false;
77+
}
78+
79+
// Only handle Enter for draw mode
80+
const activeShape = pm.Draw.getActiveShape();
81+
if (!activeShape) {
82+
return false;
83+
}
84+
85+
// Get the active draw instance
86+
const drawInstance = pm.Draw[activeShape];
87+
if (!drawInstance || !drawInstance._finishShape) {
88+
return false;
89+
}
90+
91+
// Check if the shape can be finished (has enough vertices)
92+
// For shapes that support _finishShape, try to finish
93+
// The _finishShape method itself checks if there are enough vertices
94+
const canFinish = this._canFinishShape(drawInstance, activeShape);
95+
if (!canFinish) {
96+
return false;
97+
}
98+
99+
// Prevent default behavior
100+
e.preventDefault();
101+
102+
// Finish the shape
103+
drawInstance._finishShape();
104+
105+
return true;
106+
},
107+
_canFinishShape(drawInstance, activeShape) {
108+
// Check if we can finish the current shape based on vertex count
109+
// Different shapes have different minimum vertex requirements
110+
111+
// Shapes that don't support multi-vertex drawing
112+
if (['Marker', 'CircleMarker', 'Circle', 'Text'].includes(activeShape)) {
113+
return false;
114+
}
115+
116+
// For Rectangle, check if drawing is in progress (has start point)
117+
if (activeShape === 'Rectangle') {
118+
return drawInstance._startMarker !== undefined;
119+
}
120+
121+
// For Line, Polygon, Cut - need to check vertex count
122+
if (drawInstance._layer && drawInstance._layer.getLatLngs) {
123+
const coords = drawInstance._layer.getLatLngs();
124+
125+
// Line needs at least 2 points (uses flat coords)
126+
if (activeShape === 'Line') {
127+
const flatCoords = coords.flat ? coords.flat() : coords;
128+
return flatCoords.length >= 2;
129+
}
130+
131+
// Polygon and Cut need at least 3 points
132+
// Polygon's _finishShape checks coords.length directly (not flattened)
133+
if (activeShape === 'Polygon' || activeShape === 'Cut') {
134+
return coords.length >= 3;
135+
}
136+
}
137+
138+
return false;
51139
},
52140
_unbindKeyListenerEvents() {
53141
L.DomEvent.off(document, 'keydown keyup', this._onKeyListener, this);
@@ -68,9 +156,16 @@ const createKeyboardMixins = () => ({
68156

69157
this.map.pm._fireKeyeventEvent(e, e.type, focusOn);
70158

71-
// Handle Escape key to exit active modes on keydown
72-
if (e.type === 'keydown' && e.key === 'Escape') {
73-
this._handleEscapeKey();
159+
// Handle special keys on keydown
160+
if (e.type === 'keydown') {
161+
// Handle Escape key to exit active modes
162+
if (e.key === 'Escape') {
163+
this._handleEscapeKey(e);
164+
}
165+
// Handle Enter key to finish drawing
166+
if (e.key === 'Enter') {
167+
this._handleEnterKey(e);
168+
}
74169
}
75170
},
76171
_onBlur(e) {

0 commit comments

Comments
 (0)