diff options
author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-18 23:41:15 +0800 |
---|---|---|
committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-18 23:52:49 +0800 |
commit | 4cf84d2fe96c004fef3b508ce344885db74917e2 (patch) | |
tree | 87fad915787b4621d0eb2f3e9086f54afdffa5a4 /src | |
parent | fdf3e2780edf92bd0cdb7aefc7a196e608b9cbd1 (diff) |
feat: swith focus map by tab key and click
* Use data-focus attribute and MutationObserver to move map-container
between Sementic HTML and Showcase.
* Currently set return value of docuement.onkeydown in editor, then
reset onkeydown function in dumbymap. Maybe there is a better way?
* CSS to make placeholder not so obvious
Diffstat (limited to 'src')
-rw-r--r-- | src/css/dumbymap.css | 27 | ||||
-rw-r--r-- | src/dumbymap.mjs | 282 | ||||
-rw-r--r-- | src/editor.mjs | 32 |
3 files changed, 208 insertions, 133 deletions
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css index 6554ceb..03c6c2a 100644 --- a/src/css/dumbymap.css +++ b/src/css/dumbymap.css | |||
@@ -40,8 +40,11 @@ | |||
40 | 40 | ||
41 | .Showcase { | 41 | .Showcase { |
42 | order: 2; | 42 | order: 2; |
43 | #mapPlaceholder { | 43 | display: none; |
44 | display: none; | 44 | |
45 | .map-container { | ||
46 | width: 100% !important; | ||
47 | height: 100% !important; | ||
45 | } | 48 | } |
46 | } | 49 | } |
47 | 50 | ||
@@ -74,9 +77,11 @@ | |||
74 | } | 77 | } |
75 | 78 | ||
76 | .map-container { | 79 | .map-container { |
80 | background: white; | ||
77 | margin: 2px; | 81 | margin: 2px; |
78 | &.focus { | 82 | border: 3px solid transparent; |
79 | border: solid gray; | 83 | &[data-focus=true] { |
84 | border: 3px solid gray; | ||
80 | } | 85 | } |
81 | } | 86 | } |
82 | 87 | ||
@@ -90,6 +95,11 @@ | |||
90 | width: fit-content; | 95 | width: fit-content; |
91 | flex: 0 0; | 96 | flex: 0 0; |
92 | } | 97 | } |
98 | [data-placeholder] { | ||
99 | box-sizing: border-box; | ||
100 | border: 5px solid white; | ||
101 | background: lightgray; | ||
102 | margin: 0; | ||
93 | } | 103 | } |
94 | } | 104 | } |
95 | 105 | ||
@@ -116,6 +126,10 @@ | |||
116 | display: none; | 126 | display: none; |
117 | pointer-events: none; | 127 | pointer-events: none; |
118 | } | 128 | } |
129 | |||
130 | .Showcase { | ||
131 | display: block; | ||
132 | } | ||
119 | } | 133 | } |
120 | 134 | ||
121 | .DumbyMap[data-layout=side] { | 135 | .DumbyMap[data-layout=side] { |
@@ -191,9 +205,14 @@ | |||
191 | } | 205 | } |
192 | } | 206 | } |
193 | } | 207 | } |
208 | |||
194 | > :not(.draggable-block) { | 209 | > :not(.draggable-block) { |
195 | display: none; | 210 | display: none; |
196 | } | 211 | } |
212 | |||
213 | [data-placeholder] { | ||
214 | max-width: 50px; | ||
215 | } | ||
197 | } | 216 | } |
198 | } | 217 | } |
199 | 218 | ||
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 6b64ee4..5800351 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -217,90 +217,6 @@ export const generateMaps = async (container, callback) => { | |||
217 | childRect.bottom < parentRect.bottom - offset | 217 | childRect.bottom < parentRect.bottom - offset |
218 | } | 218 | } |
219 | //}}} | 219 | //}}} |
220 | // Render Maps {{{ | ||
221 | |||
222 | const afterEachMapLoaded = (mapContainer) => { | ||
223 | const focusClickedMap = () => { | ||
224 | if (container.getAttribute('data-layout') !== 'none') return | ||
225 | |||
226 | container.querySelectorAll('.map-container') | ||
227 | .forEach(c => c.classList.remove('focus')) | ||
228 | mapContainer.classList.add('focus') | ||
229 | } | ||
230 | mapContainer.onclick = focusClickedMap | ||
231 | } | ||
232 | |||
233 | // Set unique ID for map container | ||
234 | const mapIdList = [] | ||
235 | const assignMapId = (config) => { | ||
236 | let mapId = config.id | ||
237 | if (!mapId) { | ||
238 | mapId = config.use?.split('/')?.at(-1) | ||
239 | let counter = 2 | ||
240 | while (mapIdList.includes(mapId)) { | ||
241 | mapId = `${config.use}.${counter}` | ||
242 | counter++ | ||
243 | } | ||
244 | config.id = mapId | ||
245 | } | ||
246 | mapIdList.push(mapId) | ||
247 | return config | ||
248 | } | ||
249 | |||
250 | // Render each code block with "language-map" class | ||
251 | const render = renderWith(config => ({ width: "100%", ...config })) | ||
252 | const renderTargets = Array.from(container.querySelectorAll('pre:has(.language-map)')) | ||
253 | .map(async (target) => { | ||
254 | // Get text in code block starts with '```map' | ||
255 | const configText = target.querySelector('.language-map') | ||
256 | .textContent | ||
257 | // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content | ||
258 | // replace it by normal space | ||
259 | .replace(/\u00A0/g, '\u0020') | ||
260 | |||
261 | let configList = [] | ||
262 | try { | ||
263 | configList = parseConfigsFromYaml(configText).map(assignMapId) | ||
264 | } catch (_) { | ||
265 | console.warn('Fail to parse yaml config for element', target) | ||
266 | } | ||
267 | |||
268 | // Render maps | ||
269 | return render(target, configList) | ||
270 | .then(results => { | ||
271 | results.forEach((mapByConfig) => { | ||
272 | if (mapByConfig.status === 'fulfilled') { | ||
273 | afterEachMapLoaded(mapByConfig.value) | ||
274 | return mapByConfig.value | ||
275 | } else { | ||
276 | console.error('Fail to render target element', mapByConfig.reason) | ||
277 | } | ||
278 | }) | ||
279 | }) | ||
280 | }) | ||
281 | |||
282 | const renderAllTargets = Promise.all(renderTargets) | ||
283 | renderAllTargets.then(() => { | ||
284 | console.info('Finish Rendering') | ||
285 | |||
286 | const maps = htmlHolder.querySelectorAll('.map-container') ?? [] | ||
287 | Array.from(maps) | ||
288 | .forEach(ele => { | ||
289 | callback(ele) | ||
290 | const markers = geoLinks | ||
291 | .filter(link => !link.targets || link.targets.includes(ele.id)) | ||
292 | .map(link => ({ | ||
293 | xy: link.xy, | ||
294 | title: link.url.pathname | ||
295 | })) | ||
296 | ele?.renderer?.addMarkers(markers) | ||
297 | }) | ||
298 | |||
299 | htmlHolder.querySelectorAll('.marker') | ||
300 | .forEach(marker => htmlHolder.anchors.push(marker)) | ||
301 | }) | ||
302 | |||
303 | //}}} | ||
304 | // Draggable Blocks{{{ | 220 | // Draggable Blocks{{{ |
305 | // Add draggable part for blocks | 221 | // Add draggable part for blocks |
306 | htmlHolder.blocks = Array.from(htmlHolder.querySelectorAll('.draggable-block')) | 222 | htmlHolder.blocks = Array.from(htmlHolder.querySelectorAll('.draggable-block')) |
@@ -322,16 +238,35 @@ export const generateMaps = async (container, callback) => { | |||
322 | 238 | ||
323 | // }}} | 239 | // }}} |
324 | // CSS observer {{{ | 240 | // CSS observer {{{ |
325 | |||
326 | // Set focusArea | 241 | // Set focusArea |
327 | const showcase = document.createElement('div') | 242 | const showcase = document.createElement('div') |
328 | container.appendChild(showcase) | 243 | container.appendChild(showcase) |
329 | showcase.classList.add('Showcase') | 244 | showcase.classList.add('Showcase') |
330 | const mapPlaceholder = document.createElement('div') | ||
331 | mapPlaceholder.id = 'mapPlaceholder' | ||
332 | showcase.appendChild(mapPlaceholder) | ||
333 | 245 | ||
334 | // Layout{{{ | 246 | // Focus Map {{{ |
247 | const mapFocusObserver = () => new MutationObserver((mutations) => { | ||
248 | const mutation = mutations.at(-1) | ||
249 | const target = mutation.target | ||
250 | const focus = target.getAttribute(mutation.attributeName) === 'true' | ||
251 | const shouldBeInShowcase = focus && getComputedStyle(showcase).display !== 'none' | ||
252 | |||
253 | if (shouldBeInShowcase) { | ||
254 | if (showcase.contains(target)) return | ||
255 | |||
256 | const placeholder = document.createElement('div') | ||
257 | placeholder.setAttribute('data-placeholder', target.id) | ||
258 | placeholder.style.width = target.style.width | ||
259 | target.parentElement.replaceChild(placeholder, target) | ||
260 | showcase.appendChild(target) | ||
261 | } else if (showcase.contains(target)) { | ||
262 | const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`) | ||
263 | if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`) | ||
264 | placeholder.parentElement.replaceChild(target, placeholder) | ||
265 | placeholder.remove() | ||
266 | } | ||
267 | }) | ||
268 | // }}} | ||
269 | // Layout {{{ | ||
335 | 270 | ||
336 | // press key to switch layout | 271 | // press key to switch layout |
337 | const layouts = ['none', 'side', 'overlay'] | 272 | const layouts = ['none', 'side', 'overlay'] |
@@ -339,41 +274,62 @@ export const generateMaps = async (container, callback) => { | |||
339 | 274 | ||
340 | // FIXME Use UI to switch layouts | 275 | // FIXME Use UI to switch layouts |
341 | const originalKeyDown = document.onkeydown | 276 | const originalKeyDown = document.onkeydown |
342 | document.onkeydown = (event) => { | 277 | document.onkeydown = (e) => { |
343 | originalKeyDown(event) | 278 | const event = originalKeyDown(e) |
279 | if (!event) return | ||
280 | |||
344 | if (event.key === 'x' && container.querySelector('.map-container')) { | 281 | if (event.key === 'x' && container.querySelector('.map-container')) { |
282 | e.preventDefault() | ||
345 | let currentLayout = container.getAttribute('data-layout') | 283 | let currentLayout = container.getAttribute('data-layout') |
346 | currentLayout = currentLayout ? currentLayout : 'none' | 284 | currentLayout = currentLayout ? currentLayout : 'none' |
347 | const nextLayout = layouts[(layouts.indexOf(currentLayout) + 1) % layouts.length] | 285 | const nextIndex = (layouts.indexOf(currentLayout) + 1) % layouts.length |
286 | const nextLayout = layouts[nextIndex] | ||
348 | 287 | ||
349 | container.setAttribute("data-layout", nextLayout) | 288 | container.setAttribute("data-layout", nextLayout) |
350 | } | 289 | } |
290 | |||
291 | if (event.key === 'Tab') { | ||
292 | e.preventDefault() | ||
293 | |||
294 | const selector = '.map-container, [data-placeholder]' | ||
295 | const candidates = Array.from(htmlHolder.querySelectorAll(selector)) | ||
296 | if (candidates.length === 0) return | ||
297 | |||
298 | const currentFocus = htmlHolder.querySelector('.map-container[data-focus=true]') | ||
299 | ?? htmlHolder.querySelector('[data-placeholder]') | ||
300 | Array.from(container.querySelectorAll('.map-container')).forEach(e => | ||
301 | e.removeAttribute('data-focus') | ||
302 | ); | ||
303 | const index = currentFocus | ||
304 | ? (candidates.indexOf(currentFocus) + (event.shiftKey ? -1 : 1)) % candidates.length | ||
305 | : 0 | ||
306 | const nextFocus = candidates.at(index) | ||
307 | nextFocus.setAttribute('data-focus', "true") | ||
308 | } | ||
351 | } | 309 | } |
352 | 310 | ||
353 | // observe layout change | 311 | // observe layout change |
354 | const layoutObserver = new MutationObserver(() => { | 312 | const layoutObserver = new MutationObserver((mutations) => { |
355 | const layout = container.getAttribute('data-layout') | 313 | const mutation = mutations.at(-1) |
356 | htmlHolder.blocks.forEach(b => b.style.display = "block") | 314 | const layout = container.getAttribute(mutation.attributeName) |
357 | 315 | ||
358 | if (layout === 'none') { | 316 | // Trigger Mutation Observer |
359 | mapPlaceholder.innerHTML = "" | 317 | const focusMap = container.querySelector('.map-container[data-focus=true]') |
360 | const map = showcase.querySelector('.map-container') | 318 | ?? container.querySelector('.map-container') |
361 | // Swap focused map and palceholder in markdown | 319 | focusMap?.setAttribute('data-focus', 'true') |
362 | if (map) { | 320 | |
363 | mapPlaceholder.parentElement?.replaceChild(map, mapPlaceholder) | 321 | // Check empty block with map-container in showcase |
364 | showcase.append(mapPlaceholder) | 322 | htmlHolder.blocks.forEach(b => { |
365 | } | 323 | const contentChildren = Array.from(b.querySelectorAll(':scope > :not(.draggable)')) ?? [] |
366 | } else { | 324 | if (contentChildren.length === 1 |
367 | // If paceholder is not set, create one and put map into focusArea | 325 | && elementsWithMapConfig.includes(contentChildren[0]) |
368 | if (showcase.contains(mapPlaceholder)) { | 326 | && !contentChildren[0].querySelector('.map-container') |
369 | const mapContainer = container.querySelector('.map-container.focus') ?? container.querySelector('.map-container') | 327 | ) { |
370 | mapPlaceholder.innerHTML = `<div>Placeholder</div>` | 328 | b.style.display = "none" |
371 | // TODO Get snapshot image | 329 | } else { |
372 | // mapPlaceholder.src = map.map.getCanvas().toDataURL() | 330 | b.style.display = "block" |
373 | mapContainer.parentElement?.replaceChild(mapPlaceholder, mapContainer) | ||
374 | showcase.appendChild(mapContainer) | ||
375 | } | 331 | } |
376 | } | 332 | }) |
377 | 333 | ||
378 | if (layout === 'overlay') { | 334 | if (layout === 'overlay') { |
379 | let [x, y] = [0, 0]; | 335 | let [x, y] = [0, 0]; |
@@ -398,7 +354,9 @@ export const generateMaps = async (container, callback) => { | |||
398 | block.removeAttribute('style') | 354 | block.removeAttribute('style') |
399 | try { | 355 | try { |
400 | block.draggableInstance.remove() | 356 | block.draggableInstance.remove() |
401 | } catch (_) { null } | 357 | } catch (_) { |
358 | null | ||
359 | } | ||
402 | }) | 360 | }) |
403 | } | 361 | } |
404 | }); | 362 | }); |
@@ -411,5 +369,99 @@ export const generateMaps = async (container, callback) => { | |||
411 | onRemove(htmlHolder, () => layoutObserver.disconnect()) | 369 | onRemove(htmlHolder, () => layoutObserver.disconnect()) |
412 | //}}} | 370 | //}}} |
413 | //}}} | 371 | //}}} |
372 | // Render Maps {{{ | ||
373 | |||
374 | const afterEachMapLoaded = (mapContainer) => { | ||
375 | const focusClickedMap = () => { | ||
376 | container.querySelectorAll('.map-container') | ||
377 | .forEach(c => c.removeAttribute('data-focus')) | ||
378 | mapContainer.setAttribute('data-focus', true) | ||
379 | } | ||
380 | mapContainer.onclick = focusClickedMap | ||
381 | mapContainer.setAttribute('tabindex', "-1") | ||
382 | |||
383 | const observer = mapFocusObserver() | ||
384 | mapFocusObserver().observe(mapContainer, { | ||
385 | attributes: true, | ||
386 | attributeFilter: ["data-focus"], | ||
387 | attributeOldValue: true | ||
388 | }); | ||
389 | onRemove(mapContainer, () => observer.disconnect()) | ||
390 | } | ||
391 | |||
392 | // Set unique ID for map container | ||
393 | const mapIdList = [] | ||
394 | const assignMapId = (config) => { | ||
395 | let mapId = config.id | ||
396 | if (!mapId) { | ||
397 | mapId = config.use?.split('/')?.at(-1) | ||
398 | let counter = 1 | ||
399 | while (!mapId || mapIdList.includes(mapId)) { | ||
400 | mapId = `${config.use ?? "unnamed"}-${counter}` | ||
401 | counter++ | ||
402 | } | ||
403 | config.id = mapId | ||
404 | } | ||
405 | mapIdList.push(mapId) | ||
406 | return config | ||
407 | } | ||
408 | |||
409 | // Render each code block with "language-map" class | ||
410 | const elementsWithMapConfig = Array.from(container.querySelectorAll('pre:has(.language-map)') ?? []) | ||
411 | const render = renderWith(config => ({ width: "100%", ...config })) | ||
412 | const renderTargets = elementsWithMapConfig | ||
413 | .map(async (target) => { | ||
414 | // Get text in code block starts with '```map' | ||
415 | const configText = target.querySelector('.language-map') | ||
416 | .textContent | ||
417 | // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content | ||
418 | // replace it by normal space | ||
419 | .replace(/\u00A0/g, '\u0020') | ||
420 | |||
421 | let configList = [] | ||
422 | try { | ||
423 | configList = parseConfigsFromYaml(configText).map(assignMapId) | ||
424 | } catch (_) { | ||
425 | console.warn('Fail to parse yaml config for element', target) | ||
426 | } | ||
427 | |||
428 | // Render maps | ||
429 | return render(target, configList) | ||
430 | .then(results => { | ||
431 | results.forEach((mapByConfig) => { | ||
432 | if (mapByConfig.status === 'fulfilled') { | ||
433 | afterEachMapLoaded(mapByConfig.value) | ||
434 | return mapByConfig.value | ||
435 | } else { | ||
436 | console.error('Fail to render target element', mapByConfig.reason) | ||
437 | } | ||
438 | }) | ||
439 | }) | ||
440 | }) | ||
441 | |||
442 | const renderAllTargets = Promise.all(renderTargets) | ||
443 | .then(() => { | ||
444 | console.info('Finish Rendering') | ||
445 | |||
446 | const maps = htmlHolder.querySelectorAll('.map-container') ?? [] | ||
447 | Array.from(maps) | ||
448 | .forEach(ele => { | ||
449 | callback(ele) | ||
450 | const markers = geoLinks | ||
451 | .filter(link => !link.targets || link.targets.includes(ele.id)) | ||
452 | .map(link => ({ | ||
453 | xy: link.xy, | ||
454 | title: link.url.pathname | ||
455 | })) | ||
456 | ele?.renderer?.addMarkers(markers) | ||
457 | }) | ||
458 | |||
459 | htmlHolder.querySelectorAll('.marker') | ||
460 | .forEach(marker => htmlHolder.anchors.push(marker)) | ||
461 | |||
462 | return maps | ||
463 | }) | ||
464 | |||
465 | //}}} | ||
414 | return renderAllTargets | 466 | return renderAllTargets |
415 | } | 467 | } |
diff --git a/src/editor.mjs b/src/editor.mjs index b2c4d54..168e66b 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
@@ -40,7 +40,8 @@ const editor = new EasyMDE({ | |||
40 | shortcuts: { | 40 | shortcuts: { |
41 | "map": "Ctrl-Alt-M", | 41 | "map": "Ctrl-Alt-M", |
42 | "debug": "Ctrl-Alt-D", | 42 | "debug": "Ctrl-Alt-D", |
43 | "toggleUnorderedList": "Ctrl-Shift-L", | 43 | "toggleUnorderedList": null, |
44 | "toggleOrderedList": null, | ||
44 | }, | 45 | }, |
45 | toolbar: [ | 46 | toolbar: [ |
46 | { | 47 | { |
@@ -156,12 +157,12 @@ const debounceForMap = (() => { | |||
156 | } | 157 | } |
157 | })() | 158 | })() |
158 | 159 | ||
159 | const afterMapRendered = (mapHolder) => { | 160 | const afterMapRendered = (_) => { |
160 | mapHolder.oncontextmenu = (event) => { | 161 | // mapHolder.oncontextmenu = (event) => { |
161 | event.preventDefault() | 162 | // event.preventDefault() |
162 | const lonLat = mapHolder.renderer.unproject([event.x, event.y]) | 163 | // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) |
163 | // TODO... | 164 | // // TODO... |
164 | } | 165 | // } |
165 | } | 166 | } |
166 | 167 | ||
167 | const updateDumbyMap = () => { | 168 | const updateDumbyMap = () => { |
@@ -174,7 +175,6 @@ updateDumbyMap() | |||
174 | 175 | ||
175 | // Re-render HTML by editor content | 176 | // Re-render HTML by editor content |
176 | cm.on("change", (_, change) => { | 177 | cm.on("change", (_, change) => { |
177 | console.log('change', change.text) | ||
178 | updateDumbyMap() | 178 | updateDumbyMap() |
179 | addClassToCodeLines() | 179 | addClassToCodeLines() |
180 | completeForCodeBlock(change) | 180 | completeForCodeBlock(change) |
@@ -345,7 +345,6 @@ const handleTypingInCodeBlock = (anchor) => { | |||
345 | // }}} | 345 | // }}} |
346 | // FUNCTION: get suggestions by current input {{{ | 346 | // FUNCTION: get suggestions by current input {{{ |
347 | const getSuggestions = (anchor) => { | 347 | const getSuggestions = (anchor) => { |
348 | let suggestions = [] | ||
349 | const text = cm.getLine(anchor.line) | 348 | const text = cm.getLine(anchor.line) |
350 | 349 | ||
351 | // Clear marks on text | 350 | // Clear marks on text |
@@ -445,7 +444,7 @@ const getSuggestions = (anchor) => { | |||
445 | ) | 444 | ) |
446 | return rendererSuggestions.length > 0 ? rendererSuggestions : [] | 445 | return rendererSuggestions.length > 0 ? rendererSuggestions : [] |
447 | } | 446 | } |
448 | return suggestions | 447 | return [] |
449 | } | 448 | } |
450 | // }}} | 449 | // }}} |
451 | // {{{ FUNCTION: Show element about suggestions | 450 | // {{{ FUNCTION: Show element about suggestions |
@@ -505,11 +504,9 @@ cm.on("cursorActivity", (_) => { | |||
505 | }); | 504 | }); |
506 | // }}} | 505 | // }}} |
507 | // EVENT: keydown for suggestions {{{ | 506 | // EVENT: keydown for suggestions {{{ |
507 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'] | ||
508 | cm.on('keydown', (_, e) => { | 508 | cm.on('keydown', (_, e) => { |
509 | 509 | if (!cm.hasFocus || !keyForSuggestions.includes(e.key) || suggestionsEle.style.display === 'none') return; | |
510 | // Only the following keys are used | ||
511 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'].includes(e.key) | ||
512 | if (!keyForSuggestions || suggestionsEle.style.display === 'none') return; | ||
513 | 510 | ||
514 | // Directly add a newline when no suggestion is selected | 511 | // Directly add a newline when no suggestion is selected |
515 | const currentSuggestion = suggestionsEle.querySelector('.container__suggestion.focus') | 512 | const currentSuggestion = suggestionsEle.querySelector('.container__suggestion.focus') |
@@ -545,6 +542,13 @@ cm.on('keydown', (_, e) => { | |||
545 | document.onkeydown = (e) => { | 542 | document.onkeydown = (e) => { |
546 | if (e.altKey && e.ctrlKey && e.key === 'm') { | 543 | if (e.altKey && e.ctrlKey && e.key === 'm') { |
547 | toggleEditing() | 544 | toggleEditing() |
545 | e.preventDefault() | ||
546 | return null | ||
547 | } | ||
548 | if (cm.hasFocus()) { | ||
549 | return null | ||
550 | } else { | ||
551 | return e | ||
548 | } | 552 | } |
549 | } | 553 | } |
550 | 554 | ||