diff --git a/ase/properties.cc b/ase/properties.cc
index e85286b7..27bd0a82 100644
--- a/ase/properties.cc
+++ b/ase/properties.cc
@@ -98,7 +98,7 @@ JSONIPC_INHERIT (LambdaPropertyImpl, Property);
void
LambdaPropertyImpl::notify()
{
- emit_event ("change", identifier());
+ emit_notify (identifier());
}
Value
@@ -204,7 +204,7 @@ void
Bag::on_events (const String &eventselector, const EventHandler &eventhandler)
{
for (auto p : props)
- connections.push_back (p->on_event ("change", eventhandler));
+ connections.push_back (p->on_event (eventselector, eventhandler));
}
} // Properties
diff --git a/ase/server.cc b/ase/server.cc
index 616fc51f..a2059625 100644
--- a/ase/server.cc
+++ b/ase/server.cc
@@ -94,7 +94,7 @@ Preferences::access_properties (const EventHandler &eventhandler)
bag += Text (&plugin_path, _("Plugin Path"), "", STANDARD + "searchpath",
_("Search path of directories, seperated by \";\", used to find plugins. This path "
"is searched for in addition to the standard plugin location on this system."));
- bag.on_events ("change", eventhandler);
+ bag.on_events ("notify", eventhandler);
return bag.props;
}
diff --git a/ui/Makefile.mk b/ui/Makefile.mk
index bd1c8641..77daad5e 100644
--- a/ui/Makefile.mk
+++ b/ui/Makefile.mk
@@ -336,7 +336,7 @@ $>/.tscheck.done: ui/types.d.ts ui/tsconfig.json $(ui/tscheck.deps) ui/Makefile.
$(QECHO) RUN tscheck
$Q cp ui/tsconfig.json ui/types.d.ts $>/ui/
@ # tsc *.js needs to find node_modules/ in the directory hierarchy ("moduleResolution": "node")
- -$Q cd $>/ && node_modules/.bin/tsc -p ui/tsconfig.json --pretty false |& ../misc/colorize.sh
+ -$Q cd $>/ && node_modules/.bin/tsc -p ui/tsconfig.json $${INSIDE_EMACS:+--pretty false}
$Q touch $@
$>/ui/.build2-stamp: $>/.tscheck.done
tscheck: $>/node_modules/.npm.done
diff --git a/ui/b/basics.js b/ui/b/basics.js
new file mode 100644
index 00000000..8a32e7c0
--- /dev/null
+++ b/ui/b/basics.js
@@ -0,0 +1,78 @@
+// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
+// @ts-check
+
+import { JsExtract } from '../little.js';
+import * as Util from '../util.js';
+
+//
+const STYLE_URL = await JsExtract.css_url (import.meta);
+JsExtract.scss`
+@import 'mixins.scss';
+
+h-flex {
+ display: flex; flex-basis: auto; flex-direction: row;
+ flex-wrap: nowrap; align-items: stretch; align-content: stretch; }
+h-flex[inline] { display: inline-flex; }
+
+v-flex {
+ display: flex; flex-basis: auto; flex-direction: column;
+ flex-wrap: nowrap; align-items: stretch; align-content: stretch; }
+v-flex[inline] { display: inline-flex; }
+
+c-grid { display: grid; }
+c-grid[inline] { display: inline-grid; }
+`;
+
+/** # PUSH-BUTTON - wrapper for an ordinary HTMLElement */
+class PushButton extends HTMLElement {
+ constructor() { super(); }
+ connectedCallback()
+ {
+ // LitCompnent: super.connectedCallback();
+ Util.add_style_sheet (this, STYLE_URL);
+ }
+}
+customElements.define ('push-button', PushButton);
+
+// Util.add_style_sheet (this, STYLE_URL);
+
+/** # B-HFLEX
+ * Horizontal [flex](https://developer.mozilla.org/en-US/docs/Web/CSS/flex) container element.
+ */
+class HFlex extends HTMLElement {
+ constructor() { super(); }
+ connectedCallback()
+ {
+ // LitCompnent: super.connectedCallback();
+ Util.add_style_sheet (this, STYLE_URL);
+ }
+}
+customElements.define ('h-flex', HFlex);
+
+/** # B-VFLEX
+ * Vertical [flex](https://developer.mozilla.org/en-US/docs/Web/CSS/flex) container element.
+ */
+class VFlex extends HTMLElement {
+ constructor() { super(); }
+ connectedCallback()
+ {
+ // LitCompnent: super.connectedCallback();
+ Util.add_style_sheet (this, STYLE_URL);
+ }
+}
+customElements.define ('v-flex', VFlex);
+
+/** # B-CGRID
+ * Simple [grid](https://developer.mozilla.org/en-US/docs/Web/CSS/grid) container element.
+ * See also [Grid Container](https://www.w3.org/TR/css-grid-1/#grid-containers)
+ * [Grid visual cheatsheet](http://grid.malven.co/)
+ */
+class CGrid extends HTMLElement {
+ constructor() { super(); }
+ connectedCallback()
+ {
+ // LitCompnent: super.connectedCallback();
+ Util.add_style_sheet (this, STYLE_URL);
+ }
+}
+customElements.define ('c-grid', CGrid);
diff --git a/ui/b/choice.vue b/ui/b/choice.vue
deleted file mode 100644
index c2a40a35..00000000
--- a/ui/b/choice.vue
+++ /dev/null
@@ -1,205 +0,0 @@
-
-
-
- # B-CHOICE
- This element provides a choice popup to choose from a set of options.
- It supports the Vue
- [v-model](https://vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model)
- protocol by emitting an `input` event on value changes and accepting inputs via the `value` prop.
- ## Props:
- *value*
- : Integer, the index of the choice value to be displayed.
- *choices*
- : List of choices: `[ { icon, label, blurb }... ]`
- ## Events:
- *update:value (value)*
- : Value change notification event, the first argument is the new value.
-
-
-
-
-
-
-
- {{ nick() }}
- ⬍
-
-
-
-
-
-
diff --git a/ui/b/choiceinput.js b/ui/b/choiceinput.js
new file mode 100644
index 00000000..ccae4e4a
--- /dev/null
+++ b/ui/b/choiceinput.js
@@ -0,0 +1,256 @@
+// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
+// @ts-check
+
+import { LitComponent, html, nothing, JsExtract, live, docs, ref } from '../little.js';
+import * as Util from '../util.js';
+
+/** # B-CHOICE
+ * This element provides a choice popup to choose from a set of options.
+ * It supports the Vue
+ * [v-model](https://vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model)
+ * protocol by emitting an `input` event on value changes and accepting inputs via the `value` prop.
+ * ## Props:
+ * *value*
+ * : Integer, the index of the choice value to be displayed.
+ * *choices*
+ * : List of choices: `[ { icon, label, blurb }... ]`
+ * ## Events:
+ * *valuechange*
+ * : Value change notification event.
+ */
+
+//
+const STYLE_URL = await JsExtract.css_url (import.meta);
+JsExtract.scss`
+@import 'mixins.scss';
+b-choiceinput {
+ display: flex;
+ flex-basis: auto;
+ flex-flow: row nowrap;
+ align-items: stretch;
+ align-content: stretch;
+ position: relative;
+ margin: 0;
+ white-space: nowrap;
+ user-select: none;
+ &.b-choice-big {
+ justify-content: left; text-align: left;
+ padding: .1em 0;
+ }
+ &.b-choice-small {
+ justify-content: center; text-align: center;
+ padding: 0;
+ }
+ b-objecteditor &.b-choice {
+ text-align: left;
+ justify-content: left; text-align: left;
+ padding: 0;
+ flex: 1 1 auto;
+ }
+ .-nick {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 1em; flex: 1 1 auto;
+ }
+ .-arrow {
+ flex: 0 0 auto; width: 1em;
+ margin: 0 0 0 .3em;
+ }
+ &.b-choice-small .-arrow {
+ display: none;
+ }
+ .b-choice-current {
+ align-self: center;
+ padding-left: 0.3em;
+ width: 100%;
+ margin: 0;
+ white-space: nowrap; overflow: hidden;
+ .b-choice-big & {
+ flex-grow: 1;
+ justify-content: space-between;
+ padding: $b-button-radius 0 $b-button-radius .5em;
+ }
+ .b-choice-small & {
+ width: 100%; height: 1.33em;
+ justify-content: center;
+ padding: 2px;
+ }
+ @include b-style-outset();
+ }
+}
+.b-choiceinput-contextmenu {
+ .b-choice-label { display: block; white-space: pre-line; }
+ .b-choice-line1,
+ .b-choice-line2 { display: block; white-space: pre-line; font-size: 90%; color: $b-style-fg-secondary; }
+ .b-choice-line3 { display: block; white-space: pre-line; font-size: 90%; color: $b-style-fg-notice; }
+ .b-choice-line4 { display: block; white-space: pre-line; font-size: 90%; color: $b-style-fg-warning; }
+ .b-menuitem {
+ &:focus, &.active, &:active {
+ .b-choice-line1, .b-choice-line2, .b-choice-line3,
+ .b-choice-line4 { filter: $b-style-fg-filter; } //* adjust to inverted menuitem */
+ } }
+ .b-menuitem {
+ white-space: pre-line;
+ }
+}
+`;
+
+
+//
+const HTML = (t, d) => html`
+ t.pophere = h)} tabindex="0" >
+ ${t.nick()}
+ ⬍
+
+`;
+const CONTEXTMENU_HTML = (t) => html`
+
+`;
+const CONTEXTMENU_ITEM = (t, c) => html`
+
+ ${ c.label }
+ ${ c.blurb }
+ ${ c.line2 }
+ ${ c.notice }
+ ${ c.warning }
+
+`;
+
+//
+class BChoiceInput extends LitComponent {
+ render()
+ {
+ return [
+ HTML (this),
+ !this.need_cmenu ? nothing : CONTEXTMENU_HTML (this),
+ ];
+ }
+ createRenderRoot()
+ {
+ Util.add_style_sheet (this, STYLE_URL);
+ return this;
+ }
+ static properties = {
+ value: { type: String, },
+ title: { type: String, },
+ small: { type: Boolean, },
+ prop: { type: Object, },
+ choices: { type: Array },
+ need_cmenu: { state: true }, // internal
+ };
+ constructor() {
+ super();
+ Util.define_reactive (this, {
+ value_: { value: '' }, // this.value_ = '';
+ });
+ this.value = '';
+ this.small = false;
+ this.prop = null;
+ this.choices = [];
+ this.need_cmenu = false;
+ this.cmenu = null;
+ this.pophere = null;
+ const this_popup_menu = this.popup_menu.bind (this);
+ this.addEventListener ('click', this_popup_menu);
+ this.addEventListener ('mousedown', this_popup_menu);
+ this.addEventListener ('keydown', this.keydown.bind (this));
+ }
+ updated (changed_props)
+ {
+ if (changed_props.has ('small')) {
+ this.classList.remove (this.small ? 'b-choice-big' : 'b-choice-small');
+ this.classList.add (!this.small ? 'b-choice-big' : 'b-choice-small');
+ }
+ if (changed_props.has ('value'))
+ this.value_ = this.value; // may cause re-render
+ this.setAttribute ('data-tip', this.data_tip());
+ }
+ mchoices()
+ {
+ const mchoices = [];
+ for (let i = 0; i < this.choices.length; i++) {
+ const c = Object.assign ({}, this.choices[i]);
+ c.uri = c.uri || c.ident;
+ mchoices.push (c);
+ }
+ return mchoices;
+ }
+ index()
+ {
+ const mchoices = this.mchoices();
+ for (let i = 0; i < mchoices.length; i++)
+ if (mchoices[i].uri == this.value_)
+ return i;
+ for (let i = 0; i < mchoices.length; i++)
+ if (mchoices[i].ident == this.value_)
+ return i;
+ return 999e99;
+ }
+ data_tip()
+ {
+ const mchoices = this.mchoices();
+ const index = this.index();
+ let tip = "**CLICK** Select Choice";
+ if (!this.prop?.label_ || !mchoices || index >= mchoices.length)
+ return tip;
+ const c = mchoices[index];
+ let val = "**" + this.prop.label_ + "** ";
+ val += c.label;
+ return val + " " + tip;
+ }
+ nick()
+ {
+ const mchoices = this.mchoices();
+ const index = this.index();
+ if (!mchoices || index >= mchoices.length)
+ return "";
+ const c = mchoices[index];
+ return c.label ? c.label : '';
+ }
+ activate (uri)
+ {
+ if (this.cmenu) {
+ // close popup to remove focus guards
+ this.cmenu.close();
+ this.need_cmenu = false;
+ }
+ this.value_ = uri;
+ this.value = this.value_; // becomes Event.target.value
+ this.dispatchEvent (new Event ('valuechange', { composed: true }));
+ }
+ popup_menu (event)
+ {
+ if (!this.cmenu) { // force synchronous rendering to create this.cmenu
+ this.need_cmenu = true; // does this.requestUpdate();
+ this.performUpdate();
+ }
+ this.pophere.focus();
+ this.cmenu.popup (event, { origin: this.pophere });
+ }
+ keydown (event)
+ {
+ if (this.cmenu?.open)
+ return;
+ // allow selection changes with UP/DOWN while menu is closed
+ if (event.keyCode == Util.KeyCode.DOWN || event.keyCode == Util.KeyCode.UP)
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ const mchoices = this.mchoices();
+ let index = this.index();
+ if (mchoices) {
+ index += event.keyCode == Util.KeyCode.DOWN ? +1 : -1;
+ if (index >= 0 && index < mchoices.length)
+ this.activate (mchoices[index].uri);
+ }
+ }
+ else if (event.keyCode == Util.KeyCode.ENTER)
+ this.popup_menu (event);
+ }
+}
+customElements.define ('b-choiceinput', BChoiceInput);
diff --git a/ui/b/fed-number.vue b/ui/b/fed-number.vue
deleted file mode 100644
index bd025e4d..00000000
--- a/ui/b/fed-number.vue
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
- # B-FED-NUMBER
- A field-editor for integer or floating point number ranges.
- The input `value` will be constrained to take on an amount between `min` and `max` inclusively.
- ## Properties:
- *value*
- : Contains the number being edited.
- *min*
- : The minimum amount that `value` can take on.
- *max*
- : The maximum amount that `value` can take on.
- *step*
- : A useful amount for stepwise increments.
- *allowfloat*
- : Unless this setting is `true`, numbers are constrained to integer values.
- *readonly*
- : Make this component non editable for the user.
- ## Events:
- *input*
- : This event is emitted whenever the value changes through user input or needs to be constrained.
-
-
-
-
-
-
-
-
-
diff --git a/ui/b/fed-object.vue b/ui/b/fed-object.vue
deleted file mode 100644
index cf69d0c5..00000000
--- a/ui/b/fed-object.vue
+++ /dev/null
@@ -1,199 +0,0 @@
-
-
-
- # B-FED-OBJECT
- A field-editor for object input.
- A copy of the input value is edited, update notifications are provided via
- an `input` event.
- ## Properties:
- *value*
- : Object with properties to be edited.
- *readonly*
- : Make this component non editable for the user.
- *debounce*
- : Delay in milliseconds for `input` event notifications.
- ## Events:
- *input*
- : This event is emitted whenever the value changes through user input or needs to be constrained.
-
-
-
-
-
-
-
-
-
-
- {{ group.name }}
-
-
-
-
- {{ prop.label_ }}
-
-
-
-
-
-
-
- ⊗
-
-
-
-
-
-
-
-
diff --git a/ui/b/fed-switch.vue b/ui/b/fed-switch.vue
deleted file mode 100644
index 7280388f..00000000
--- a/ui/b/fed-switch.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
- # B-FED-SWITCH
- A field-editor switch to change between on and off.
- ## Properties:
- *value*
- : Contains a boolean indicating whether the switch is on or off.
- *readonly*
- : Make this component non editable for the user.
- ## Events:
- *input*
- : This event is emitted whenever the value changes through user input or needs to be constrained.
-
-
-
-
-
-
-
-
-
diff --git a/ui/b/numberinput.js b/ui/b/numberinput.js
new file mode 100644
index 00000000..4a3f389a
--- /dev/null
+++ b/ui/b/numberinput.js
@@ -0,0 +1,157 @@
+// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
+// @ts-check
+
+import { LitComponent, html, JsExtract, live, docs, ref } from '../little.js';
+import * as Util from '../util.js';
+
+/** # B-FED-NUMBER
+ * A field-editor for integer or floating point number ranges.
+ * The input `value` will be constrained to take on an amount between `min` and `max` inclusively.
+ * ## Properties:
+ * *value*
+ * : Contains the number being edited.
+ * *min*
+ * : The minimum amount that `value` can take on.
+ * *max*
+ * : The maximum amount that `value` can take on.
+ * *step*
+ * : A useful amount for stepwise increments.
+ * *allowfloat*
+ * : Unless this setting is `true`, numbers are constrained to integer values.
+ * *readonly*
+ * : Make this component non editable for the user.
+ * ## Events:
+ * *valuechange*
+ * : This event is emitted whenever the value changes through user input or needs to be constrained.
+ */
+
+//
+const STYLE_URL = await JsExtract.css_url (import.meta);
+JsExtract.scss`
+@import 'mixins.scss';
+b-numberinput {
+ display: flex; justify-content: flex-end;
+ label {
+ display: flex; justify-content: flex-end;
+ flex-grow: 1;
+ }
+ input[type='range'] {
+ flex-grow: 1;
+ margin: auto 1em auto 0;
+ @include b-style-hrange-input;
+ flex: 1 1 auto; /* grow beyond minimum width */
+ max-width: 50%; /* avoid excessive sizes */
+ width: 1.5em; /* minimum width */
+ }
+ input[type='number'] {
+ text-align: right;
+ outline-width: 0; border: none;
+ @include b-style-number-input;
+ }
+}
+`;
+
+//