|
|
@@ -721,6 +721,53 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Bind `stm-attribute.{name}`: a boolean result toggles the attribute, any
|
|
|
+ * other defined value sets it, and `undefined` removes it.
|
|
|
+ *
|
|
|
+ * For booleans, prefer the element's IDL property when one exists (e.g.
|
|
|
+ * `checked`, `disabled`, `hidden`) — setting the content attribute is
|
|
|
+ * unreliable for these once the control is "dirty".
|
|
|
+ */
|
|
|
+ function handleAttribute(el, scope) {
|
|
|
+ var attrs = el.attributes;
|
|
|
+ for (var i = 0; i < attrs.length; i++) {
|
|
|
+ var attr = attrs[i];
|
|
|
+ if (attr.name.indexOf('stm-attribute.') === 0) {
|
|
|
+ var name = attr.name.slice('stm-attribute.'.length);
|
|
|
+ if (!name) continue;
|
|
|
+ var value = evalExpr(attr.value, scope);
|
|
|
+ if (value === undefined) {
|
|
|
+ el.removeAttribute(name);
|
|
|
+ } else if (typeof value === 'boolean') {
|
|
|
+ if (name in el && typeof el[name] === 'boolean') el[name] = value;
|
|
|
+ else if (value) el.setAttribute(name, '');
|
|
|
+ else el.removeAttribute(name);
|
|
|
+ } else {
|
|
|
+ el.setAttribute(name, value === null ? '' : String(value));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Apply `stm-disabled`: when the predicate is truthy, disable the element and
|
|
|
+ * its descendant form controls; otherwise enable them.
|
|
|
+ */
|
|
|
+ function handleDisabled(el, scope) {
|
|
|
+ if (!el.hasAttribute('stm-disabled')) return;
|
|
|
+ setDisabledCascade(el, !!evalExpr(el.getAttribute('stm-disabled'), scope));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Set the `disabled` attribute on an element and its descendant controls. */
|
|
|
+ function setDisabledCascade(el, on) {
|
|
|
+ var nodes = [el].concat(Array.prototype.slice.call(
|
|
|
+ el.querySelectorAll('button,input,select,textarea,fieldset')));
|
|
|
+ nodes.forEach(function (n) {
|
|
|
+ if (on) n.setAttribute('disabled', ''); else n.removeAttribute('disabled');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Bind `stm-text` / `stm-html`. Per the "leave original DOM content" policy,
|
|
|
* an unresolved value (`undefined`) leaves the element untouched; otherwise
|
|
|
@@ -838,6 +885,8 @@
|
|
|
function processInner(el, scope) {
|
|
|
el.__stmScope = scope; // latest scope, used when an action fires
|
|
|
handleClass(el, scope);
|
|
|
+ handleAttribute(el, scope);
|
|
|
+ handleDisabled(el, scope);
|
|
|
handleTextHtml(el, scope);
|
|
|
wireAction(el);
|
|
|
render(el, scope);
|
|
|
@@ -893,28 +942,37 @@
|
|
|
|
|
|
/**
|
|
|
* Toggle the busy state of an action element: disables the element and its
|
|
|
- * descendant form controls and applies `stm-busy-class` classes while busy,
|
|
|
- * restoring prior state when not.
|
|
|
+ * descendant form controls and applies `stm-busy-class` classes while busy.
|
|
|
+ *
|
|
|
+ * On un-busy the disabled state is not "restored" to a captured snapshot
|
|
|
+ * (that would clobber a `stm-disabled` value that `renderAll` applied during
|
|
|
+ * the response); instead the nodes are re-enabled and `stm-disabled` is
|
|
|
+ * re-evaluated, which re-disables any that should remain disabled.
|
|
|
*/
|
|
|
function setBusyState(el, busy) {
|
|
|
var nodes = [el].concat(Array.prototype.slice.call(
|
|
|
el.querySelectorAll('button,input,select,textarea,fieldset')));
|
|
|
var busyClasses = (el.getAttribute('stm-busy-class') || '').split(/\s+/).filter(Boolean);
|
|
|
if (busy) {
|
|
|
- el.__stmDisabled = [];
|
|
|
- nodes.forEach(function (n) {
|
|
|
- if (!('disabled' in n)) return;
|
|
|
- el.__stmDisabled.push([n, n.disabled]);
|
|
|
- n.disabled = true;
|
|
|
- });
|
|
|
+ nodes.forEach(function (n) { n.setAttribute('disabled', ''); });
|
|
|
busyClasses.forEach(function (c) { el.classList.add(c); });
|
|
|
} else {
|
|
|
- (el.__stmDisabled || []).forEach(function (pair) { pair[0].disabled = pair[1]; });
|
|
|
- el.__stmDisabled = [];
|
|
|
+ nodes.forEach(function (n) { n.removeAttribute('disabled'); });
|
|
|
busyClasses.forEach(function (c) { el.classList.remove(c); });
|
|
|
+ reapplyDisabled(el);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** Re-apply `stm-disabled` to an element and any descendants that carry it. */
|
|
|
+ function reapplyDisabled(el) {
|
|
|
+ var scope = el.__stmScope || stateScope();
|
|
|
+ if (el.hasAttribute('stm-disabled')) handleDisabled(el, scope);
|
|
|
+ var scoped = el.querySelectorAll('[stm-disabled]');
|
|
|
+ Array.prototype.forEach.call(scoped, function (n) {
|
|
|
+ handleDisabled(n, n.__stmScope || scope);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/** Apply or remove `stm-on-error` classes on an action element. */
|
|
|
function applyErrorClasses(el, on) {
|
|
|
if (!el || !el.hasAttribute || !el.hasAttribute('stm-on-error')) return;
|
|
|
@@ -947,6 +1005,9 @@
|
|
|
var action = evalExpr(el.getAttribute('stm-action'), scope);
|
|
|
if (!action || typeof action.uri !== 'string') return;
|
|
|
|
|
|
+ // stm-confirm: gate the action behind a confirm() dialog (literal message).
|
|
|
+ if (el.hasAttribute('stm-confirm') && !window.confirm(el.getAttribute('stm-confirm'))) return;
|
|
|
+
|
|
|
applyErrorClasses(el, false); // clear error state on a new attempt
|
|
|
var data = collectData(el);
|
|
|
setBusyState(el, true);
|