diff --git a/ui/b/aboutdialog.js b/ui/b/aboutdialog.js index d68ef6cb..ced9d608 100644 --- a/ui/b/aboutdialog.js +++ b/ui/b/aboutdialog.js @@ -3,10 +3,11 @@ import { LitComponent, html, render, noChange, JsExtract, docs, ref } from '../little.js'; import * as Util from "../util.js"; +import * as Dom from "../dom.js"; // == HTML == const HTML = (t, d) => html` - t.dialog = h)} @cancel=${t.close_dialog} @pointerdown=${t.backdrop_click}> + t.dialog = h)} @close=${t.close_dialog}>
About ANKLANG
@@ -14,7 +15,7 @@ const HTML = (t, d) => html` ${INFOS_HTML (t, d)}
`; const INFOS_HTML = (t, d) => @@ -61,7 +62,7 @@ export class BAboutDialog extends LitComponent { } if (this.shown && !this.dialog.open && this.info_pairs.length > 0) { document.startViewTransition (async () => { - this.dialog.showModal(); + Dom.show_modal (this.dialog); await Promise.all ([this.updateComplete, info_promise]); }); } @@ -81,13 +82,6 @@ export class BAboutDialog extends LitComponent { this.dialog.close(); super.disconnectedCallback(); } - backdrop_click (event) - { - if (event.target === event.currentTarget && // target must be DIALOG or backdrop - (event.offsetX < 0 || event.offsetX > event.target.offsetWidth || // click must be outside - event.offsetY < 0 || event.offsetY > event.target.offsetHeight)) // which is backdrop - this.close_dialog (event); - } } customElements.define ('b-aboutdialog', BAboutDialog); diff --git a/ui/b/app.js b/ui/b/app.js index 9c77ea94..c9d85d83 100644 --- a/ui/b/app.js +++ b/ui/b/app.js @@ -99,13 +99,13 @@ export class AppClass { } async load_project_checked (project_or_path) { const err = await this.load_project (project_or_path); - if (err !== Ase.Error.NONE) - App.async_button_dialog ("Project Loading Error", - "Failed to open project.\n" + - displayfs (project_or_path) + ":\n" + - await Ase.server.error_blurb (err), [ - { label: 'Dismiss', autofocus: true }, - ], 'ERROR'); + if (err !== Ase.Error.NONE) { + let errblurb = Ase.server.error_blurb (err); + let msg = '# File IO Error\n \n \n'; + msg += 'Failed to load project:\n\n'; + msg += '`' + displayfs (project_or_path) + ": " + await errblurb + '`'; + App.show_notice (msg); + } return err; } async load_project (project_or_path) { diff --git a/ui/b/contextmenu.js b/ui/b/contextmenu.js index 5279f919..114896dd 100644 --- a/ui/b/contextmenu.js +++ b/ui/b/contextmenu.js @@ -62,6 +62,7 @@ import { LitComponent, html, render, noChange, JsExtract, docs, ref } from '../l import * as Util from "../util.js"; import * as Kbd from '../kbd.js'; import { text_content, get_uri, valid_uri, has_uri } from '../dom.js'; +import * as Dom from "../dom.js"; // == STYLE == JsExtract.css` @@ -362,7 +363,7 @@ class BContextMenu extends LitComponent { return (async () => { await this.updateComplete; // needed to access this.dialog this.reposition = true; - this.dialog.showModal(); + Dom.show_modal (this.dialog); this.blur(); App.zmove(); // force changes to be picked up // check items (and this used to handle auto-focus) diff --git a/ui/b/crawlerdialog.js b/ui/b/crawlerdialog.js index 9e76024d..5b2dce7e 100644 --- a/ui/b/crawlerdialog.js +++ b/ui/b/crawlerdialog.js @@ -27,6 +27,7 @@ import { Signal, State, Computed, Watcher, tracking_wrapper } from "../signal.js import { get_uri } from '../dom.js'; import * as Util from "../util.js"; import * as Kbd from "../kbd.js"; +import * as Dom from "../dom.js"; // == STYLE == JsExtract.css` @@ -195,7 +196,7 @@ class BCrawlerDialog extends LitComponent { if (!this.shown && this.dialog.open) this.dialog.close(); if (this.shown && !this.dialog.open) - this.dialog.showModal(); + Dom.show_modal (this.dialog); } connectedCallback() { diff --git a/ui/b/devicepanel.js b/ui/b/devicepanel.js index 6e083c78..ba375e2c 100644 --- a/ui/b/devicepanel.js +++ b/ui/b/devicepanel.js @@ -55,10 +55,9 @@ b-devicepanel { } position: relative; &::after { - position: absolute; left:0; top:0; right:0; bottom:0; - content: ' '; pointer-events: none; + @apply absolute pointer-events-none inset-0; + content: ' '; z-index: 9; //* raise above scrolled siblings */ box-shadow: inset -10px 0 7px -7px #000, inset 10px 0 7px -7px #000; - z-index: 9; //* raise above scrolled siblings */ } }`; diff --git a/ui/b/icon.js b/ui/b/icon.js index ade0ef46..f2ed0642 100644 --- a/ui/b/icon.js +++ b/ui/b/icon.js @@ -33,7 +33,7 @@ import * as Dom from "../dom.js"; // == STYLE == JsExtract.css` b-icon { // not using shadow-root here - display: inline-flex !important; + display: inline-flex; place-content: center center; flex-wrap: wrap; /* needed for align-content:center */ &[hflip] { transform: scaleX(-1); } diff --git a/ui/b/menubar.js b/ui/b/menubar.js index 700bcedf..c50612d4 100644 --- a/ui/b/menubar.js +++ b/ui/b/menubar.js @@ -39,7 +39,7 @@ const HTML = (t, d) => html` - t.filemenu = h)} id="g-filemenu" .activate=${activate} .isactive=${isactive} startfocus1 > + t.filemenu = h)} id="g-filemenu" .activate=${activate} .isactive=${isactive} > @@ -57,7 +57,7 @@ const HTML = (t, d) => html` - t.editmenu = h)} id="g-editmenu" .activate=${activate} .isactive=${isactive} startfocus > + t.editmenu = h)} id="g-editmenu" .activate=${activate} .isactive=${isactive} > @@ -69,7 +69,7 @@ const HTML = (t, d) => html` - t.viewmenu = h)} id="g-viewmenu" .activate=${activate} .isactive=${isactive} startfocus > + t.viewmenu = h)} id="g-viewmenu" .activate=${activate} .isactive=${isactive} > ${ELECTRON_MENUITEMS (t)} @@ -92,7 +92,7 @@ const HTML = (t, d) => html` - t.helpmenu = h)} id="g-helpmenu" .activate=${activate} .isactive=${isactive} startfocus > + t.helpmenu = h)} id="g-helpmenu" .activate=${activate} .isactive=${isactive} > @@ -185,7 +185,7 @@ async function activate (uri, event) (Electron ? [ 'Discard Changes', { label: 'Cancel', autofocus: true }, 'Save' ] : [ 'Discard Changes', { label: 'Cancel', autofocus: true }, - { label: 'Save', disabled: true } ]), + { label: 'Save', disabled: false } ]), 'QUESTION'); v = await v; if (v == 0) @@ -301,21 +301,18 @@ async function save_project (asnew = false) { if (replace != 1) return false; } - const err = await App.save_project (filename); - if (err === Ase.Error.NONE) - { - filename = await Data.project.saved_filename(); - let msg = '### Project Saved\n'; - msg += ' \n \nProject saved to: ``' + displayfs (filename) + '``\n'; - App.show_notice (msg); - return true; - } - await App.async_button_dialog ("File IO Error", - "Failed to save project.\n" + - displayfs (filename) + ": " + - await Ase.server.error_blurb (err), [ - { label: 'Dismiss', autofocus: true } - ], 'ERROR'); - return false; + let msg, err = await App.save_project (filename); + if (err === Ase.Error.NONE) { + filename = await Data.project.saved_filename(); // get canonicalized form + msg = '### Project Saved\n \n \n'; + msg += 'Project successfully saved to:\n\n`' + displayfs (filename) + '`\n'; + } else { + let errblurb = Ase.server.error_blurb (err); + msg = '# File IO Error\n \n \n'; + msg += 'Failed to save project:\n\n'; + msg += '`' + displayfs (filename) + ": " + await errblurb + '`'; + } + App.show_notice (msg); + return err === Ase.Error.NONE; } let save_project_last_dir = "~MUSIC"; diff --git a/ui/dom.js b/ui/dom.js index 509d025a..bc24f7cd 100644 --- a/ui/dom.js +++ b/ui/dom.js @@ -98,3 +98,46 @@ export function text_content (element, with_children = true) s += element.childNodes[i].textContent; return s; } + +/// Show a `dialog` via showModal() and close it on backdrop clicks. +export function show_modal (dialog) +{ + if (dialog.open) return; + // close dialog on backdrop clicks, but: + // - avoid matching text-select drags that end up on backdrop area + // - avoid matching Enter-click event coordinates from input-submit with clientX*clientY==0 + // - avoid matching an outside popup click, after a previous pointerdown+Escape combination + // - avoid re-popup by clicking on outside menu button and closing early on pointerdown + let pointer_outside = false; // must reset on every dialog.showModal() + const pointerdown = event => { + pointer_outside = (event.buttons && event.target === dialog && // backdrop has target==dialog + (event.offsetX < 0 || event.offsetX >= event.target.offsetWidth || + event.offsetY < 0 || event.offsetY >= event.target.offsetHeight)); + }; + const pointerup = event => { + if (pointer_outside && event.target === dialog && // backdrop as target is dialog + (event.offsetX < 0 || event.offsetX >= event.target.offsetWidth || + event.offsetY < 0 || event.offsetY >= event.target.offsetHeight)) + dialog.close(); + else + pointer_outside = false; + }; + const mousedown = event => { + // prevent focus on the dialog itself + if (event.buttons) + event.preventDefault(); + }; + const capture = { capture: true }; + const close = event => { + dialog.removeEventListener ('pointerdown', pointerdown, capture); + dialog.removeEventListener ('pointerup', pointerup); + dialog.removeEventListener ('mousedown', mousedown); + dialog.removeEventListener ('close', close); + }; + dialog.addEventListener ('pointerdown', pointerdown, capture); + dialog.addEventListener ('pointerup', pointerup); + dialog.addEventListener ('mousedown', mousedown); + dialog.addEventListener ('close', close); + dialog.showModal(); + return dialog; +}