/** @preserve Eloquence WEBDLG v2
 (C) Copyright Marxmeier Software AG, 2020-2025
 Version: 2.0.20
 $Id: 00-base.js,v 28.146 2025/07/09 09:18:17 mike Exp $
*//*======================================================================
   WEBDLG v2 namespace / globals
========================================================================*/

var eq = eq || {};
eq.c = eq.c || {};   // Classes
eq.p = eq.p || {};   // Properties:
eq.p.version = 20020; // Version 2.00.20 (major*10,000 + minor*100 + build)

/*========================================================================
   class Obj
   Generic base class.
========================================================================*/

eq.c.Obj = function() {};

/*------------------------------------------------------------------------
   static Obj.subClass()
   Subclassing convenience helper.
------------------------------------------------------------------------*/

(eq.c.Obj.setClassName = function(name, cls) {
   var pd = Object.getOwnPropertyDescriptor(cls.prototype.constructor, 'name');
   if (pd !== undefined)
      pd.value = name;
   else
      pd = {
         value : name,
         configurable : true,
         enumerable : false,
         writable : false
      };
   Object.defineProperty(cls.prototype.constructor, 'name', pd);
})("Obj", eq.c.Obj);

eq.c.Obj.subClass = function(name, cls) {
   var base = this;
   cls.prototype = Object.create(base.prototype);
   Object.defineProperty(cls.prototype, 'constructor', {
      value : cls,
      configurable : false,
      enumerable : false,
      writable : true
   });
   eq.c.Obj.setClassName(name, cls);
   cls.subClass = function(n, c) { return base.subClass.call(this, n, c) };
   return eq.c[name] = cls;
};

/** @preserve $Id: 05-const.js,v 28.84 2025/07/09 09:16:36 mike Exp $
*//*======================================================================
   static MainOp
   API->Main message opcodes.
========================================================================*/

eq.MainOp = {
   apiThreadStarted   : 1,
   ssnNewOrResume     : 2,
   inject             : 3,
   message            : 4,
   login              : 5,
   applicationStopped : 6,
   applicationFailed  : 7,
   popupPos           : 8,
   warnLeavingPage    : 9,
   windowFeature      : 10,
   windowFeatures     : 11,
   grid               : 12,
   layout             : 13,
   notifyBusy         : 14,
   typeAhead          : 15,
   cancelCurrentCall  : 16,
   update             : 17,
   updated            : 18,
   synced             : 19,
   openUrl            : 20,
   beep               : 21,
   playSound          : 22,
   startApp           : 23,
   activate           : 24,
   resume             : 25,
   addControl         : 26,
   modifyControl      : 27,
   deleteControl      : 28,
   invalidate         : 29,
   dlg_DRAW           : 30,
   dlg_DO             : 31,
   dlg_POPUP_BOX      : 32
};

/*========================================================================
   static ApiOp
   Main->API message opcodes.
========================================================================*/

eq.ApiOp = {
   documentComplete : 1,
   installPlugins   : 2,
   injectDone       : 3,
   openWebSocket    : 4,
   closeWebSocket   : 5,
   onLoginEvent     : 6,
   onEvent          : 7,
   onPopupEvent     : 8,
   onSynced         : 9,
   onDo             : 10,
   onHelp           : 11,
   invalidate       : 12,
   update           : 13,
   updated          : 14,
   canceled         : 15,
   grid             : 16
};

/*========================================================================
   static UpdateTag
   Main->API update request/response tags.
========================================================================*/

eq.UpdateTag = {
   changed      : 1,  // Changed elements
   dialogDo     : 2,  // Dialog.do triggered by user
   mouseButton2 : 3,  // Secondary mouse button pressed
   ctxMenuOpen  : 4,  // Open context menu
   ctxMenuAct   : 5,  // Update context menu item active state
   tabOrder     : 6,  // Update tab order (TabBox tab switched)
   colResized   : 7,  // ListBox column resized
   colMoved     : 8,  // ListBox column moved
   sortSequence : 9,  // Listbox sort sequence modified
   lineOrder    : 10, // Update ListBox line order (server request)
   plgUpdate    : 11, // Update Plugin (server request)
   plgGetVal    : 12  // Get Plugin attribute value (server request)
};

/*========================================================================
   static DlgRq.tag
   DLG request tags.
========================================================================*/

eq.RqTag  = {
   child : 1, // Child control

   // Common attributes.

   cssid       : 2,  // CSS id
   cssclsDel   : 3,  // Delete CSS class(es)
   cssclsAdd   : 4,  // Add CSS class(es)
   font        : 5,  // Font face/size/style
   raster      : 6,  // Raster
   posSize     : 7,  // Position/size
   size        : 8,  // Size, ToolBar children
   border      : 9,  // Border
   rule        : 10, // Rule
   ruleMode    : 11, // When to submit rule (Tree/ListBox.singleclick)
   tabOnEnter  : 12, // Enter key navigates focus
   visible     : 13, // Visibility
   sensitive   : 14, // Sensitivity
   inactive    : 15, // Inactive, eg. TabBox tab
   tabStop     : 16, // Tab stop
   bgColor     : 17, // Background color
   fgColor     : 18, // Foreground color
   focusColor  : 19, // Focus color
   btnMask     : 20, // Mouse button mask
   contextMenu : 21, // Context menu
   url         : 22, // Url
   help        : 23, // Help url
   toolTip     : 24, // Tool tip

   // Drag&drop attributes.

   dndMode        : 27, // Drag&drop mode flags
   dropRule       : 28, // Local drop rule
   globalDropRule : 29, // Global drop rule

   // Dialog attributes.

   dialogState   : 30, // Resizepolicy, maximized
   dialogPosSize : 31, // Dialog position/size
   logoIcon      : 32, // Logo/icon
   fKey          : 33, // Function keys
   dialogTimer   : 34, // Dialog timer rule, delay
   ruleButton2   : 35, // Secondary mouse button rule
   dialogDo      : 36, // Dialog.do

   // Dialog, GroupBox attributes.

   margin  : 37, // Dialog/GroupBox margin
   bgImage : 38, // Dialog/GroupBox background image and fill mode
   vx      : 39, // X scroll position
   vy      : 40, // Y scroll position
   vWidth  : 41, // Visible width
   vHeight : 42, // Visible height

   // GroupBox, MenuItem attributes

   accel : 43, // Accelerator

   // Button, Menu, StaticText attributes.

   text       : 45, // Text, also Dialog/GroupBox title
   textPos    : 46, // Text position
   align      : 47, // Align
   active     : 48, // Active
   radioGroup : 49, // RadioButton group
   icon       : 50, // PushButton icon
   iconActive : 51, // PushButton icon when active
   type       : 52, // MenuItem type
   itemId     : 53, // Context menu, original MenuItem id
   objId      : 54, // Context menu, requesting object id
   scrollable : 55, // Context menu, scrollable

   // EditText, ComboBox attributes.

   multiline    : 56, // Multiline
   readonly     : 57, // Read-only
   wrapmode     : 58, // Wrap mode
   maxchars     : 59, // Maxchars
   content      : 60, // Content
   ispassword   : 61, // IsPassword
   fitfont      : 62, // Fitfont width/height
   autocomplete : 63, // tcrule, tcdelay, tcminchars
   autotab      : 64, // Autotab

   // ListBox, PopText, Tree attributes.

   vcRule      : 65, // Rule: view has changed
   columns     : 66, // Columns
   colOrder    : 67, // Column order
   colState    : 68, // Column states
   colTitle    : 69, // Column titles
   lineOrder   : 70, // Line order
   activeLine  : 71, // Active line, model order
   vActiveLine : 72, // Active line, view order
   topItem     : 73, // First visible line
   rowHeight   : 74, // Row height
   status      : 75, // Tree status
   bgColor2    : 76, // Alternate background color
   fgColor2    : 77, // Alternate foreground color
   gridColor   : 78, // Grid color
   colToolTip  : 79, // Column tool tips
   lineToolTip : 80, // Line tool tips
   emptyNode   : 81, // Tree empty node rule/auto
   separator   : 82, // ListBox separator (client-only)

   // TabBox attributes.

   tabs        : 83, // Tabs
   selectedTab : 84, // Selected tab

   // Splitter, ProgressBar attributes.

   panes       : 85, // Setup splitter panes
   vertical    : 86, // Split vertical
   position    : 87, // Divider position
   quickExpand : 88, // Quick-expand controls
   value       : 89, // ProgressBar value, empty=indeterminate

   // ComboBox attributes.

   items   : 94, // List items
   boxSize : 95, // Box width/height
   open    : 96, // Auto-open when focused

   // Image attributes.

   scaling : 97, // Image scaling

   // Plugin attributes.

   plgInt    : 98,  // INT attribute value
   plgString : 99,  // STRING attribute value
   plgBinary : 100, // BINARY attribute value

   // Global properties

   popupPos      : 101, // Popup window position
   baseUrl       : 102, // Base url
   helpBaseUrl   : 103, // Help base url
   windowFeature : 104, // Window feature
   layout        : 105, // Dialog layout mode
   notifyBusy    : 106, // Beep-notification when busy
   typeAhead     : 107, // Type-ahead enabled/disabled
   openUrl       : 108, // Open url
   openHelpUrl   : 109, // Open help url
   startApp      : 110, // Start application
   activate      : 111, // Activate window
   beep          : 112, // System.beep
   soundUrl      : 113  // System.sound url
};

/*========================================================================
   static RsValType
   Response changed element value type.
========================================================================*/

eq.RsValType = {
   idFocus       : 1,  // Focus object id, STRING
   ruleOnce      : 2,  // Submit this rule once, INT
   textValue     : 3,  // Text value, STRING
   buttonChecked : 4,  // Button checked, UINT
   caretPos      : 5,  // Caret position, UINT
   activeLine    : 6,  // Active line, UINT
   activeLineCol : 7,  // Active line and column, UINT line/col
   topItem       : 8,  // First visible line, UINT
   selectedTab   : 9,  // Selected tab, UINT
   splitterPos   : 10, // Splitter position, UINT
   scrollPos     : 11, // GroupBox scroll position, UINT x/y
   status        : 12, // ListBox/Tree status, ARRAY
   colOrder      : 13, // ListBox column order, ARRAY
   colWidth      : 14, // ListBox column widths, ARRAY
   lineOrder     : 15, // ListBox line order, ARRAY
   emptyNode     : 16, // Tree emptynode index, UINT
   ruleKey       : 17, // Rule submission key code, UINT
   mouseBtns     : 18, //                 mouse buttons, UINT
   mouseCnt      : 19, //                 mouse click count, UINT
   mouseMods     : 20, //                 mouse modifier keys, UINT
   dialogMax     : 21, // Dialog maximized, UINT
   dialogPos     : 22, // Dialog position, INT x/y
   dialogSize    : 23, // Dialog size, INT w/h
   dragFrom      : 24, // Drag origin object id, STRING
   dragContent   : 25, // Drag content, STRING
   dropContent   : 26, // Drop content, STRING
   dropAction    : 27, // Drop action, UINT
   dropPos       : 28, // Drop position, UINT
   dropColumn    : 29, // Drop column, INT
   dropLine      : 30, // Drop line, INT
   dropElement   : 31, // HtmlView drop element, STRING
   plgVoid       : 32, // Plugin VOID attribute value
   plgInt        : 33, // Plugin INT attribute value
   plgString     : 34, // Plugin STRING attribute value
   plgBinary     : 35  // Plugin BINARY attribute value
};

/*========================================================================
   static PopupPos
   Popup window position constants.
========================================================================*/

eq.PopupPos = {
   top     : 1,
   left    : 2,
   login   : 4,
   message : 8,
   box     : 16
};

/*========================================================================
   static DlgCall
   DLG call constants.
========================================================================*/

eq.DlgCall = {
   set : 6 // DLG SET
};

/*========================================================================
   static DlgValType
   DLG value type constants.
========================================================================*/

eq.DlgValType = {
   tInt    : 1,
   tString : 2
};

/*========================================================================
   static LayoutMode
   Layout mode constants.
========================================================================*/

eq.LayoutMode = {
   inline    : 1,
   maximized : 2,
   dialog    : 4
};

/*========================================================================
   static DialogMode, DialogState, DialogRaster
   Dialog constants.
========================================================================*/

eq.DialogMode = {
   none       : 0,
   layout     : 1,
   scale      : 2,
   inline     : 3,
   modeMask   : 3, // Mask out mode bits
   stateShift : 2  // Shift down state bits
};

eq.DialogState = {
   maximized : 1
};

eq.DialogRaster = {
   pos  : 1,
   size : 2
};

/*========================================================================
   static Margin
   Dialog/GroupBox margin constants.
========================================================================*/

eq.Margin = {
   top    : 1,
   right  : 2,
   bottom : 4,
   left   : 8
};

/*========================================================================
   static BgFill
   Dialog/GroupBox background image fill mode constants.
========================================================================*/

eq.BgFill = {
   tile    : 0,
   topLeft : 1,
   center  : 2,
   scale   : 3
};

/*========================================================================
   static FontStyle
   Font style constants.
========================================================================*/

eq.FontStyle = {
   bold   : 1,
   italic : 2
};

/*========================================================================
   static TextPos
   Text position constants.
========================================================================*/

eq.TextPos = {
   right  : 1,
   left   : 2,
   bottom : 3,
   top    : 4
};

/*========================================================================
   static ColState
   ListBox column state constants.
========================================================================*/

eq.ColState = {
   alignMask : 3,   // Mask out align bits
   visible   : 4,   // Column is visible
   raster    : 8,   // If set: column width *= sizeRaster
   movable   : 16,  // Column can be moved
   sizable   : 32,  // Column can be resized
   sortable  : 64,  // Column is sortable
   userHide  : 128  // User can show/hide the column (column context menu)
};

/*========================================================================
   static ColType
   ListBox column type constants.
========================================================================*/

eq.ColType = {
   colAlphaNofold : 0,
   colNumeric     : 1,
   colBoolean     : 2,
   colBarGraph    : 3,
   colAlphaFold   : 5,
   colDate        : 6
};

/*========================================================================
   static MenuType
   Menu type, active constants.
========================================================================*/

eq.MenuType = {
   item  : 0,
   sep   : 1,
   radio : 2,
   check : 3
};

/*========================================================================
   static TabState
   TabBox tab state constants.
========================================================================*/

eq.TabState = {
   visible   : 1,
   sensitive : 2
};

/*========================================================================
   static RuleType
   CSS rule type constants.
========================================================================*/

eq.RuleType = {
   bgColor    : 1,
   bgColor2   : 2,
   bgImage    : 3,
   childFont  : 4,
   fgColor    : 5,
   fgColor2   : 6,
   focusColor : 7,
   font       : 8,
   gridColor  : 9,
   height     : 10,
   width      : 11
};

/*========================================================================
   static Plugin attribute descriptor flags.
========================================================================*/

eq.atrGET = 1;   // DLG GET attribute
eq.atrSET = 2;   // DLG SET attribute
eq.atrIDX = 4;   // May be indexed
eq.atrINT = 8;   // Data type: INTEGER
eq.atrSTR = 16;  // Data type: STRING
eq.atrBIN = 32;  // Data type: BINARY
eq.atrPRI = 64;  // Has priority
eq.atrNOC = 128; // Not cached
eq.atrLST = 256; // Stored as list
eq.atrSYN = 512; // Synchronize immediately

/*========================================================================
   static PluginFlags
   Plugin class flags.
========================================================================*/

eq.PluginFlags = {
   focusable  : 4,
   tabOnEnter : 8
};

/*========================================================================
   static KeyMod
   Key modifier constants.
========================================================================*/

eq.KeyMod = {
   shift : 1,
   ctrl  : 2,
   alt   : 4,
   meta  : 8
};

/*========================================================================
   static DnD
   Drag&drop mode flags.
========================================================================*/

eq.Dnd = {
   local     : 1,
   global    : 2,
   move      : 4,
   copy      : 8,
   link      : 16,
   all       : 28, // move+copy+link
   dropExact : 32
};

/** @preserve $Id: 05-util.js,v 28.68 2024/05/23 09:45:35 mike Exp $
*//*======================================================================
   static Dom
   DOM utilities.
========================================================================*/

eq.Dom = {};

/*------------------------------------------------------------------------
   static Dom.viewRect()
   Obtain current view rectangle.
------------------------------------------------------------------------*/

eq.Dom.viewRect = function() {
   return {
      x : window.pageXOffset,
      y : window.pageYOffset,
      w : document.documentElement.clientWidth,
      h : document.documentElement.clientHeight
   };
};

/*------------------------------------------------------------------------
   static Dom.scrollIntoView()
   Scroll element into view if necessary.
------------------------------------------------------------------------*/

eq.Dom.scrollIntoView = function(dlg, el) {
   // Scroll Dialog and/or GroupBox if necessary.
   var
      l = el,
      p, cl, lcl, erc, prc, vm, hm,
      modified, v0, v1, v2, v3, sd, sx, sy;
   while ((p = l.parentNode) && p !== document) {
      if (cl = p.classList) {
         if (cl.contains('eq-dialog'))
            break;
         if (cl.contains('eq-pane') && lcl && lcl.contains('eq-view')) {
            prc = p.getBoundingClientRect();
            if (erc === undefined)
               erc = el.getBoundingClientRect();
            modified = false;
            if ((v0 = erc.top) < (v1 = prc.top)) {
               if (vm === undefined)
                  vm = dlg.grid.hr * 2;
               p.scrollTop = Math.floor(p.scrollTop - v1 + v0 - vm);
               modified = true;
            }
            else if ((v2 = erc.bottom) > (v3 = prc.bottom)) {
               if (vm === undefined)
                  vm = dlg.grid.hr * 2;
               sd = v2 - v3 + vm;
               if (v0 - sd >= v1) {
                  p.scrollTop = Math.ceil(p.scrollTop + sd);
                  modified = true;
               }
            }
            if ((v0 = erc.left) < (v1 = prc.left)) {
               if (hm === undefined)
                  hm = dlg.grid.wr * 2;
               p.scrollLeft = Math.floor(p.scrollLeft - v1 + v0 - hm);
               modified = true;
            }
            else if ((v2 = erc.right) > (v3 = prc.right)) {
               if (hm === undefined)
                  hm = dlg.grid.wr * 2;
               sd = v2 - v3 + hm;
               if (v0 - sd >= v1) {
                  p.scrollLeft = Math.ceil(p.scrollLeft + sd);
                  modified = true;
               }
            }
            if (modified)
               erc = undefined;
         }
      }
      l = p;
      lcl = cl;
   }
   // Scroll document if necessary.
   if (erc === undefined)
      erc = el.getBoundingClientRect();
   modified = false;
   if ((v0 = erc.top) < 0) {
      if (vm === undefined)
         vm = dlg.grid.hr * 2;
      sy = Math.floor(window.pageYOffset + v0 - vm);
      modified = true;
   }
   else if ((v2 = erc.bottom) > (v3 = document.documentElement.clientHeight)) {
      if (vm === undefined)
         vm = dlg.grid.hr * 2;
      sd = v2 - v3 + vm;
      if (v0 - sd >= 0) {
         sy = Math.ceil(window.pageYOffset + sd);
         modified = true;
      }
   }
   if ((v0 = erc.left) < 0) {
      if (hm === undefined)
         hm = dlg.grid.wr * 2;
      sx = Math.floor(window.pageXOffset + v0 - hm);
      modified = true;
   }
   else if ((v2 = erc.right) > (v3 = document.documentElement.clientWidth)) {
      if (hm === undefined)
         hm = dlg.grid.wr * 2;
      sd = v2 - v3 + hm;
      if (v0 - sd >= 0) {
         sx = Math.ceil(window.pageXOffset + sd);
         modified = true;
      }
   }
   if (modified)
      window.scrollTo(sx !== undefined ? sx : window.pageXOffset,
                      sy !== undefined ? sy : window.pageYOffset);
};

/*------------------------------------------------------------------------
   static Dom.setContent()
   Set element text or HTML content.
------------------------------------------------------------------------*/

eq.Dom.setContent = function(el, cn, checkAccel, removeAccel) {
   var html = this.reIsHtml.exec(cn);
   if (html) {
      el.textContent = ''; // Delete children.
      this.setHtmlContent(html[1], el);
      if (checkAccel) {
         var
            n = el.getElementsByTagName('U'),
            isCharOrDigit = this.reCharOrDigit;
         for (var i = 0, l = n.length; i < l; i++) {
            var u = n[i], c = u.childNodes, t, v, a;
            if (   c.length === 1
                && (t = c[0]).nodeType === Node.TEXT_NODE
                && isCharOrDigit.test(v = t.nodeValue)) {
               if (removeAccel) {
                  u.parentNode.replaceChild(document.createTextNode(v), u);
                  return null;
               }
               return v;
            }
         }
      }
   }
   else if (checkAccel) {
      var
         re = this.reAccel,
         isCharOrDigit = this.reCharOrDigit,
         r, v, i, u;
      while (r = re.exec(cn))
         if ((v = r[1]) !== undefined && isCharOrDigit.test(v)) {
            if (removeAccel) {
               v = ((i = r.index) ? cn.substring(0, i) : '') + v;
               if (cn.length > (i += 2))
                  v += cn.substring(i);
               el.textContent = v;
               re.lastIndex = 0; // Reset regexp.
               return null;
            }
            el.textContent = ''; // Delete children.
            if (i = r.index)
               el.appendChild(document.createTextNode(cn.substring(0, i)));
            (u = document.createElement('U')).textContent = v;
            el.appendChild(u);
            if (cn.length > (i += 2))
               el.appendChild(document.createTextNode(cn.substring(i)));
            re.lastIndex = 0; // Reset regexp.
            return v;
         }
      el.textContent = cn;
   }
   else
      el.textContent = cn;
   return null;
};

eq.Dom.setTextContent = function(el, cn) {
   var html = this.reIsHtml.exec(cn);
   if (html)
      el.textContent = cn.replace(this.reUnHtml, '');
   else
      el.textContent = cn;
};

eq.Dom.setHtmlContent = function(cn, body, head) {
   var
      html = this.ParseHtml.parseFromString(cn, 'text/html'),
      from, child, at, l, i, a;
   if (head && (from = html.head))
      while (child = from.firstChild)
         head.appendChild(child);
   if (body && (from = html.body)) {
      if (body.tagName === 'BODY') {
         l = (at = from.getAttributeNames()).length;
         for (i = 0; i < l; i++) {
            a = at[i];
            body.setAttribute(a, from.getAttribute(a));
         }
      }
      while (child = from.firstChild)
         body.appendChild(child);
   }
};

eq.Dom.isHtmlContent = function(cn) {
   return this.reIsHtml.test(cn);
};

eq.Dom.ParseHtml = new DOMParser();
eq.Dom.reIsHtml = new RegExp('^<html>(.*?)(</html>)?$', 'i');
eq.Dom.reUnHtml = new RegExp('<[^>]*>', 'g');
eq.Dom.reAccel = new RegExp('&(.)?', 'g');
eq.Dom.reCharOrDigit = new RegExp('^[A-Z0-9]$', 'i');

/*------------------------------------------------------------------------
   static Dom.decodeAccelKey()
   Decode accelerator key event.
------------------------------------------------------------------------*/

eq.Dom.decodeAccelKey = function(key, kmod, e) {
   if (key.length === 0)
      return null;
   if (key.length === 1) {
      if (kmod && !this.reCharOrDigit.test(key)) {
         var code = e.code;
         if (code !== undefined)
            key = code.replace(this.reAccelCode, '');
         else {
            // IE
            key = String.fromCharCode(e.keyCode);
         }
      }
   }
   else switch (key) {
      case 'Add':
         key = '+';
         break;
      case 'ArrowDown':
         key = 'Down';
         break;
      case 'ArrowLeft':
         key = 'Left';
         break;
      case 'ArrowRight':
         key = 'Right';
         break;
      case 'ArrowUp':
         key = 'Up';
         break;
      case 'Del':
         key = 'Delete';
         break;
      case 'Divide':
         key = '/';
         break;
      case 'Esc': // IE
         key = 'Escape';
         break;
      case 'Multiply':
         key = '*';
         break;
      case 'Spacebar': // IE
         key = 'Space';
         break;
      case 'Subtract':
         key = '-';
         break;
      case 'Unidentified':
         switch (e.code) {
            case 'NumpadDecimal':
               key = ',';
               break;
            default:
               return null;
         }
   }
   return key;
};

eq.Dom.reAccelCode = new RegExp('^(Digit|Key|Numpad)');

/*------------------------------------------------------------------------
   static Dom.decodeAccelAttr()
   Decode accelerator attribute value.
------------------------------------------------------------------------*/

eq.Dom.decodeAccelAttr = function(v, tag) {
   var ls = v.split(' '), km = eq.KeyMod, kmod = 0;
   for (var i = 0, l = ls.length; i < l; i++) {
      switch (v = ls[i].toUpperCase()) {
         case 'SHIFT':
            kmod |= km.shift;
            break;
         case 'CTRL':
            kmod |= km.ctrl;
            break;
         case 'ALT':
         case 'ALTGRAPH':
            kmod |= km.alt;
            break;
         case 'META':
            kmod |= km.meta;
            break;
         default:
            v = v.replace(this.reAccelAttr, '');
            if (kmod === km.shift && v.length === 1)
               kmod = 0;
            if (tag) {
               var k = '';
               switch (v) {
                  case 'ADD':
                     k = '+';
                     break;
                  case 'BACKSPACE':
                     k = "\u232B";
                     break;
                  case 'DELETE':
                     k = "\u2326";
                     break;
                  case 'DIVIDE':
                     k = '/';
                     break;
                  case 'DOWN':
                     k = "\u21E3";
                     break;
                  case 'END':
                     k = "\u21F2";
                     break;
                  case 'ENTER':
                     k = "\u23CE";
                     break;
                  case 'ESCAPE':
                     k = "\u238B";
                     break;
                  case 'HOME':
                     k = "\u21F1";
                     break;
                  case 'LEFT':
                     k = "\u21E0";
                     break;
                  case 'MULTIPLY':
                     k = '*';
                     break;
                  case 'PAGEDOWN':
                     k = "\u21DF";
                     break;
                  case 'PAGEUP':
                     k = "\u21DE";
                     break;
                  case 'RIGHT':
                     k = "\u2192";
                     break;
                  case 'SPACE':
                     k = "\u2423";
                     break;
                  case 'SUBTRACT':
                     k = '-';
                     break;
                  case 'TAB':
                     k = "\u21E5";
                     break;
                  case ' ':
                  case 'UP':
                     k = "\u21E1";
                     break;
                  default:
                     k = v;
               }
               if (kmod) {
                  var t = '';
                  if (kmod & km.shift)
                     t += "\u21E7";
                  if (kmod & km.ctrl)
                     t += "\u2303";
                  if (kmod & km.alt)
                     t += "\u2325";
                  if (kmod & km.meta)
                     t += "\u2318";
                  tag.tag = t + k;
               }
               else
                  tag.tag = k;
            }
            return kmod ? String.fromCharCode(kmod+48) + v : v;

      }
   }
   return null;
};

eq.Dom.reAccelAttr = new RegExp('(^KP)|(^NUMPAD)|_', 'ig');

/*------------------------------------------------------------------------
   static Dom.copyContent()
   Copy element text or HTML content.
------------------------------------------------------------------------*/

eq.Dom.copyContent = function(to, from) {
   to.textContent = ''; // Delete children.
   if (from) {
      var child = from.firstChild;
      while (child) {
         to.appendChild(child.cloneNode(true));
         child = child.nextSibling;
      }
   }
};

/*------------------------------------------------------------------------
   static Dom.copyFont() .copyColors()
   Copy font/color styles.
------------------------------------------------------------------------*/

eq.Dom.copyFont = function(to, from) {
   var
      cs = window.getComputedStyle(from),
      s = to.style;
   s.fontFamily = cs.fontFamily;
   s.fontSize = cs.fontSize;
   s.fontStyle = cs.fontStyle;
   s.fontWeight = cs.fontWeight;
};

eq.Dom.copyColors = function(to, from) {
   var
      cs = window.getComputedStyle(from),
      s = to.style;
   s.color = cs.color;
   s.backgroundColor = cs.backgroundColor;
};

/*------------------------------------------------------------------------
   static Dom.setFont()
   Set element font styles.
------------------------------------------------------------------------*/

eq.Dom.setFont = function(el, fn) {
   var
      s = el.style,
      fs = eq.FontStyle,
      ffc, fsz, fst, fSet, fstSet;
   if (ffc = fn.fc) {
      s.fontFamily = ffc;
      fSet = true;
   }
   else
      s.fontFamily = '';
   if (fsz = fn.sz) {
      s.fontSize = fsz;
      fSet = true;
   }
   else
      s.fontSize = '';
   if ((fst = fn.st) & fs.italic) {
      s.fontStyle = 'italic';
      fstSet = fSet = true;
   }
   if (fst & fs.bold) {
      s.fontWeight = 'bold';
      fstSet = fSet = true;
   }
   if (!fstSet)
      s.fontStyle = '';
   return fSet;
};

/*------------------------------------------------------------------------
   static Dom.adjustElementFontSize()
   Adjust element font size by application font size factor.
------------------------------------------------------------------------*/

eq.Dom.adjustElementFontSize = function(el) {
   var sf = eq.app.fnSizeFactor;
   if (sf && sf !== 1) {
      var s = el.style, sz = s.fontSize, r, n;
      if (sz && (r = this.reFontSize.exec(sz))) {
         n = Number(r[1]);
         if (n === n)
            s.fontSize = (Math.round(n * sf * 1000) / 1000) + r[2];
      }
   }
};

eq.Dom.reFontSize = new RegExp('^([0-9.]+)(.+)$');

/*------------------------------------------------------------------------
   static Dom.resolveWindowFeatures()
   Resolve window.open() windowFeatures,
   allow coordinates to be specified as a percentage.
------------------------------------------------------------------------*/

eq.Dom.resolveWindowFeatures = function(wf) {
   var
      re = this.resolveWindowFeatures.re,
      sc = window.screen, out = "", i = 0, r, k, v;
   while (r = re.exec(wf)) {
      switch (k = r[1]) {
         case 'left':
         case 'screenX':
         case 'width':
         case 'innerWidth':
            v = sc.availWidth * r[7] / 100;
            break;
         case 'top':
         case 'screenY':
         case 'height':
         case 'innerHeight':
            v = sc.availHeight * r[7] / 100;
            break;
         default:
            continue;
      }
      if (r[2] && v < r[3]) // Minimum?
         v = r[3];
      if (r[4] && v > r[5]) // Maximum?
         v = r[5];
      out += wf.substring(i, r.index) + k + '=' + Math.round(v);
      i = r.index + r[0].length;
   }
   return out + wf.substring(i);
};

eq.Dom.resolveWindowFeatures.re =
   new RegExp('([a-z]+)=(([0-9]+)<)?(([0-9]+)>)?(([0-9]+)%)', 'ig');

/*------------------------------------------------------------------------
   static Dom.updateQueryString()
   Update query string, add/replace or remove parameter.
------------------------------------------------------------------------*/

eq.Dom.updateQueryString = function(url, key, val) {
   var
      r = this.updateQueryString.re.exec(url),
      q = r[2];
   if (q) {
      var re = new RegExp('([?&])' + key + '=[^&#]*', 'i');
      if (q.match(re)) {
         if (val !== undefined)
            q = q.replace(re, '$1' + key + '=' + val);
         else {
            q = q.replace(re, '');
            if (q.length && q.charAt(0) === '&')
               q = q.replace('&', '?');
         }
      }
      else if (val !== undefined)
         q = q + '&' + key + '=' + val;
   }
   else if (val !== undefined)
      q = '?' + key + '=' + val;
   url = r[1];
   if (q)
      url += q;
   if (r[3])
      url += r[3];
   return url;
};

eq.Dom.updateQueryString.re = new RegExp('^([^?#]+)([?][^#]*)?(#.*)?$');

/*------------------------------------------------------------------------
   static Dom.zIndex()
   Return numeric element z-index.
------------------------------------------------------------------------*/

eq.Dom.zIndex = function(el) {
   var z = window.getComputedStyle(el).zIndex;
   return +z === +z ? +z : 0;
};

/*------------------------------------------------------------------------
   static Dom.zIndexMax()
   Return element's children highest z-index.
------------------------------------------------------------------------*/

eq.Dom.zIndexMax = function(el) {
   var n = el.children, maxZ = 0;
   for (var i = n.length; --i >= 0;) {
      var z = window.getComputedStyle(n[i]).zIndex;
      if (+z === +z && +z > maxZ)
         maxZ = +z;
   }
   return maxZ;
};

/*------------------------------------------------------------------------
   static Dom.eqControl()
   Obtain 'eq-control' element.
------------------------------------------------------------------------*/

eq.Dom.eqControl = function(el) {
   var c;
   while (el && el !== document) {
      if ((c = el.classList) && c.contains('eq-control'))
         return el;
      el = el.parentNode;
   }
   return null;
};

/*------------------------------------------------------------------------
   static Dom.eqDialog()
   Obtain 'eq-dialog' element.
------------------------------------------------------------------------*/

eq.Dom.eqDialog = function(el) {
   var c;
   while (el && el !== document) {
      if ((c = el.classList) && c.contains('eq-dialog'))
         return el;
      el = el.parentNode;
   }
   return null;
};

/*------------------------------------------------------------------------
   static Dom.eqOverlay()
   Obtain 'eq-overlay' element.
------------------------------------------------------------------------*/

eq.Dom.eqOverlay = function(el) {
   var c;
   while (el && el !== document) {
      if ((c = el.classList) && c.contains('eq-overlay'))
         return el;
      el = el.parentNode;
   }
   return null;
};

/*------------------------------------------------------------------------
   static Dom.isRootChild()
   Check whether element is child of root element.
------------------------------------------------------------------------*/

eq.Dom.isRootChild = function(el) {
   var cl;
   while (el && el !== document) {
      if (cl = el.classList)
         if (cl.contains('eq-root'))
            return true;
      el = el.parentNode;
   }
   return false;
};

/*------------------------------------------------------------------------
   static Dom.isMenu()
   Check whether element is menu element.
------------------------------------------------------------------------*/

eq.Dom.isMenu = function(el) {
   var cl;
   while (el && el !== document) {
      if (cl = el.classList)
         if (cl.contains('eq-menubar') || cl.contains('eq-menu'))
            return true;
      el = el.parentNode;
   }
   return false;
};

/*------------------------------------------------------------------------
   static Dom.closeMenu()
   Close menu if necessary.
------------------------------------------------------------------------*/

eq.Dom.closeMenu = function() {
   var app = eq.app, id = app.menuOpen;
   if (id) {
      var el = document.getElementById(id);
      if (el && el.classList.contains('eq-open')) {
         el.classList.remove('eq-open');
         var n = el.getElementsByClassName('eq-open');
         for (var i = n.length; --i >= 0;)
            n[i].classList.remove('eq-open');
      }
      app.menuOpen = undefined;
      // Close tooltip if any.
      app.constructor.ttClose();
   }
};

/*------------------------------------------------------------------------
   static Dom.parentIndex()
   Return child element parent index.
------------------------------------------------------------------------*/

eq.Dom.parentIndex = function(el) {
   var i = 0;
   while (el = el.previousElementSibling)
      i++;
   return i;
};

/*------------------------------------------------------------------------
   static Dom.firstChildByTagName()
   Return first child element matching tag name.
------------------------------------------------------------------------*/

eq.Dom.firstChildByTagName = function(el, tagName) {
   el = el.firstElementChild;
   while (el && el.tagName !== tagName)
      el = el.nextElementSibling;
   return el;
};

/*------------------------------------------------------------------------
   static Dom.inputElement()
   Return descendant input element.
------------------------------------------------------------------------*/

eq.Dom.inputElement = function(el) {
   var n = el.getElementsByTagName('INPUT');
   if (n.length !== 1)
      throw new Error("BUG: InputElement id:" + el.id +
         " elements:" + n.length + ", expected: 1");
   return n[0];
};

/*------------------------------------------------------------------------
   static Dom.consumeEvent()
   Consume event.
------------------------------------------------------------------------*/

eq.Dom.consumeEvent = function(e) {
   e.preventDefault();
   e.stopImmediatePropagation();
};

/*------------------------------------------------------------------------
   static Dom.fireEvent()
   Fire event on element.
------------------------------------------------------------------------*/

eq.Dom.fireEvent = function(t, ty) {
   var e = document.createEvent("Event");
   e.initEvent(ty, true, true);
   t.dispatchEvent(e);
};

/*------------------------------------------------------------------------
   static Dom.fireMouseEvent()
   Fire mouse event on element.
------------------------------------------------------------------------*/

eq.Dom.fireMouseEvent = function(t, ty, ea) {
   var e = document.createEvent("MouseEvent");
   e.initEvent(ty, true, true);
   if (ea !== undefined)
      e.eq = ea;
   t.dispatchEvent(e);
};

/*------------------------------------------------------------------------
   static Dom.fireClick()
   Fire mouse click on element.
------------------------------------------------------------------------*/

eq.Dom.fireClick = function(t, ea) {
   this.fireMouseEvent(t, 'mousedown', ea);
   this.fireMouseEvent(t, 'mouseup', ea);
   this.fireMouseEvent(t, 'click', ea);
};

/*------------------------------------------------------------------------
   static Dom.isEnterOrSpace()
   Check for ENTER or SPACE key.
------------------------------------------------------------------------*/

eq.Dom.isEnterOrSpace = function(key) {
   switch (key) {
      case 'Enter':
         return 10;
      case ' ':
      case 'Spacebar': // IE
         return 32;
   }
   return 0;
};

/*------------------------------------------------------------------------
   static Dom.onNextCycle()
   Invoke callback on next event cycle.
------------------------------------------------------------------------*/

eq.Dom.onNextCycle = function(cb, arg) {
   var self = this.onNextCycle, q = self.queue;
   q.push({ cb : cb, arg : arg });
   if (q.length === 1)
      window.setTimeout(self.exec);
};

eq.Dom.onNextCycle.queue = [];

eq.Dom.onNextCycle.exec = function() {
   var self = eq.Dom.onNextCycle, q = self.queue;
   for (var i = 0, l = q.length; i < l; i++) {
      var c = q[i], a = c.arg;
      if (a !== undefined)
         c.cb(a);
      else
         c.cb();
   }
   if (l)
      q.splice(0, l);
   if (q.length)
      window.setTimeout(self.exec);
};

/*------------------------------------------------------------------------
   static Dom.cancelNextCycle()
   Cancel callback pending for next event cycle.
------------------------------------------------------------------------*/

eq.Dom.cancelNextCycle = function(cb) {
   var q = this.onNextCycle.queue;
   for (var i = q.length; --i >= 0;)
      if (q[i].cb === cb) {
         q.splice(i, 1);
         break;
      }
};

/*------------------------------------------------------------------------
   static Dom.onNextFrame()
   Invoke callback on next animation frame.
------------------------------------------------------------------------*/

eq.Dom.onNextFrame = function(el, qi) {
   var
      self = this.onNextFrame,
      q = self.queue[self.queue_i],
      qv = q.get(el);
   if (qv === undefined) {
      q.set(el, [ qi ]);
      if (q.size === 1)
         window.requestAnimationFrame(self.exec);
      return;
   }
   var cb = qi.cb;
   for (var i = 0, l = qv.length; i < l; i++)
      if (qv[i].cb === cb) {
         qv[i] = qi;
         return;
      }
   qv.push(qi);
};

eq.Dom.onNextFrame.queue = [ new Map(), new Map() ];
eq.Dom.onNextFrame.queue_i = 0;

eq.Dom.onNextFrame.exec = function() {
   var self = eq.Dom.onNextFrame, q = self.queue[self.queue_i];
   if (++self.queue_i === self.queue.length)
      self.queue_i = 0;
   q.forEach(self.commit);
   q.clear();
};

eq.Dom.onNextFrame.commit = function(qv, el) {
   for (var i = 0, l = qv.length; i < l; i++) {
      var qi = qv[i];
      qi.cb(el, qi);
   }
};

/*========================================================================
   class Timers extends Obj
   Manage multiple timers with common context.
========================================================================*/

eq.c.Obj.subClass('Timers', function() {
   eq.c.Obj.call(this);
   this.m = new Map();
});

/*------------------------------------------------------------------------
   Timers.dispose()
   Stop and dispose all timers.
------------------------------------------------------------------------*/

eq.c.Timers.prototype.dispose = function() {
   this.m.forEach(eq.c.Timers.cbDispose);
   this.m.clear();
};

eq.c.Timers.cbDispose = function(t) {
   window.clearTimeout(t.id);
};

/*------------------------------------------------------------------------
   Timers.add()
   Add timer.
------------------------------------------------------------------------*/

eq.c.Timers.prototype.add = function(delay, elapsed, canceled, arg) {
   var t = { elapsed : elapsed, canceled : canceled, arg : arg };
   t.id = window.setTimeout(eq.c.Timers.cbElapsed, delay, this, t);
   this.m.set(t.id, t);
   return t.id;
};

/*------------------------------------------------------------------------
   Timers.cancel()
   Stop specific timer, invoke canceled() callback.
------------------------------------------------------------------------*/

eq.c.Timers.prototype.cancel = function(id) {
   var t = this.m.get(id), canceled;
   if (t) {
      window.clearTimeout(id);
      this.m.delete(id);
      if (canceled = t.canceled)
         canceled(t.arg);
   }
};

/*------------------------------------------------------------------------
   Timers.cancelAll()
   Stop all timers, invoke canceled() callbacks.
------------------------------------------------------------------------*/

eq.c.Timers.prototype.cancelAll = function() {
   this.m.forEach(eq.c.Timers.cbCancel);
   this.m.clear();
};

eq.c.Timers.cbCancel = function(t) {
   window.clearTimeout(t.id);
   var canceled = t.canceled;
   if (canceled)
      canceled(t.arg);
};

/*------------------------------------------------------------------------
   static Timers.cbElapsed()
   Handle elapsed timer.
------------------------------------------------------------------------*/

eq.c.Timers.cbElapsed = function(self, t) {
   self.m.delete(t.id);
   var elapsed = t.elapsed;
   if (elapsed)
      elapsed(t.arg);
};

/** @preserve $Id: 05-window.js,v 28.21 2025/03/13 14:28:12 mike Exp $
*//*======================================================================
   class Window extends Obj
   Window class.
========================================================================*/

eq.c.Obj.subClass('Window', function(root, parent, dlg) {
   eq.c.Obj.call(this);

   var rcl = root.classList;
   if (   !rcl.contains('eq-window')
       && !rcl.contains('eq-dialog'))
      throw new Error("BUG: Invalid window root");

   if (dlg)
      rcl.add('eq-d' + dlg.id);

   // Add to app window list.

   var app = eq.app, wl, wr, i, l, s = root.style;
   if ((wl = app.wl) === undefined)
      app.wl = [];
   else if (wl.length === 0)
      wl = undefined;

   if (!parent)
      parent = document.body;

   if (wl === undefined) {
      wl = app.wl;
      s.zIndex = eq.Dom.zIndex(parent) + 1;
   }
   else {
      for (i = 0, l = wl.length;;) {
         if ((wr = wl[i].root) === undefined)
            throw new Error("BUG: Window root undefined");
         if (++i === l) {
            wr.classList.remove('eq-active');
            s.zIndex = eq.Dom.zIndex(wr) + 1;
            break;
         }
      }
   }

   this.root = root;
   this.dlg = dlg;
   this.modified = {};
   wl.push(this);
   s.visibility = 'hidden';
   parent.appendChild(root);
});

/*------------------------------------------------------------------------
   Window.close()
   Close Window.
------------------------------------------------------------------------*/

eq.c.Window.prototype.close = function() {
   var r, wl, i, l, z, wr;
   if ((r = this.root) === undefined)
      throw new Error("BUG: Window root undefined");

   // Remove from app window list.

   if ((wl = eq.app.wl) === undefined)
      throw new Error("BUG: Window list undefined");

   for (i = wl.length; --i >= 0;)
      if (wl[i] === this) {
         wl.splice(i, 1);
         z = eq.Dom.zIndex(r);
         for (l = wl.length; i < l; i++) {
            if ((wr = wl[i].root) === undefined)
               throw new Error("BUG: Window root undefined");
            wr.style.zIndex = z++;
         }
         break;
      }
   if (i === -1)
      throw new Error("BUG: Window not in list");

   r.parentNode.removeChild(r);
   this.root = undefined;
   this.dlg = undefined;
   this.modified = undefined;
   this.pos = undefined;
   this.whenVisible = undefined;
   this.forcedInline = undefined;

   if (l = wl.length)
      return wl[l - 1];

   return null;
};

/*------------------------------------------------------------------------
   static Window.setupFrame()
   Setup Window frame.
------------------------------------------------------------------------*/

eq.c.Window.setupFrame = function(child) {
   var ccl = child.classList;
   if (   !ccl.contains('eq-dialog')
       && !ccl.contains('eq-popup-box')
       && !ccl.contains('eq-message-dialog')
       && !ccl.contains('eq-login-dialog'))
      throw new Error("BUG: Invalid window frame child");
   var
      f = document.createElement('DIV'),
      p = document.createElement('DIV'),
      c = document.createElement('DIV'),
      l = document.createElement('DIV'),
      t = document.createElement('DIV'),
      m = document.createElement('DIV'),
      x = document.createElement('DIV'),
      i, n;
   f.className = 'eq-window';
   p.className = 'eq-pane';
   c.className = 'eq-caption';
   l.className = 'eq-logo eq-move';
   c.appendChild(l);
   t.className = 'eq-title eq-move';
   t.appendChild(document.createTextNode('\u00A0'));
   t.appendChild(document.createElement('SPAN'));
   c.appendChild(t);
   m.className = 'eq-max';
   c.appendChild(m);
   x.className = 'eq-close';
   c.appendChild(x);
   p.appendChild(c);
   p.appendChild(child);
   f.appendChild(p);
   for (i = 0; i < 8; i++) {
      switch (i) {
         case 0:
            n = 'eq-top';
            break;
         case 1:
            n = 'eq-top eq-right';
            break;
         case 2:
            n = 'eq-right';
            break;
         case 3:
            n = 'eq-bottom eq-right';
            break;
         case 4:
            n = 'eq-bottom';
            break;
         case 5:
            n = 'eq-bottom eq-left';
            break;
         case 6:
            n = 'eq-left';
            break;
         case 7:
            n = 'eq-top eq-left';
      }
      p = document.createElement('DIV');
      p.className = n + ' eq-resize';
      f.appendChild(p);
      // Set 'mousedown' handler,
      // global handler might not trigger on new body children (Safari).
      f.onmousedown = eq.c.App.onGlobalMouseDownOrClick;
   }
   return f;
};

/*------------------------------------------------------------------------
   Window.setPos()
   Set Window position.
------------------------------------------------------------------------*/

eq.c.Window.prototype.setPos = function(x, y) {
   if (x === -1 || y === -1)
      eq.Dom.onNextFrame(this, {
         cb : eq.c.Window.setPos.center,
         x : x,
         y : y
      });
   else {
      var r;
      if ((r = this.root) === undefined)
         throw new Error("BUG: Window root undefined");
      eq.c.Window.setPos(this, r, x, y);
   }
};

eq.c.Window.setPos = function(w, r, x, y) {
   var s = r.style, v;
   if (+x === +x)
      s.left = x > 0 ? x + 'px' : 0;
   else
      s.left = x;
   if (+y === +y)
      s.top = y > 0 ? y + 'px' : 0;
   else
      s.top = y;
   eq.Dom.onNextFrame(w, { cb : eq.c.Window.setPos.makeVisible });
};

eq.c.Window.setPos.center = function(w, qi) {
   var
      cls = eq.c.Window,
      r, rrc, x, y, wl, i, p, prc, pw, pwrc;
   if ((r = w.root) === undefined) {
      // Window does no longer exist.
      cls.setPos.makeVisible(w);
      return;
   }
   rrc = r.getBoundingClientRect();
   x = qi.x;
   y = qi.y;
   if ((wl = eq.app.wl) !== undefined)
      for (i = wl.length; --i >= 0;)
         if (wl[i] === w) {
            if (i > 0) {
               if ((pw = wl[i - 1].root) === undefined)
                  throw new Error("BUG: Window root undefined");
               if ((p = r.offsetParent) !== document.body)
                  prc = p.getBoundingClientRect();
               pwrc = pw.getBoundingClientRect();
               if (pwrc.width === 0 || pwrc.height === 0)
                  break;
               if (x === -1) {
                  x = (pwrc.width - rrc.width) / 2 + pwrc.left;
                  if (prc !== undefined)
                     x -= prc.left;
               }
               if (y === -1) {
                  y = (pwrc.height - rrc.height) / 2 + pwrc.top;
                  if (prc !== undefined)
                     y -= prc.top;
               }
               cls.setPos(w, r, x, y);
               return;
            }
            break;
         }

   if ((p = r.offsetParent) === document.body) {
      var v = eq.Dom.viewRect();
      if (x === -1)
         x = (v.w - rrc.width) / 2 + v.x;
      if (y === -1)
         y = (v.h - rrc.height) / 2 + v.y;
   }
   else {
      prc = p.getBoundingClientRect();
      if (x === -1)
         x = (prc.width - rrc.width) / 2;
      if (y === -1)
         y = (prc.height - rrc.height) / 2;
   }

   cls.setPos(w, r, x, y);
};

eq.c.Window.setPos.makeVisible = function(w) {
   var r, v;
   if ((r = w.root) !== undefined) {
      // Try to avoid flicker by converting base Dialog Window
      // to inline Dialog if necessary before making Window root visible.
      if (!w.makeBaseDialogInline())
         r.style.visibility = '';
   }
   if ((v = w.whenVisible) !== undefined) {
      w.whenVisible = undefined;
      eq.Dom.onNextFrame(w, v);
   }
};

/*------------------------------------------------------------------------
   Window.setTitle()
   Set Window title.
------------------------------------------------------------------------*/

eq.c.Window.prototype.setTitle = function(title) {
   var r, fi, el;
   if ((r = this.root) === undefined)
      throw new Error("BUG: Window root undefined");
   if (fi = this.forcedInline)
      fi.t = title;
   else if (el = r.querySelector(
                  '.eq-window>.eq-pane>.eq-caption>.eq-title>span'))
      eq.Dom.setTextContent(el, title);
};

/*------------------------------------------------------------------------
   Window.setIcon()
   Set Window icon.
------------------------------------------------------------------------*/

eq.c.Window.prototype.setIcon = function(icon) {
   var r, fi, el, s = icon ? "url('" + icon + "')" : '';
   if ((r = this.root) === undefined)
      throw new Error("BUG: Window root undefined");
   if (fi = this.forcedInline)
      fi.ic = s;
   else if (el = r.querySelector('.eq-window>.eq-pane>.eq-caption>.eq-logo'))
      el.style.backgroundImage = s;
};

/*------------------------------------------------------------------------
   Window.canClose()
   Return whether Window has active close button.
------------------------------------------------------------------------*/

eq.c.Window.prototype.canClose = function() {
   var r, fi, el;
   if ((r = this.root) === undefined)
      throw new Error("BUG: Window root undefined");
   if (fi = this.forcedInline)
      return fi.c;
   if (el = r.querySelector('.eq-window>.eq-pane>.eq-caption>.eq-close'))
      return el.offsetParent !== null;
   return false;
};

/*------------------------------------------------------------------------
   Window.activate()
   Activate Window.
------------------------------------------------------------------------*/

eq.c.Window.prototype.activate = function() {
   var r, wl, i, l, z, wr, cl;
   if ((r = this.root) === undefined)
      throw new Error("BUG: Window root undefined");

   if ((wl = eq.app.wl) === undefined)
      throw new Error("BUG: Window list undefined");

   // Make topmost in app window list.

   for (l = wl.length, i = l; --i >= 0;) {
      if (wl[i] === this) {
         r.classList.add('eq-active');
         if (i !== l - 1) {
            wl.splice(i, 1);
            z = eq.Dom.zIndex(r);
            for (l = wl.length; i < l;) {
               if ((wr = wl[i].root) === undefined)
                  throw new Error("BUG: Window root undefined");
               wr.style.zIndex = z++;
               if (++i === l)
                  wr.classList.remove('eq-active');
            }
            r.style.zIndex = z;
            wl.push(this);
            return true;
         }
         return false;
      }
   }
   throw new Error("BUG: Window not in list");
};

/*------------------------------------------------------------------------
   Window.makeInline()
   Convert Window to inline Dialog if necessary.
------------------------------------------------------------------------*/

eq.c.Window.prototype.makeInline = function(save) {
   var r, d, dlg, cl, dv, t, l, s;
   if ((r = this.root) === undefined)
      throw new Error("BUG: Window root undefined");
   if (r.classList.contains('eq-window')) {
      if (!(d = r.querySelector('.eq-window>.eq-pane>.eq-dialog')))
         throw new Error("BUG: no Dialog in Window");
      cl = r.classList;
      if (save) {
         dv = d.querySelector('.eq-dialog>.eq-pane>.eq-view');
         t = r.querySelector('.eq-window>.eq-pane>.eq-caption>.eq-title>span');
         l = r.querySelector('.eq-window>.eq-pane>.eq-caption>.eq-logo');
         this.forcedInline = {
            x : r.offsetLeft,
            y : r.offsetTop,
            w : dv.offsetWidth,
            h : dv.offsetHeight,
            t : t ? t.textContent : undefined,
            l : l ? l.style.backgroundImage : undefined,
            r : cl.contains('eq-resize'),
            c : cl.contains('eq-close'),
            m : cl.contains('eq-max')
         };
      }
      (s = d.style).visibility = 'hidden';
      s.zIndex = eq.Dom.zIndex(r);
      (cl = d.classList).add('eq-inline');
      if (dlg = this.dlg)
         cl.add('eq-d' + dlg.id);
      r.parentNode.replaceChild(d, r);
      return this.root = d;
   }
   return null;
};

/*------------------------------------------------------------------------
   Window.makeFramed()
   Convert inline Dialog to Window if necessary.
------------------------------------------------------------------------*/

eq.c.Window.prototype.makeFramed = function(restore) {
   var
      cls = eq.c.Window,
      fi = this.forcedInline,
      d, dlg, parent, z, r, s, cl, el;
   if ((d = this.root) === undefined)
      throw new Error("BUG: Window root undefined");
   if (d.classList.contains('eq-dialog') && (!restore || fi !== undefined)) {
      z = eq.Dom.zIndex(d);
      r = cls.setupFrame((parent = d.parentNode).removeChild(d));
      (s = r.style).visibility = 'hidden';
      s.zIndex = z;
      d.style.zIndex = '';
      (cl = d.classList).remove('eq-inline');
      cl.remove('eq-max');
      if (dlg = this.dlg)
         cl.remove('eq-d' + dlg.id);
      cl = r.classList;
      if (dlg)
         cl.add('eq-d' + dlg.id);
      if (restore) {
         this.forcedInline = undefined;
         el = d.querySelector('.eq-dialog>.eq-pane>.eq-view');
         s.left = fi.x > 0 ? fi.x + 'px' : 0;
         s.top = fi.y > 0 ? fi.y + 'px' : 0;
         (s = el.style).width = fi.w > 0 ? fi.w + 'px' : 0;
         s.height = fi.h > 0 ? fi.h + 'px' : 0;
         if (fi.t) {
            el = r.querySelector(
                  '.eq-window>.eq-pane>.eq-caption>.eq-title>span');
            el.textContent = fi.t;
         }
         if (fi.l) {
            el = r.querySelector('.eq-window>.eq-pane>.eq-caption>.eq-logo');
            el.style.backgroundImage = fi.l;
         }
         if (fi.r)
            cl.add('eq-resize');
         else
            cl.remove('eq-resize');
         if (fi.c)
            cl.add('eq-close');
         else
            cl.remove('eq-close');
         if (fi.m)
            cl.add('eq-max');
         else
            cl.remove('eq-max');
      }
      parent.appendChild(r);
      return this.root = r;
   }
   return null;
};

/*------------------------------------------------------------------------
   static Window.makeBaseDialogInline()
   Convert base Dialog Window to inline Dialog if necessary.
------------------------------------------------------------------------*/

eq.c.Window.prototype.makeBaseDialogInline = function() {
   var app = eq.app, wl, lm, r;
   if ((wl = app.wl) !== undefined && wl.length === 1 && wl[0] === this) {
      // Inline base Dialog, forced by layout mode?
      if (   (lm = app.layoutMode) !== undefined
          && (lm & eq.LayoutMode.inline) && (lm & eq.LayoutMode.dialog)) {
         if ((r = this.root) === undefined)
            throw new Error("BUG: Window root undefined");
         if (r = this.makeInline(true)) {
            if (lm & eq.LayoutMode.maximized)
               r.classList.add('eq-max');
            else
               r.classList.remove('eq-max');
            r.style.visibility = '';
            return true;
         }
      }
   }
   return false;
};

/*------------------------------------------------------------------------
   static Window.zOrderModified()
   Window list stack order has been modified.
------------------------------------------------------------------------*/

eq.c.Window.zOrderModified = function() {
   var app = eq.app, wl, lm, i, w, r, cl, active, hide;
   if ((wl = app.wl) !== undefined) {

      // Inline base Dialog, forced by layout mode?
      if (   (lm = app.layoutMode) !== undefined
          && (!(lm & eq.LayoutMode.inline) || !(lm & eq.LayoutMode.dialog)))
         lm = undefined;

      // Adjust Windows from top to bottom.

      for (active = true, i = wl.length; --i >= 0;) {
         w = wl[i];
         if ((r = w.root) === undefined)
            throw new Error("BUG: Window root undefined");
         if (lm !== undefined && w.dlg !== undefined) {
            if (i === 0) {
               // Base Dialog, make inline.
               if (r = w.makeInline(true)) {
                  if (lm & eq.LayoutMode.maximized)
                     r.classList.add('eq-max');
                  else
                     r.classList.remove('eq-max');
                  r.style.visibility = '';
               }
            }
            else {
               // Make framed if previously made inline.
               if (r = w.makeFramed(true))
                  r.style.visibility = '';
            }
         }
         if ((r = w.root) === undefined)
            throw new Error("BUG: Window root undefined");
         cl = r.classList;
         if (active === undefined)
            cl.remove('eq-active');
         else {
            active = undefined;
            cl.add('eq-active');
         }
         if (hide)
            cl.add('eq-hidden');
         else {
            cl.remove('eq-hidden');
            // Windows below topmost inline Dialog become invisible.
            if (!cl.contains('eq-window'))
               hide = true;
         }
      }
   }
};

/*------------------------------------------------------------------------
   static Window.fromTarget()
   Obtain Window from target element.
------------------------------------------------------------------------*/

eq.c.Window.fromTarget = function(t) {
   var wl, cl, i, w;

   // Locate target in app window list.

   if ((wl = eq.app.wl) !== undefined) {
      while (t && t !== document) {
         if (cl = t.classList) {
            if (cl.contains('eq-dialog') || cl.contains('eq-popup-box'))
               break;
            if (cl.contains('eq-window')) {
               for (i = wl.length; --i >= 0;)
                  if ((w = wl[i]).root === t)
                     return w;
               break;
            }
         }
         t = t.parentNode;
      }
   }
   return null;
};

/*------------------------------------------------------------------------
   Window.onMouseDownOrClick()
   Window 'mousedown' and 'click' event handler.
------------------------------------------------------------------------*/

eq.c.Window.moveMode = {
   left   : 1,
   top    : 2,
   right  : 4,
   bottom : 8,
   size   : 16
};

eq.c.Window.prototype.onMouseDownOrClick = function(e) {
   if (e.type === 'mousedown') {
      var
         cls = eq.c.Window,
         r = this.root,
         t = e.target,
         p = t.parentNode,
         cl = t.classList,
         app, mm, mode, pcl;
      if (p && cl) {
         app = eq.app;
         mm = cls.moveMode;
         if (cl.contains('eq-resize') && p === r) {
            if (cl.contains('eq-top')) {
               mode = mm.top + mm.size;
               if (cl.contains('eq-left'))
                  mode += mm.left;
               else if (cl.contains('eq-right'))
                  mode += mm.right;
            }
            else if (cl.contains('eq-bottom')) {
               mode = mm.bottom;
               if (cl.contains('eq-left'))
                  mode += mm.left + mm.size;
               else if (cl.contains('eq-right'))
                  mode += mm.right;
            }
            else if (cl.contains('eq-left'))
               mode = mm.left + mm.size;
            else if (cl.contains('eq-right'))
               mode = mm.right;
         }
         else {
            while (p && p !== r) {
               if ((pcl = p.classList) && pcl.contains('eq-caption'))
                  break;
               cl = pcl;
               t = p;
               p = t.parentNode;
            }
            if (   p && (pcl = p.classList) && pcl.contains('eq-caption')
                && (p = p.parentNode)
                && (pcl = p.classList) && pcl.contains('eq-pane')
                && (p = p.parentNode) && p === r) {
               if (cl.contains('eq-close')) {
                  cl.add('eq-click');
                  var d;
                  if ((d = this.dlg) !== undefined) {
                     if (d === app.currDlg)
                        app.submit(document.body, { type : 'close' });
                     else
                        d.pendingClose = true;
                  }
                  else
                     this.close();
                  return true;
               }
               if (cl.contains('eq-max')) {
                  if ((cl = p.classList).contains('eq-max')) {
                     cl.remove('eq-max');
                     var pos = this.pos;
                     if (pos !== undefined) {
                        this.pos = undefined;
                        cls.setPos(this, r, pos.x, pos.y);
                     }
                  }
                  else {
                     this.pos = { x : r.offsetLeft, y : r.offsetTop };
                     cl.add('eq-max');
                  }
                  this.modified.maximized = true;
                  return true;
               }
               if (cl.contains('eq-move'))
                  if (!p.classList.contains('eq-max'))
                     mode = mm.left + mm.top;
            }
         }
         if (mode !== undefined) {
            var m = {
               w : this,
               x : e.clientX,
               y : e.clientY,
               mode : mode
            };
            if ((p = r.offsetParent) === document.body)
               m.px = m.py = 0;
            else {
               var prc = p.getBoundingClientRect();
               m.px = prc.left;
               m.py = prc.top;
            }
            app.moveCapture(m, cls.onMove);
            return true;
         }
      }
   }
   return false;
};

/*------------------------------------------------------------------------
   static Window.onMove()
   Move Window.
------------------------------------------------------------------------*/

eq.c.Window.onMove = function(e, m) {
   if (!m.busy) {
      m.busy = true;
      eq.Dom.onNextFrame(m.w, {
         cb : eq.c.Window.onMove.exec,
         x : e.clientX,
         y : e.clientY,
         m : m
      });
   }
};

eq.c.Window.onMove.exec = function(w, qi) {
   var
      cls = eq.c.Window,
      mm = cls.moveMode,
      cx = qi.x,
      cy = qi.y,
      m = qi.m,
      mode = m.mode,
      sz = mode & mm.size,
      r = w.root,
      rc, chk;
   if (r === undefined) {
      // Window does no longer exist.
      m.busy = undefined;
      return;
   }
   rc = r.getBoundingClientRect();
   if (mode & mm.left) {
      // Left position modified.
      if (cx !== m.x && (m.maxX === undefined || cx <= m.maxX)) {
         var
            left = rc.left + window.pageXOffset - m.px,
            x = left + cx - m.x;
         if (x < 0) {
            x = 0;
            cx = m.x - left;
         }
         r.style.left = x ? x + 'px' : 0;
         w.modified.pos = true;
         if (!sz)
            m.x = cx;
         else {
            // Width modified.
            if (chk === undefined) {
               var dv = r.querySelector('.eq-dialog>.eq-pane>.eq-view');
               chk = { rc : rc, dv : dv, drc : dv.getBoundingClientRect() };
            }
            chk.dv.style.width = (left + chk.drc.width - x) + 'px';
            chk.cx = cx;
            chk.cw = rc.width;
         }
      }
   }
   if (mode & mm.top) {
      // Top position modified.
      if (cy !== m.y && (m.maxY === undefined || cy <= m.maxY)) {
         var
            top = rc.top + window.pageYOffset - m.py,
            y = top + cy - m.y;
         if (y < 0) {
            y = 0;
            cy = m.y - top;
         }
         r.style.top = y ? y + 'px' : 0;
         w.modified.pos = true;
         if (!sz)
            m.y = cy;
         else {
            // Height modified.
            if (chk === undefined) {
               var dv = r.querySelector('.eq-dialog>.eq-pane>.eq-view');
               chk = { rc : rc, dv : dv, drc : dv.getBoundingClientRect() };
            }
            chk.dv.style.height = (top + chk.drc.height - y) + 'px';
            chk.cy = cy;
            chk.cw = rc.height;
         }
      }
   }
   if (mode & mm.right) {
      // Width modified.
      if (cx !== m.x && (m.minX === undefined || cx >= m.minX)) {
         if (chk === undefined) {
            var dv = r.querySelector('.eq-dialog>.eq-pane>.eq-view');
            chk = { rc : rc, dv : dv, drc : dv.getBoundingClientRect() };
         }
         chk.dv.style.width = (chk.drc.width + cx - m.x) + 'px';
         chk.cx = cx;
         chk.cw = rc.width;
      }
   }
   if (mode & mm.bottom) {
      if (cy !== m.y && (m.minY === undefined || cy >= m.minY)) {
         // Height modified.
         if (chk === undefined) {
            var dv = r.querySelector('.eq-dialog>.eq-pane>.eq-view');
            chk = { rc : rc, dv : dv, drc : dv.getBoundingClientRect() };
         }
         chk.dv.style.height = (chk.drc.height + cy - m.y) + 'px';
         chk.cy = cy;
         chk.ch = rc.height;
      }
   }
   if (chk === undefined)
      m.busy = undefined;
   else
      eq.Dom.onNextFrame(w, {
         cb : cls.onMove.chk,
         m : m,
         chk : chk
      });
};

eq.c.Window.onMove.chk = function(w, qi) {
   var
      m = qi.m,
      r = w.root,
      chk = qi.chk,
      rc = r.getBoundingClientRect();
   // New Window size may need to stabilize, could require multiple frames.
   if (   (chk.cw !== undefined && chk.cw !== rc.width)
       || (chk.ch !== undefined && chk.ch !== rc.height)) {
      if (chk.cw !== undefined)
         chk.cw = rc.width;
      if (chk.ch !== undefined)
         chk.ch = rc.height;
   }
   // When done, wait another frame.
   else if (chk.done) {
      m.busy = undefined;
      return;
   }
   // Check width/height constraints.
   else {
      if (chk.cx !== undefined) {
         if (rc.width !== chk.rc.width) {
            m.x = chk.cx;
            m.minX = m.maxX = undefined;
            w.modified.size = true;
         }
         else {
            // Constrained by min/max width, reset to previous position/size.
            r.style.left = (chk.rc.left + window.pageXOffset - m.px) + 'px';
            chk.dv.style.width = chk.drc.width + 'px';
            if (m.minX === undefined && m.maxX === undefined) {
               if (chk.cx < m.x) {
                  m.minX = m.x;
                  m.maxX = undefined;
               }
               else {
                  m.maxX = m.x;
                  m.minX = undefined;
               }
            }
         }
      }
      if (chk.cy !== undefined) {
         if (rc.height !== chk.rc.height) {
            m.y = chk.cy;
            m.minY = m.maxY = undefined;
            w.modified.size = true;
         }
         else {
            // Constrained by min/max height, reset to previous position/size.
            r.style.top = (chk.rc.top + window.pageYOffset - m.py) + 'px';
            chk.dv.style.height = chk.drc.height + 'px';
            if (m.minY === undefined && m.maxY === undefined) {
               if (chk.cy < m.y) {
                  m.minY = m.y;
                  m.maxY = undefined;
               }
               else {
                  m.maxY = m.y;
                  m.minY = undefined;
               }
            }
         }
      }
      chk.done = true;
   }
   eq.Dom.onNextFrame(w, qi);
};

/** @preserve $Id: 10-dlg.js,v 28.46 2025/03/13 14:28:12 mike Exp $
*//*======================================================================
   WEBDLG v2 namespace / globals
   Map of DLG element classes.
========================================================================*/

eq.d = eq.d || new Map();

/*========================================================================
   class DlgElement extends Obj
   DLG element base class.
========================================================================*/

eq.c.Obj.subClass('DlgElement', function(dlg, el) {
   eq.c.Obj.call(this);
   this.cssRules = null;
   this.tvNodes = null;
   this.accel = null;
   this.toolTip = null;
   this.ddi = undefined;
});

eq.c.DlgElement.prototype.sensitive = true;

/*------------------------------------------------------------------------
   static DlgElement.addClass()
   Subclassing convenience helper: Add DLG element class.
------------------------------------------------------------------------*/

eq.c.DlgElement.addClass = function(name, cls) {
   this.subClass(name, cls);
   eq.d.set(name, cls);
   return cls;
};

/*------------------------------------------------------------------------
   DlgElement.dispose()
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.dispose = function(dlg, id) {
   var
      rules = this.cssRules,
      l = rules ? rules.length : 0;
   if (l) {
      var
         stl = eq.app.stl,
         sheet = stl.sheet,
         owner = stl.owner,
         rl = owner.length,
         i = 0, ri, ci;
      if ((ri = ci = owner.indexOf(id)) === -1)
         throw new Error(this.constructor.name + ".dispose(" + id +
            ") " + l + " stale CSS rules");
      do {
         sheet.deleteRule(ci);
         i++;
      } while (++ri < rl && owner[ri] === id);
      owner.splice(ci, i);
      rules.length = 0;
      this.cssRules = null;
      if (i !== l)
         throw new Error(this.constructor.name + ".dispose(" + id +
            ") CSS rules inconsistent:" + l + "/" + i);
   }

   var tvNodes = this.tvNodes;
   if (tvNodes) {
      var
         cls = eq.c.DlgElement,
         tv = cls.triggerVisible,
         wh = tv.wh,
         t, arg;
      for (var i = 0, l = tvNodes.length; i < l; i++)
         if ((arg = wh.get(t = tvNodes[i]))) {
            arg.forEach(cls.disposeTvNodes, this);
            if (arg.size === 0) {
               wh.delete(t);
               if (wh.size === 0)
                  tv.ob.disconnect();
            }
         }
      tvNodes.length = 0;
      this.tvNodes = null;
   }

   var accel = this.accel;
   if (accel !== null) {
      var
         da = dlg.accel,
         di = da.get(accel);
      if (di === id)
         da.delete(accel);
      this.accel = null;
   }

   this.toolTip = null;
   this.ddi = undefined;
};

eq.c.DlgElement.disposeTvNodes = function(arg, id, m) {
   var i = 0;
   while (i < arg.length) {
      if (arg[i].self === this)
         arg.splice(i, 1);
      else
         i++;
   }
   if (arg.length === 0)
      m.delete(id);
};

/*------------------------------------------------------------------------
   DlgElement.updateID()
   Element identifier has been modified.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.updateId = function(dlg, id, oldId) {
   var
      rules = this.cssRules,
      l = rules ? rules.length : 0;
   if (l) {
      var
         cls = eq.c.DlgElement,
         stl = eq.app.stl,
         sheet = stl.sheet,
         owner = stl.owner,
         rl = owner.length,
         i = 0, ri;
      if ((ri = owner.indexOf(oldId)) === -1)
         throw new Error(this.constructor.name + ".updateId(" + oldId +
            ") " + l + " stale CSS rules");
      do {
         sheet.deleteRule(ri);
         cls.insertCssRule(sheet, ri, id, rules[i++].rule);
         owner[ri] = id;
      } while (++ri < rl && owner[ri] === oldId);
      if (i !== l)
         throw new Error(this.constructor.name + ".updateId(" + oldId +
            ") CSS rules inconsistent:" + l + "/" + i);
   }
   else if ((l = eq.app.stl.owner.indexOf(oldId)) !== -1)
      throw new Error(this.constructor.name + ".updateId(" + oldId +
         ") CSS rules inconsistent:" + l);

   var accel = this.accel;
   if (accel !== null) {
      var
         da = dlg.accel,
         di = da.get(accel);
      if (di === oldId)
         da.set(accel, id);
   }
};

/*------------------------------------------------------------------------
   DlgElement.setAttr()
   Set element attributes.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.setAttr = function(dlg, id, el, rt) {
   var rqTag = eq.RqTag;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri], tag = rc.t;
      switch (tag) {
         case rqTag.sensitive: {
            var
               sensitive = !!(rc.v & 1),
               parentDisabled = rc.v >> 1,
               n, i, eds, ed, ch, cid;
            if (parentDisabled) {
               this.sensitive = sensitive;
               this.parentDisabled = parentDisabled;
               this.setSensitive(dlg, id, el, false);
            }
            else if (this.sensitive !== sensitive) {
               // 'sensitive' status changed.
               this.sensitive = sensitive;
               if (!this.parentDisabled)
                  this.setSensitive(dlg, id, el, sensitive);
               if (i = (n = el.querySelectorAll('.eq-control')).length) {
                  // Inherit 'sensitive' status to children.
                  eds = dlg.eds;
                  for (; --i >= 0;) {
                     if ((ed = eds.get(cid = (ch = n[i]).id)) === undefined)
                        throw new Error(this.constructor.name + ".setAttr(" +
                           id + ") sensitive BUG: child " +
                           cid + " not registered");
                     if (sensitive) {
                        if (ed.parentDisabled && --ed.parentDisabled <= 0) {
                           ed.parentDisabled = undefined;
                           if (ed.sensitive)
                              ed.setSensitive(dlg, cid, ch, true);
                        }
                     }
                     else {
                        // Parent is disabled.
                        if (ed.parentDisabled)
                           ed.parentDisabled++;
                        else {
                           ed.parentDisabled = 1;
                           if (ed.sensitive)
                              ed.setSensitive(dlg, cid, ch, false);
                        }
                     }
                  }
               }
            }
            break;
         }

         case rqTag.toolTip:
            if (this.toolTip = rc.v)
               eq.app.ttAttach(el);
            else {
               eq.app.ttDetach(el);
               this.toolTip = null;
            }
            break;

         default:
            throw new Error(this.constructor.name + ".setAttr(" + id +
               ") failed: tag " + tag);
      }
   }
};

// Delayed invocation on next event cycle.
eq.c.DlgElement.setAttr = function(arg) {
   arg.t.setAttr(arg.d, arg.i, arg.e, arg.r);
};

/*------------------------------------------------------------------------
   static DlgElement.setFont()
   Set element-specific 'font' styles.
------------------------------------------------------------------------*/

eq.c.DlgElement.setFont = function(el, fn) {
   if (eq.Dom.setFont(el, fn))
      el.classList.add('eq-font');
   else
      el.classList.remove('eq-font');
};

/*------------------------------------------------------------------------
   DlgElement.setSensitive()
   Set element-specific 'sensitive' properties.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.setSensitive = function(dlg, id, el, sensitive) {
   var cl = el.classList;
   if (sensitive)
      cl.remove('eq-disabled');
   else
      cl.add('eq-disabled');
};

/*------------------------------------------------------------------------
   DlgElement.setAccel()
   Set element accelerator.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.setAccel = function(dlg, id, key, kmod) {
   var accel = this.accel, ac;
   if (key === undefined || key === null)
      ac = null;
   else if (kmod)
      ac = String.fromCharCode(kmod+48) + key.toUpperCase();
   else
      ac = key.toUpperCase();
   if (ac !== accel) {
      var da = dlg.accel;
      if (accel !== null) {
         var di = da.get(accel);
         if (di === id)
            da.delete(accel);
      }
      if (ac !== null && !da.has(ac))
         da.set(this.accel = ac, id);
      else
         this.accel = null;
   }
};

/*------------------------------------------------------------------------
   DlgElement.onEvent()
   Filter and/or handle action event.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onEvent = function(dlg, id, el, e) {
   return true;
};

/*------------------------------------------------------------------------
   DlgElement.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   return false;
};

/*------------------------------------------------------------------------
   DlgElement.onAccel()
   Handle accelerator key event.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onAccel = function(dlg, id, el) {
   eq.Dom.fireClick(this.focusElement(dlg, el));
};

/*------------------------------------------------------------------------
   DlgElement.onTypeAhead()
   Process type-ahead key stroke.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onTypeAhead = function(dlg, id, el, key) {
   var tab;
   switch (key) {
      case 'ShiftTab':
         tab = -1;
         break;
      case 'Tab':
         tab = 1;
         break;
      case 'ShiftEnter':
         tab = -1;
         // FALLTHROUGH
      case 'Enter': {
         var cl = el.classList;
         if (cl.contains('eq-enter')) {
            // Invalid: 'Enter key has special function' not handled.
            return false;
         }
         if (dlg.fkey && dlg.fkey.length > 0 && dlg.fkey[0]) {
            // Invalid: Dialog.cr function key not handled.
            break;
         }
         if (tab === undefined && !cl.contains('eq-noenter')) {
            // .tabonenter enabled.
            tab = 1;
         }
      }
   }

   if (tab) {
      // Navigate to next/previous element in tab order.
      var
         app = eq.app,
         elNext = dlg.nextTab(id, tab === -1),
         idNext;
      if (elNext) {
         app.pendingTabMode = tab;
         app.pendingTabModeId = idNext = elNext.id;
         app.setFocus(idNext, elNext);
      }
      else {
         // No other element found in tab order,
         // submit 'blur' event if registered.
         app.processEvent(dlg, {
            type : 'blur',
            target : this.focusElement(dlg, el)
         }, id, el, this);
      }
      return true;
   }

   return false;
};

/*------------------------------------------------------------------------
   DlgElement.onSubmit()
   Prepare submission.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onSubmit = function(dlg, id, el, ev) {
};

/*------------------------------------------------------------------------
   DlgElement.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onChange = function(dlg, id, el, e) {
   return true;
};

/*------------------------------------------------------------------------
   DlgElement.onToolTip()
   Obtain tooltip text or null if undefined.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onToolTip = function(el) {
   return this.toolTip;
};

/*------------------------------------------------------------------------
   DlgElement.onDragStart()
   Start drag&drop action.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onDragStart = function(dlg, id, el, t, e, dnd) {
   return false;
};

/*------------------------------------------------------------------------
   static DlgElement.setDropModes()
   Set vald drag&drop modes (move/copy/link).
------------------------------------------------------------------------*/

eq.c.DlgElement.setDropModes = function(e, mode) {
   var md = eq.Dnd, ea;
   if (mode & md.move) {
      if (mode & md.copy)
         ea = (mode & md.link) ? 'all' : 'copyMove';
      else
         ea = (mode & md.link) ? 'linkMove' : 'move';
   }
   else if (mode & md.copy)
      ea = (mode & md.link) ? 'copyLink' : 'copy';
   else
      ea = (mode & md.link) ? 'link ' : 'none';
   e.dataTransfer.effectAllowed = ea;
};

/*------------------------------------------------------------------------
   DlgElement.onDragOver()
   Return drop target when dragging over element.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onDragOver = function(dlg, id, el, t) {
   return null;
};

/*------------------------------------------------------------------------
   DlgElement.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onDrop = function(dlg, id, el, t, dnd) {
   return 0;
};

/*------------------------------------------------------------------------
   DlgElement.focusElement()
   Obtain focus element.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.focusElement = function(dlg, el) {
   return el;
};

/*------------------------------------------------------------------------
   DlgElement.focusGained()
   Element has gained focus.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.focusGained = function(dlg, ct) {
};

/*------------------------------------------------------------------------
   DlgElement.focusLost()
   Element has lost focus.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.focusLost = function(dlg, el) {
};

/*------------------------------------------------------------------------
   DlgElement.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.changed = function(dlg, id, el, ty) {
   return null;
};

/*------------------------------------------------------------------------
   DlgElement.onUpdate()
   Act upon API thread update request.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onUpdate = function(dlg, id, el, tag, atr, idx) {
   throw new Error("BUG: " + this.constructor.name + ".onUpdate(" + id +
      ") not implemented");
};

/*------------------------------------------------------------------------
   DlgElement.onUpdateReturn()
   Handle asynchronous onUpdate() return.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onUpdateReturn = function(dlg, id, val) {
   var ddi = this.ddi, app = eq.app;
   if (!ddi)
      throw new Error("BUG: " + this.constructor.name + ".onUpdate(" +
         id + ") undefined context");
   this.ddi = undefined;
   ddi.o = eq.ApiOp.updated;
   ddi.d = val;
   app.api.postMessage(ddi);
   if (dlg.temp)
      app.closeDialog(dlg);
};

/*------------------------------------------------------------------------
   DlgElement.onMessage()
   Process window 'message' event.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.onMessage = function(dlg, id, el, msg) {
};

/*------------------------------------------------------------------------
   DlgElement.updated()
   Act upon API thread update response.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.updated = function(dlg, id, el, tag, arg) {
   var ut = eq.UpdateTag;
   switch (tag) {
      case ut.mouseButton2: {
         var
            app = eq.app,
            rule = arg.rule;
         app.onceRule = rule;
         app.onceId = id;
         app.submit(el, {
            id : id,
            type : 'click'
         });
         break;
      }

      case ut.ctxMenuOpen: {
         var
            dm = eq.Dom,
            et = arg.et,
            y = arg.y,
            menu, st, fn, rl, cl, mb;
         // Create context menu overlay.
         menu = dlg.addControl(et);
         (st = menu.style).left = arg.x + window.pageXOffset + 'px';
         st.top = y + window.pageYOffset + 'px';
         // Derive font from MenuBar or Dialog or set default font.
         fn = eq.app.fnDefault;
         for (var i = dlg.root.length; --i >= 0;)
            if ((cl = (rl = dlg.root[i]).classList).contains('eq-dialog')) {
               if ((mb = rl.firstElementChild).classList.contains('eq-menubar'))
                  rl = mb;
               else if (!cl.contains('eq-root-font'))
                  rl = null;
               if (rl) {
                  dm.copyFont(menu, rl);
                  dm.adjustElementFontSize(menu);
                  menu.classList.add('eq-root-font');
                  fn = null;
               }
               break;
            }
         if (fn) {
            if (dm.setFont(menu, fn)) {
               dm.adjustElementFontSize(menu);
               menu.classList.add('eq-root-font');
            }
            else
               menu.classList.add('eq-default-font');
         }
         // Save requesting element id.
         dlg.eds.get(menu.id).contextMenuId = arg.id;
         dm.onNextFrame(menu, {
            cb : eq.c.DlgElement.showContextMenu,
            y  : y
         });
         break;
      }

      default:
         throw new Error(this.constructor.name + ".updated(" + id +
            ") invalid tag " + tag);
   }
};

eq.c.DlgElement.showContextMenu = function(menu, qi) {
   eq.app.makeOverlay(menu, menu, eq.c.Menu.closeContextMenu);
   if (menu.classList.contains('eq-scroll'))
      menu.style.maxHeight =
         (document.documentElement.clientHeight - qi.y - 2) + 'px';
};

/*------------------------------------------------------------------------
   DlgElement.setCssRule()
   Set application CSS rule for element.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.setCssRule = function(id, type, rule) {
   var
      cls = eq.c.DlgElement,
      rules = this.cssRules,
      stl = eq.app.stl,
      sheet = stl.sheet,
      owner = stl.owner,
      rl = owner.length,
      i, l, r, ri, ci;
   if (!rules)
      rules = this.cssRules = [];
   for (i = 0, l = rules.length; i < l; i++)
      if (type === (r = rules[i]).type) {
         // Modify or delete existing rule.
         if ((ri = owner.indexOf(id)) !== -1) {
            ci = i;
            while (i-- > 0)
               if (++ri >= rl || owner[ri] !== id) {
                  ri = -1;
                  break;
               }
         }
         if (ri === -1)
            throw new Error(this.constructor.name + ".setCssRule(" + id +
               ") not found, type:" + type);
         sheet.deleteRule(ri);
         if (rule) {
            r.rule = rule;
            cls.insertCssRule(sheet, ri, id, rule);
         }
         else {
            rules.splice(ci, 1);
            owner.splice(ri, 1);
            if (rules.length === 0)
               this.cssRules = null;
         }
         return;
      }
   // Add new rule.
   if ((ri = owner.indexOf(id)) !== -1) {
      i = 1;
      while (++ri < rl && owner[ri] === id)
         i++;
      if (i !== l)
         throw new Error(this.constructor.name + ".setCssRule(" + id +
            ") inconsistent:" + l + "/" + i + ", type:" + type);
   }
   else {
      if (l)
         throw new Error(this.constructor.name + ".setCssRule(" + id +
            ") inconsistent:" + l + ", type:" + type);
      ri = rl;
   }
   if (rule) {
      rules.push({ rule: rule, type : type });
      cls.insertCssRule(sheet, ri, id, rule);
      owner.splice(ri, 0, id);
   }
};

eq.c.DlgElement.insertCssRule = function(sheet, idx, id, rule) {
   var s;
   if (Array.isArray(rule)) {
      s = '';
      for (var i = 0, l = rule.length; i < l; i++)
         s += '#' + id + rule[i];
   }
   else
      s = '#' + id + rule;
   sheet.insertRule(s, idx);
};

/*------------------------------------------------------------------------
   DlgElement.duplicateCssRules()
   Duplicate application CSS element rules to new ID.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.duplicateCssRules = function(id) {
   var
      rules = this.cssRules,
      l = rules ? rules.length : null;
   if (l) {
      var
         cls = eq.c.DlgElement,
         stl = eq.app.stl,
         sheet = stl.sheet,
         owner = stl.owner,
         rl = owner.length,
         i, nid;
      for (i = 1;; i++)
         if (owner.indexOf(nid = id + '-' + i) === -1)
            break;
      for (i = 0; i < l; i++, rl++) {
         cls.insertCssRule(sheet, rl, nid, rules[i].rule);
         owner.push(nid);
      }
      return nid;
   }
   return '';
};

/*------------------------------------------------------------------------
   DlgElement.whenVisible()
   Invoke callback if element is visible or when it becomes visible.
------------------------------------------------------------------------*/

eq.c.DlgElement.prototype.whenVisible = function(dlg, id, el, cb) {
   // HTMLElement.offsetParent === null when not displayed.
   if (el.offsetParent !== null) {
      cb.call(this, dlg, id, el);
      return;
   }
   // Element not displayed.
   var
      cls = eq.c.DlgElement,
      tvNodes = this.tvNodes,
      t, p, tv, wh, arg, a;
   // Locate parent to watch for className or style.display mutation.
   for (t = el; p = t.parentNode; t = p)
      if (p.offsetParent !== null)
         break;
   if (!t) {
      console.error(
         "DlgElement.whenVisible: No parent to watch for '" + id +"'");
      return;
   }
   if (tvNodes === null)
      tvNodes = this.tvNodes = [ t ];
   else if (tvNodes.indexOf(t) === -1)
      tvNodes.push(t);
   if ((arg = (wh = (tv = cls.triggerVisible).wh).get(t)) === undefined) {
      wh.set(t, arg = new Map());
      tv.ob.observe(t, cls.onTriggerVisibleMode);
   }
   if ((a = arg.get(id)) === undefined)
      arg.set(id, a = []);
   for (var i = 0, l = a.length; i < l; i++)
      if (a[i].cb === cb)
         return;
   a.push({
      cb   : cb,
      self : this,
      dlg  : dlg,
      el   : el
   });
};

eq.c.DlgElement.onTriggerVisibleMode = {
   attributes : true,
   attributeFilter : [ 'class', 'style' ]
};

eq.c.DlgElement.onTriggerVisible  = function(mu, ob) {
   var
      cls = eq.c.DlgElement,
      tv = cls.triggerVisible,
      wh = tv.wh;
   for (var i = 0, l = mu.length; i < l; i++) {
      var
         m = mu[i],
         t = m.target,
         arg = wh.get(t);
      if (arg && window.getComputedStyle(t).display !== 'none') {
         // Element now visible.
         arg.forEach(cls.onTriggerVisibleExec, t);
         arg.clear();
         wh.delete(t);
         if (wh.size === 0)
            tv.ob.disconnect();
      }
   }
};

eq.c.DlgElement.onTriggerVisibleExec = function(arg, id) {
   for (var i = 0, l = arg.length; i < l; i++) {
      var
         a = arg[i],
         self = a.self,
         tvNodes = self.tvNodes,
         i;
      a.cb.call(self, a.dlg, id, a.el);
      if (tvNodes && (i = tvNodes.indexOf(this)) !== -1) {
         tvNodes.splice(i, 1);
         if (tvNodes.length === 0)
            self.tvNodes = null;
      }
   }
   arg.length = 0;
};

eq.c.DlgElement.triggerVisible = {
   ob : new MutationObserver(eq.c.DlgElement.onTriggerVisible),
   wh : new Map()
};

/*========================================================================
   final class Dlg extends Obj
   DLG Dialog instance.
========================================================================*/

eq.c.Obj.subClass('Dlg', function(id) {
   eq.c.Obj.call(this);

   // Dialog id.
   this.id = id;

   // Layout grid.
   this.grid = {
      // wr : Width reference 1ch
      // wp : Width pixels
      // wf : Width ch factor
      // hr : Height reference 1em
      // hp : Height pixels
      // hf : Height em factor
   };

   // Window.
   this.w = null;

   // Root elements.
   this.root = [];

   // Element descriptors.
   this.eds = new Map();

   // Tab order.
   this.tabOrder = null;

   // Accelerators.
   this.accel = new Map();
});

delete eq.c.Dlg.subClass; // final

/*------------------------------------------------------------------------
   Dlg.close()
   Close Dialog instance.
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.close = function() {
   var cls = this.constructor, app = eq.c.App;

   // Reset current Dialog if necessary.
   this.resetCurrDlg();

   // Close tooltip if any.
   app.ttClose();

   // Restore original page title if necessary.
   if (document.title === this.title) {
      var t = app.title;
      if (t !== document.title) {
         document.title = t;
         eq.app.updateSessionList();
      }
   }

   // Remove root elements.
   for (var i = this.root.length; --i >= 0;) {
      var el = this.root[i];
      app.removeRootListeners(el);
      if (!el.classList.contains('eq-dialog'))
         el.parentNode.removeChild(el);
      else {
         // Close Window.
         if (this.w === null)
            throw new Error("Dlg.close(" + this.id + ") BUG: no Window");
         this.w.close();
         this.w = null;
      }
   }
   this.root.length = 0;
   if (this.w !== null)
      throw new Error("Dlg.close(" + this.id + ") BUG: Window not closed");

   // Clear accelerators.
   this.accel.clear();

   // Clear element descriptors.
   this.eds.forEach(cls.cbDisposeDlgElement, this);
   this.eds.clear();

   // Clear tab order.
   this.tabOrder = null;

   // Clear pending close request.
   this.pendingClose = undefined;

   // Clear pending whenReady action.
   this.whenReady = undefined;
   this.whenReadyArg = undefined;

   // Delete properties.
   this.layout = undefined;
   this.title = undefined;
   this.icon = undefined;
   this.fkey = undefined;
   this.timer = undefined;
   this.ddo = undefined;

   // Close temporary container if necesary.
   if (this.temp) {
      var p = document.getElementById('eq-temp');
      if (p)
         p.parentNode.removeChild(p);
   }
};

eq.c.Dlg.cbDisposeDlgElement = function(ed, id) {
   ed.dispose(this, id);
};

eq.c.Dlg.prototype.resetCurrDlg = function() {
   var app = eq.app;
   if (this === app.currDlg) {
      if (this.whenReady) {
         // Call pending whenReady action.
         this.callWhenReady();
      }
      app.currDlg = null;
      app.focus = null;
      app.changed.clear();
      app.setIdle(this);
   }
   else if (this.whenReady && this.ddo)
      throw new Error("Dlg.resetCurrDlg BUG: whenReady pending");
   this.ddo = undefined;
   this.whenReady = undefined;
   this.whenReadyArg = undefined;
};

eq.c.Dlg.prototype.callWhenReady = function() {
   var cb = this.whenReady, arg = this.whenReadyArg;
   this.whenReady = undefined;
   this.whenReadyArg = undefined;
   cb(this, arg);
};

/*------------------------------------------------------------------------
   Dlg.addRoot()
   Add Dialog root element.
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.addRoot = function(el) {

   // TODO: dlgAnchor

   var cl = el.classList, wc, m, lm, l, r;
   el.style.display = 'none';
   cl.add('eq-root');
   if (cl.contains('eq-dialog')) {
      if (this.w !== null)
         throw new Error("Dlg.addRoot(" + this.id + ") BUG: Window exists");
      wc = eq.c.Window;
      m = eq.LayoutMode;
      if (   (lm = eq.app.layoutMode) !== undefined
          && (lm & m.inline) && !(lm & m.dialog)) {
         // Inline Dialog, forced by layout mode.
         r = el;
      }
      else {
         lm = undefined;
         if (   (l = this.layout) === undefined
             || l.mode === eq.DialogMode.inline) {
            // Inline Dialog.
            r = el;
         }
         else {
            // Dialog in frame.
            r = wc.setupFrame(el);
         }
      }
      this.w = new wc(r, document.getElementById('eq-dialog'), this);
      if (lm !== undefined) {
         // Inline Dialog, forced by layout mode.
         cl.add('eq-inline');
         if (lm & m.maximized)
            cl.add('eq-max');
         else
            cl.remove('eq-max');
      }
   }
   else
      throw new Error("Dlg.addRoot(" + this.id + ") BUG: not a Dialog");

   this.root.push(el);
   eq.c.App.addRootListeners(el);
};

/*------------------------------------------------------------------------
   Dlg.removeElement()
   Remove element from Dialog.
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.removeElement = function(el) {
   var r = this.root, eds = this.eds, i, empty, n, id, ed;
   if ((i = r.indexOf(el)) !== -1) {
      // Root element.
      r.splice(i, 1);
      empty = r.length === 0;
   }

   if (!empty) {
      // Remove from element descriptors.
      n = el.querySelectorAll('[id]');
      for (i = n.length; --i >= 0;) {
         if ((ed = eds.get(id = n[i].id)) !== undefined) {
            ed.dispose(this, id);
            eds.delete(id);
         }
      }
      if ((ed = eds.get(id = el.id)) !== undefined) {
         ed.dispose(this, id);
         eds.delete(id);
      }
   }

   if (empty === undefined)
      el.parentNode.removeChild(el);
   else {
      // Root element.
      eq.c.App.removeRootListeners(el);
      if (!el.classList.contains('eq-dialog'))
         el.parentNode.removeChild(el);
      else {
         // Close Window.
         if (this.w === null)
            throw new Error("Dlg.removeElement(" + this.id +
               ") BUG: no Window");
         this.w.close();
         this.w = null;
      }
   }

   return empty;
};

/*------------------------------------------------------------------------
   Dlg.visible()
   Make Dialog visible.
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.visible = function() {
   var cls = this.constructor, whenReady = true;
   for (var ri = this.root.length; --ri >= 0;) {
      var rl = this.root[ri], st, fn;
      if (rl.classList.contains('eq-dialog')) {
         if (this.w === null)
            throw new Error("Dlg.visible(" + this.id + ") BUG: no Window");

         // Validate existing panel elements,
         // relink hidden MenuBar and hidden/misplaced ToolBar(s).

         // Try to avoid flicker while relinking.
         if ((st = rl.style).display === 'none') {
            st.visibility = 'hidden';
            st.display = '';
         }
         if (   rl !== this.w.root
             && (st = this.w.root.style).display === 'none') {
            st.visibility = 'hidden';
            st.display = '';
         }

         var
            dc = rl.querySelector(
               '.eq-dialog>.eq-pane>.eq-view>.eq-container'),
            dp = dc.parentNode.parentNode,
            el = rl.firstElementChild,
            tc = 'eq-top',
            cl, ch;
         while (el) {
            if ((cl = el.classList).contains('eq-panel')) {
               if (   cl.contains('eq-hidden')
                   || (cl.contains('eq-toolbar') && !cl.contains(tc))) {
                  var nx = el.nextElementSibling;
                  dc.appendChild(el);
                  cl.remove('eq-rt');
                  cl.remove('eq-panel');
                  cl.remove('eq-horz');
                  el = nx;
                  continue;
               }
            }
            else if (cl.contains('eq-pane')) {
               ch = el.firstElementChild;
               tc = 'eq-left';
               while (ch) {
                  if ((cl = ch.classList).contains('eq-panel')) {
                     if (   cl.contains('eq-hidden')
                         || (cl.contains('eq-toolbar') && !cl.contains(tc))) {
                        var nx = ch.nextElementSibling;
                        dc.appendChild(ch);
                        cl.remove('eq-rt');
                        cl.remove('eq-panel');
                        cl.remove('eq-vert');
                        ch = nx;
                        continue;
                     }
                  }
                  else if (cl.contains('eq-view'))
                     tc = 'eq-right';
                  ch = ch.nextElementSibling;
               }
               tc = 'eq-bottom';
            }
            el = el.nextElementSibling;
         }

         // MenuBar.
         if ((cl = (el = rl.firstElementChild).classList).contains('eq-idle'))
            cl = (el = el.nextElementSibling).classList;
         if (!cl.contains('eq-menubar')) {
            // Put first visible MenuBar into top-most panel.
            if (ch = dc.querySelector('.eq-menubar:not(.eq-hidden)')) {
               (cl = ch.classList).add('eq-rt');
               cl.add('eq-panel');
               rl.insertBefore(ch, el);
            }
         }

         // StatusBar.
         if ((cl = (el = rl.lastElementChild).classList).contains('eq-idle'))
            cl = (el = el.previousElementSibling).classList;
         if (!cl.contains('eq-statusbar')) {
            // Put first visible StatusBar into bottom-most panel.
            if (ch = dc.querySelector('.eq-statusbar:not(.eq-hidden)')) {
               (cl = ch.classList).add('eq-rt');
               cl.add('eq-panel');
               rl.appendChild(ch);
            }
         }

         // ToolBars.
         var n = dc.querySelectorAll('.eq-toolbar:not(.eq-hidden)');
         for (var i = 0, l = n.length; i < l; i++) {
            if ((cl = (el = n[i]).classList).contains('eq-top')) {
               cl.add('eq-rt');
               cl.add('eq-panel');
               cl.add('eq-horz');
               if ((ch = rl.firstElementChild).classList.contains('eq-idle'))
                  ch = ch.nextElementSibling;
               if (ch.classList.contains('eq-menubar'))
                  rl.insertBefore(el, ch.nextElementSibling);
               else
                  rl.insertBefore(el, ch);
            }
            else if (cl.contains('eq-bottom')) {
               cl.add('eq-rt');
               cl.add('eq-panel');
               cl.add('eq-horz');
               if ((ch = rl.lastElementChild).classList.contains('eq-idle'))
                  ch = ch.previousElementSibling;
               if (ch.classList.contains('eq-statusbar'))
                  rl.insertBefore(el, ch);
               else
                  rl.appendChild(el);
            }
            else if (cl.contains('eq-left')) {
               cl.add('eq-rt');
               cl.add('eq-panel');
               cl.add('eq-vert');
               dp.insertBefore(el, dp.firstElementChild);
            }
            else if (cl.contains('eq-right')) {
               cl.add('eq-rt');
               cl.add('eq-panel');
               cl.add('eq-vert');
               dp.appendChild(el);
            }
         }

         // Update font if necessary.
         if ((fn = this.fn) !== undefined) {
            var
               id = rl.id,
               ed = this.eds.get(id),
               fs = eq.FontStyle,
               ffc = fn.fc, // Font face
               fsz = fn.sz, //      size
               fst = fn.st, //      style
               fsf = eq.app.fnSizeFactor,
               frl;
            this.fn = undefined;
            if (ed === undefined)
               throw new Error("Dlg.visible(" + this.id +
                  ") BUG: not registered");
            if (ffc || fsz || (fst & (fs.bold | fs.italic))) {
               frl = ",.eq-d" + this.id + "{";
               if (ffc)
                  frl += "font-family:" + ffc + ";";
               if (fsz)
                  frl += "font-size:" + fsz + ";";
               if (fst & fs.bold)
                  frl += "font-weight:bold;";
               if (fst & fs.italic)
                  frl += "font-style:italic;";
               frl += "}";
               (cl = rl.classList).add('eq-root-font');
               cl.remove('eq-default-font');
            }
            else {
               (cl = rl.classList).add('eq-default-font');
               cl.remove('eq-root-font');
            }
            ed.setCssRule(id, eq.RuleType.font, frl);
            if (fsf && fsf !== 1)
               frl = " .eq-fn,.eq-d" + this.id + " .eq-fn{font-size:" +
                     (Math.round(fsf * 1000) / 10) + "%}";
            else
               frl = undefined;
            ed.setCssRule(id, eq.RuleType.childFont, frl);
         }

         // Make Window visible on next frame,
         // set position and title if necessary.
         eq.Dom.onNextFrame(this, {
            cb : cls.commitVisible,
            rl : rl,
            rc : rl.getBoundingClientRect()
         });
         whenReady = false;
      }
      else {
         // Make root element visible.
         rl.style.display = '';
      }
   }
   // Call whenReady action if necessary.
   if (whenReady && this.whenReady)
      this.callWhenReady();
};

eq.c.Dlg.commitVisible = function(d, qi) {
   var
      app = eq.app,
      m = eq.LayoutMode,
      dm = eq.DialogMode,
      w = d.w,
      rl = qi.rl,
      makeVisible = true,
      r, rc, l, lm, cl;
   if (w === null) {
      // Window does no longer exist,
      // call whenReady action if necessary.
      if (d.whenReady)
         d.callWhenReady();
      return;
   }
   if ((r = w.root) === undefined)
      throw new Error("Dlg.commitVisible(" + d.id +
         ") BUG: Window root undefined");
   // Dialog size may need to stabilize, could require an additional frame.
   if (rc = qi.rc) {
      var rrc = rl.getBoundingClientRect();
      if (rc.width !== rrc.width || rc.height !== rrc.height) {
         eq.Dom.onNextFrame(d, { cb : eq.c.Dlg.commitVisible, rl : rl });
         return;
      }
   }
   if ((l = d.layout) === undefined) {
      if (rl !== r)
         rl.style.visibility = '';
   }
   else if (   (lm = app.layoutMode) !== undefined
            && (lm & m.inline) && !(lm & m.dialog)) {
      // Inline Dialog, forced by layout mode.
      d.layout = undefined;
      if (rl !== r)
         rl.style.visibility = '';
   }
   else {
      d.layout = undefined;
      if (l.mode !== undefined) {
         if (l.mode === dm.inline) {
            // Inline Dialog.
            if (w.makeInline())
               r = w.root;
         }
         else {
            // Dialog in frame.
            if (w.makeFramed())
               r = w.root;
         }
      }
      if ((cl = r.classList).contains('eq-dialog')) {
         if (rl !== r)
            rl.style.visibility = '';
      }
      else {
         // Dialog in frame.
         w.setTitle(d.title || app.title);
         w.setIcon(d.icon);
         if (l.mode !== undefined)
            if (l.mode === dm.layout) {
               cl.add('eq-resize');
               if (l.state !== undefined)
                  if (l.state & eq.DialogState.maximized)
                     cl.add('eq-max');
                  else
                     cl.remove('eq-max');
            }
            else {
               cl.remove('eq-resize');
               cl.remove('eq-max');
            }
         if (rl.classList.contains('eq-rule'))
            cl.add('eq-close');
         else {
            cl.remove('eq-close');
            d.pendingClose = undefined;
         }
         if (rl !== r)
            rl.style.visibility = '';
         if (l.x !== undefined && l.y !== undefined) {
            w.whenVisible = { cb : eq.c.Dlg.whenVisible, d : d };
            makeVisible = false;
            w.setPos(l.x, l.y);
         }
      }
   }
   if (makeVisible)
      eq.Dom.onNextFrame(d, { cb : eq.c.Dlg.makeVisible });
};

eq.c.Dlg.makeVisible = function(d) {
   var w = d.w, r;
   if (w === null) {
      // Window does no longer exist,
      // call whenReady action if necessary.
      if (d.whenReady)
         d.callWhenReady();
      return;
   }
   if ((r = w.root) === undefined)
      throw new Error("Dlg.makeVisible(" + d.id +
         ") BUG: Window root undefined");
   // Try to avoid flicker by converting base Dialog Window
   // to inline Dialog if necessary before making Window root visible.
   // See also: eq.c.Window.setPos.makeVisible()
   if (!w.makeBaseDialogInline())
      r.style.visibility = '';
   eq.Dom.onNextFrame(w, { cb : eq.c.Dlg.whenVisible, d : d });
};

eq.c.Dlg.whenVisible = function(w, qi) {
   var d = qi.d, app, f, el, ed;
   // Call whenReady action if necessary.
   if (d.whenReady)
      d.callWhenReady();
   // Adjust Window list stack order if necessary.
   eq.c.Window.zOrderModified();
   // Restore focus.
   if ((d = w.dlg) && d === (app = eq.app).currDlg
                   && (f = app.focus) !== null
                   && (el = document.getElementById(f))
                   && el.classList.contains('eq-focused')
                   && (ed = d.eds.get(f)) !== undefined)
      eq.Dom.onNextCycle(eq.c.App.setElementFocus, {
         d : d,
         e : el,
         f : ed.focusElement(d, el)
      });
};

/*------------------------------------------------------------------------
   Dlg.addControl()
   Add DLG control to document from DOM element template(s).
   See: api-dlg.js Dlg.prototype.addChild()
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.addControl = function(et, elParent, rootId) {
   var root;
   if (rootId === undefined && !(rootId = et.id))
      throw new Error("addControl: root id not set");

   while (true) {
      // Create element.
      var
         tg = et.tg,
         el = document.createElement(tg !== undefined ? tg : 'DIV');
      this.updateControl(rootId, el, et);
      if (root === undefined)
         root = el;

      // Add to DOM.
      if (elParent !== undefined)
         elParent.appendChild(el);
      elParent = el;

      // Nested template.
      if (et.et === undefined)
         break;
      et = et.et;
   }

   // Children.
   var ch = et.ch;
   if (ch !== undefined) {
      for (var i = 0, l = ch.length; i < l; i++) {
         var ct = ch[i], id = ct.id;
         if (id === '*')
            ct.id = (id = rootId) + '-' + (i+1);
         this.addControl(ct, elParent, id || rootId);
      }
   }

   return root;
};

/*------------------------------------------------------------------------
   Dlg.modifyControl()
   Modify DLG control from DOM element template(s).
   See: api-dlg.js Dlg.prototype.addChild()
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.modifyControl = function(rootId, el, et) {
   while (true) {
      var tg = et.tg;
      if (tg !== undefined && tg !== el.tagName) {
         // Tag modified, recreate element, retain 'eq-event' if set.
         var en = document.createElement(tg), cl = el.classList;
         if (cl && cl.contains('eq-event'))
            en.classList.add('eq-event');
         el.parentNode.replaceChild(en, el);
         el = en;
      }

      // Update element.
      this.updateControl(rootId, el, et);

      // Nested template?
      if (et.et === undefined)
         break;
      // Yes, use first child which is not 'runtime-created'.
      var cn = el.children;
      el = undefined;
      et = et.et;
      for (var i = 0, l = cn.length; i < l; i++) {
         var ci = cn[i];
         if (!ci.classList.contains('eq-rt')) {
            el = ci;
            break;
         }
      }
      if (el === undefined)
         throw new Error("modifyControl: id '" + rootId +
            "' nested template, no child");
   }

   // Children.
   var ch = et.ch;
   if (ch !== undefined) {
      var cn = el.children;
      for (var i = 0, l = ch.length; i < l; i++) {
         var ci = ch[i];
         if (i >= cn.length) {
            var tg = ci.tg;
            el.appendChild(document.createElement(
               tg !== undefined ? tg : 'DIV'));
         }
         this.modifyControl(rootId, cn[i], ci);
      }
      while (cn.length > l)
         el.removeChild(cn[l]);
   }
};

/*------------------------------------------------------------------------
   Dlg.updateControl()
   Update DLG control from DOM element template.
   See: api-dlg.js Dlg.prototype.addChild()
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.updateControl = function(rootId, el, et) {
   var eds, ed, edRoot;
   for (var a in et)
      switch (a) {
         case 'id': {
            // id
            var id = el.id, nid = et.id;
            if (id !== nid) {
               if (id) {
                  if (   ed === undefined
                      && (ed = (eds = this.eds).get(id)) === undefined)
                     throw new Error("BUG: updateControl id:" + id +
                        " not registered");
                  eds.delete(id);
                  eds.set(nid, ed);
                  ed.updateId(this, nid, id);
               }
               el.id = nid;
            }
            break;
         }

         case 'dcn': {
            // DLG class name.
            var id = el.id, cls;
            if (!id)
               throw new Error("BUG: updateControl '" + et.dcn +
                  "' id undefined");
            if (   ed !== undefined
                || (ed = (eds = this.eds).get(id)) !== undefined)
               throw new Error("BUG: updateControl id:" + id +
                  "already registered as '" + ed.constructor.name + "'");
            if ((cls = eq.d.get(et.dcn)) === undefined)
               cls = eq.c.DlgElement;
            eds.set(id, new cls(this, el));
            break;
         }

         case 'hc':
            // Text or HTML content.
            eq.Dom.setContent(el, et.hc);
            break;

         case 'hca': {
            // Text or HTML content w/ optional accelerator.
            if (   edRoot === undefined
                &&    (edRoot = (eds || (eds = this.eds)).get(rootId))
                   === undefined)
               throw new Error("BUG: updateControl id:" + rootId +
                  " not registered");
            var ac = eq.Dom.setContent(el, et.hca, true);
            edRoot.setAccel(this, rootId, ac, eq.KeyMod.alt);
            break;
         }

         case 'ec':
            // EditText content.
            el.value = et.ec;
            if (el.tagName === 'TEXTAREA')
               el.textContent = et.ec;
            break;

         case 'at': {
            // Attributes.
            for (var at in et.at) {
               var a = et.at[at];
               if (a !== null) {
                  if (at in el)
                     el[at] = a;
                  else
                     el.setAttribute(at, a);
               }
               else
                  el.removeAttribute(at);
            }
            break;
         }

         case 'clr': {
            // Remove class names.
            var cl = et.clr;
            for (var i = 0, l = cl.length; i < l; i++)
               el.classList.remove(cl[i]);
            break;
         }

         case 'cla': {
            // Add class names.
            var cl = et.cla;
            for (var i = 0, l = cl.length; i < l; i++)
               el.classList.add(cl[i]);
            break;
         }

         case 'st': {
            // Styles.
            for (var st in et.st) {
               var s = et.st[st];
               el.style.setProperty(st, s !== null ? s : '');
            }
            break;
         }

         case 'ds': {
            // Data attributes.
            for (var ds in et.ds) {
               var da = et.ds[ds];
               if (da !== null)
                  el.dataset[ds] = da;
               else
                  delete el.dataset[ds];
            }
            break;
         }

         case 'rt':
         case 'rtd': {
            // Value by request tag.
            if (   edRoot === undefined
                &&    (edRoot = (eds || (eds = this.eds)).get(rootId))
                   === undefined)
               throw new Error("BUG: updateControl id:" + rootId +
                  " not registered");
            if (a === 'rt')
               edRoot.setAttr(this, rootId, el, et.rt);
            else
               eq.Dom.onNextCycle(eq.c.DlgElement.setAttr, {
                  t : edRoot,
                  d : this,
                  i : rootId,
                  e : el,
                  r : et.rtd
               });
            break;
         }

         case 'cr': {
            // CSS rules.
            if (   edRoot === undefined
                &&    (edRoot = (eds || (eds = this.eds)).get(rootId))
                   === undefined)
               throw new Error("BUG: updateControl id:" + rootId +
                  " not registered");
            var cr = et.cr, cri;
            for (var i = 0, l = cr.length; i < l; i++) {
               cri = cr[i];
               edRoot.setCssRule(rootId, cri.ty, cri.rl);
            }
            break;
         }

         case 'ev': {
            // Events.
            var ev = et.ev;
            if (   edRoot === undefined
                &&    (edRoot = (eds || (eds = this.eds)).get(rootId))
                   === undefined)
               throw new Error("BUG: updateControl id:" + rootId +
                  " not registered");
            if (ev.a !== undefined)
               edRoot.a = ev.a !== null ? ev.a : undefined;
            if (ev.c !== undefined)
               edRoot.c = ev.c !== null ? ev.c : undefined;
            if (ev.m !== undefined)
               edRoot.m = ev.m !== null ? ev.m : undefined;
            if (ev.f !== undefined)
               edRoot.f = ev.f !== null ? ev.f : undefined;
            if (edRoot.a !== undefined || edRoot.c !== undefined)
               el.classList.add('eq-event');
            else
               el.classList.remove('eq-event');
            break;
         }

         case 'dlg': {
            var d = et.dlg, w;
            if (d.l !== undefined)
               this.layout = d.l ? d.l : undefined;
            if (d.t !== undefined) {
               this.title = d.t ? d.t : undefined;
               if ((w = this.w) && w.root)
                  w.setTitle(this.title || eq.c.App.title);
            }
            if (d.ic !== undefined) {
               this.icon = d.ic ? d.ic : undefined;
               if ((w = this.w) && w.root)
                  w.setIcon(this.icon);
            }
            if (d.f !== undefined)
               this.fkey = d.f ? d.f : undefined;
            if (d.tm !== undefined)
               this.timer = d.tm ? d.tm : undefined;
            if (d.d !== undefined) {
               if (d.d)
                  this.ddo = d.d;
               else if (this.ddo) {
                  this.ddo = undefined;
                  this.resetCurrDlg();
               }
            }
            break;
         }
      }
};

/*------------------------------------------------------------------------
   Dlg.calcGrid()
   Calculate layout grid.
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.calcGrid = function(fn) {
   var
      cls = this.constructor,
      grid = this.grid,
      body = document.body,
      w, h;
   for (var pass = 0; pass < 2; pass++) {
      var
         gt = cls.getGridTemplate(),
         s = gt.style,
         c = gt.firstElementChild,
         fs = eq.FontStyle,
         ffc, fsz, fst, r;
      if (ffc = fn.fc)
         s.fontFamily = ffc;
      if (fsz = fn.sz)
         s.fontSize = fsz;
      if ((fst = fn.st) & fs.bold)
         s.fontWeight = 'bold';
      if (fst & fs.italic)
         s.fontStyle = 'italic';
      if (pass === 0) {
         // Width/height reference 1ch/1em.
         (s = c.style).width = '1ch';
         s.height = '1em';
         body.appendChild(gt);
         r = c.getBoundingClientRect();
         grid.wr = w = r.width;
         grid.hr = h = r.height;
      }
      else {
         // Letter 'X' width/height.
         c.textContent = 'X';
         body.appendChild(gt);
         r = c.getBoundingClientRect();
         grid.wf = (grid.wp = Math.ceil(r.width)) / w;
         grid.hf = (grid.hp = Math.ceil(r.height)) / h;
      }
      body.removeChild(gt);
   }
};

eq.c.Dlg.getGridTemplate = function() {
   var gt = this.gridTemplate;
   if (gt === undefined) {
      this.gridTemplate = gt = document.createElement('DIV');
      gt.className = 'eq-grid';
      gt.style.zIndex = eq.Dom.zIndex(document.body) - 1;
      gt.appendChild(document.createElement('P'));
   }
   return gt.cloneNode(true);
};

/*------------------------------------------------------------------------
   Dlg.copyFontToElement()
   Copy Dialog font to element.
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.copyFontToElement = function(el) {
   var dm = eq.Dom, rl, cl, fn;
   for (var i = this.root.length; --i >= 0;)
      if (   (cl = (rl = this.root[i]).classList).contains('eq-dialog')
          && cl.contains('eq-root-font')) {
         dm.copyFont(el, rl);
         dm.adjustElementFontSize(el);
         return true;
      }
   if ((fn = eq.app.fnDefault) && dm.setFont(el, fn)) {
      dm.adjustElementFontSize(el);
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   Dlg.nextTab()
   Locate next/previous element in tab order.
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.nextTab = function(id, toPrevious) {
   var tb = this.tabOrder, l;
   if (tb !== null && (l = tb.length)) {
      var
         i = tb.indexOf(id),
         j = i === -1 ? 0 : (toPrevious ? i - 1 : i + 1),
         el;
      while (true) {
         if (toPrevious) {
            if (j === -1)
               j = l - 1;
         }
         else {
            if (j === l)
               j = 0;
         }
         if (j === i) {
            // No other element found in tab order.
            break;
         }
         if (i === -1)
            i = 0;

         el = document.getElementById(id = tb[j]);
         if (el && !el.classList.contains('eq-notab')) {
            // Found next/previous element in tab order.
            return el;
         }

         if (toPrevious)
            j--;
         else
            j++;
      }
   }
   return null;
};

/*------------------------------------------------------------------------
   Dlg.isAccel()
   Check for and handle accelerator key.
------------------------------------------------------------------------*/

eq.c.Dlg.prototype.isAccel = function(key, kmod, e) {
   if ((key = eq.Dom.decodeAccelKey(key, kmod, e)) === null)
      return false;
   var id = this.accel.get(
      kmod ? String.fromCharCode(kmod+48) + key.toUpperCase()
           : key.toUpperCase());
   if (id !== undefined) {
      var ed = this.eds.get(id), el;
      if (   ed !== undefined
          && (el = document.getElementById(id))
          && !el.classList.contains('eq-disabled')) {
         eq.app.cancelUserAction();
         ed.onAccel(this, id, el);
         return true;
      }
   }
   return false;
};

/*------------------------------------------------------------------------
   Dlg drag&drop event handlers.
------------------------------------------------------------------------*/

eq.c.Dlg.dndType = 'application/com.eloquence.webdlg2';

eq.c.Dlg.onDragStart = function(e) {
   var dm = eq.Dom, app, dlg, el, id, ed, dnd;
   if (   !(app = eq.app).idle
       && (dlg = app.currDlg)) {
      app.pendingMouseDown = undefined;
      app.pendingClick = undefined;
      app.stopPendingClickTimer();
      if (this.constructor.name === 'Window') {
         // Special case: HtmlView.
         if (!(el = document.getElementById(id = this.name)))
            id = undefined;
      }
      else if (el = dm.eqControl(this))
         id = el.id;
      if (id && !el.classList.contains('eq-disabled')) {
         if (!(ed = dlg.eds.get(id)))
            throw new Error("Dlg.onDragStart BUG: " + id + " not registered");
         dnd = { serial : ++app.dndSerial, ch : [] };
         if (ed.onDragStart(dlg, id, el, this, e, dnd)) {
            if (dnd.drag) {
               // Dialog-local drag.
               e.dataTransfer.setData(dlg.constructor.dndType,
                                      dnd.serial.toString());
               app.dnd = dnd;
            }
            return;
         }
      }
   }
   e.dataTransfer.clearData();
   dm.consumeEvent(e);
};

eq.c.Dlg.onDragOver = function(e) {
   e.preventDefault();
   var app = eq.app, dnd, drag, dlg, t, dm, el, id, ed;
   if ((dnd = app.dnd) && (drag = dnd.drag)) {
      if (app.idle || !(dlg = app.currDlg)) {
         if (el = drag.over) {
            el.classList.remove('eq-dragover');
            drag.over = null;
         }
         return;
      }
      if (   (el = (dm = eq.Dom).eqControl(t = e.target))
          && !el.classList.contains('eq-disabled')
          && (ed = dlg.eds.get(id = el.id))) {
         t = ed.onDragOver(dlg, id, el, t);
         if (el = drag.over)
            el.classList.remove('eq-dragover');
         if (t)
            t.classList.add('eq-dragover');
         drag.over = t;
      }
   }
};

eq.c.Dlg.onDrop = function(e) {
   var
      dm = eq.Dom,
      app = eq.app,
      dlg, dt, da, fl, fi, dnd, drop, i, l;
   dm.consumeEvent(e);
   if (!app.idle && (dlg = app.currDlg)) {
      if ((da = (dt = e.dataTransfer).getData(eq.c.Dlg.dndType)).length) {
         // Dialog-local drop.
         if (!(dnd = app.dnd) || dnd.serial !== Number(da)) {
            // Invalid,
            app.dnd = null;
            return;
         }
         dnd.drop = drop = {};
      }
      else if ((da = dt.getData('text/plain')).length) {
         // Global drop, plain text.
         drop = { content : da };
         dnd = { ch : [], drop : drop };
      }
      else if ((fl = dt.files) && (l = fl.length)) {
         // Global drop, file list.
         da = '';
         for (i = 0; i < l; i++)
            da += fl.item(i).name + '\n';
         drop = { content : da };
         dnd = { ch : [], drop : drop };
      }
      else {
         // Improper data format.
         app.dnd = null;
         return;
      }
      switch (dt.dropEffect) {
         case 'move':
            drop.action = 1;
            break;
         case 'copy':
            drop.action = 2;
            break;
         case 'link':
            drop.action = 4;
            break;
      }
      var
         rl = 0,
         t = e.target,
         el, cl, dl, id, ed, ch;
      if (this.constructor.name === 'Window') {
         // Special case: HtmlView.
         el = document.getElementById(id = this.name);
      }
      else
         el = t;
      while (el = dm.eqControl(el)) {
         if ((cl = el.classList).contains('eq-disabled')) {
            el = null;
            break;
         }
         if (el.classList.contains('eq-dialog'))
            dl = el;
         if (!(ed = dlg.eds.get(id = el.id)))
            throw new Error("Dlg.onDrop BUG: " + id + " not registered");
         if ((rl = ed.onDrop(dlg, id, el, t, dnd)) || dl)
            break;
         el = el.parentNode;
      }
      if (el && rl) {
         // Obtained drop rule, notify Dialog if not already done.
         if (!dl) {
            if (!(dl = dm.eqDialog(el)))
               throw new Error("Dlg.onDrop BUG: " + id + " no Dialog");
            if (!(ed = dlg.eds.get(dl.id)))
               throw new Error("Dlg.onDrop BUG: Dialog " + dl.id +
                  " not registered");
            ed.onDrop(dlg, dl.id, dl, dl, dnd);
         }
         // Set 'dnd' element changes.
         for (i = 0, l = (ch = dnd.ch).length; i < l; i++)
            app.setChanged(ch[i], 'dnd');
         // Issue rule.
         app.onceRule = rl;
         app.onceId = id;
         app.submit(el, { id : id, type : 'dnd' });
         return;
      }
   }
   app.dnd = null;
};

/** @preserve $Id: 10-login.js,v 28.7 2022/11/23 10:11:28 mike Exp $
*//*======================================================================
   final class LoginDialog extends Window
   Login dialog class.
========================================================================*/

eq.c.Window.subClass('LoginDialog', function(login, save) {
   var
      cls = this.constructor,
      base = eq.c.Window,
      str = eq.strings,
      app = eq.app,
      dm = eq.Dom,
      accel, ld, n, nl, el, lb, sp, bt, r, fn, w, wMax, pos, ppos;

   if (cls.singleton)
      throw new Error("BUG: login dialog singleton exists");
   cls.singleton = this;

   this.accel = new Map();

   if (ld = document.getElementById('eq-login-dialog')) {
      ld = ld.cloneNode(true);
      ld.removeAttribute('id');
      ld.classList.add('eq-login-dialog');
      ld.style.display = '';
   }
   else {
      ld = document.createElement('DIV');
      ld.className = 'eq-login-dialog';

      el = document.createElement('DIV');
      dm.setContent(el, str.get('login.prompt'));
      ld.appendChild(el);

      el = document.createElement('DIV');
      el.className = 'eq-title';
      ld.appendChild(el);

      lb = document.createElement('LABEL');
      sp = document.createElement('SPAN');
      sp.className = 'eq-label';
      accel = dm.setContent(sp, str.get('login.label.name'), true);
      lb.appendChild(sp);
      el = document.createElement('INPUT');
      el.type = 'text';
      el.className = 'eq-login';
      lb.appendChild(el);
      ld.appendChild(lb);
      if (accel !== null)
         this.accel.set(accel.toUpperCase(), el);

      lb = document.createElement('LABEL');
      sp = document.createElement('SPAN');
      sp.className = 'eq-label';
      accel = dm.setContent(sp, str.get('login.label.pass'), true);
      lb.appendChild(sp);
      el = document.createElement('INPUT');
      el.type = 'password';
      el.className = 'eq-password';
      lb.appendChild(el);
      ld.appendChild(lb);
      if (accel !== null)
         this.accel.set(accel.toUpperCase(), el);

      bt = document.createElement('DIV');
      bt.className = 'eq-button';

      if (save !== undefined) {
         lb = document.createElement('LABEL');
         lb.className = 'eq-save';
         el = document.createElement('INPUT');
         el.type = 'checkbox';
         lb.appendChild(el);
         sp = document.createElement('SPAN');
         accel = dm.setContent(sp, str.get('login.save'), true);
         lb.appendChild(sp);
         bt.appendChild(lb);
         if (accel !== null)
            this.accel.set(accel.toUpperCase(), el);
      }
      else
         bt.appendChild(document.createElement('DIV'));

      el = document.createElement('BUTTON');
      el.type = 'button';
      el.className = 'eq-ok';
      accel = dm.setContent(el, str.get('login.ok'), true);
      bt.appendChild(el);
      if (accel !== null)
         this.accel.set(accel.toUpperCase(), el);

      el = document.createElement('BUTTON');
      el.type = 'button';
      el.className = 'eq-cancel';
      accel = dm.setContent(el, str.get('login.cancel'), true);
      bt.appendChild(el);
      if (accel !== null)
         this.accel.set(accel.toUpperCase(), el);

      ld.appendChild(bt);
   }

   // Finalize dialog elements.

   n = ld.getElementsByClassName('eq-title');
   for (var i = 0, l = n.length; i < l; i++)
      dm.setContent(n[i], app.title);

   n = ld.getElementsByClassName('eq-login');
   if (n.length !== 1)
      throw new Error("LoginDialog: " +
         n.length + " Login fields, expected: 1");
   (el = this.elLogin = n[0]).oninput = cls.onInput;
   if (login)
      el.value = login;
   else
      el.classList.add('eq-focused');

   n = ld.getElementsByClassName('eq-password');
   if (n.length !== 1)
      throw new Error("LoginDialog: " +
         n.length + " Password fields, expected: 1");
   (el = this.elPswd = n[0]).oninput = cls.onInput;
   if (login)
      el.classList.add('eq-focused');

   n = ld.getElementsByClassName('eq-ok');
   if (n.length !== 1)
      throw new Error("LoginDialog: " +
         n.length + " OK buttons, expected: 1");
   (el = this.elOk = n[0]).onclick = cls.onOk;
   el.disabled = true;

   nl = ld.querySelectorAll('.eq-save input');
   if (nl.length) {
      if (nl.length > 1)
         throw new Error("LoginDialog: " +
            nl.length + " Save buttons, expected: 1");
      (el = this.elSave = nl[0]).checked = !!save;
   }

   n = ld.getElementsByClassName('eq-cancel');
   if (n.length) {
      if (n.length > 1)
         throw new Error("LoginDialog: " +
            n.length + " Cancel buttons, expected: 1");
      (el = this.elCancel = n[0]).onclick = cls.onCancel;
   }

   ld.onfocus = cls.onFocus;

   // Create Window.
   base.call(this, base.setupFrame(ld));
   (r = this.root).classList.add('eq-close');
   this.notifyOnClose = true;

   // Set default font.
   if ((fn = app.fnDefault) && dm.setFont(r, fn))
      dm.adjustElementFontSize(r);

   this.whenVisible = { cb : cls.setFocus };
   this.setTitle(app.title);
   this.setIcon(app.logoIcon);

   // Make labels equal width.
   wMax = 0;
   n = ld.getElementsByClassName('eq-label');
   for (var i = 0, l = n.length; i < l; i++) {
      w = Number(window.getComputedStyle(n[i]).width.replace(/px$/,''));
      if (w && w > wMax)
         wMax = w;
   }
   if (wMax)
      for (var i = 0, l = n.length; i < l; i++)
         n[i].style.width = wMax + 'px';

   if (   (pos = app.popupPos) !== undefined
       && (pos & (ppos = eq.PopupPos).login)) {
      this.setPos(pos & ppos.left ? 0 : -1,
                  pos & ppos.top ? 0 : -1);
   }
   else
      this.setPos(-1, -1);

   this.activate();
   window.addEventListener('keydown', cls.onKey, true);
});

delete eq.c.LoginDialog.subClass; // final

/*------------------------------------------------------------------------
   static LoginDialog.singleton
   Global login dialog instance.
------------------------------------------------------------------------*/

eq.c.LoginDialog.singleton = null;

/*------------------------------------------------------------------------
   LoginDialog.close()
   Close login dialog, submit event.
------------------------------------------------------------------------*/

eq.c.LoginDialog.prototype.close = function() {
   var cls = this.constructor, base = eq.c.Window;
   if (this !== cls.singleton)
      throw new Error("BUG: unexpected login dialog invocation");
   window.removeEventListener('keydown', cls.onKey, true);
   this.accel.clear();
   this.accel = null;
   if (this.notifyOnClose) {
      // Notify API thread, login dialog canceled.
      eq.app.api.postMessage({ o : eq.ApiOp.onLoginEvent });
   }
   base.prototype.close.call(this);
   cls.singleton = null;
};

eq.c.LoginDialog.close = function() {
   if (this.singleton)
      this.singleton.close();
};

/*------------------------------------------------------------------------
   LoginDialog focus management
------------------------------------------------------------------------*/

eq.c.LoginDialog.setFocus = function(e) {
   var cls = eq.c.LoginDialog, self = cls.singleton;
   if (self) {
      if (!cls.onFocus(e))
         eq.Dom.onNextCycle(cls.commitFocus);
      return true;
   }
   return false;
};

eq.c.LoginDialog.commitFocus = function() {
   var self = eq.c.LoginDialog.singleton, f;
   if (self && (f = self.root.querySelector('.eq-focused')))
      f.focus();
};

eq.c.LoginDialog.prototype.setFocusElement = function(el) {
   var n = this.root.querySelectorAll('.eq-focused');
   for (var i = n.length; --i >= 0;)
      n[i].classList.remove('eq-focused');
   el.classList.add('eq-focused');
   el.focus();
};

eq.c.LoginDialog.prototype.focusable = function() {
   var n = [ this.elLogin, this.elPswd ];
   if (this.elSave)
      n.push(this.elSave);
   n.push(this.elOk);
   if (this.elCancel)
      n.push(this.elCancel);
   return n;
};

eq.c.LoginDialog.onFocus = function(e) {
   var self = eq.c.LoginDialog.singleton, t;
   if (e && (t = e.target)) {
      var n = self.focusable(), el;
      for (var i = n.length; --i >= 0;)
         if ((el = n[i]) === e.target) {
            if (!el.classList.contains('eq-focused'))
               self.setFocusElement(el);
            return true;
         }
   }
   return false;
};

/*------------------------------------------------------------------------
   LoginDialog event handlers
------------------------------------------------------------------------*/

eq.c.LoginDialog.onOk = function(e) {
   var
      self = eq.c.LoginDialog.singleton,
      login = self.elLogin.value,
      pswd = self.elPswd.value;
   if (login && pswd) {
      // Notify API thread, login dialog confirmed.
      eq.app.api.postMessage({
         o : eq.ApiOp.onLoginEvent,
         l : login,
         p : pswd,
         s : self.elSave ? self.elSave.checked : true
      });
      self.notifyOnClose = false;
      self.close();
   }
};

eq.c.LoginDialog.onCancel = function(e) {
   eq.c.LoginDialog.singleton.close();
};

eq.c.LoginDialog.onKey = function(e) {
   var
      self = eq.c.LoginDialog.singleton,
      key = e.key,
      dm = eq.Dom,
      el, k;
   if (   key.length === 1 && e.altKey
       && (k = dm.decodeAccelKey(key, eq.KeyMod.alt, e)) !== null
       && (el = self.accel.get(k.toUpperCase()))) {
      if (   el.tagName === 'INPUT'
          && (el.type === 'text' || el.type === 'password'))
         el.focus();
      else
         dm.fireClick(el);
      dm.consumeEvent(e);
      return;
   }
   var
      n = self.focusable(),
      ni;
   el = document.activeElement;
   while (el && el !== document && el !== self.root) {
      if ((ni = n.indexOf(el)) !== -1)
         break;
      el = el.parentNode;
   }
   if (ni !== -1) {
      switch (key) {
         case 'Escape':
         case 'Esc': // IE
            self.close();
            dm.consumeEvent(e);
            break;
         case ' ':
         case 'Spacebar': // IE
            switch (el) {
               case self.elOk:
               case self.elCancel:
               case self.elSave:
                  dm.fireClick(el);
                  dm.consumeEvent(e);
                  return;
            }
            break;
         case 'Enter':
            switch (el) {
               case self.elOk:
               case self.elCancel:
               case self.elSave:
                  dm.fireClick(el);
                  dm.consumeEvent(e);
                  return;
               case self.elPswd: {
                  var
                     login = self.elLogin.value,
                     pswd = self.elPswd.value;
                  if (login && pswd) {
                     // Notify API thread, login dialog confirmed.
                     eq.app.api.postMessage({
                        o : eq.ApiOp.onLoginEvent,
                        l : login,
                        p : pswd,
                        s : self.elSave ? self.elSave.checked : true
                     });
                     self.notifyOnClose = false;
                     self.close();
                     dm.consumeEvent(e);
                     return;
                  }
               }
            }
            // FALLTHROUGH
         case 'Tab': {
            do {
               if (e.shiftKey) {
                  if (--ni < 0)
                     ni = n.length - 1;
               }
               else if (++ni == n.length)
                  ni = 0;
            } while ((el = n[ni]).disabled);
            self.setFocusElement(el);
            dm.consumeEvent(e);
         }
      }
   }
};

eq.c.LoginDialog.onInput = function(e) {
   var
      self = eq.c.LoginDialog.singleton,
      login = self.elLogin.value,
      pswd = self.elPswd.value;
   self.elOk.disabled = !(login && pswd);
};

/** @preserve $Id: 10-message.js,v 28.10 2022/11/23 10:11:28 mike Exp $
*//*======================================================================
   final class MessageDialog extends Window
   Message dialog class.
========================================================================*/

eq.c.Window.subClass('MessageDialog', function(msg, timeout, onCancel) {
   var
      cls = this.constructor,
      base = eq.c.Window,
      app = eq.app,
      dm = eq.Dom,
      accel, md, el, ep, r, dlg, fn, pos, ppos;

   if (cls.singleton)
      cls.singleton.close();
   cls.singleton = this;

   this.accel = new Map();

   md = document.createElement('DIV');
   md.className = 'eq-message-dialog';

   el = document.createElement('SPAN');
   dm.setContent(el, msg);
   md.appendChild(el);

   ep = document.createElement('DIV');
   el = document.createElement('BUTTON');
   el.className = 'eq-focused';
   el.type = 'button';
   if (onCancel) {
      this.onCancel = onCancel;
      accel = dm.setContent(el, eq.strings.get('message.cancel'), true);
   }
   else
      accel = dm.setContent(el, eq.strings.get('message.ok'), true);
   if (accel !== null)
      this.accel.set(accel.toUpperCase(), el);
   el.onclick = cls.close;
   ep.appendChild(el);
   md.appendChild(ep);

   // Create Window.
   base.call(this, base.setupFrame(md));
   (r = this.root).classList.add('eq-close');

   // Derive font from top Dialog or set default font.
   if (!(dlg = app.topDialog()) || !dlg.copyFontToElement(r))
      if ((fn = app.fnDefault) && dm.setFont(r, fn))
         dm.adjustElementFontSize(r);

   this.whenVisible = { cb : cls.setFocus };
   this.setTitle(app.title);
   this.setIcon(app.logoIcon);

   if (   (pos = app.popupPos) !== undefined
       && (pos & (ppos = eq.PopupPos).message)) {
      this.setPos(pos & ppos.left ? 0 : -1,
                  pos & ppos.top ? 0 : -1);
   }
   else
      this.setPos(-1, -1);

   this.activate();
   window.addEventListener('keydown', cls.onKey, true);
   if (timeout)
      this.closeTimeout = window.setTimeout(cls.close, timeout);
});

delete eq.c.MessageDialog.subClass; // final

eq.c.MessageDialog.setFocus = function() {
   var self = eq.c.MessageDialog.singleton, f;
   if (self && (f = self.root.querySelector('.eq-focused'))) {
      f.focus();
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   static MessageDialog.singleton
   Global message dialog instance.
------------------------------------------------------------------------*/

eq.c.MessageDialog.singleton = null;

/*------------------------------------------------------------------------
   MessageDialog.close()
   Close message dialog.
------------------------------------------------------------------------*/

eq.c.MessageDialog.prototype.close = function() {
   var cls = this.constructor, base = eq.c.Window;
   if (this !== cls.singleton)
      throw new Error("BUG: unexpected message dialog invocation");
   if (this.closeTimeout !== undefined) {
      window.clearTimeout(this.closeTimeout);
      this.closeTimeout = undefined;
   }
   window.removeEventListener('keydown', cls.onKey, true);
   this.accel.clear();
   this.accel = null;
   base.prototype.close.call(this);
   if (this.onCancel)
      this.onCancel();
   cls.singleton = null;
};

eq.c.MessageDialog.close = function() {
   var cls = eq.c.MessageDialog;
   if (cls.singleton)
      cls.singleton.close();
};

/*------------------------------------------------------------------------
   static MessageDialog.onKey()
   Message dialog key event handler.
------------------------------------------------------------------------*/

eq.c.MessageDialog.onKey = function(e) {
   var
      self = eq.c.MessageDialog.singleton,
      key = e.key,
      dm = eq.Dom,
      el, k;
   if (   key.length === 1 && e.altKey
       && (k = dm.decodeAccelKey(key, eq.KeyMod.alt, e)) !== null
       && (el = self.accel.get(k.toUpperCase()))) {
      dm.fireClick(el);
      dm.consumeEvent(e);
      return;
   }
   switch (key) {
      case 'Escape':
      case 'Esc': // IE
      case ' ':
      case 'Spacebar': // IE
      case 'Enter':
         eq.c.MessageDialog.singleton.close();
         dm.consumeEvent(e);
   }
};

/** @preserve $Id: 10-popup.js,v 28.31 2022/11/23 10:11:28 mike Exp $
*//*======================================================================
   final class PopupBox extends Window
   POPUP BOX class.
========================================================================*/

eq.c.Window.subClass('PopupBox', function(pb) {
   var
      cls = this.constructor,
      base = eq.c.Window,
      dm = eq.Dom,
      app = eq.app,
      pp, el, msg, btn, btnDefault, btnText, s, ac, r, dlg, fn, pos, ppos;

   if (cls.singleton)
      throw new Error("BUG: POPUP BOX singleton exists");
   cls.singleton = this;

   this.accel = new Map();

   pp = document.createElement('DIV'),
   pp.className = 'eq-popup-box';

   // Message.
   msg = pb.msg.split("\n");
   for (var i = 0, l = msg.length; i < l; i++) {
      el = document.createElement('P');
      dm.setContent(el, msg[i]);
      pp.appendChild(el);
   }

   // Buttons.
   btn = document.createElement('DIV');
   btn.className = 'eq-pushbutton';
   btnDefault = (btnDefault = pb.btnDefault) > 0 ? btnDefault - 1 : 0;
   for (var i = 0, l = pb.btn.length; i < l; i++) {
      el = document.createElement('BUTTON');
      el.type = 'button';
      if (   dm.isHtmlContent(btnText = pb.btn[i])
          || btnText.indexOf('&') !== -1) {
         if (   (ac = dm.setContent(el, btnText, true)) !== null
             && !this.accel.has((ac = ac.toUpperCase())))
            this.accel.set(ac, el);
      }
      else {
         for (var bi = 0, bl = btnText.length; bi < bl; bi++) {
            if (bi)
               s = btnText.substring(0, bi) + '&' + btnText.substring(bi);
            else
               s = '&' + btnText.substring(bi);
            if ((ac = dm.setContent(el, s, true)) === null)
               break;
            if (!this.accel.has((ac = ac.toUpperCase()))) {
               this.accel.set(ac, el);
               break;
            }
         }
         s = '&' + s;
      }
      if (i === btnDefault)
         el.classList.add('eq-focused');
      btn.appendChild(el);
   }
   pp.appendChild(btn);

   // Create Window.
   base.call(this, base.setupFrame(pp));
   r = this.root;

   // Derive font from top Dialog or set default font.
   if (!(dlg = app.topDialog()) || !dlg.copyFontToElement(r))
      if ((fn = app.fnDefault) && dm.setFont(r, fn))
         dm.adjustElementFontSize(r);

   this.whenVisible = { cb : cls.setFocus };
   this.setTitle(pb.title || app.title);
   this.setIcon(app.logoIcon);

   if (   (pos = app.popupPos) !== undefined
       && (pos & (ppos = eq.PopupPos).box)) {
      this.setPos(pos & ppos.left ? 0 : -1,
                  pos & ppos.top ? 0 : -1);
   }
   else
      this.setPos(-1, -1);

   this.activate();
});

delete eq.c.PopupBox.subClass; // final

eq.c.PopupBox.setFocus = function() {
   var self = eq.c.PopupBox.singleton, f;
   if (self && (f = self.root.querySelector('.eq-focused'))) {
      f.focus();
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   static PopupBox.singleton
   Global POPUP BOX instance.
------------------------------------------------------------------------*/

eq.c.PopupBox.singleton = null;

/*------------------------------------------------------------------------
   PopupBox.close()
   Close POPUP BOX, submit event.
------------------------------------------------------------------------*/

eq.c.PopupBox.prototype.close = function(e) {
   var cls = this.constructor, base = eq.c.Window;
   if (this !== cls.singleton)
      throw new Error("BUG: unexpected POPUP BOX invocation");
   if (e !== undefined) {
      var t = e.target;
      if (   base.fromTarget(t) === this
          && this.onMouseDownOrClick(e)) {
         e.stopPropagation();
         return false;
      }

      if (e.type !== 'click')
         return false;

      var i = this.buttonIndex(t);
      if (i === -1) {
         // Not a button.
         return false;
      }

      // Notify API thread.
      eq.app.api.postMessage({
         o : eq.ApiOp.onPopupEvent,
         d : i + 1
      });

      e.stopPropagation();
   }

   this.accel.clear();
   this.accel = null;
   base.prototype.close.call(this);
   cls.singleton = null;
   return true;
};

/*------------------------------------------------------------------------
   PopupBox.onKey()
   POPUP BOX keyboard event handler.
------------------------------------------------------------------------*/

eq.c.PopupBox.prototype.onKey = function(e) {
   var
      cls = this.constructor,
      key = e.key,
      dm = eq.Dom,
      k, el, bi;
   if (this !== cls.singleton)
      throw new Error("BUG: unexpected POPUP BOX invocation");

   if (   key.length === 1 
       && (k = dm.decodeAccelKey(key, e.ctrlKey || e.altKey || e.metaKey
                                    ? eq.KeyMod.alt : 0, e)) !== null
       && (el = this.accel.get(key.toUpperCase()))) {
      dm.fireClick(el);
      return true; // Consume event.
   }

   if ((bi = this.buttonIndex(document.activeElement)) !== -1) {
      var
         b = this.root.querySelector('.eq-popup-box>.eq-pushbutton').children,
         i, l;
      switch (key) {
         case 'Enter':
         case ' ':
         case 'Spacebar': // IE
            dm.fireClick(b[bi]);
            return true; // Consume event.
         case 'Tab':
            i = e.shiftKey ? -1 : 1;
            break;
         case 'Home':
            bi = i = 0;
            break;            
         case 'End':
            bi = 0;
            i = -1;
            break;            
         case 'PageUp':
         case 'ArrowUp':
         case 'Up':
         case 'ArrowLeft':
         case 'Left':
            i = -1;
            break;
         case 'PageDown':
         case 'ArrowDown':
         case 'Down':
         case 'ArrowRight':
         case 'Right':
            i = 1;
      }

      if (i !== undefined) {
         l = b.length;
         if ((bi += i) < 0)
            bi = l - 1;
         else if (bi >= l)
            bi = 0;
         for (i = 0; i < l; i++)
            b[i].classList.remove('eq-focused');
         (el = b[bi]).classList.add('eq-focused');
         el.focus();
         return true; // Consume event.
      }
   }

   return false;
};

/*------------------------------------------------------------------------
   PopupBox.isEvent()
   Check if event applies to POPUP BOX.
------------------------------------------------------------------------*/

eq.c.PopupBox.prototype.isEvent = function(e) {
   var t = e.target, r = this.root;
   while (t && t !== document && t !== r)
      t = t.parentNode;
   return t === r;
};

/*------------------------------------------------------------------------
   PopupBox.buttonIndex()
   Get POPUP BOX button index of specified target.
------------------------------------------------------------------------*/

eq.c.PopupBox.prototype.buttonIndex = function(t) {
   if (t) {
      var p;
      while ((p = t.parentNode) && p !== document && p !== this.root) {
         var cl = p.classList;
         if (cl && cl.contains('eq-pushbutton')) {
            var b = this.root.querySelector(
               '.eq-popup-box>.eq-pushbutton').children;
            for (var i = 0, l = b.length; i < l; i++)
               if (t === b[i])
                  return i;
            break;
         }
         t = p;
      }
   }
   return -1;
};

/** @preserve $Id: 10-strings.js,v 28.4 2022/10/07 12:44:58 mike Exp $
*//*======================================================================
   Obtain string from configuration.
========================================================================*/

eq.strings.get = function(key) {
   var
      p = key.split('.'),
      m = this.m,
      l, s, r;
   if (m !== undefined) {
      if ((l = eq.app.locale.tag) && (s = m.get(l)))
         if (r = this.fromPath(s, p))
            return r;
      if ((s = m.get('default')))
         if (r = this.fromPath(s, p))
            return r;
   }
   return this.fromPath(this.default, p);
};

eq.strings.fromPath = function(s, p) {
   for (var i = 0, l = p.length; i < l; i++)
      if (!(s = s[p[i]]))
         break;
   return s;
};

/*========================================================================
   Default strings.
========================================================================*/

eq.strings.default = {
   login : {
      prompt : "Please enter your login and password to start:",
      label  : {
         name : "&Login:",
         pass : "&Password:"
      },
      ok     : "L&ogin",
      cancel : "&Cancel",
      save   : "&Save login"
   },
   message : {
      ok        : "&Dismiss",
      cancel    : "&Cancel",
      dlgListen : "<html>Waiting for DLG connection on port"
   }
};

/** @preserve $Id: 20-checkbox.js,v 28.9 2022/10/19 13:06:30 mike Exp $
*//*======================================================================
   final class CheckBox extends DlgElement
   DLG CheckBox element class.
========================================================================*/

eq.c.DlgElement.addClass('CheckBox', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
});

delete eq.c.CheckBox.subClass; // final

/*------------------------------------------------------------------------
   CheckBox.setSensitive()
   Set element-specific 'sensitive' properties.
------------------------------------------------------------------------*/

eq.c.CheckBox.prototype.setSensitive = function(dlg, id, el, sensitive) {
   eq.Dom.inputElement(el).disabled = !sensitive;
   eq.c.DlgElement.prototype.setSensitive.call(this, dlg, id, el, sensitive);
};

/*------------------------------------------------------------------------
   CheckBox.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.CheckBox.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   var dm = eq.Dom, kc = dm.isEnterOrSpace(key);
   if (kc && (kc !== 10 || el.classList.contains('eq-noenter'))) {
      // SPACE key, or ENTER key and .tabonenter disabled.
      dm.fireClick(dm.inputElement(el), { key : kc });
      dm.consumeEvent(e);
      return true;
   }

   return false;
};

/*------------------------------------------------------------------------
   CheckBox.onTypeAhead()
   Process type-ahead key stroke.
------------------------------------------------------------------------*/

eq.c.CheckBox.prototype.onTypeAhead = function(dlg, id, el, key) {
   var kc;
   switch (key) {
      case 'Enter':
      case 'ShiftEnter':
         if (el.classList.contains('eq-noenter')) {
            // .tabonenter disabled.
            kc = 10;
         }
         break;
      case ' ':
         kc = 32;
         break;
   }

   if (kc !== undefined) {
      var dm = eq.Dom;
      dm.fireClick(dm.inputElement(el), { key : kc });
      return true;
   }

   return eq.c.DlgElement.prototype.onTypeAhead.call(this, dlg, id, el, key);
};

/*------------------------------------------------------------------------
   CheckBox.onSubmit()
   Prepare submission, enqueue change if not already in queue.
------------------------------------------------------------------------*/

eq.c.CheckBox.prototype.onSubmit = function(dlg, id, el, ev) {
   var app = eq.app;
   if (!app.changed.has(id))
      app.changed.set(id, 'input');
};

/*------------------------------------------------------------------------
   CheckBox.focusElement()
   Obtain focus element.
------------------------------------------------------------------------*/

eq.c.CheckBox.prototype.focusElement = function(dlg, el) {
   return eq.Dom.inputElement(el);
};

/*------------------------------------------------------------------------
   CheckBox.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.CheckBox.prototype.changed = function(dlg, id, el, ty) {
   var ch = {
      id : id,
      ty : eq.RsValType.buttonChecked,
      iv : el.checked ? 1 : 0
   };
   return ch;
};

/** @preserve $Id: 20-combobox.js,v 28.26 2025/04/02 13:55:48 mike Exp $
*//*======================================================================
   final class ComboBox extends DlgElement
   DLG ComboBox element class.
========================================================================*/

eq.c.DlgElement.addClass('ComboBox', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.tc = null;
   this.dnd = null;
   this.dndMode = 0;
   this.dropRule = 0;
   this.globalDropRule = 0;
});

delete eq.c.ComboBox.subClass; // final

/*------------------------------------------------------------------------
   ComboBox.dispose()
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.dispose = function(dlg, id) {
   this.tc = null;
   this.dnd = null;
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   static ComboBox.line()
   Obtain ComboBox line from item list.
------------------------------------------------------------------------*/

eq.c.ComboBox.line = function(t, co) {
   var c, cl;
   while (t) {
      if (!(c = t.parentNode))
         break;
      if (cl = c.classList) {
         if (cl.contains('eq-column')) {
            if (co !== undefined)
               co.c = c;
            return t;
         }
         if (cl.contains('eq-combobox')) {
            if (co !== undefined)
               co.ct = c;
            break;
         }
      }
      t = t.parentNode;
   }
   return undefined;
};

/*------------------------------------------------------------------------
   static ComboBox.select()
   Select ComboBox line in item list.
------------------------------------------------------------------------*/

eq.c.ComboBox.select = function(el, c, ci) {
   if (c === undefined)
      if (!(c = el.querySelector('.eq-combobox>.eq-column')))
         c = document.body.querySelector('body>.eq-combobox.eq-column');
   // Deselect.
   var n = c.querySelectorAll('.eq-column>.eq-selected');
   for (var i = 0, l = n.length; i < l; i++)
      n[i].classList.remove('eq-selected');
   // Select.
   if (ci && c === ci.parentNode) {
      ci.classList.add('eq-selected');
      // Scroll into view if necessary.
      if (c.children.length > 1)
         this.scrollIntoView(ci, { cb : this.scrollIntoView });
   }
};

/*------------------------------------------------------------------------
   static ComboBox.scrollIntoView()
   Scroll ComboBox line into view if necessary.
------------------------------------------------------------------------*/

eq.c.ComboBox.scrollIntoView = function(ci, qi) {
   var c = ci.parentNode, n = c.children, bh, lh;
   if (n.length > 1) {
      if (   eq.app.idle
          || (bh = c.getBoundingClientRect().height) <= 0
          || (lh = n[0].getBoundingClientRect().height) <= 0) {
         // Application idle, or height not available yet.
         eq.Dom.onNextFrame(ci, qi);
         return;
      }
      var bt = c.scrollTop, lt = ci.offsetTop;
      if (lt < bt)
         c.scrollTop = lt;
      else if (lt + lh > bt + bh)
         c.scrollTop = Math.ceil(lt + lh - bh + 2);
   }
};

/*------------------------------------------------------------------------
   static ComboBox.open()
   Open ComboBox item list.
------------------------------------------------------------------------*/

eq.c.ComboBox.open = function(el, c) {
   var cls = eq.c.ComboBox, cl = el.classList;
   cls.open.ctx = null;
   if (!cl.contains('eq-open')) {
      if (c === undefined)
         c = el.querySelector('.eq-combobox>.eq-column');
      var
         ch = el.firstElementChild,
         v = ch.value,
         vl = v.length,
         rc = el.getBoundingClientRect(),
         s = c.style,
         ccl = c.classList,
         ci, w, crc;
      if (vl) {
         // Pre-select current entry.
         var n = c.children, ni, nc;
         for (var i = 0, l = n.length; i < l; i++) {
            nc = (ni = n[i]).textContent;
            if (vl < nc.length) {
               if (v !== nc.substring(0, vl))
                  continue;
            }
            else if (v !== nc)
               continue;
            ci = ni;
            break;
         }
      }
      // Set overlay position and width.
      s.left = rc.left + window.pageXOffset + 'px';
      s.top = rc.bottom + window.pageYOffset + 'px';
      if (w = s.maxWidth)
         s.width = w;
      else
         s.width = rc.width + 'px';
      // Make overlay.
      eq.Dom.copyFont(c, ch);
      eq.Dom.copyColors(c, el);
      ccl.add('eq-combobox');
      if (cl.contains('eq-border'))
         ccl.add('eq-border');
      else
         ccl.remove('eq-border');
      eq.app.makeOverlay(el, c, cls.close);
      cl.add('eq-open');
      cl.add('eq-enter');
      if (  (crc = c.getBoundingClientRect()).bottom
          > window.pageYOffset + document.documentElement.clientHeight) {
         s.top = rc.top + window.pageYOffset - crc.height + 'px';
         cl.remove('eq-dn');
         ccl.remove('eq-dn');
         cl.add('eq-up');
         ccl.add('eq-up');
      }
      else {
         cl.remove('eq-up');
         ccl.remove('eq-up');
         cl.add('eq-dn');
         ccl.add('eq-dn');
      }
      cls.select(el, c, ci);
   }
};

eq.c.ComboBox.open.ctx = null;

/*------------------------------------------------------------------------
   static ComboBox.autoOpen()
   If configured, auto-open ComboBox item list when focused.
------------------------------------------------------------------------*/

eq.c.ComboBox.autoOpen = function(el) {
   eq.c.ComboBox.open(el);
};

eq.c.ComboBox.onFocus = function() {
   var el = this.parentNode, cl;
   if (el && (cl = el.classList)
          && cl.contains('eq-auto')
          && !cl.contains('eq-disabled')
          && !cl.contains('eq-open'))
      eq.Dom.onNextCycle(eq.c.ComboBox.autoOpen, el);
};

/*------------------------------------------------------------------------
   static ComboBox.preSelectIfOpen()
   Pre-select current entry if ComboBox item list is open.
------------------------------------------------------------------------*/

eq.c.ComboBox.preSelectIfOpen = function(el) {
   var ch, v, vl, c, n, ni, nc;
   if (   el.classList.contains('eq-open')
       && (ch = el.firstElementChild) && (vl = (v = ch.value).length)) {
      c = document.body.querySelector('body>.eq-combobox.eq-column');
      for (var i = 0, l = (n = c.children).length; i < l; i++) {
         nc = (ni = n[i]).textContent;
         if (vl < nc.length) {
            if (v !== nc.substring(0, vl))
               continue;
         }
         else if (v !== nc)
            continue;
         eq.c.ComboBox.select(el, c, ni);
         return;
      }
   }
};

/*------------------------------------------------------------------------
   static ComboBox.close()
   Close ComboBox item list.
------------------------------------------------------------------------*/

eq.c.ComboBox.close = function(el, c, commit) {
   var cl = el.classList, ccl = c.classList, opn, ctx, ci;
   if (!cl.contains('eq-open'))
      throw new Error("ComboBox.close BUG: not open, id:" + el.id);
   el.appendChild(c);
   ccl.remove('eq-combobox');
   ccl.remove('eq-border');
   cl.remove('eq-open');
   cl.remove('eq-enter');
   ctx = (opn = eq.c.ComboBox.open).ctx;
   if (commit && (ci = c.querySelector('.eq-column>.eq-selected'))) {
      var ct = el.firstElementChild, ml = ct.maxLength;
      ct.value = ml && ml > 0
               ? ci.textContent.substring(0, ml) : ci.textContent;
      ct.select();
      eq.Dom.fireEvent(el, 'input');
      if (ctx && ctx.el === el && ctx.cnt > 1)
         eq.app.submit(el, { id : el.id, type : 'input' });
   }
   if (ctx) {
      window.clearTimeout(ctx.t);
      opn.ctx = null;
   }
};

eq.c.ComboBox.close.ctx = function() {
   var opn = eq.c.ComboBox.open, ctx = opn.ctx, el, cl;
   if (ctx && (cl = (el = ctx.el).classList) && cl.contains('eq-open'))
      eq.app.closeOverlay(el, true);
};

/*------------------------------------------------------------------------
   static ComboBox.move()
   Handle ComboBox move capture.
------------------------------------------------------------------------*/

eq.c.ComboBox.move = function(e, m) {
   var c = m.c;
   if (c) {
      var
         cls = eq.c.ComboBox,
         t = e.target,
         el = m.el,
         co = {},
         ln = cls.line(t, co);
      if (el !== co.ct) {
         cls.select(el, c, ln);
         if (ln)
            m.close = true;
      }
   }
};

eq.c.ComboBox.move.done = function(m) {
   var el = m.el, a = m.a, ci;
   if (m.close) {
      if (   el.classList.contains('eq-auto')
          && (ci = document.body.querySelector(
                     'body>.eq-combobox.eq-column>.eq-selected'))) {
         var ct = el.firstElementChild, ml = ct.maxLength;
         ct.value = ml && ml > 0
                  ? ci.textContent.substring(0, ml) : ci.textContent;
         ct.select();
         eq.Dom.fireEvent(el, 'input');
      }
      else
         eq.app.closeOverlay(el, true);
   }
   if (a !== undefined)
      a.classList.remove('eq-click');
   return true;
};

/*------------------------------------------------------------------------
   static ComboBox.onInput()
   Handle ComboBox input event if autocomplete is active.
------------------------------------------------------------------------*/

eq.c.ComboBox.onInput = function(e) {
   var t, dlg, ed, tc, el;
   if (   (t = e.target).tagName === 'INPUT'
       && (dlg = eq.app.currDlg) && (ed = dlg.eds.get(this.id))
       && (tc = ed.tc)) {
      if ((el = t.parentNode).classList.contains('eq-open')) {
         // Deselect if open.
         eq.c.ComboBox.select(el,
            document.body.querySelector('body>.eq-combobox.eq-column'));
      }
      ed.textChanged(this, t, tc);
   }
};

eq.c.ComboBox.prototype.textChanged = function(el, t, tc) {
   var l = t.value.length, llast = tc.llast, app;
   if (l < llast || (l > llast && l >= tc.lmin)) {
      app = eq.app;
      if (tc.tid)
         app.dialogTimers.cancel(tc.tid);
      tc.tid = app.dialogTimers.add(
         tc.delay, this.constructor.onAutoComplete, null, {
            self : this,
            el   : el
         });
   }
   tc.llast = l;
};

eq.c.ComboBox.onAutoComplete = function(arg) {
   var
      self = arg.self,
      el = arg.el,
      id = el.id,
      app = eq.app;
   app.onceRule = self.tc.rule;
   app.onceId = id;
   app.submit(arg.el, { id : id, type : 'timer' });
};

/*------------------------------------------------------------------------
   ComboBox.setAttr()
   Set ComboBox attributes.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.setAttr = function(dlg, id, el, rt) {
   var tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.content: {
            var tc = this.tc;
            if (tc && (tc.llast = rc.l))
               eq.Dom.onNextCycle(this.constructor.preSelectIfOpen, el);
            break;
         }

         case tag.items: {
            var
               dm = eq.Dom,
               v = rc.v,
               l = v.length,
               cl = el.classList,
               c, n, nl, ln;
            if (l)
               cl.remove('eq-empty');
            else
               cl.add('eq-empty');
            if (cl.contains('eq-open')) {
               c = document.body.querySelector('body>.eq-combobox.eq-column');
               if (!c)
                  throw new Error("ComboBox.setAttr BUG: open, no column");
               if (l && this.tc)
                  dm.onNextCycle(this.constructor.preSelectIfOpen, el);
            }
            else {
               if (!(c = el.querySelector('.eq-combobox>.eq-button'))) {
                  c = document.createElement('DIV');
                  c.className = 'eq-rt eq-button';
                  el.appendChild(c);
               }
               if (!(c = el.querySelector('.eq-combobox>.eq-column'))) {
                  c = document.createElement('DIV');
                  c.className = 'eq-rt eq-column';
                  el.appendChild(c);
               }
            }
            if (l)
               nl = (n = c.children).length;
            else {
               // Empty, delete children.
               c.textContent = '';
               n = c.children;
               nl = 0;
            }
            for (var i = 0; i < l; i++) {
               if (i < nl)
                  ln = n[i];
               else {
                  ln = document.createElement('DIV');
                  c.appendChild(ln);
               }
               dm.setContent(ln, v[i]);
            }
            while (n.length > l)
               c.removeChild(c.lastElementChild);
            break;
         }

         case tag.boxSize: {
            var
               w = rc.w,
               h = rc.h,
               c, s;
            if (el.classList.contains('eq-open'))
               c = document.body.querySelector('body>.eq-combobox.eq-column');
            else
               c = el.querySelector('.eq-combobox>.eq-column');
            if (!c)
               throw new Error("ComboBox.setAttr BUG: boxSize, no column");
            (s = c.style).maxWidth = w ? w : '';
            s.maxHeight = h ? h : '';
            break;
         }

         case tag.autocomplete: {
            var v = rc.v;
            if (v) {
               this.tc = {
                  rule  : v.r,
                  delay : v.d,
                  lmin  : v.m,
                  llast : eq.Dom.inputElement(el).value.length,
                  tid   : 0
               };
               el.oninput = this.constructor.onInput;
            }
            else {
               this.tc = null;
               el.oninput = null;
            }
            break;
         }

         case tag.dndMode: {
            var c = eq.Dom.inputElement(el);
            if (this.dndMode = rc.i)
               c.ondragstart = dlg.constructor.onDragStart;
            else
               c.ondragstart = null;
            break;
         }

         case tag.dropRule: {
            var
               c = eq.Dom.inputElement(el),
               cls = this.constructor,
               dlgCls;
            if ((this.dropRule = rc.i) || this.globalDropRule) {
               dlgCls = dlg.constructor;
               c.ondragover = cls.onDragOver;
               c.ondrop = dlgCls.onDrop;
            }
            else {
               c.ondragover = null;
               c.ondrop = null;
            }
            break;
         }

         case tag.globalDropRule: {
            var
               c = eq.Dom.inputElement(el),
               cls = this.constructor,
               dlgCls;
            if ((this.globalDropRule = rc.i) || this.dropRule) {
               dlgCls = dlg.constructor;
               c.ondragover = cls.onDragOver;
               c.ondrop = dlgCls.onDrop;
            }
            else {
               c.ondragover = null;
               c.ondrop = null;
            }
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   ComboBox.setSensitive()
   Set element-specific 'sensitive' properties.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.setSensitive = function(dlg, id, el, sensitive) {
   eq.Dom.inputElement(el).disabled = !sensitive;
   eq.c.DlgElement.prototype.setSensitive.call(this, dlg, id, el, sensitive);
};

/*------------------------------------------------------------------------
   ComboBox.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   var
      cls = this.constructor,
      dm = eq.Dom,
      c = el.querySelector('.eq-combobox>.eq-column'),
      d;
   switch (key) {
      case 'Enter':
         if (!c) {
            eq.app.closeOverlay(el, true);
            if (el.classList.contains('eq-noenter')) {
               // .tabonenter disabled.
               dm.consumeEvent(e);
               return true;
            }
         }
         return false;
      case 'Escape':
      case 'Esc': // IE
         if (!c)
            eq.app.closeOverlay(el, false);
         dm.consumeEvent(e);
         return true;

      // Navigation?
      case 'PageUp':
      case 'PageDown':
      case 'ArrowUp':
      case 'Up':
      case 'ArrowDown':
      case 'Down':
         if (c) {
            cls.open(el, c);
            d = 0;
         }
         break;
      case 'ArrowLeft':
      case 'Left':
      case 'ArrowRight':
      case 'Right':
         return true;
      default:
         // Not navigation, character takes precedence over accelerator.
         return !kmod && key.length === 1;
   }

   var n, ci;
   c = document.body.querySelector('body>.eq-combobox.eq-column');
   if (c && (n = c.children).length) {
      if (d === undefined)
         switch (key) {
            case 'PageUp':
               ci = n[0];
               break;
            case 'PageDown':
               ci = n[n.length - 1];
               break;
            case 'ArrowUp':
            case 'Up':
               d = -1;
               break;
            case 'ArrowDown':
            case 'Down':
               d = 1;
               break;
         }
      if (d !== undefined) {
         if (ci = c.querySelector('.eq-column>.eq-selected')) {
            var i = dm.parentIndex(ci), l = n.length;
            if ((i += d) === -1)
               i = 0;
            else if (i >= l)
               i = l - 1;
            ci = n[i];
         }
         else
            ci = n[0];
      }
      if (ci !== undefined)
         cls.select(el, c, ci);
   }

   dm.consumeEvent(e);
   return true;
};

/*------------------------------------------------------------------------
   ComboBox.onTypeAhead()
   Process type-ahead key stroke.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.onTypeAhead = function(dlg, id, el, key) {
   if (key.length === 1) {
      var
         ct = eq.Dom.inputElement(el),
         ml = ct.maxLength,
         v = ct.value,
         l = v.length,
         s0, s1, tc;
      if (ml && ml > 0 && l >= ml)
         return true;
      s0 = ct.selectionStart;
      s1 = ct.selectionEnd;
      if (s0 > 0) {
         if (s1 < l)
            v = v.substring(0, s0) + key + v.substring(s1);
         else
            v = v.substring(0, s0) + key;
      }
      else if (s1 < l)
         v = key + v.substring(s1);
      else
         v = key;
      ct.value = v;
      if (tc = this.tc)
         this.textChanged(el, ct, tc);
      eq.app.setChanged(id, 'input');
      return true;
   }

   return eq.c.DlgElement.prototype.onTypeAhead.call(this, dlg, id, el, key);
};

/*------------------------------------------------------------------------
   ComboBox.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.onChange = function(dlg, id, el, e) {
   if (e.type === 'mousedown') {
      var cls = this.constructor, app = eq.app, t = e.target, cl;
      if (t && t.classList.contains('eq-button')) {
         if ((cl = el.classList).contains('eq-disabled'))
            return false;
         if (cl.contains('eq-focus') && app.setFocus(id, el)) {
            app.pendingFocus = {
               id : id,
               act : cls.open
            };
            return false;
         }
         if (cl.contains('eq-empty'))
            return false;
         var
            c = el.querySelector('.eq-combobox>.eq-column'),
            m = { el : el, a : t };
         t.classList.add('eq-click');
         if (c)
            cls.open(el, m.c = c);
         else
            m.close = true;
         app.moveCapture(m, cls.move, cls.move.done);
         eq.Dom.consumeEvent(e);
         return false;
      }
      if ((cl = el.classList).contains('eq-open')) {
         var
            m = { el : el },
            co = {},
            ln = cls.line(t, co),
            opn, ctx;
         if (co.c) {
            cls.select(el, m.c = co.c, ln);
            if (cl.contains('eq-rule')) {
               if (ctx = (opn = cls.open).ctx) {
                  ctx.cnt++;
                  app.closeOverlay(el, true);
               }
               else
                  opn.ctx = {
                     el : el,
                     cnt : 1,
                     t : window.setTimeout(
                        cls.close.ctx, eq.c.App.doubleClick.tv)
                  };
               return true;
            }
            m.close = true;
         }
         app.moveCapture(m, cls.move, cls.move.done);
         if (el !== co.ct) {
            eq.Dom.consumeEvent(e);
            return false;
         }
      }
   }
   return true;
};

/*------------------------------------------------------------------------
   ComboBox.onDragStart()
   Start drag&drop action.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.onDragStart = function(dlg, id, el, t, e, dnd) {
   var
      dndMode = this.dndMode,
      md = eq.Dnd,
      s0, s1;
   if (dndMode & md.local) {
      if ((s1 = t.selectionEnd) <= (s0 = t.selectionStart))
         return false;
      dnd.drag = {
         id      : id,
         content : t.value.substring(s0, s1)
      };
      (this.dnd = dnd).ch.push(id);
      eq.c.DlgElement.setDropModes(e, dndMode);
      return true;
   }
   if (dndMode & md.global) {
      eq.c.DlgElement.setDropModes(e, md.all);
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   static ComboBox.onDragOver()
------------------------------------------------------------------------*/

eq.c.ComboBox.onDragOver = function(e) {
   e.preventDefault();
   var app = eq.app, dnd, drag, el;
   if ((dnd = app.dnd) && (drag = dnd.drag) && (el = drag.over)) {
      drag.over = null;
      el.classList.remove('eq-dragover');
   }
};

/*------------------------------------------------------------------------
   ComboBox.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.onDrop = function(dlg, id, el, t, dnd) {
   dnd.drop.pos = eq.Dom.inputElement(el).selectionStart;
   (this.dnd = dnd).ch.push(dnd.drop.id = id);
   return dnd.drag ? this.dropRule : this.globalDropRule;
};

/*------------------------------------------------------------------------
   ComboBox.focusElement()
   Obtain focus element.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.focusElement = function(dlg, el) {
   var ct = eq.Dom.inputElement(el);
   ct.onfocus = el.classList.contains('eq-auto')
              ? this.constructor.onFocus : null;
   return ct;
};

/*------------------------------------------------------------------------
   ComboBox.focusGained()
   ComboBox has gained focus.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.focusGained = function(dlg, ct) {
   // Select on focus except on mouse click or if focus doesn't change.
   if (   eq.app.pendingMouseDown === undefined
       && ct !== document.activeElement) {
      ct.select();
      ct.scrollLeft = ct.scrollWidth;
   }
};

/*------------------------------------------------------------------------
   ComboBox.focusLost()
   ComboBox has lost focus.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.focusLost = function(dlg, el) {
   if (el.classList.contains('eq-open'))
      eq.app.closeOverlay(el);
   var ct = eq.Dom.inputElement(el);
   ct.selectionStart = ct.selectionEnd = 0;
   ct.blur();
};

/*------------------------------------------------------------------------
   ComboBox.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.ComboBox.prototype.changed = function(dlg, id, el, ty) {
   var v = [];
   for (var i = 0, l = ty.length; i < l; i++)
      switch (ty[i]) {
         case 'input':
            v.push({
               ty : eq.RsValType.textValue,
               sv : el.value
            });
            break;

         case 'dnd': {
            var dnd = this.dnd, drag, drop, sv, iv;
            if (dnd) {
               if ((drag = dnd.drag) && drag.id === id) {
                  if ((sv = drag.content) !== undefined) {
                     drag.content = null;
                     v.push({
                        ty : eq.RsValType.dragContent,
                        sv : sv
                     });
                  }
               }
               if ((drop = dnd.drop) && drop.id === id) {
                  if ((iv = drop.pos) !== undefined) {
                     v.push({
                        ty : eq.RsValType.dropPos,
                        iv : iv
                     });
                  }
               }
            }
            break;
         }
      }
   this.dnd = null;
   switch (v.length) {
      case 0:
         return null;
      case 1:
         v[0].id = id;
         return v[0];
      default:
         return {
            id : id,
            v : v
         };
   }
};

/** @preserve $Id: 20-dialog.js,v 28.1 2022/10/27 11:49:55 mike Exp $
*//*======================================================================
   final class Dialog extends DlgElement
   DLG Dialog element class.
========================================================================*/

eq.c.DlgElement.addClass('Dialog', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.dnd = null;
   this.dropRule = 0;
   this.globalDropRule = 0;
});

delete eq.c.Dialog.subClass; // final

/*------------------------------------------------------------------------
   Dialog.dispose()
------------------------------------------------------------------------*/

eq.c.Dialog.prototype.dispose = function(dlg, id) {
   this.dnd = null;
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   Dialog.setAttr()
   Set Dialog attributes.
------------------------------------------------------------------------*/

eq.c.Dialog.prototype.setAttr = function(dlg, id, el, rt) {
   var tag = eq.RqTag, brt, dlgCls = dlg.constructor;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.dropRule:
            if ((this.dropRule = rc.i) || this.globalDropRule) {
               el.ondragover = dlgCls.onDragOver;
               el.ondrop = dlgCls.onDrop;
            }
            else {
               el.ondragover = null;
               el.ondrop = null;
            }
            break;

         case tag.globalDropRule:
            if ((this.globalDropRule = rc.i) || this.dropRule) {
               el.ondragover = dlgCls.onDragOver;
               el.ondrop = dlgCls.onDrop;
            }
            else {
               el.ondragover = null;
               el.ondrop = null;
            }
            break;

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   Dialog.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.Dialog.prototype.onDrop = function(dlg, id, el, t, dnd) {
   (this.dnd = dnd).ch.push(id);
   return dnd.drag ? this.dropRule : this.globalDropRule;
};

/*------------------------------------------------------------------------
   Dialog.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.Dialog.prototype.changed = function(dlg, id, el, ty) {
   var v = [];
   for (var i = 0, l = ty.length; i < l; i++)
      switch (ty[i]) {
         case 'dnd': {
            var dnd = this.dnd, drag, drop, sv, iv;
            if (dnd) {
               if (drag = dnd.drag) {
                  if ((sv = drag.id) !== undefined) {
                     drag.id = null;
                     v.push({
                        ty : eq.RsValType.dragFrom,
                        sv : sv
                     });
                  }
               }
               if (drop = dnd.drop) {
                  if ((sv = drop.content) !== undefined) {
                     drop.content = null;
                     v.push({
                        ty : eq.RsValType.dropContent,
                        sv : sv
                     });
                  }
                  if ((iv = drop.action) !== undefined) {
                     v.push({
                        ty : eq.RsValType.dropAction,
                        iv : iv
                     });
                  }
               }
            }
            break;
         }
      }
   this.dnd = null;
   switch (v.length) {
      case 0:
         return null;
      case 1:
         v[0].id = id;
         return v[0];
      default:
         return {
            id : id,
            v : v
         };
   }
};

/** @preserve $Id: 20-edittext.js,v 28.16 2023/12/18 12:42:29 mike Exp $
*//*======================================================================
   final class EditText extends DlgElement
   DLG EditText element class.
========================================================================*/

eq.c.DlgElement.addClass('EditText', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.hardWrap = false;
   this.autoTab = 0;
   this.ff = null;
   this.tc = null;
   this.dnd = null;
   this.dndMode = 0;
   this.dropRule = 0;
   this.globalDropRule = 0;
   this.ignoreNextBlur = false;
});

delete eq.c.EditText.subClass; // final

/*------------------------------------------------------------------------
   EditText.dispose()
------------------------------------------------------------------------*/

eq.c.EditText.prototype.dispose = function(dlg, id) {
   this.ff = null;
   this.tc = null;
   this.dnd = null;
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   static EditText.onInput()
   Handle EditText input event if autocomplete, autotab are active.
------------------------------------------------------------------------*/

eq.c.EditText.onInput = function(e) {
   var app, t, dlg, id, self, tc, at, ml, n;
   if (   (t = e.target).tagName === 'INPUT'
       && (dlg = (app = eq.app).currDlg)
       && (self = dlg.eds.get(id = this.id))) {
      if (tc = self.tc)
         self.textChanged(this, t, tc);
      if (   ((at = self.autoTab) & 1)
          && (ml = t.maxLength) > 0
          && t.value.length >= ml
          && (n = dlg.nextTab(id, false))) {
         if (at & 4) {
           self.ignoreNextBlur = true;
           eq.Dom.onNextCycle(self.constructor.nextEventCycle, this);
         }
         app.setFocus(n.id, n);
      }
   }
};

eq.c.EditText.prototype.textChanged = function(el, t, tc) {
   var l = t.value.length, llast = tc.llast, app;
   if (l < llast || (l > llast && l >= tc.lmin)) {
      app = eq.app;
      if (tc.tid)
         app.dialogTimers.cancel(tc.tid);
      tc.tid = app.dialogTimers.add(
         tc.delay, this.constructor.onAutoComplete, null, {
            self : this,
            el   : el
         });
   }
   tc.llast = l;
};

eq.c.EditText.onAutoComplete = function(arg) {
   var
      self = arg.self,
      el = arg.el,
      id = el.id,
      app = eq.app;
   app.onceRule = self.tc.rule;
   app.onceId = id;
   app.submit(arg.el, { id : id, type : 'timer' });
};

/*------------------------------------------------------------------------
   EditText.wrapCols()
   EditText.fitFont()
------------------------------------------------------------------------*/

eq.c.EditText.prototype.wrapCols = function(dlg, id, el) {
   if (this.hardWrap) {
      var
         ct = el.firstElementChild,
         cr = ct.getBoundingClientRect(),
         cw, st, body, r, rw;
      if (cw = cr.width) {
         st = this.constructor.getFontSizeTemplate(),
         eq.Dom.copyFont(st, ct);
         (body = document.body).appendChild(st);
         r = st.getBoundingClientRect();
         if (rw = r.width)
            ct.cols = Math.floor(cw / rw);
         body.removeChild(st);
      }
   }
};

eq.c.EditText.prototype.fitFont = function(dlg, id, el) {
   var ff = this.ff;
   if (ff) {
      var
         body = document.body,
         st = this.constructor.getFontSizeTemplate(),
         ct = el.firstElementChild,
         cs = ct.style,
         es = el.style,
         r, rv, fv;
      eq.Dom.copyFont(st, ct);
      body.appendChild(st);
      r = st.getBoundingClientRect();
      if ((rv = r.width) && (fv = ff.w)) {
         cs.width = (fv * rv) + 'px';
         es.width = '';
      }
      if ((rv = r.height) && (fv = ff.h)) {
         cs.height = (fv * rv) + 'px';
         es.height = '';
      }
      body.removeChild(st);
   }
};

eq.c.EditText.getFontSizeTemplate = function() {
   var st = this.fontSizeTemplate;
   if (st === undefined) {
      this.fontSizeTemplate = st = document.createElement('DIV');
      st.className = 'eq-grid';
      st.textContent = 'X';
      st.style.zIndex = eq.Dom.zIndex(document.body) - 1;
   }
   return st.cloneNode(true);
};

/*------------------------------------------------------------------------
   EditText.setAttr()
   Set EditText attributes.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.setAttr = function(dlg, id, el, rt) {
   var tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.multiline:
            // (Re-)Initialization.
            this.hardWrap = false;
            this.autoTab = 0;
            this.ff = null;
            this.tc = null;
            break;

         case tag.content: {
            var tc = this.tc;
            if (tc)
               tc.llast = rc.l;
            break;
         }

         case tag.wrapmode: {
            if (this.hardWrap = !!rc.b)
               this.whenVisible(dlg, id, el, this.wrapCols);
            else
               el.firstElementChild.removeAttribute('cols');
            break;
         }

         case tag.fitfont: {
            var ff = rc.ff;
            if (ff) {
               this.ff = ff;
               this.whenVisible(dlg, id, el, this.fitFont);
            }
            else {
               var s = el.firstElementChild.style;
               s.width = s.height = '';
               this.ff = null;
            }
            break;
         }

         case tag.autocomplete: {
            var v = rc.v, ct;
            if (v && (ct = el.firstElementChild).tagName === 'INPUT') {
               this.tc = {
                  rule  : v.r,
                  delay : v.d,
                  lmin  : v.m,
                  llast : ct.value.length,
                  tid   : 0
               };
               el.oninput = this.constructor.onInput;
            }
            else {
               this.tc = null;
               el.oninput = null;
            }
            break;
         }

         case tag.autotab:
            if (this.autoTab = rc.i)
               el.oninput = this.constructor.onInput;
            else
               el.oninput = null;
            break;

         case tag.dndMode: {
            var ct = el.firstElementChild;
            if (this.dndMode = rc.i)
               ct.ondragstart = dlg.constructor.onDragStart;
            else
               ct.ondragstart = null;
            break;
         }

         case tag.dropRule: {
            var
               ct = el.firstElementChild,
               cls = this.constructor,
               dlgCls;
            if ((this.dropRule = rc.i) || this.globalDropRule) {
               dlgCls = dlg.constructor;
               ct.ondragover = cls.onDragOver;
               ct.ondrop = dlgCls.onDrop;
            }
            else {
               ct.ondragover = null;
               ct.ondrop = null;
            }
            break;
         }

         case tag.globalDropRule: {
            var
               ct = el.firstElementChild,
               cls = this.constructor,
               dlgCls;
            if ((this.globalDropRule = rc.i) || this.dropRule) {
               dlgCls = dlg.constructor;
               ct.ondragover = cls.onDragOver;
               ct.ondrop = dlgCls.onDrop;
            }
            else {
               ct.ondragover = null;
               ct.ondrop = null;
            }
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   EditText.setSensitive()
   Set element-specific 'sensitive' properties.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.setSensitive = function(dlg, id, el, sensitive) {
   var n = el.getElementsByTagName('INPUT');
   if (n.length === 0)
      n = el.getElementsByTagName('TEXTAREA');
   if (n.length !== 1)
      throw new Error("BUG: EditText.setSensitive id:" + el.id +
         " elements:" + n.length + ", expected: 1");
   n[0].disabled = !sensitive;
   eq.c.DlgElement.prototype.setSensitive.call(this, dlg, id, el, sensitive);
};

/*------------------------------------------------------------------------
   EditText.onEvent()
   Filter and/or handle action event.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.onEvent = function(dlg, id, el, e) {
   return e.type !== 'blur' || !this.ignoreNextBlur;
};

eq.c.EditText.nextEventCycle = function(self) {
   self.ignoreNextBlur = false;
};

/*------------------------------------------------------------------------
   EditText.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   switch (key) {
      case 'ArrowUp':
      case 'Up':
      case 'ArrowDown':
      case 'Down':
         return el.getElementsByTagName('TEXTAREA').length != 0;
      case 'Backspace':
      case 'ArrowLeft':
      case 'Left': {
         var at = this.autoTab, ct, n, dm;
         if (   (at & 2)
             && (ct = el.firstElementChild)
             && ct.selectionStart === 0
             && (n = dlg.nextTab(id, true))) {
            dm = eq.Dom;
            if (at & 4) {
              this.ignoreNextBlur = true;
              dm.onNextCycle(this.constructor.nextEventCycle, this);
            }
            eq.app.setFocus(n.id, n);
            dm.consumeEvent(e);
         }
         return true;
      }
      case 'ArrowRight':
      case 'Right': {
         var at = this.autoTab, ct, n, dm;
         if (   (at & 1)
             && (ct = el.firstElementChild)
             && ct.selectionStart >= ct.maxLength
             && ct.value.length
             && (n = dlg.nextTab(id, false))) {
            dm = eq.Dom;
            if (at & 4) {
              this.ignoreNextBlur = true;
              dm.onNextCycle(this.constructor.nextEventCycle, this);
            }
            eq.app.setFocus(n.id, n);
            dm.consumeEvent(e);
         }
         return true;
      }
   }

   // Character takes precedence over accelerator.
   return !kmod && key.length === 1;
};

/*------------------------------------------------------------------------
   EditText.onTypeAhead()
   Process type-ahead key stroke.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.onTypeAhead = function(dlg, id, el, key) {
   var ct = el.firstElementChild, ch;
   switch (key) {
      case 'Enter':
         if (ct.tagName === 'TEXTAREA')
            ch = '\n';
         break;
      default:
         if (key.length === 1)
            ch = key;
   }

   if (ch !== undefined) {
      var
         ml = ct.maxLength,
         v = ct.value,
         l = v.length,
         s0, s1, tc;
      if (ml && ml > 0 && l >= ml)
         return true;
      s0 = ct.selectionStart;
      s1 = ct.selectionEnd;
      if (s0 > 0) {
         if (s1 < l)
            v = v.substring(0, s0) + ch + v.substring(s1);
         else
            v = v.substring(0, s0) + ch;
      }
      else if (s1 < l)
         v = ch + v.substring(s1);
      else
         v = ch;
      ct.value = v;
      if (ct.tagName === 'TEXTAREA')
         ct.textContent = v;
      else if (tc = this.tc)
         this.textChanged(el, ct, tc);
      eq.app.setChanged(id, 'input');
      return true;
   }

   return eq.c.DlgElement.prototype.onTypeAhead.call(this, dlg, id, el, key);
};

/*------------------------------------------------------------------------
   EditText.onDragStart()
   Start drag&drop action.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.onDragStart = function(dlg, id, el, t, e, dnd) {
   var
      dndMode = this.dndMode,
      md = eq.Dnd,
      s0, s1;
   if (dndMode & md.local) {
      if ((s1 = t.selectionEnd) <= (s0 = t.selectionStart))
         return false;
      dnd.drag = {
         id      : id,
         content : t.value.substring(s0, s1)
      };
      (this.dnd = dnd).ch.push(id);
      eq.c.DlgElement.setDropModes(e, dndMode);
      return true;
   }
   if (dndMode & md.global) {
      eq.c.DlgElement.setDropModes(e, md.all);
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   static EditText.onDragOver()
------------------------------------------------------------------------*/

eq.c.EditText.onDragOver = function(e) {
   e.preventDefault();
   var app = eq.app, dnd, drag, el;
   if ((dnd = app.dnd) && (drag = dnd.drag) && (el = drag.over)) {
      drag.over = null;
      el.classList.remove('eq-dragover');
   }
};

/*------------------------------------------------------------------------
   EditText.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.onDrop = function(dlg, id, el, t, dnd) {
   dnd.drop.pos = el.firstElementChild.selectionStart;
   (this.dnd = dnd).ch.push(dnd.drop.id = id);
   return dnd.drag ? this.dropRule : this.globalDropRule;
};

/*------------------------------------------------------------------------
   EditText.focusElement()
   Obtain focus element.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.focusElement = function(dlg, el) {
   var n = el.getElementsByTagName('INPUT');
   if (n.length === 0)
      n = el.getElementsByTagName('TEXTAREA');
   if (n.length !== 1)
      throw new Error("BUG: EditText.focusElement id:" + el.id +
         " elements:" + n.length + ", expected: 1");
   return n[0];
};

/*------------------------------------------------------------------------
   EditText.focusGained()
   EditText has gained focus.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.focusGained = function(dlg, ct) {
   // Select on focus except on mouse click or if focus doesn't change.
   if (   eq.app.pendingMouseDown === undefined
       && ct !== document.activeElement) {
      if (ct.tagName === 'INPUT') {
         ct.select();
         ct.scrollLeft = ct.scrollWidth;
      }
      else
         ct.selectionStart = ct.selectionEnd = 0;
   }
};

/*------------------------------------------------------------------------
   EditText.focusLost()
   EditText has lost focus.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.focusLost = function(dlg, el) {
   var ct = el.firstElementChild;
   if (ct.tagName === 'INPUT')
      ct.selectionStart = ct.selectionEnd = 0;
   ct.blur();
};

/*------------------------------------------------------------------------
   EditText.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.EditText.prototype.changed = function(dlg, id, el, ty) {
   var v = [];
   for (var i = 0, l = ty.length; i < l; i++)
      switch (ty[i]) {
         case 'input':
            v.push({
               ty : eq.RsValType.textValue,
               sv : el.value
            });
            break;

         case 'blur': {
            var iv = el.selectionStart;
            if (iv >= 0)
               v.push({
                  ty : eq.RsValType.caretPos,
                  iv : iv
               });
            break;
         }

         case 'dnd': {
            var dnd = this.dnd, drag, drop, sv, iv;
            if (dnd) {
               if ((drag = dnd.drag) && drag.id === id) {
                  if ((sv = drag.content) !== undefined) {
                     drag.content = null;
                     v.push({
                        ty : eq.RsValType.dragContent,
                        sv : sv
                     });
                  }
               }
               if ((drop = dnd.drop) && drop.id === id) {
                  if ((iv = drop.pos) !== undefined) {
                     v.push({
                        ty : eq.RsValType.dropPos,
                        iv : iv
                     });
                  }
               }
            }
            break;
         }
      }
   this.dnd = null;
   switch (v.length) {
      case 0:
         return null;
      case 1:
         v[0].id = id;
         return v[0];
      default:
         return {
            id : id,
            v : v
         };
   }
};

/** @preserve $Id: 20-groupbox.js,v 28.17 2025/01/29 10:32:06 mike Exp $
*//*======================================================================
   final class GroupBox extends DlgElement
   DLG GroupBox element class.
========================================================================*/

eq.c.DlgElement.addClass('GroupBox', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.attrAccel = null;
   this.dropRule = 0;
   this.globalDropRule = 0;
   this.scrl = null;
});

delete eq.c.GroupBox.subClass; // final

/*------------------------------------------------------------------------
   GroupBox.dispose()
------------------------------------------------------------------------*/

eq.c.GroupBox.prototype.dispose = function(dlg, id) {
   var accel = this.attrAccel;
   if (accel !== null) {
      var
         da = dlg.accel,
         di = da.get(accel);
      if (di === id)
         da.delete(accel);
      this.attrAccel = null;
   }

   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   GroupBox.updateID()
   Element identifier has been modified.
------------------------------------------------------------------------*/

eq.c.GroupBox.prototype.updateId = function(dlg, id, oldId) {
   var accel = this.attrAccel;
   if (accel !== null) {
      var
         da = dlg.accel,
         di = da.get(accel);
      if (di === oldId)
         da.set(accel, id);
   }

   eq.c.DlgElement.prototype.updateId.call(this, dlg, id, oldId);
};

/*------------------------------------------------------------------------
   static GroupBox.scroll()
   Scroll GroupBox when possible.
------------------------------------------------------------------------*/

eq.c.GroupBox.scroll = function(el, qi) {
   if (   eq.app.idle
       || !el.offsetWidth || !el.offsetHeight
       || !el.getClientRects().length) {
      // Cannot scroll yet.
      eq.Dom.onNextFrame(el, qi);
      return;
   }
   qi.self.scrl = null;
   el.dataset.scroll = 'ignore';
   var v;
   if ((v = qi.left) >= 0)
      el.scrollLeft = v;
   if ((v = qi.top) >= 0)
      el.scrollTop = v;
};

/*------------------------------------------------------------------------
   static GroupBox.onScroll()
   Handle GroupBox scroll event.
------------------------------------------------------------------------*/

eq.c.GroupBox.onScroll = function(e) {
   if ('scroll' in this.dataset)
      delete this.dataset.scroll;
   else {
      var el = this.parentNode, cl;
      if (el && (cl = el.classList) && cl.contains('eq-groupbox')) {
         // 'scroll' change event.
         eq.app.setChanged(el.id, 'scroll');
      }
   }
};

/*------------------------------------------------------------------------
   GroupBox.setAttr()
   Set GroupBox attributes.
------------------------------------------------------------------------*/

eq.c.GroupBox.prototype.setAttr = function(dlg, id, el, rt) {
   var tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.vx: {
            var scrl = this.scrl, cls = this.constructor;
            if (scrl)
               scrl.left = rc.i;
            else
               scrl = this.scrl = {
                  cb   : cls.scroll,
                  self : this,
                  left : rc.i,
                  top  : -1
               };
            cls.scroll(el.lastElementChild, scrl);
            break;
         }

         case tag.vy: {
            var scrl = this.scrl, cls = this.constructor;
            if (scrl)
               scrl.top = rc.i;
            else
               scrl = this.scrl = {
                  cb   : cls.scroll,
                  self : this,
                  left : -1,
                  top  : rc.i
               };
            cls.scroll(el.lastElementChild, scrl);
            break;
         }

         case tag.vWidth:
         case tag.vHeight:
            if (rc.s) {
               el.classList.add('eq-scroll');
               el.lastElementChild.onscroll = eq.c.GroupBox.onScroll;
            }
            else {
               el.classList.remove('eq-scroll');
               el.lastElementChild.onscroll = null;
            }
            break;

         case tag.text: {
            var
               v = rc.v,
               t = el.firstElementChild,
               cl = el.classList,
               sp, ac;
            if (v) {
               if (!t || !t.classList.contains('eq-title')) {
                  t = document.createElement('DIV');
                  t.className = 'eq-rt eq-title';
                  t.appendChild(document.createElement('DIV'));
                  sp = document.createElement('SPAN');
                  sp.className = 'eq-fn';
                  t.appendChild(sp);
                  t.appendChild(document.createElement('DIV'));
                  el.insertBefore(t, el.firstElementChild);
               }
               ac = eq.Dom.setContent(t.children[1], v, true);
               this.setAccel(dlg, id, ac, eq.KeyMod.alt);
               cl.add('eq-title');
            }
            else if (t && t.classList.contains('eq-title')) {
               el.removeChild(t);
               cl.remove('eq-title');
            }
            break;
         }

         case tag.accel: {
            var
               accel = this.attrAccel,
               ac = eq.Dom.decodeAccelAttr(rc.v);
            if (ac !== accel) {
               var da = dlg.accel;
               if (accel !== null) {
                  var di = da.get(accel);
                  if (di === id)
                     da.delete(accel);
               }
               if (ac !== null && !da.has(ac))
                  da.set(this.attrAccel = ac, id);
               else
                  this.attrAccel = null;
            }
            break;
         }

         case tag.dropRule: {
            var c = el.lastElementChild, dlgCls;
            if ((this.dropRule = rc.i) || this.globalDropRule) {
               dlgCls = dlg.constructor;
               c.ondragover = dlgCls.onDragOver;
               c.ondrop = dlgCls.onDrop;
            }
            else {
               c.ondragover = null;
               c.ondrop = null;
            }
            break;
         }

         case tag.globalDropRule: {
            var c = el.lastElementChild, dlgCls;
            if ((this.globalDropRule = rc.i) || this.dropRule) {
               dlgCls = dlg.constructor;
               c.ondragover = dlgCls.onDragOver;
               c.ondrop = dlgCls.onDrop;
            }
            else {
               c.ondragover = null;
               c.ondrop = null;
            }
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   GroupBox.onAccel()
   Handle accelerator key event.
------------------------------------------------------------------------*/

eq.c.GroupBox.prototype.onAccel = function(dlg, id, el) {
   var tb = eq.Dom.eqControl(el.parentNode);
   if (tb && tb.classList.contains('eq-tabbox')) {
      // TabBox child.
      var ed = dlg.eds.get(tb.id);
      if (ed === undefined)
         throw new Error("GroupBox.onAccel(" +
            el.id + ") BUG: TabBox " + tb.id + " not registered");
      ed.onTabAccel(tb, el);
      return;
   }
   // GroupBox, set focus on next descendant.
   if (el = el.querySelector('.eq-focus:not(.eq-disabled)'))
      eq.app.setFocus(el.id, el);
};

/*------------------------------------------------------------------------
   GroupBox.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.GroupBox.prototype.onDrop = function(dlg, id, el, t, dnd) {
   return dnd.drag ? this.dropRule : this.globalDropRule;
};

/*------------------------------------------------------------------------
   GroupBox.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.GroupBox.prototype.changed = function(dlg, id, el, ty) {
   var ch = {
      id : id,
      ty : eq.RsValType.scrollPos,
      x  : el.scrollLeft,
      y  : el.scrollTop
   };
   return ch;
};

/** @preserve $Id: 20-htmlview.js,v 28.15 2024/01/23 09:13:08 mike Exp $
*//*======================================================================
   final class HtmlView extends DlgElement
   DLG HtmlView element class.
========================================================================*/

eq.c.DlgElement.addClass('HtmlView', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.dnd = null;
   this.dndMode = 0;
   this.dropRule = 0;
   this.globalDropRule = 0;
});

delete eq.c.HtmlView.subClass; // final

eq.c.HtmlView.re = new RegExp('^eloq:([0-9]*)(/(.*))?$', 'i');
eq.c.HtmlView.ctx = new Map();
eq.c.HtmlView.timeout = undefined;

/*------------------------------------------------------------------------
   HtmlView.dispose()
------------------------------------------------------------------------*/

eq.c.HtmlView.prototype.dispose = function(dlg, id) {
   this.dnd = null;
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   HtmlView context timeout.
------------------------------------------------------------------------*/

eq.c.HtmlView.startTimeout = function() {
   if (this.timeout !== undefined)
      window.clearTimeout(this.timeout);
   this.timeout = window.setTimeout(this.timeoutElapsed, 10000);
};

eq.c.HtmlView.timeoutElapsed = function() {
   var cls = eq.c.HtmlView;
   cls.timeout = undefined;
   cls.ctx.clear();
};

/*------------------------------------------------------------------------
   static HtmlView.ready()
------------------------------------------------------------------------*/

eq.c.HtmlView.ready = function() {
   var
      cls = eq.c.HtmlView,
      id = this.name,
      ctx = cls.ctx.get(id),
      self, dlg, url, cn;
   if (ctx) {
      self = ctx.self;
      dlg = ctx.dlg;
      url = ctx.url;
      cn = ctx.cn;
      cls.ctx.delete(id);
   }
   try {
      var
         w = this.contentWindow,
         d = w.document,
         b = d.body,
         n;
      w.name = id;
      if (cn) {
         b.style.margin = "0";
         eq.Dom.setHtmlContent(cn, b, d.head);
      }
      n = b.querySelectorAll('a[href^="eloq:"]');
      for (var i = 0, l = n.length; i < l; i++) {
         var a = n[i];
         a.dataset.eqLink = a.href;
         a.href = '#';
         a.addEventListener('click', cls.clicked);
      }
      if (self)
         self.dndUpdate(id, this);
   }
   catch (e) {
      console.warn("HtmlView:" + id + (url ? " '" + url + "' " : " ") +
         e.name + ": " + e.message);
   }
};

/*------------------------------------------------------------------------
   static HtmlView.clicked()
------------------------------------------------------------------------*/

eq.c.HtmlView.clicked = function(e) {
   var id = e.view.name, el, ln;
   if (id && (el = document.getElementById(id)) && (ln = this.dataset.eqLink))
      eq.c.HtmlView.submit(id, el, ln);
   e.preventDefault();
   e.stopImmediatePropagation();
};

/*------------------------------------------------------------------------
   static HtmlView.submit()
------------------------------------------------------------------------*/

eq.c.HtmlView.submit = function(id, el, ln) {
   if (!el.classList.contains('eq-disabled')) {
      var
         app = eq.app,
         r = this.re.exec(ln),
         rule;
      if (r) {
         if (r.length > 0 && r[1].length)
            rule = r[1];
         if (r.length > 2 && r[3])
            try {
               el.dataset.eqLink = decodeURIComponent(r[3]);
            }
            catch (e) {
               delete el.dataset.eqLink;
            }
      }
      app.setChanged(id, 'change');
      if (rule !== undefined || el.classList.contains('eq-rule')) {
         if (rule !== undefined) {
            app.onceRule = rule;
            app.onceId = id;
         }
         app.submit(el, {
            id : id,
            type : 'change'
         });
      }
   }
};

/*------------------------------------------------------------------------
   HtmlView.onMessage()
   Process window 'message' event.
------------------------------------------------------------------------*/

eq.c.HtmlView.prototype.onMessage = function(dlg, id, el, msg) {
   this.constructor.submit(id, el, msg);
};

/*------------------------------------------------------------------------
   static HtmlView.createFrame()
------------------------------------------------------------------------*/

eq.c.HtmlView.createFrame = function(name) {
   var f = document.createElement('IFRAME');
   f.name = name;
   f.onload = this.ready;
   return f;
};

/*------------------------------------------------------------------------
   HtmlView.setAttr()
   Set HtmlView attributes.
------------------------------------------------------------------------*/

eq.c.HtmlView.prototype.setAttr = function(dlg, id, el, rt) {
   var cls = this.constructor, tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.url: {
            var ch = el.firstElementChild;
            if (ch && ch.tagName !== 'IFRAME') {
               el.removeChild(ch);
               ch = null;
            }
            if (!ch)
               el.appendChild(ch = cls.createFrame(id));
            ch.src = rc.v;
            cls.startTimeout();
            cls.ctx.set(id, {
               self : this,
               dlg  : dlg,
               url  : rc.v
            });
            break;
         }

         case tag.content: {
            var ch = el.firstElementChild;
            switch (rc.ty) {
               case 0: // text/html
                  if (ch && ch.tagName !== 'IFRAME') {
                     el.removeChild(ch);
                     ch = null;
                  }
                  if (!ch)
                     el.appendChild(ch = cls.createFrame(id));
                  ch.src = 'about:blank';
                  cls.startTimeout();
                  cls.ctx.set(id, {
                     self : this,
                     dlg  : dlg,
                     cn   : rc.v
                  });
                  break;

               case 1: { // text/plain
                  if (ch && ch.tagName !== 'TEXTAREA') {
                     el.removeChild(ch);
                     ch = null;
                  }
                  if (!ch) {
                     ch = document.createElement('TEXTAREA');
                     ch.className = 'eq-fn';
                     ch.readOnly = true;
                     // Disable wrap (IE),
                     // auto-capitalization (WebKit non-standard),
                     // auto-completion, auto-correction, spell-checking.
                     ch.wrap = 'off'; // IE
                     ch.autocapitalize = 'none';
                     ch.autocomplete = 'off';
                     ch.autocorrect = 'off';
                     ch.spellcheck = false;
                     el.appendChild(ch);
                  }
                  ch.value = ch.textContent = rc.v;
                  cls.ctx.delete(id);
                  this.dndUpdate(id, ch);
                  break;
               }

               default:
                  throw new Error("HtmlView.setAttr: tag " + rc.t +
                     " invalid type " + rc.ty);
            }
            break;
         }

         case tag.dndMode:
            this.dndMode = rc.i;
            this.dndUpdate(id, el.firstElementChild);
            break;

         case tag.dropRule:
            this.dropRule = rc.i;
            this.dndUpdate(id, el.firstElementChild);
            break;

         case tag.globalDropRule:
            this.globalDropRule = rc.i;
            this.dndUpdate(id, el.firstElementChild);
            break;

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   HtmlView.dndUpdate()
   Update drag&drop properties.
------------------------------------------------------------------------*/

eq.c.HtmlView.prototype.dndUpdate = function(id, ch) {
   if (ch)
      try {
         if (ch.tagName === 'IFRAME')
            ch = ch.contentWindow;
         else if (ch.tagName !== 'TEXTAREA')
            throw new Error("HtmlView.dndUpdate " + id +
               " invalid tagName:" + ch.tagName);
         var dlgCls;
         if (this.dndMode)
            ch.ondragstart = (dlgCls || (dlgCls = eq.c.Dlg)).onDragStart;
         else
            ch.ondragstart = null;
         if (this.dropRule || this.globalDropRule) {
            ch.ondragover = this.constructor.onDragOver;
            ch.ondrop = (dlgCls || eq.c.Dlg).onDrop;
         }
         else {
            ch.ondragover = null;
            ch.ondrop = null;
         }
      }
      catch (e) {}
};

/*------------------------------------------------------------------------
   HtmlView.onDragStart()
   Start drag&drop action.
------------------------------------------------------------------------*/

eq.c.HtmlView.prototype.onDragStart = function(dlg, id, el, t, e, dnd) {
   var ch = el.firstElementChild;
   if (ch)
      try {
         var cn;
         if (ch.tagName === 'IFRAME') {
            ch = ch.contentWindow;
            cn = ch.getSelection().toString();
         }
         else {
            if (ch.tagName !== 'TEXTAREA')
               throw new Error("HtmlView.onDragStart " + id +
                  " invalid tagName:" + ch.tagName);
            var s0, s1;
            if ((s1 = ch.selectionEnd) > (s0 = ch.selectionStart))
               cn = ch.value.substring(s0, s1);
         }
         if (cn) {
            var
               dndMode = this.dndMode,
               md = eq.Dnd;
            if (dndMode & md.local) {
               dnd.drag = {
                  id      : id,
                  content : cn
               };
               (this.dnd = dnd).ch.push(id);
               eq.c.DlgElement.setDropModes(e, dndMode);
               return true;
            }
            if (dndMode & md.global) {
               eq.c.DlgElement.setDropModes(e, md.all);
               return true;
            }
         }
      }
      catch (e) {}
   return false;
};

/*------------------------------------------------------------------------
   static HtmlView.onDragOver()
------------------------------------------------------------------------*/

eq.c.HtmlView.onDragOver = function(e) {
   e.preventDefault();
   var app = eq.app, dnd, drag, el;
   if ((dnd = app.dnd) && (drag = dnd.drag) && (el = drag.over)) {
      drag.over = null;
      el.classList.remove('eq-dragover');
   }
};

/*------------------------------------------------------------------------
   HtmlView.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.HtmlView.prototype.onDrop = function(dlg, id, el, t, dnd) {
   var ch = el.firstElementChild;
   if (ch)
      try {
         if (ch.tagName === 'IFRAME') {
            while (t && !t.id)
               t = t.parentNode;
            if (t)
               dnd.drop.element = t.id;
            dnd.drop.pos = 0;
         }
         else {
            if (ch.tagName !== 'TEXTAREA')
               throw new Error("HtmlView.onDrop " + id +
                  " invalid tagName:" + ch.tagName);
            dnd.drop.pos = ch.selectionStart;
         }
         (this.dnd = dnd).ch.push(dnd.drop.id = id);
         return dnd.drag ? this.dropRule : this.globalDropRule;
      }
      catch (e) {}
   return 0;
};

/*------------------------------------------------------------------------
   HtmlView.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.HtmlView.prototype.changed = function(dlg, id, el, ty) {
   var v = [];
   for (var i = 0, l = ty.length; i < l; i++)
      switch (ty[i]) {
         case 'change': {
            var sv = el.dataset.eqLink;
            if (sv)
               delete el.dataset.eqLink;
            v.push({
               ty : eq.RsValType.textValue,
               sv : sv || ''
            });
            break;
         }

         case 'dnd': {
            var dnd = this.dnd, drag, drop, sv, iv;
            if (dnd) {
               if ((drag = dnd.drag) && drag.id === id) {
                  if ((sv = drag.content) !== undefined) {
                     drag.content = null;
                     v.push({
                        ty : eq.RsValType.dragContent,
                        sv : sv
                     });
                  }
               }
               if ((drop = dnd.drop) && drop.id === id) {
                  if ((iv = drop.pos) !== undefined) {
                     v.push({
                        ty : eq.RsValType.dropPos,
                        iv : iv
                     });
                  }
                  if ((sv = drop.element) !== undefined) {
                     drop.element = null;
                     v.push({
                        ty : eq.RsValType.dropElement,
                        sv : sv
                     });
                  }
               }
            }
            break;
         }
      }
   this.dnd = null;
   switch (v.length) {
      case 0:
         return null;
      case 1:
         v[0].id = id;
         return v[0];
      default:
         return {
            id : id,
            v : v
         };
   }
};

/** @preserve $Id: 20-listbox.js,v 28.50 2025/03/18 15:21:55 mike Exp $
*//*======================================================================
   final class ListBox extends DlgElement
   DLG ListBox element class.
========================================================================*/

eq.c.DlgElement.addClass('ListBox', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.activeCol = 0;
   this.lineOrder = null;
   this.ttHeader = null;
   this.ttLine = null;
   this.dnd = null;
   this.dndMode = 0;
   this.dropRule = 0;
   this.globalDropRule = 0;
});

delete eq.c.ListBox.subClass; // final

/*------------------------------------------------------------------------
   ListBox.dispose()
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.dispose = function(dlg, id) {
   var a;
   if ((a = this.lineOrder) !== null) {
      a.length = 0;
      this.lineOrder = null;
   }
   if ((a = this.ttHeader) !== null) {
      a.length = 0;
      this.ttHeader = null;
   }
   if ((a = this.ttLine) !== null) {
      a.length = 0;
      this.ttLine = null;
   }
   this.dnd = null;
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   static ListBox.select()
   Select ListBox line.
------------------------------------------------------------------------*/

eq.c.ListBox.select = function(el, li) {
   // Deselect.
   var n = el.querySelectorAll('.eq-column>.eq-selected');
   for (var i = 0, l = n.length; i < l; i++)
      n[i].classList.remove('eq-selected');
   // Select.
   if (li !== undefined) {
      n = el.querySelectorAll('.eq-column');
      for (var i = 0, l = n.length; i < l; i++) {
         var c = n[i].children;
         if (li < c.length)
            c[li].classList.add('eq-selected');
      }
   }
};

/*------------------------------------------------------------------------
   static ListBox.scrollIntoView()
   Scroll ListBox line into view if necessary.
------------------------------------------------------------------------*/

eq.c.ListBox.scrollIntoView = function(el, li) {
   var
      n = el.querySelector('.eq-column').children,
      bx, bh, lh, app;
   if (li < n.length) {
      bx = n[0].parentNode.parentNode;
      bh = bx.getBoundingClientRect().height;
      lh = n[0].getBoundingClientRect().height;
      if (bh > 0 && lh > 0) {
         var bt = bx.scrollTop, lt = n[li].offsetTop;
         if (lt < bt) {
            bx.scrollTop = lt;
            app = eq.app;
         }
         else if (lt + lh > bt + bh) {
            bx.scrollTop = Math.ceil(lt + lh - bh + 2);
            app = eq.app;
         }
         // 'scroll' change event.
         if (app)
            app.setChanged(el.id, 'scroll');
      }
   }
};

/*------------------------------------------------------------------------
   static ListBox.onScroll()
   Handle ListBox scroll event.
------------------------------------------------------------------------*/

eq.c.ListBox.onScroll = function() {
   if ('scroll' in this.dataset)
      delete this.dataset.scroll;
   else
      eq.Dom.onNextFrame(this, { cb : eq.c.ListBox.onScroll.exec });
};

eq.c.ListBox.onScroll.exec = function(el) {
   var cl = el.classList, lb;
   if (cl && cl.contains('eq-pane') && (lb = el.parentNode)
                                    && (cl = lb.classList)) {
      if (cl.contains('eq-listbox')) {
         // 'scroll' change event.
         eq.app.setChanged(lb.id, 'scroll');
         if (cl.contains('eq-multi')) {
            // Scroll multi-column ListBox header.
            var h = el.previousElementSibling;
            if (h)
               h.style.marginLeft = '-' + el.scrollLeft + 'px';
         }
      }
   }
};

/*------------------------------------------------------------------------
   static ListBox.setTopItem()
------------------------------------------------------------------------*/

eq.c.ListBox.setTopItem = function(el, qi) {
   var
      c = el.querySelector('.eq-column'),
      n = c.children,
      bx = c.parentNode,
      st = 0;
   if (n.length > 1) {
      var ch, bh, lh, smax;
      if (   eq.app.idle
          || (ch = c.getBoundingClientRect().height) <= 0
          || (bh = bx.getBoundingClientRect().height) <= 0
          || (lh = n[0].getBoundingClientRect().height) <= 0) {
         // Application idle, or height not available yet.
         eq.Dom.onNextFrame(el, qi);
         return;
      }
      if ((smax = ch - bh) > 0) {
         var li = qi.li, lo, vi;
         if ((lo = qi.self.lineOrder) !== null) {
            // topItem model->view.
            if ((vi = lo.indexOf(li)) === -1) {
               // topItem not in lineOrder.
               li = 0;
            }
            else
               li = vi;
         }
         if (lh > 0 && (st = li * lh) > smax)
            st = Math.ceil(smax + 2);
      }
   }
   bx.dataset.scroll = 'ignore';
   bx.scrollTop = st;
};

/*------------------------------------------------------------------------
   ListBox.setAutoWidth()
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.setAutoWidth = function(dlg, id, el) {
   eq.Dom.onNextFrame(el, {
      cb  : this.constructor.execAutoWidth,
      dlg : dlg
   });
};

eq.c.ListBox.execAutoWidth = function(el, qi) {
   var
      hn = el.firstElementChild.children,
      pn = el.lastElementChild.children,
      wmin = 2 * qi.dlg.grid.wr;
   for (var i = 0, ai = 0, l = pn.length; i < l; i++) {
      var c, cw, h, hw;
      if ((c = pn[i]).classList.contains('eq-auto')) {
         cw = Number(window.getComputedStyle(c).width.replace(/px$/,''));
         if (isNaN(cw)) {
            // Column width not available yet.
            if (ai !== 0)
               throw new Error("ListBox.execAutoWidth BUG: column[" +
                  i + "] width NaN, autowidth:" + ai);
            eq.Dom.onNextFrame(el, qi);
            return;
         }
         h = hn[i];
         hw = Number(window.getComputedStyle(h).width.replace(/px$/,''));
         if (isNaN(hw))
            throw new Error("ListBox.execAutoWidth BUG: header[" +
               i + "] width NaN, autowidth:" + ai);
         if (hw > cw)
            cw = hw;
         if (cw < wmin)
            cw = wmin;
         h.style.width = c.style.width = cw + 'px';
         ai++;
      }
   }
};

/*------------------------------------------------------------------------
   static ListBox.colResize()
   Handle ListBox column-resize move capture.
------------------------------------------------------------------------*/

eq.c.ListBox.colResize = function(e, m) {
   if (!m.busy) {
      m.busy = true;
      eq.Dom.onNextFrame(m.el, {
         cb : eq.c.ListBox.colResize.exec,
         x : e.clientX,
         m : m
      });
   }
};

eq.c.ListBox.colResize.exec = function(el, qi) {
   var
      x = qi.x,
      m = qi.m;
   if (x !== m.x) {
      var
         h = m.h,
         c = m.c,
         w = m.w + x - m.x,
         wmin = 2 * m.dlg.grid.wr;
      if (w >= wmin) {
         h.style.width = c.style.width = w + 'px';
         m.x = x;
         m.w = w;
      }
      else {
         h.style.width = c.style.width = wmin + 'px';
         m.x -= m.w - wmin;
         m.w = wmin;
      }
      m.modified = true;
   }
   m.busy = undefined;
};

eq.c.ListBox.colResize.done = function(m) {
   if (m.modified) {
      var app = eq.app, id = m.el.id;
      app.setChanged(id, 'resize');
      app.apiUpdate(id, eq.UpdateTag.colResized, {
         col : eq.Dom.parentIndex(m.h),
         width : m.w
      });
   }
   return false;
};

/*------------------------------------------------------------------------
   static ListBox.colMove()
   Handle ListBox column move capture.
------------------------------------------------------------------------*/

eq.c.ListBox.colMove = function(e, m) {
   if (!m.busy) {
      m.busy = true;
      eq.Dom.onNextFrame(m.el, {
         cb : eq.c.ListBox.colMove.exec,
         x : e.clientX,
         m : m
      });
   }
};

eq.c.ListBox.colMove.exec = function(el, qi) {
   var
      m = qi.m,
      n = m.el.getElementsByClassName('eq-space'),
      cx = qi.x,
      cw = m.cw,
      mx = m.x,
      lx = m.lx,
      from = m.f,
      to = m.t,
      x, ox, ws, i, j, l, w, nj, p;
   if (n.length !== 2) {
      m.busy = undefined;
      return;
   }
   if ((x = lx + cx - mx) < 0) {
      x = 0;
      cx = mx - lx;
   }
   if (x > lx)
      ox = m.ow;
   else if (x < lx)
      ox = 0;
   else {
      m.busy = undefined;
      return;
   }
   for (ox += x, ws = 0, i = 0, l = cw.length; i < l; i++) {
      if (i === to)
         w = cw[from];
      else if (i >= from && i < to)
         w = cw[i + 1];
      else
         w = cw[i];
      ws += w;
      if (ox < ws - w / 2)
         break;
   }
   for (j = 0; j < 2; j++) {
      p = (nj = n[j]).parentNode;
      if (i < l)
         p.insertBefore(nj, p.children[i]);
      else
         p.appendChild(nj);
   }
   m.t = eq.Dom.parentIndex(nj);
   m.ob.style.left = (m.lx = x) + 'px';
   m.x = cx;
   m.busy = undefined;
};

eq.c.ListBox.colMove.done = function(m) {
   var
      app = eq.app,
      el = m.el,
      from = m.f,
      to = m.t,
      id, tt, ttt;
   app.closeOverlay(el);
   if (to !== from) {
      id = el.id;
      app.setChanged(id, 'move');
      app.apiUpdate(id, eq.UpdateTag.colMoved, { from : from, to : to });
      if ((tt = m.self.ttHeader) !== null)
         tt.splice(to, 0, tt.splice(from, 1)[0]);
   }
   return false;
};

eq.c.ListBox.colMove.closeOverlay = function(el, ov) {
   var
      n = el.getElementsByClassName('eq-space'),
      ob = ov.firstElementChild,
      id;
   if (n.length !== 2)
      throw new Error("ListBox.colMove.closeOverlay BUG: " + n.length +
         " eq-space elements");
   var c = n[0];
   c.parentNode.replaceChild(ob.firstElementChild.firstElementChild, c);
   var c = n[0];
   c.parentNode.replaceChild(ob.lastElementChild.firstElementChild, c);
   if (id = ob.id)
      eq.app.removeStyles(id);
   ov.parentNode.removeChild(ov);
};


/*------------------------------------------------------------------------
   ListBox.setAttr()
   Set ListBox attributes.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.setAttr = function(dlg, id, el, rt) {
   var
      cls = this.constructor,
      app = eq.app, dm = eq.Dom,
      tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.columns: {
            var a, cl = el.classList;
            if ((a = this.lineOrder) !== null) {
               a.length = 0;
               this.lineOrder = null;
            }
            if ((a = this.ttHeader) !== null) {
               a.length = 0;
               this.ttHeader = null;
            }
            if (rc.l === 0) {
               // Single-column ListBox.
               if (cl.contains('eq-single'))
                  // Delete children.
                  el.firstElementChild.firstElementChild.textContent = '';
               else {
                  el.textContent = ''; // Delete children.
                  cl.remove('eq-multi');
                  cl.add('eq-single');

                  var p = document.createElement('DIV');
                  p.className = 'eq-pane';
                  p.onscroll = cls.onScroll;

                  var c = document.createElement('DIV');
                  c.className = 'eq-column';

                  p.appendChild(c);
                  el.appendChild(p);
               }
            }
            else {
               // Multi-column ListBox.
               if (cl.contains('eq-multi')) {
                  // Delete children.
                  el.firstElementChild.textContent = '';
                  el.lastElementChild.textContent = '';
               }
               else {
                  el.textContent = ''; // Delete children.
                  cl.remove('eq-single');
                  cl.add('eq-multi');

                  var h = document.createElement('DIV');
                  h.className = 'eq-caption';

                  var p = document.createElement('DIV');
                  p.className = 'eq-pane';
                  p.onscroll = cls.onScroll;

                  el.appendChild(h);
                  el.appendChild(p);
               }
            }
            break;
         }

         case tag.colOrder: {
            var
               colMove = rc.m,
               l = colMove.length,
               h = el.firstElementChild,
               hn = h.children,
               p = el.lastElementChild,
               pn = p.children,
               tt = this.ttHeader,
               to, from;
            for (to = 0; to < l; to++)
               if ((from = colMove[to]) !== to) {
                  h.insertBefore(hn[from], hn[to]);
                  p.insertBefore(pn[from], pn[to]);
                  if (tt !== null)
                     tt.splice(to, 0, tt.splice(from, 1)[0]);
               }
            break;
         }

         case tag.colState: {
            var
               base = eq.c.DlgElement,
               colShow = rc.s,
               userHide = rc.uh,
               l = colShow.length,
               h = el.firstElementChild,
               hn = h.children,
               p = el.lastElementChild,
               pn = p.children,
               ct = eq.ColType,
               i, hi, pi, cs, c, sp, cl, st, autoWidth, hasContent;
            for (i = hi = pi = 0; i < l; i++) {
               if ((cs = colShow[i]) === null) {
                  if (hi < hn.length)
                     h.removeChild(hn[hi]);
                  if (pi < pn.length)
                     p.removeChild(pn[pi]);
                  continue;
               }
               // Header.
               if (hi < hn.length)
                  sp = (c = hn[hi]).firstElementChild;
               else {
                  app.ttAttach(c = document.createElement('DIV'));
                  c.appendChild(sp = document.createElement('SPAN'));
                  h.appendChild(c);
               }
               hi++;
               if (cs.w !== null)
                  c.style.width = cs.w;
               else {
                  c.style.width = '';
                  autoWidth = true;
               }
               st = sp.style;
               base.setFont(sp, cs.fn);
               if (cs.bc !== null)
                  st.backgroundColor = cs.bc;
               if (cs.fc !== null)
                  st.color = cs.fc;
               c.className = '';
               cl = c.classList;
               if (cs.movable)
                  cl.add('eq-move');
               if (cs.sizable)
                  cl.add('eq-resize');
               if (cs.sortable)
                  cl.add('eq-sort');
               if (userHide)
                  cl.add('eq-cmc'); // Context menu child element.
               // Column.
               if (pi < pn.length) {
                  if ((c = pn[pi]).firstElementChild)
                     hasContent = true;
               }
               else {
                  c = document.createElement('DIV');
                  p.appendChild(c);
               }
               pi++;
               c.className = 'eq-column';
               cl = c.classList;
               switch (cs.type) {
                  case ct.colBoolean:
                     cl.add('eq-bool');
                     break;
                  case ct.colBarGraph:
                     cl.add('eq-bar');
               }
               st = c.style;
               if (cs.w !== null)
                  st.width = cs.w;
               else {
                  cl.add('eq-auto');
                  st.width = '';
               }
               switch (cs.align) {
                  case 0:
                     st.textAlign = '';
                     break;
                  case 1:
                     st.textAlign = 'center';
                     break;
                  case 2:
                     st.textAlign = 'right';
               }
            }
            if (autoWidth && hasContent)
               this.whenVisible(dlg, id, el, this.setAutoWidth);
            break;
         }

         case tag.colTitle: {
            var
               colTitle = rc.ct,
               l = colTitle.length,
               hn = el.firstElementChild.children,
               i, s;
            for (i = 0; i < l; i++)
               if ((s = colTitle[i]) !== undefined && s.length)
                  dm.setContent(hn[i].firstElementChild, s);
               else
                  hn[i].firstElementChild.textContent = ' ';
            break;
         }

         case tag.lineOrder: {
            var
               sl = el.querySelector('.eq-selected'),
               lo = this.lineOrder,
               ls = rc.s,
               si, h;
            if (sl) {
               // Save selection (old lineOrder).
               si = dm.parentIndex(sl);
               if (lo !== null) {
                  // selection view->model.
                  if (si >= lo.length)
                     throw new Error("ListBox.setAttr lineOrder: selection " +
                        si + " >= lineOrder " + lo.length);
                  si = lo[si];
               }
            }
            if (lo !== null)
               lo.length = 0;
            if ((lo = rc.o) !== null && lo.length === 0)
               lo = null;
            this.lineOrder = lo;
            if (sl) {
               // Restore selection (new lineOrder).
               if (lo !== null) {
                  // selection model->view.
                  if ((si = lo.indexOf(si)) === -1) {
                     // Selection not in new lineOrder.
                     sl.classList.remove('eq-selected');
                  }
               }
               if (si !== -1) {
                  cls.select(el, si);
                  cls.scrollIntoView(el, si);
               }
            }
            if ((h = el.firstElementChild)
                  .classList.contains('eq-caption')) {
               var
                  n = h.querySelectorAll(
                     '.eq-caption>.eq-up,.eq-caption>.eq-dn'),
                  l = n.length, cl;
               for (var i = 0; i < l; i++) {
                  cl = n[i].classList;
                  cl.remove('eq-up');
                  cl.remove('eq-dn');
                  cl.remove('eq-first');
                  cl.remove('eq-second');
               }
               if (l = ls.length) {
                  var
                     hc = h.children,
                     hl = hc.length;
                  for (var i = 0; i < l; i++) {
                     var
                        li = ls[i],
                        ci = li.i;
                     if (ci < hl) {
                        cl = hc[ci].classList;
                        if (li.d)
                           cl.add('eq-dn');
                        else
                           cl.add('eq-up');
                        if (i === 0)
                           cl.add('eq-first');
                        else if (i === 1)
                           cl.add('eq-second');
                     }
                  }
               }
            }
            break;
         }

         case tag.toolTip:
            if (this.toolTip = rc.v)
               app.ttAttach(el.lastElementChild);
            else {
               app.ttDetach(el.lastElementChild);
               this.toolTip = null;
            }
            break;

         case tag.colToolTip: {
            var tt = this.ttHeader;
            if (tt !== null)
               tt.length = 0;
            this.ttHeader = rc.ctt;
            break;
         }

         case tag.content: {
            var
               n = el.querySelectorAll('.eq-column'),
               nl = n.length,
               c = rc.c,
               lo = this.lineOrder,
               sl = el.querySelector('.eq-selected'),
               si = sl ? dm.parentIndex(sl) : -1,
               tt = this.ttLine,
               ttl = tt !== null ? tt.length : 0,
               dnd = this.dndMode,
               h, ni, np, nc, nn, ln, ll, cl,
               vi, i, cell, ccl, cst, s, dlgCls, autoWidth;
            if (nl !== c.length)
               throw new Error("ListBox.setAttr content: " +
                  "column count " + c.length + ", expected: " + nl);
            if (dnd)
               dlgCls = eq.c.Dlg;
            if (!(h = el.firstElementChild).classList.contains('eq-caption'))
               h = null;
            for (ni = 0; ni < nl; ni++) {
               ll = (ln = c[ni]) ? ln.length : 0;
               if (lo !== null && lo.length < ll)
                  throw new Error("ListBox.setAttr content: " +
                     "lineOrder " + lo.length + ", min. expected: " + ll);
               np = n[ni];
               if (ll)
                  nn = (nc = np.children).length;
               else {
                  // Empty, delete children.
                  np.textContent = '';
                  nc = np.children;
                  nn = 0;
               }
               cl = np.classList;
               if (cl.contains('eq-auto')) {
                  autoWidth = true;
                  np.style.width = '';
                  if (h)
                     h.children[ni].style.width = '';
               }
               for (vi = 0; vi < ll; vi++) {
                  i = lo !== null ? lo[vi] : vi;
                  if (vi < nn) {
                     cell = nc[vi];
                     (ccl = cell.classList).remove('eq-active');
                     if (vi === si)
                        ccl.add('eq-selected');
                     else
                        ccl.remove('eq-selected');
                     if (dnd) {
                        cell.draggable = true;
                        cell.ondragstart = dlgCls.onDragStart;
                     }
                     else {
                        cell.draggable = false;
                        cell.ondragstart = null;
                     }
                     if (tt !== null && i < ttl && tt[i])
                        app.ttAttach(cell);
                  }
                  else {
                     cell = document.createElement('DIV');
                     if (vi === si)
                        cell.className = 'eq-fn eq-selected';
                     else
                        cell.className = 'eq-fn';
                     ccl = cell.classList;
                     if (dnd) {
                        cell.draggable = true;
                        cell.ondragstart = dlgCls.onDragStart;
                     }
                     np.appendChild(cell);
                     if (tt !== null) {
                        if (i < ttl && tt[i])
                           app.ttAttach(cell);
                        else
                           app.ttDetach(cell);
                     }
                  }
                  cst = cell.style;
                  s = ln[i];
                  if (cl.contains('eq-bool')) {
                     if (s === '1')
                        ccl.add('eq-active');
                     cell.textContent = ' ';
                     cst.backgroundSize = '';
                  }
                  else if (cl.contains('eq-bar')) {
                     if (s !== undefined && s.length)
                        cst.backgroundSize = s + '% 100%';
                     else
                        cst.backgroundSize = '0';
                     cell.textContent = ' ';
                  }
                  else {
                     if (s !== undefined && s.length)
                        dm.setContent(cell, s);
                     else
                        cell.textContent = ' ';
                     cst.backgroundSize = '';
                  }
               }
               while (nc.length > ll)
                  np.removeChild(np.lastElementChild);
            }
            if (autoWidth)
               this.whenVisible(dlg, id, el, this.setAutoWidth);
            break;
         }

         case tag.lineToolTip: {
            var
               tt = this.ttLine,
               ttl = tt !== null ? tt.length : 0,
               ltt = rc.ltt,
               lttl = ltt !== null ? ltt.length : 0,
               lo = this.lineOrder,
               lol = lo !== null ? lo.length : 0,
               c = el.querySelectorAll('.eq-column'),
               cl = c.length,
               vl = cl ? c[0].children.length : 0,
               vi, i, ci, cc, ttSet;
            for (vi = 0; vi < vl; vi++) {
               if (lo === null)
                  i = vi;
               else {
                  // tool tip view->model.
                  if (vi >= lol) {
                     // vl is max(old,new), ignore if out of range.
                     continue;
                  }
                  i = lo[vi];
               }
               if (ttSet = ltt !== null && i < lttl && ltt[i])
                  ttSet = tt === null || i >= ttl || !tt[i];
               else if (tt !== null && i < ttl && tt[i])
                  for (ci = 0; ci < cl; ci++)
                     if (vi < (cc = c[ci].children).length)
                        app.ttDetach(cc[vi]);
               if (ttSet)
                  for (ci = 0; ci < cl; ci++)
                     if (vi < (cc = c[ci].children).length)
                        app.ttAttach(cc[vi]);
            }
            if (tt !== null)
               tt.length = 0;
            this.ttLine = ltt;
            break;
         }

         case tag.activeLine: {
            var
               li = rc.i,
               lo, vi;
            if (li > 0) {
               li--;
               if ((lo = this.lineOrder) !== null) {
                  // activeLine model->view.
                  if ((vi = lo.indexOf(li)) === -1) {
                     // activeLine not in lineOrder.
                     cls.select(el);
                     break;
                  }
                  li = vi;
               }
               cls.select(el, li);
               cls.scrollIntoView(el, li);
            }
            else
               cls.select(el);
            break;
         }

         case tag.vActiveLine: {
            var li = rc.i;
            if (li > 0) {
               cls.select(el, --li);
               cls.scrollIntoView(el, li);
            }
            else
               cls.select(el);
            break;
         }

         case tag.topItem:
            cls.setTopItem(el, {
               cb   : cls.setTopItem,
               self : this,
               li   : rc.i
            });
            break;

         case tag.dndMode: {
            var
               n = el.querySelectorAll('.eq-column>div'),
               onDragStart, cell;
            if (this.dndMode = rc.i) {
               onDragStart = dlg.constructor.onDragStart;
               for (var i = 0, l = n.length; i < l; i++) {
                  (cell = n[i]).draggable = true;
                  cell.ondragstart = onDragStart;
               }
            }
            else
               for (var i = 0, l = n.length; i < l; i++) {
                  (cell = n[i]).draggable = false;
                  cell.ondragstart = null;
               }
            break;
         }

         case tag.dropRule: {
            var p = el.lastElementChild, dlgCls;
            if ((this.dropRule = rc.i) || this.globalDropRule) {
               dlgCls = dlg.constructor;
               p.ondragover = dlgCls.onDragOver;
               p.ondrop = dlgCls.onDrop;
            }
            else {
               p.ondragover = null;
               p.ondrop = null;
            }
            break;
         }

         case tag.globalDropRule: {
            var p = el.lastElementChild, dlgCls;
            if ((this.globalDropRule = rc.i) || this.dropRule) {
               dlgCls = dlg.constructor;
               p.ondragover = dlgCls.onDragOver;
               p.ondrop = dlgCls.onDrop;
            }
            else {
               p.ondragover = null;
               p.ondrop = null;
            }
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   ListBox.onEvent()
   Filter and/or handle action event.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.onEvent = function(dlg, id, el, e) {
   if (e.type === 'keydown')
      return true;
   var t = e.target, cl, tp;
   while (t && (!(cl = t.classList) || !cl.contains('eq-listbox'))) {
      if (cl && cl.contains('eq-column'))
         break;
      if ((tp = t.parentNode) && (cl = tp.classList)
                              && cl.contains('eq-column'))
         return true;
      t = tp;
   }
   return false;
};

/*------------------------------------------------------------------------
   ListBox.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   var
      cls = this.constructor,
      app = eq.app,
      dm = eq.Dom,
      kc = dm.isEnterOrSpace(key);
   if (kc) {
      var s = el.querySelector('.eq-column>.eq-selected');
      if (s) {
         e.eq = { key : kc };
         app.processEvent(dlg, e, id, el, this);
      }
      dm.consumeEvent(e);
      return true;
   }

   switch (key) {
      case 'Escape':
      case 'Esc': // IE
         cls.select(el);
         dm.consumeEvent(e);
         return true;

      // Navigation?
      case 'Home':
      case 'End':
      case 'PageUp':
      case 'PageDown':
      case 'ArrowUp':
      case 'Up':
      case 'ArrowDown':
      case 'Down':
         break;

      default:
         // No.
         return false;
   }

   var
      n = el.querySelector('.eq-listbox .eq-column').children,
      d = 0,
      p = -1;
   if (n.length) {
      switch (key) {
         case 'Home':
            p = 0;
            kc = 36;
            break;
         case 'End':
            p = n.length - 1;
            kc = 35;
            break;
         case 'PageUp':
            d = -10;
            kc = 33;
            break;
         case 'PageDown':
            d = 10;
            kc = 34;
            break;
         case 'ArrowUp':
         case 'Up':
            d = -1;
            kc = 38;
            break;
         case 'ArrowDown':
         case 'Down':
            d = 1;
            kc = 40;
            break;
      }
      var ln = null, bx, bh, lh, li;
      if (p !== -1)
         ln = n[p];
      else if (d !== 0) {
         if ((d < -1 || d > 1) && n.length > 1) {
            // Page up/down, more than one line,
            // calculate number of visible lines.
            bx = n[0].parentNode.parentNode;
            bh = bx.getBoundingClientRect().height;
            lh = n[0].getBoundingClientRect().height;
            if (bh > 0 && lh > 0)
               d = d > 0 ? Math.floor(bh / lh) : Math.ceil(-(bh / lh));
         }
         var s = el.querySelector('.eq-column>.eq-selected');
         if (s) {
            p = dm.parentIndex(s);
            if ((p += d) < 0)
               p = 0;
            else if (p >= n.length)
               p = n.length - 1;

            ln = n[p];
         }
         else if (d > 0)
            ln = n[0];
      }

      if (ln !== null) {
         // Select line if necessary.
         if (!ln.classList.contains('eq-selected')) {
            app.setChanged(id, 'mousedown');
            cls.select(el, li = dm.parentIndex(ln));
            this.activeCol = 0;
            cls.scrollIntoView(el, li);
            // Submit on selection if enabled.
            if (this.f) {
               e.eq = { key : kc };
               app.processEvent(dlg, e, id, el, this);
            }
         }
      }
   }

   dm.consumeEvent(e);
   return true;
};

/*------------------------------------------------------------------------
   ListBox.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.onChange = function(dlg, id, el, e) {
   var
      cls = this.constructor,
      dm = eq.Dom,
      t = e.target,
      md = e.type === 'mousedown',
      cl, tp;
   while (t && (!(cl = t.classList) || !cl.contains('eq-listbox'))) {
      if (cl) {
         if (cl.contains('eq-column'))
            break;
         if (md && cl.contains('eq-resize') && t === e.target) {
            var
               i = dm.parentIndex(t),
               c = t.parentNode.nextElementSibling.firstElementChild;
            while (i--)
               c = c.nextElementSibling;
            eq.app.moveCapture({
               dlg : dlg,
               el : el,
               h : t,
               c : c,
               x : e.clientX,
               w : Number(window.getComputedStyle(t).width.replace(/px$/,''))
            }, cls.colResize, cls.colResize.done);
            dm.consumeEvent(e);
            return false;
         }
      }
      if ((tp = t.parentNode) && (cl = tp.classList)) {
         if (cl.contains('eq-column')) {
            cls.select(el, dm.parentIndex(t));
            this.activeCol = dm.parentIndex(tp) + 1;
            break;
         }
         if (md && cl.contains('eq-caption')) {
            if (t === e.target)
               break;
            eq.app.mouseDownTimers.add(150,
               cls.beginColMove, cls.colHeaderClicked, {
                  self : this,
                  id   : id,
                  el   : el,
                  e    : e,
                  hd   : t
               });
            dm.consumeEvent(e);
            return false;
         }
      }
      t = tp;
   }
   return true;
};

eq.c.ListBox.colHeaderClicked = function(arg) {
   var hd = arg.hd;
   if (hd.classList.contains('eq-sort')) {
      var
         self = arg.self,
         app = eq.app,
         id = arg.id,
         e = arg.e,
         tt = self.ttLine;
      app.setChanged(id, 'sort');
      if (tt !== null) {
         // Reset line tool tips.
         var
            ttl = tt.length,
            lo = self.lineOrder,
            lol = lo !== null ? lo.length : 0,
            c = arg.el.querySelectorAll('.eq-column'),
            cl = c.length,
            vl = cl ? c[0].children.length : 0,
            vi, i, ci, cc;
         for (vi = 0; vi < vl; vi++) {
            if (lo === null)
               i = vi;
            else {
               // tool tip view->model.
               if (vi >= lol)
                  throw new Error("ListBox.colMove.done: line " +
                     vi + " >= lineOrder " + lol);
               i = lo[vi];
            }
            if (i < ttl && tt[i])
               for (ci = 0; ci < cl; ci++)
                  if (vi < (cc = c[ci].children).length)
                     app.ttDetach(cc[vi]);
         }
      }
      app.apiUpdate(id, eq.UpdateTag.sortSequence, {
         col : eq.Dom.parentIndex(hd),
         add : e.ctrlKey || e.shiftKey
      });
   }
};

eq.c.ListBox.beginColMove = function(arg) {
   var
      self = arg.self,
      id = arg.id,
      el = arg.el,
      e = arg.e,
      hd = arg.hd;
   if (hd.classList.contains('eq-move')) {
      var
         cls = self.constructor,
         dm = eq.Dom,
         app = eq.app,
         from = dm.parentIndex(hd),
         pa = hd.parentNode,
         rc = pa.getBoundingClientRect(),
         cw = [],
         ov, ovs, ob, obs, oc, op, ow, c, n, x, h, sp;
      // Overlay clipping box.
      ov = document.createElement('DIV');
      (ovs = ov.style).position = 'absolute';
      ovs.overflow = 'hidden';
      if (el.parentNode.querySelector('.eq-root-font #' + id)) {
         ov.className = 'eq-root-font';
         eq.Dom.copyFont(ov, el);
      }
      else
         ov.className = 'eq-default-font';
      // Container.
      ob = document.createElement('DIV');
      ob.className = 'eq-listbox eq-multi';
      if (el.classList.contains('eq-focused'))
         ob.className += ' eq-focused';
      ov.appendChild(ob);
      // Header.
      oc = document.createElement('DIV');
      oc.className = 'eq-caption';
      oc.style.height = rc.height + 'px';
      ob.appendChild(oc);
      // Column.
      op = document.createElement('DIV');
      op.className = 'eq-pane';
      ob.appendChild(op);
      // Caption left, top, height.
      ovs.left = ((x = rc.left) + window.pageXOffset) + 'px';
      ovs.top = (rc.top + window.pageYOffset) + 'px';
      ovs.width = rc.width + 'px';
      h = rc.height;
      // Pane height.
      rc = (c = pa.nextElementSibling).getBoundingClientRect();
      ovs.height = (obs = ob.style).height = (rc.height + h) + 'px';
      // Column left, width.
      n = c.children;
      for (var i = 0, l = cw.length = n.length; i < l; i++) {
         rc = n[i].getBoundingClientRect();
         cw[i] = rc.width;
         if (i === from) {
            obs.left = (x = rc.left - x) + 'px';
            obs.width = (ow = rc.width) + 'px';
            c = n[i];
         }
      }
      // Spacer elements.
      sp = document.createElement('DIV');
      sp.className = 'eq-space';
      sp.style.width = hd.style.width;
      sp.appendChild(document.createElement('DIV'));
      oc.appendChild(pa.replaceChild(sp, hd));
      sp = document.createElement('DIV');
      sp.className = 'eq-column eq-space';
      sp.style.width = c.style.width;
      sp.appendChild(document.createElement('DIV'));
      op.appendChild((pa = pa.nextElementSibling).replaceChild(sp, c));
      // Activate overlay, capture column move.
      ob.id = self.duplicateCssRules(id);
      app.makeOverlay(el, ov, cls.colMove.closeOverlay);
      op.scrollTop = pa.scrollTop;
      app.moveCapture({
         self : self,
         el : el,
         ob : ob,
         ow : ow,
         cw : cw,
         lx : x,
         x : e.clientX,
         f : from,
         t : from
      }, cls.colMove, cls.colMove.done);
   }
};

/*------------------------------------------------------------------------
   ListBox.onToolTip()
   Obtain tooltip text or null if undefined.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.onToolTip = function(el) {
   var cl, tp, tt, i, lo, tc;
   while (el && (!(cl = el.classList) || !cl.contains('eq-listbox'))) {
      if (cl) {
         if (cl.contains('eq-caption'))
            return null;
         if (cl.contains('eq-column'))
            break;
      }
      if ((tp = el.parentNode) && (cl = tp.classList)) {
         if (cl.contains('eq-caption')) {
            if ((tt = this.ttHeader) !== null) {
               i = eq.Dom.parentIndex(el);
               if (i < tt.length && (tc = tt[i]))
                  return tc;
            }
            return (tc = el.textContent) ? tc : null;
         }
         if (cl.contains('eq-column')) {
            if ((tt = this.ttLine) !== null) {
               i = eq.Dom.parentIndex(el);
               if ((lo = this.lineOrder) !== null) {
                  // tool tip view->model.
                  if (i >= lo.length)
                     break;
                  i = lo[i];
               }
               if (i < tt.length && (tc = tt[i]))
                  return tc;
            }
            break;
         }
      }
      el = tp;
   }

   return eq.c.DlgElement.prototype.onToolTip.call(this, el);
};

/*------------------------------------------------------------------------
   ListBox.onDragStart()
   Start drag&drop action.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.onDragStart = function(dlg, id, el, t, e, dnd) {
   var
      dndMode = this.dndMode,
      md = eq.Dnd,
      tc, li, n, i, l;
   if (dndMode & md.local) {
      dnd.drag = { id : id };
      eq.c.DlgElement.setDropModes(e, dndMode);
      return true;
   }
   if (dndMode & md.global) {
      if (el.classList.contains('eq-multi')) {
         li = eq.Dom.parentIndex(t);
         n = t.parentNode.parentNode.children;
         for (tc = '', i = 0, l = n.length; i < l; i++) {
            if (i)
               tc += '\t';
            tc += n[i].children[li].textContent;
         }
      }
      else
         tc = t.textContent;
      e.dataTransfer.setData('text/plain', tc);
      eq.c.DlgElement.setDropModes(e, md.all);
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   ListBox.onDragOver()
   Return drop target when dragging over element.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.onDragOver = function(dlg, id, el, t) {
   var p, cl;
   while (t && (p = t.parentNode)) {
      if ((cl = p.classList) && cl.contains('eq-column'))
         return t;
      if (p === el || (cl && cl.contains('eq-pane')))
         break;
      t = p;
   }
   return null;
};

/*------------------------------------------------------------------------
   ListBox.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.onDrop = function(dlg, id, el, t, dnd) {
   var p, cl, dm, li, lo;
   while (t && (p = t.parentNode)) {
      if ((cl = p.classList) && cl.contains('eq-column'))
         break;
      if (p === el || (cl && cl.contains('eq-pane'))) {
         if (!(this.dndMode & eq.Dnd.dropExact)) {
            dnd.drop.column = -1;
            dnd.drop.line = 0;
            (this.dnd = dnd).ch.push(dnd.drop.id = id);
            return dnd.drag ? this.dropRule : this.globalDropRule;
         }
         return 0;
      }
      t = p;
   }
   if (t && p) {
      li = (dm = eq.Dom).parentIndex(t);
      if ((lo = this.lineOrder) !== null) {
         // drop.line view->model.
         if (li >= lo.length)
            throw new Error("ListBox.onDrop: dropLine " + li +
               " >= lineOrder " + lo.length);
         li = lo[li];
      }
      dnd.drop.line = li + 1;
      dnd.drop.column = dm.parentIndex(p);
      (this.dnd = dnd).ch.push(dnd.drop.id = id);
      return dnd.drag ? this.dropRule : this.globalDropRule;
   }
   return 0;
};

/*------------------------------------------------------------------------
   ListBox.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.changed = function(dlg, id, el, ty) {
   var v = [];
   for (var i = 0, l = ty.length; i < l; i++)
      switch (ty[i]) {
         case 'mousedown': {
            var
               ln = el.querySelector('.eq-selected'),
               lo, li;
            if (ln) {
               lo = this.lineOrder;
               li = eq.Dom.parentIndex(ln);
               if (lo !== null) {
                  // activeLine view->model.
                  if (li >= lo.length)
                     throw new Error("ListBox.changed: activeLine " + li +
                        " >= lineOrder " + lo.length);
                  li = lo[li];
               }
               v.push({
                  ty : eq.RsValType.activeLineCol,
                  i1 : li + 1,
                  i2 : this.activeCol
               });
            }
            else
               v.push({
                  ty : eq.RsValType.activeLineCol,
                  i1 : 0,
                  i2 : 0
               });
            break;
         }

         case 'scroll': {
            var
               n = el.querySelector('.eq-column').children,
               lo = this.lineOrder,
               li = 0, lh;
            if (n.length > 1)
               if ((lh = n[0].getBoundingClientRect().height) > 0)
                  li = Math.round(n[0].parentNode.parentNode.scrollTop / lh);
            if (lo !== null) {
               // topItem view->model.
               if (li >= lo.length)
                  throw new Error("ListBox.changed: topItem " + li +
                     " >= lineOrder " + lo.length);
               li = lo[li];
            }
            v.push({
               ty : eq.RsValType.topItem,
               iv : li
            });
            break;
         }

         case 'resize':
            v.push({ ty : eq.RsValType.colWidth });
            break;

         case 'move':
            v.push({ ty : eq.RsValType.colOrder });
            break;

         case 'sort':
            v.push({ ty : eq.RsValType.lineOrder });
            break;

         case 'dnd': {
            var dnd = this.dnd, drag, drop, iv;
            if (dnd) {
               if ((drop = dnd.drop) && drop.id === id) {
                  if ((iv = drop.column) !== undefined) {
                     v.push({
                        ty : eq.RsValType.dropColumn,
                        iv : iv
                     });
                  }
                  if ((iv = drop.line) !== undefined) {
                     v.push({
                        ty : eq.RsValType.dropLine,
                        iv : iv
                     });
                  }
               }
            }
            break;
         }
      }
   this.dnd = null;
   switch (v.length) {
      case 0:
         return null;
      case 1:
         v[0].id = id;
         return v[0];
      default:
         return {
            id : id,
            v : v
         };
   }
};

/*------------------------------------------------------------------------
   ListBox.updated()
   Act upon API thread update response.
------------------------------------------------------------------------*/

eq.c.ListBox.prototype.updated = function(dlg, id, el, tag, arg) {
   var ut = eq.UpdateTag;
   switch (tag) {
      case ut.sortSequence:
      case ut.ctxMenuAct:
         this.setAttr(dlg, id, el, arg.rt);
         // FALLTHROUGH
      case ut.colResized:
      case ut.colMoved:
         if ('rule' in el.dataset) {
            var
               app = eq.app,
               rule = el.dataset.rule;
            if (+rule === +rule) {
               app.onceRule = rule;
               app.onceId = id;
               app.submit(el, {
                  id : id,
                  type : 'change'
               });
            }
         }
         break;

      default:
         eq.c.DlgElement.prototype.updated.call(this, dlg, id, el, tag, arg);
   }
};

/** @preserve $Id: 20-menuitem.js,v 28.14 2025/09/16 10:01:57 mike Exp $
*//*======================================================================
   final class MenuItem extends DlgElement
   DLG MenuItem element class.
========================================================================*/

eq.c.DlgElement.addClass('MenuItem', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.textPos = 0;
   this.type = eq.MenuType.item;
   this.attrAccel = null;
   this.menuAccel = null;
});

delete eq.c.MenuItem.subClass; // final

/*------------------------------------------------------------------------
   MenuItem.dispose()
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.dispose = function(dlg, id) {
   var accel = this.attrAccel;
   if (accel !== null) {
      var
         da = dlg.accel,
         di = da.get(accel);
      if (di === id)
         da.delete(accel);
      this.attrAccel = null;
   }

   var accel = this.menuAccel;
   if (accel !== null) {
      var
         el = document.getElementById(id),
         dm, ed, da, di;
      if (el && (el = (dm = eq.Dom).eqControl(el.parentNode))
             && el.classList.contains('eq-menu')
             && (ed = dlg.eds.get(el.id)) !== undefined) {
         if (ed.constructor.name !== 'Menu')
            throw new Error("MenuItem.dispose(" + id +
               ") BUG: parent " + el.id + " not Menu");
         if (   (da = ed.accelMap) !== null
             && (di = da.get(accel)) === id)
            da.delete(accel);
      }
      this.menuAccel = null;
   }

   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   MenuItem.updateID()
   Element identifier has been modified.
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.updateId = function(dlg, id, oldId) {
   var accel = this.attrAccel;
   if (accel !== null) {
      var
         da = dlg.accel,
         di = da.get(accel);
      if (di === oldId)
         da.set(accel, id);
   }

   var accel = this.menuAccel;
   if (accel !== null) {
      var
         dm = eq.Dom,
         el = document.getElementById(id),
         ed, da, di;
      if (el && (el = dm.eqControl(el.parentNode))
             && el.classList.contains('eq-menu')
             && (ed = dlg.eds.get(el.id)) !== undefined) {
         if (ed.constructor.name !== 'Menu')
            throw new Error("MenuItem.dispose(" + id +
               ") BUG: parent " + el.id + " not Menu");
         if (   (da = ed.accelMap) !== null
             && (di = da.get(accel)) === oldId)
            da.set(accel, id);
      }
   }

   eq.c.DlgElement.prototype.updateId.call(this, dlg, id, oldId);
};

/*------------------------------------------------------------------------
   MenuItem.setAttr()
   Set MenuItem attributes.
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.setAttr = function(dlg, id, el, rt) {
   var dm = eq.Dom, tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.textPos:
            this.textPos = rc.v;
            break;

         case tag.type: {
            var
               ty = this.type = rc.v,
               cl = el.classList,
               mt = eq.MenuType;
            if (ty === mt.sep) {
               cl.add('eq-sep');
               el.textContent = ''; // Delete children.
            }
            else {
               cl.remove('eq-sep');
               var
                  tp = eq.TextPos,
                  et, lb, ch, l, b;
               switch (this.textPos) {
                  case tp.right:
                     cl.remove('eq-vert');
                     cl.add('eq-horz');
                     et = [ 'IMG', 'SPAN' ];
                     break;
                  case tp.left:
                     cl.remove('eq-vert');
                     cl.add('eq-horz');
                     et = [ 'SPAN', 'IMG' ];
                     break;
                  case tp.bottom:
                     cl.remove('eq-horz');
                     cl.add('eq-vert');
                     et = [ 'IMG', 'SPAN' ];
                     break;
                  case tp.top:
                     cl.remove('eq-horz');
                     cl.add('eq-vert');
                     et = [ 'SPAN', 'IMG' ];
                     break;
                  default:
                     cl.remove('eq-horz');
                     cl.remove('eq-vert');
               }
               if (lb = dm.firstChildByTagName(el, 'LABEL'))
                  l = (ch = lb.children).length;
               else {
                  lb = document.createElement('LABEL');
                  lb.className = 'eq-rt eq-fn';
                  el.insertBefore(lb, el.firstElementChild);
                  l = 0;
               }
               if (et) {
                  if (l !== 2 || ch[0].tagName !== et[0]
                              || ch[1].tagName !== et[1]) {
                     if (l)
                        lb.textContent = ''; // Delete children.
                     lb.appendChild(document.createElement(et[0]));
                     lb.appendChild(document.createElement(et[1]));
                  }
               }
               else if (l)
                  lb.textContent = ''; // Delete children.
               b = dm.firstChildByTagName(el, 'INPUT');
               switch (ty) {
                  case mt.radio:
                  case mt.check: {
                     var bid;
                     if (b)
                        bid = b.id;
                     else {
                        b = document.createElement('INPUT');
                        b.id = bid = el.id + '-1';
                        b.className = 'eq-rt';
                        el.insertBefore(b, el.firstElementChild);
                     }
                     lb.htmlFor = bid;
                     if (ty === mt.radio) {
                        b.type = 'radio';
                        // Set button group.
                        var ep = el;
                        while (ep = ep.parentNode)
                           if (bid = ep.id) {
                              b.name = bid;
                              break;
                           }
                     }
                     else {
                        b.type = 'checkbox';
                        b.removeAttribute('name');
                     }
                     break;
                  }
                  default:
                     lb.removeAttribute('for');
                     if (b)
                        el.removeChild(b);
               }
            }
            break;
         }

         case tag.text: {
            var lb = dm.firstChildByTagName(el, 'LABEL');
            if (lb) {
               var
                  sp = dm.firstChildByTagName(lb, 'SPAN'),
                  mb = el, cl, mn, ac, ed;
               while (mb = dm.eqControl(mb.parentNode)) {
                  if ((cl = mb.classList).contains('eq-menubar')) {
                     if (cl.contains('eq-overlay'))
                        mb = null;
                     break;
                  }
                  if (cl.contains('eq-menu') && !mn)
                     mn = mb;
               }
               ac = dm.setContent(sp || lb, rc.v, true, !mb);
               if (mb && mn) {
                  // Set menu accelerator.
                  if ((ed = dlg.eds.get(mn.id)) === undefined)
                     throw new Error("MenuItem.setAttr(" + id +
                        ") text BUG: parent " + mn.id + " not registered");
                  if (ed.constructor.name !== 'Menu')
                     throw new Error("MenuItem.setAttr(" + id +
                        ") text BUG: parent " + mn.id + " not Menu");
                  this.menuAccel = ed.setMenuAccel(id, this.menuAccel, ac);
               }
            }
            break;
         }

         case tag.accel: {
            var
               accel = this.attrAccel,
               acTag = {},
               ac = dm.decodeAccelAttr(rc.v, acTag);
            if (ac !== accel) {
               var
                  da = dlg.accel,
                  tg = el.querySelector('.eq-acc'),
                  lb;
               if (accel !== null) {
                  var di = da.get(accel);
                  if (di === id)
                     da.delete(accel);
               }
               if (ac !== null && !da.has(ac)) {
                  da.set(this.attrAccel = ac, id);
                  if (!tg) {
                     tg = document.createElement('DIV');
                     tg.className = 'eq-acc';
                     el.appendChild(tg);
                  }
                  tg.textContent = acTag.tag;
               }
               else {
                  this.attrAccel = null;
                  if (tg)
                     tg.parentNode.removeChild(tg);
               }
            }
            break;
         }

         case tag.icon: {
            var
               lb = dm.firstChildByTagName(el, 'LABEL'),
               im;
            if (lb && (im = dm.firstChildByTagName(lb, 'IMG')))
               im.src = rc.v;
            break;
         }

         case tag.active: {
            var ch = el.firstElementChild;
            if (ch && ch.tagName === 'INPUT')
               ch.checked = rc.v !== 0;
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   MenuItem.setSensitive()
   Set element-specific 'sensitive' properties.
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.setSensitive = function(dlg, id, el, sensitive) {
   var ch = el.firstElementChild;
   if (ch && ch.tagName === 'INPUT')
      ch.disabled = !sensitive;
   eq.c.DlgElement.prototype.setSensitive.call(this, dlg, id, el, sensitive);
};

/*------------------------------------------------------------------------
   MenuItem.onEvent()
   Filter and/or handle action event.
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.onEvent = function(dlg, id, el, e) {
   var dm = eq.Dom, app = eq.app, ov;
   if (ov = dm.eqOverlay(el)) {
      // Context menu item.
      dm.consumeEvent(e);
      if ('rule' in el.dataset) {
         var
            rule = el.dataset.rule,
            ed = dlg.eds.get(ov.id);
         // Obtain requesting element.
         id = ed.contextMenuId;
         el = id ? document.getElementById(id) : null;
         if (el && +rule === +rule) {
            app.onceRule = rule;
            app.onceId = id;
            app.submit(el, {
               id : id,
               type : 'contextmenu'
            });
            return false;
         }
      }
      app.closeOverlay(ov);
      return false;
   }
   return true;
};

/*------------------------------------------------------------------------
   MenuItem.onAccel()
   Handle accelerator key event.
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.onAccel = function(dlg, id, el) {
   // Simulate INPUT event if necessary.
   var
      dm = eq.Dom,
      ci, ck, nm, p, ni, op, cl;
   if (   (ci = el.firstElementChild) && ci.tagName === 'INPUT'
       && (ck = ci.checked) !== undefined) {
      if (nm = ci.name) {
         // Radio button in group.
         if (!ck && (p = el.parentNode)) {
            var n = p.querySelectorAll('li>input[name="' + nm + '"]');
            for (var i = n.length; --i >= 0;)
               if ((ni = n[i]) !== ci)
                  ni.checked = false;
            ci.checked = true;
         }
      }
      else {
         // Checkbox.
         ci.checked = !ck;
      }
   }

   // Flash top-level menu if item is in closed menu.
   for (p = el, op = true, ni = null; p = dm.eqControl(p.parentNode);) {
      if ((cl = p.classList).contains('eq-menubar'))
         break;
      if (cl.contains('eq-menu')) {
         ni = p;
         if (!cl.contains('eq-open'))
            op = false;
      }
   }

   dm.closeMenu();
   if (ni && !op) {
      ni.classList.add('eq-flash');
      window.setTimeout(this.constructor.flashMenu, 100, ni);
   }

   eq.app.processEvent(dlg, { type : 'click', target : el }, id, el, this);
};

eq.c.MenuItem.flashMenu = function(ni) {
   ni.classList.remove('eq-flash');
};

/*------------------------------------------------------------------------
   MenuItem.onSubmit()
   Prepare submission, enqueue change if not already in queue.
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.onSubmit = function(dlg, id, el, ev) {
   var mt = eq.MenuType, app;
   switch (this.type) {
      case mt.radio:
      case mt.check:
         if (!(app = eq.app).changed.has(id))
            app.changed.set(id, 'input');
   }
};

/*------------------------------------------------------------------------
   MenuItem.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.onChange = function(dlg, id, el, e) {
   var dm = eq.Dom, app, ov;
   if (ov = dm.eqOverlay(el)) {
      // Context menu item, remove from changed elements.
      (app = eq.app).changed.delete(id);
      if ('obj' in el.dataset) {
         id = el.dataset.obj;
         app.apiUpdate(id, eq.UpdateTag.ctxMenuAct, {
            c : dm.parentIndex(el)
         });
      }
      else if ('item' in el.dataset) {
         id = el.dataset.item;
         app.apiUpdate(null, eq.UpdateTag.ctxMenuAct, { i : id });
         // Check/toggle original element if present.
         var ch = document.getElementById(id), ci, ck, nm;
         if (ch && (ci = ch.firstElementChild) && ci.tagName === 'INPUT'
                && (ck = ci.checked) !== undefined) {
            if (nm = ci.name) {
               // Radio button in group.
               if (!ck && (ch = ch.parentNode)) {
                  var n = ch.querySelectorAll('li>input[name="' + nm + '"]');
                  for (var i = n.length; --i >= 0;)
                     if ((ch = n[i]) !== ci)
                        ch.checked = false;
                  ci.checked = true;
               }
            }
            else {
               // Checkbox.
               ci.checked = !ck;
            }
         }
      }
      if (!('rule' in el.dataset))
         app.closeOverlay(ov);
   }
   else if (e.menuMouseUp) {
      // 'mouseup' on menu item, simulate INPUT event if necessary.
      var ci, ck, nm;
      if (   (ci = el.firstElementChild) && ci.tagName === 'INPUT'
          && (ck = ci.checked) !== undefined) {
         if (nm = ci.name) {
            // Radio button in group.
            if (!ck && (el = el.parentNode)) {
               var n = el.querySelectorAll('li>input[name="' + nm + '"]');
               for (var i = n.length; --i >= 0;)
                  if ((el = n[i]) !== ci)
                     el.checked = false;
               ci.checked = true;
            }
         }
         else {
            // Checkbox.
            ci.checked = !ck;
         }
      }
   }
   return true;
};

/*------------------------------------------------------------------------
   MenuItem.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.MenuItem.prototype.changed = function(dlg, id, el, ty) {
   var mt = eq.MenuType, b;
   switch (this.type) {
      case mt.radio:
      case mt.check:
         if (b = eq.Dom.inputElement(el)) {
            var ch = {
               id : id,
               ty : eq.RsValType.buttonChecked,
               iv : b.checked ? 1 : 0
            };
            return ch;
         }
   }
   return null;
};

/** @preserve $Id: 20-menu.js,v 28.13 2025/07/09 07:39:19 mike Exp $
*//*======================================================================
   class Menu extends DlgElement
   DLG Menu element class.
========================================================================*/

eq.c.DlgElement.addClass('Menu', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.menuAccel = null;
   this.accelMap = null;
   this.textPos = 0;
});

/*------------------------------------------------------------------------
   Menu.dispose()
------------------------------------------------------------------------*/

eq.c.Menu.prototype.dispose = function(dlg, id) {
   if (this.accelMap !== null) {
      this.accelMap.clear();
      this.accelMap = null;
   }

   var accel = this.menuAccel;
   if (accel !== null) {
      var
         el = document.getElementById(id),
         dm, ed, da, di;
      if (el && (el = (dm = eq.Dom).eqControl(el.parentNode))
             && el.classList.contains('eq-menu')
             && (ed = dlg.eds.get(el.id)) !== undefined) {
         if (ed.constructor.name !== 'Menu')
            throw new Error("Menu.dispose(" + id +
               ") BUG: parent " + el.id + " not Menu");
         if (   (da = ed.accelMap) !== null
             && (di = da.get(accel)) === id)
            da.delete(accel);
      }
      this.menuAccel = null;
   }

   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   Menu.updateID()
   Element identifier has been modified.
------------------------------------------------------------------------*/

eq.c.Menu.prototype.updateId = function(dlg, id, oldId) {
   var accel = this.menuAccel;
   if (accel !== null) {
      var
         dm = eq.Dom,
         el = document.getElementById(id),
         ed, da, di;
      if (el && (el = dm.eqControl(el.parentNode))
             && el.classList.contains('eq-menu')
             && (ed = dlg.eds.get(el.id)) !== undefined) {
         if (ed.constructor.name !== 'Menu')
            throw new Error("Menu.dispose(" + id +
               ") BUG: parent " + el.id + " not Menu");
         if (   (da = ed.accelMap) !== null
             && (di = da.get(accel)) === oldId)
            da.set(accel, id);
      }
   }

   eq.c.DlgElement.prototype.updateId.call(this, dlg, id, oldId);
};

/*------------------------------------------------------------------------
   static Menu.addClass()
   Subclassing convenience helper: Add DLG element class.
------------------------------------------------------------------------*/

eq.c.Menu.addClass = function(name, cls) {
   this.subClass(name, cls);
   eq.d.set(name, cls);
   return cls;
};

/*------------------------------------------------------------------------
   static Menu.openMenu()
   Open menu.
------------------------------------------------------------------------*/

eq.c.Menu.openMenu = function(dlg, el) {
   var
      p = el,
      cl, did, d, rc, drc, prc;
   while ((p = p.parentNode) && p !== document)
      if ((cl = p.classList) && cl.contains('eq-menubar')) {
         cl = el.classList;
         cl.remove('eq-rtl');
         cl.add('eq-ltr');
         cl.add('eq-open');
         if (el = (p = el).lastElementChild) {
            did = '.eq-d' + dlg.id;
            d = document.querySelector('.eq-dialog'+ did);
            if (!d)
               d = document.querySelector('.eq-window' + did + ' .eq-dialog');
            if (d) {
               rc = el.getBoundingClientRect();
               drc = d.getBoundingClientRect();
               if (rc.right > drc.right) {
                  prc = p.getBoundingClientRect();
                  if (prc.right - rc.width >= drc.left) {
                     cl.remove('eq-ltr');
                     cl.add('eq-rtl');
                  }
               }
            }
         }
         return;
      }
};

/*------------------------------------------------------------------------
   static Menu.closeContextMenu()
   Close context menu overlay.
------------------------------------------------------------------------*/

eq.c.Menu.closeContextMenu = function(el) {
   var dlg = eq.app.currDlg;
   if (!dlg)
      throw new Error("BUG: Menu.closeContextMenu no current dialog");
   dlg.removeElement(el);
};

/*------------------------------------------------------------------------
   Menu.setAttr()
   Set Menu attributes.
------------------------------------------------------------------------*/

eq.c.Menu.prototype.setAttr = function(dlg, id, el, rt) {
   var dm = eq.Dom, tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.textPos: {
            var
               cl = el.classList,
               tp = eq.TextPos,
               et, lb, ch, l;
            switch (this.textPos = rc.v) {
               case tp.right:
                  cl.remove('eq-vert');
                  cl.add('eq-horz');
                  et = [ 'IMG', 'SPAN' ];
                  break;
               case tp.left:
                  cl.remove('eq-vert');
                  cl.add('eq-horz');
                  et = [ 'SPAN', 'IMG' ];
                  break;
               case tp.bottom:
                  cl.remove('eq-horz');
                  cl.add('eq-vert');
                  et = [ 'IMG', 'SPAN' ];
                  break;
               case tp.top:
                  cl.remove('eq-horz');
                  cl.add('eq-vert');
                  et = [ 'SPAN', 'IMG' ];
                  break;
               default:
                  cl.remove('eq-horz');
                  cl.remove('eq-vert');
            }
            if (lb = dm.firstChildByTagName(el, 'LABEL'))
               l = (ch = lb.children).length;
            else {
               lb = document.createElement('LABEL');
               lb.className = 'eq-rt eq-fn';
               el.insertBefore(lb, el.firstElementChild);
               l = 0;
            }
            if (et) {
               if (l !== 2 || ch[0].tagName !== et[0]
                           || ch[1].tagName !== et[1]) {
                  if (l)
                     lb.textContent = ''; // Delete children.
                  lb.appendChild(document.createElement(et[0]));
                  lb.appendChild(document.createElement(et[1]));
               }
            }
            else if (l)
               lb.textContent = ''; // Delete children.
            break;
         }

         case tag.text: {
            var lb = dm.firstChildByTagName(el, 'LABEL');
            if (lb) {
               var
                  sp = dm.firstChildByTagName(lb, 'SPAN'),
                  mb = el, cl, mn, ac, ed;
               while (mb = dm.eqControl(mb.parentNode)) {
                  if ((cl = mb.classList).contains('eq-menubar')) {
                     if (cl.contains('eq-overlay'))
                        mb = null;
                     break;
                  }
                  if (cl.contains('eq-menu') && !mn)
                     mn = mb;
               }
               ac = dm.setContent(sp || lb, rc.v, true, !mb);
               if (mb) {
                  if (mn) {
                     // Set menu accelerator.
                     if ((ed = dlg.eds.get(mn.id)) === undefined)
                        throw new Error("Menu.setAttr(" + id +
                           ") text BUG: parent " + mn.id + " not registered");
                     if (ed.constructor.name !== 'Menu')
                        throw new Error("Menu.setAttr(" + id +
                           ") text BUG: parent " + mn.id + " not Menu");
                     this.menuAccel = ed.setMenuAccel(id, this.menuAccel, ac);
                  }
                  else {
                     // Set Dialog accelerator.
                     this.setAccel(dlg, id, ac, eq.KeyMod.alt);
                  }
               }
            }
            break;
         }

         case tag.icon: {
            var
               lb = dm.firstChildByTagName(el, 'LABEL'),
               im;
            if (lb && (im = dm.firstChildByTagName(lb, 'IMG')))
               im.src = rc.v;
            break;
         }

         case tag.align: {
            var
               cl = el.classList,
               sp;
            if (rc.v) {
               if (!cl.contains('eq-right')) {
                  cl.add('eq-right');
                  sp = document.createElement('LI');
                  sp.className = 'eq-sep';
                  el.parentNode.insertBefore(sp, el);
               }
            }
            else if (cl.contains('eq-right')) {
               cl.remove('eq-right');
               el.parentNode.removeChild(el.previousElementSibling);
            }
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   Menu.setMenuAccel()
   Set menu accelerator.
------------------------------------------------------------------------*/

eq.c.Menu.prototype.setMenuAccel = function(id, accel, key) {
   var ac = key !== null ? key.toUpperCase() : null;
   if (ac !== accel) {
      var da = this.accelMap;
      if (da === null)
         this.accelMap = da = new Map();
      if (accel !== null) {
         var di = da.get(accel);
         if (di === id)
            da.delete(accel);
      }
      if (ac === null || da.has(ac))
         return null;
      da.set(ac, id);
   }
   return ac;
};

/*------------------------------------------------------------------------
   Menu.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.Menu.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (!kmod)
   {
      switch (key) {
         case 'Escape':
         case 'Esc': { // IE
            var p, cl;
            if (   (p = el.parentNode)
                && (cl = p.classList)
                && cl.contains('eq-menubar')) {
               // MenuBar top-level menu.
               eq.Dom.closeMenu();
            }
            else
               el.classList.remove('eq-open');
            return true;
         }
         default:
            if (key.length === 1)
            {
               var da = this.accelMap, di, ed;
               if (   da !== null
                   && (di = da.get(key.toUpperCase()))
                   && (el = document.getElementById(di))
                   && (ed = dlg.eds.get(di)) !== undefined) {
                  ed.onAccel(dlg, di, el);
               }
               return true;
            }
      }
   }
   return false;
};

/*------------------------------------------------------------------------
   Menu.onAccel()
   Handle accelerator key event.
------------------------------------------------------------------------*/

eq.c.Menu.prototype.onAccel = function(dlg, id, el) {
   this.onChange(dlg, id, el, null);
};

/*------------------------------------------------------------------------
   Menu.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.Menu.prototype.onChange = function(dlg, id, el, e) {
   var
      app = eq.app,
      dm = eq.Dom,
      p = el.parentNode,
      cl,
      t = p && (cl = p.classList) && cl.contains('eq-menubar'),
      o = (cl = el.classList).contains('eq-open');
   if (dm.eqOverlay(el)) {
      // Context menu item, remove from changed elements.
      app.changed.delete(id);
      // Not a MenuBar top-level menu.
      t = false;
   }
   if (t) {
      // MenuBar top-level menu.
      dm.closeMenu();
      if (!o) {
         this.constructor.openMenu(dlg, el);
         app.menuOpen = id;
         // Close tooltip if any.
         app.constructor.ttClose();
      }
   }
   else if (o)
      cl.remove('eq-open');
   else {
      var
         cls = this.constructor,
         mo = [ el ],
         tn;
      while (p) {
         if (   ((tn = (el = p).tagName) !== 'UL' || tn !== 'LI')
             || ((p = p.parentNode) && (cl = p.classList)
                                    && cl.contains('eq-menubar'))) {
            var n = el.getElementsByClassName('eq-open');
            for (var i = n.length; --i >= 0;)
               n[i].classList.remove('eq-open');
            break;
         }
         if ((cl = p.classList) && cl.contains('eq-menu'))
            mo.push(p);
      }
      for (var i = mo.length; --i >= 0;)
         cls.openMenu(dlg, mo[i]);
   }
   return false;
};

/** @preserve $Id: 20-plugin.js,v 28.16 2025/01/28 12:12:52 mike Exp $
*//*======================================================================
   DLG plugin API version.
   API Version 1 since WEBDLG v2 version 2.0.15
   API Version 2 since WEBDLG v2 version 2.0.16
   API Version 3 since WEBDLG v2 version 2.0.18
========================================================================*/

eq.p.pluginApiVersion = 3;

/*========================================================================
   class DlgPlugin extends DlgElement
   DLG plugin base class.
========================================================================*/

eq.c.DlgElement.subClass('DlgPlugin', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.values = new Map();
   var
      cls = this.constructor,
      init = cls.instanceMethods ? this.apiInit : cls.init;
   if (init)
      init.call(this, dlg, el);
});

// Instance methods supported since API version 1.
eq.c.DlgPlugin.instanceMethods = false;

/*------------------------------------------------------------------------
   DlgPlugin.dispose()
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.dispose = function(dlg, id) {
   var
      cls = this.constructor,
      dispose = cls.instanceMethods ? this.apiDispose : cls.dispose,
      m;
   if ((m = this.values) !== null) {
      m.clear();
      this.values = null;
   }
   if (dispose)
      dispose.call(this, dlg, id);
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   static DlgPlugin.addClass()
   Subclassing convenience helper: Add DLG plugin class.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.addClass = function(name, cls) {
   this.subClass(name, cls);
   eq.d.set(name, cls);
   return cls;
};

/*------------------------------------------------------------------------
   DlgPlugin.intAttrChanged() .stringAttrChanged() .binaryAttrChanged()
   Attribute value has changed.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.intAttrChanged = function(id, type, atr, idx, iv) {
   var app = eq.app, k;
   app.setChanged(id, type);
   if (idx !== undefined && idx !== null)
      k = atr + ' ' + idx;
   else
      k = atr;
   this.values.set(k, {
      ty : eq.RsValType.plgInt,
      a  : atr,
      i  : idx,
      iv : iv
   });
};

eq.c.DlgPlugin.prototype.stringAttrChanged = function(id, type, atr, idx, sv) {
   var app = eq.app, k;
   app.setChanged(id, type);
   if (idx !== undefined && idx !== null)
      k = atr + '[' + idx + ']';
   else
      k = atr;
   this.values.set(k, {
      ty : eq.RsValType.plgString,
      a  : atr,
      i  : idx,
      sv : sv
   });
};

eq.c.DlgPlugin.prototype.binaryAttrChanged = function(id, type, atr, idx, bv) {
   var app = eq.app, k;
   app.setChanged(id, type);
   if (idx !== undefined && idx !== null)
      k = atr + '[' + idx + ']';
   else
      k = atr;
   this.values.set(k, {
      ty : eq.RsValType.plgBinary,
      a  : atr,
      i  : idx,
      bv : bv
   });
};

/*------------------------------------------------------------------------
   DlgPlugin.submit()
   Submit event.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.submit = function(id, el, type, rule) {
   var app = eq.app;
   if (rule !== undefined) {
      app.onceRule = rule;
      app.onceId = id;
   }
   app.submit(el, {
      id : id,
      type : type
   });
};

/*------------------------------------------------------------------------
   DlgPlugin.setAttr()
   Set plugin attributes.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.setAttr = function(dlg, id, el, rt) {
   var rqTag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri], tag = rc.t;
      switch (tag) {
         case rqTag.plgInt:
         case rqTag.plgString:
         case rqTag.plgBinary:
         case rqTag.font: {
            var
               cls = this.constructor,
               setAttr = cls.instanceMethods
                       ? this.apiSetAttr : cls.setAttr;
            if (setAttr)
               setAttr.call(this, dlg, id, el, rc.a, rc.i, rc.v);
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   DlgPlugin.setSensitive()
   Set element-specific 'sensitive' properties.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.setSensitive = function(dlg, id, el, sensitive) {
   var
      cls = this.constructor,
      setSensitive = cls.instanceMethods
                   ? this.apiSetSensitive : cls.setSensitive;
   if (setSensitive)
      setSensitive.call(this, dlg, id, el, sensitive);
   eq.c.DlgElement.prototype.setSensitive.call(this, dlg, id, el, sensitive);
};

/*------------------------------------------------------------------------
   DlgPlugin.onEvent()
   Filter and/or handle action event.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.onEvent = function(dlg, id, el, e) {
   var
      cls = this.constructor,
      onEvent = cls.instanceMethods ? this.apiOnEvent : cls.onEvent;
   if (onEvent)
      return onEvent.call(this, dlg, id, el, e);
   return true;
};

/*------------------------------------------------------------------------
   DlgPlugin.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   var
      cls = this.constructor,
      onKey = cls.instanceMethods ? this.apiOnKey : cls.onKey,
      rc;
   if (onKey && (rc = onKey.call(this, dlg, id, el, e)) !== false) {
      if (rc)
         eq.Dom.consumeEvent(e);
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   DlgPlugin.onTypeAhead()
   Process type-ahead key stroke.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.onTypeAhead = function(dlg, id, el, key) {
   var
      cls = this.constructor,
      onTypeAhead = cls.instanceMethods
                  ? this.apiOnTypeAhead : cls.onTypeAhead;
   if (onTypeAhead && onTypeAhead.call(this, dlg, id, el, key))
      return true;
   return eq.c.DlgElement.prototype.onTypeAhead.call(this, dlg, id, el, key);
};

/*------------------------------------------------------------------------
   DlgPlugin.onSubmit()
   Prepare submission.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.onSubmit = function(dlg, id, el, ev) {
   var
      cls = this.constructor,
      onSubmit = cls.instanceMethods ? this.apiOnSubmit : cls.onSubmit;
   if (onSubmit)
      onSubmit.call(this, dlg, id, el, ev);
};

/*------------------------------------------------------------------------
   DlgPlugin.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.onChange = function(dlg, id, el, e) {
   var
      cls = this.constructor,
      onChange = cls.instanceMethods ? this.apiOnChange : cls.onChange;
   if (onChange)
      return onChange.call(this, dlg, id, el, e);
   return true;
};

/*------------------------------------------------------------------------
   DlgPlugin.focusElement()
   Obtain focus element.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.focusElement = function(dlg, el) {
   var
      cls = this.constructor,
      focusElement = cls.instanceMethods
                   ? this.apiFocusElement : cls.focusElement;
   if (focusElement)
      return focusElement.call(this, dlg, el);
   return el;
};

/*------------------------------------------------------------------------
   DlgPlugin.focusGained()
   Element has gained focus.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.focusGained = function(dlg, ct) {
   var
      cls = this.constructor,
      focusGained = cls.instanceMethods
                  ? this.apiFocusGained : cls.focusGained;
   if (focusGained)
      return focusGained.call(this, dlg, ct);
};

/*------------------------------------------------------------------------
   DlgPlugin.focusLost()
   Element has lost focus.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.focusLost = function(dlg, el) {
   var
      cls = this.constructor,
      focusLost = cls.instanceMethods ? this.apiFocusLost : cls.focusLost;
   if (focusLost)
      return focusLost.call(this, dlg, el);
};

/*------------------------------------------------------------------------
   DlgPlugin.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.changed = function(dlg, id, el, ty) {
   var
      cls = this.constructor,
      changed = cls.instanceMethods ? this.apiChanged : cls.changed,
      v = [];
   if (changed)
      changed.call(this, dlg, id, el, ty);
   this.values.forEach(eq.c.DlgPlugin.cbChanged, v);
   this.values.clear();
   switch (v.length) {
      case 0:
         return null;
      case 1:
         v[0].id = id;
         return v[0];
      default:
         return {
            id : id,
            v : v
         };
   }
};

eq.c.DlgPlugin.cbChanged = function(val) {
   this.push(val);
};

/*------------------------------------------------------------------------
   DlgPlugin.onUpdate()
   Act upon API thread update request.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.onUpdate = function(dlg, id, el, tag, atr, idx) {
   var
      cls = this.constructor,
      ut = eq.UpdateTag;
   if (atr < 0 || atr >= cls.atr.length)
      throw new Error(cls.name + ".onUpdate(" + id +
         ") failed: attribute index " + atr + " out of range");
   switch (tag) {
      case ut.plgUpdate: {
         var onUpdate = cls.instanceMethods ? this.apiOnUpdate : cls.onUpdate;
         if (onUpdate)
            onUpdate.call(this, dlg, id, el, cls.atr[atr][0], idx);
         return undefined;
      }

      case ut.plgGetVal: {
         var
            getAttr = cls.instanceMethods ? this.apiGetAttr : cls.getAttr,
            v;
         if (   getAttr
             &&     (v = getAttr.call(this, dlg, id, el, cls.atr[atr][0], idx))
                !== undefined) {
            if (v === null)
               return null;
            if (v instanceof Uint8Array)
               return { b : v };
            if (typeof v === 'number')
               return { i : v };
            return { s : v };
         }
         return undefined;
      }

      default:
         throw new Error(cls.name + ".onUpdate(" + id +
            ") failed: tag " + tag);
   }
};

/*------------------------------------------------------------------------
   DlgPlugin.onUpdateReturn()
   Handle asynchronous onUpdate() return.
------------------------------------------------------------------------*/

eq.c.DlgPlugin.prototype.onUpdateReturn = function(dlg, id, val) {
   var v;
   if (val === undefined)
      v = undefined;
   else {
      if (val === null)
         throw new Error("BUG: " + this.constructor.name + ".onUpdate(" +
            id + ") null value");
      if (val instanceof Uint8Array)
         v = { b : val };
      else if (typeof val === 'number')
         v = { i : val };
      else
         v = { s : val };
   }

   eq.c.DlgElement.prototype.onUpdateReturn.call(this, dlg, id, v);
};

/*========================================================================
   Install plugins from declarations.
========================================================================*/

eq.plugin.install = function() {
   var m = this.m, pa = [];
   if (m !== undefined) {
      // Iterate plugin declarations, install plugins.
      m.forEach(this.cb, pa);
      m.clear();
   }
   return pa;
};

eq.plugin.cb = function(decl, name) {
   var minVersion = decl.prp.minVersion;
   if (minVersion !== undefined && minVersion > eq.p.pluginApiVersion)
      console.error("Insufficient API version " + eq.p.pluginApiVersion +
         " for plugin '" + name + "', expected: " + minVersion + " or newer");
   else if (!('plg' + name in eq.c)) {
      var
         pa = { n : name },
         plg = eq.c.DlgPlugin.addClass('plg' + name, function(dlg, el) {
            eq.c.DlgPlugin.call(this, dlg, el);
         }),
         instanceMethods, proto, install;
      delete plg.subClass; // final
      // Instance methods supported since API version 1.
      if (instanceMethods = minVersion >= 1) {
         plg.instanceMethods = true;
         proto = plg.prototype;
      }
      for (var p in decl)
         switch (p) {
            case 'atr':
               pa.a = plg.atr = decl.atr;
               break;
            case 'prp':
               pa.p = decl.prp;
               break;
            case 'static': {
               var st = decl.static;
               for (var s in st) {
                  if (s in plg)
                     console.warn("Plugin '" + name +
                        "' overrides static method: " + s);
                  plg[s] = st[s];
               }
               break;
            }
            default:
               if (instanceMethods) {
                  if (p in proto)
                     console.warn("Plugin '" + name +
                        "' overrides instance method: " + p);
                  proto[p] = decl[p];
               }
               else {
                  if (p in plg)
                     console.warn("Plugin '" + name +
                        "' overrides method: " + p);
                  plg[p] = decl[p];
               }
         }
      if (minVersion === undefined) {
         // Backward compatibility, set PRIORITY flag on all attributes.
         var a = pa.a;
         for (var i = 0, l = a.length; i < l; i++)
            a[i][1] |= eq.atrPRI;
      }
      this.push(pa);
      if (install = plg.install)
         install.call(plg);
   }
};

/** @preserve $Id: 20-poptext.js,v 28.20 2024/07/11 11:21:05 mike Exp $
*//*======================================================================
   final class PopText extends DlgElement
   DLG PopText element class.
========================================================================*/

eq.c.DlgElement.addClass('PopText', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
});

delete eq.c.PopText.subClass; // final

/*------------------------------------------------------------------------
   static PopText.line()
   Obtain PopText line from item list.
------------------------------------------------------------------------*/

eq.c.PopText.line = function(t, co) {
   var c, cl;
   while (t) {
      if (!(c = t.parentNode))
         break;
      if (cl = c.classList) {
         if (cl.contains('eq-column')) {
            if (co !== undefined)
               co.c = c;
            return t;
         }
         if (cl.contains('eq-poptext')) {
            if (co !== undefined)
               co.ct = c;
            break;
         }
      }
      t = t.parentNode;
   }
   return undefined;
};

/*------------------------------------------------------------------------
   static PopText.select()
   Select PopText line in item list.
------------------------------------------------------------------------*/

eq.c.PopText.select = function(el, c, ci) {
   if (c === undefined)
      if (!(c = el.querySelector('.eq-poptext>.eq-column')))
         c = document.body.querySelector('body>.eq-poptext.eq-column');
   // Deselect.
   var n = c.querySelectorAll('.eq-column>.eq-selected');
   for (var i = 0, l = n.length; i < l; i++)
      n[i].classList.remove('eq-selected');
   // Select.
   if (ci && c === ci.parentNode) {
      ci.classList.add('eq-selected');
      // Scroll into view if necessary.
      if ((n = c.children).length > 1)
         this.scrollIntoView(ci, { cb : this.scrollIntoView });
   }
};

/*------------------------------------------------------------------------
   static PopText.scrollIntoView()
   Scroll PopText line into view if necessary.
------------------------------------------------------------------------*/

eq.c.PopText.scrollIntoView = function(ci, qi) {
   var c = ci.parentNode, n = c.children, bh, lh;
   if (n.length > 1) {
      if (   eq.app.idle
          || (bh = c.getBoundingClientRect().height) <= 0
          || (lh = n[0].getBoundingClientRect().height) <= 0) {
         // Application idle, or height not available yet.
         eq.Dom.onNextFrame(ci, qi);
         return;
      }
      var bt = c.scrollTop, lt = ci.offsetTop;
      if (lt < bt)
         c.scrollTop = lt;
      else if (lt + lh > bt + bh)
         c.scrollTop = Math.ceil(lt + lh - bh + 2);
   }
};

/*------------------------------------------------------------------------
   static PopText.open()
   Open PopText item list.
------------------------------------------------------------------------*/

eq.c.PopText.open = function(el, c) {
   var cl = el.classList;
   if (!cl.contains('eq-open')) {
      if (c === undefined)
         c = el.querySelector('.eq-poptext>.eq-column');
      var
         cls = eq.c.PopText,
         ch = el.firstElementChild,
         v = ch.textContent,
         rc = el.getBoundingClientRect(),
         s = c.style,
         ccl = c.classList,
         ci, w, crc;
      if (v.length) {
         // Pre-select current entry.
         var n = c.children;
         for (var i = 0, l = n.length; i < l; i++)
            if (v === n[i].textContent) {
               ci = n[i];
               break;
            }
      }
      // Set overlay position and width.
      s.left = rc.left + window.pageXOffset + 'px';
      s.top = rc.bottom + window.pageYOffset + 'px';
      if (w = s.maxWidth)
         s.width = w;
      else
         s.width = rc.width + 'px';
      // Make overlay.
      eq.Dom.copyFont(c, ch);
      eq.Dom.copyColors(c, el);
      ccl.add('eq-poptext');
      if (cl.contains('eq-border'))
         ccl.add('eq-border');
      else
         ccl.remove('eq-border');
      eq.app.makeOverlay(el, c, cls.close);
      cl.add('eq-open');
      cl.add('eq-enter');
      if (  (crc = c.getBoundingClientRect()).bottom
          > window.pageYOffset + document.documentElement.clientHeight) {
         s.top = rc.top + window.pageYOffset - crc.height + 'px';
         cl.remove('eq-dn');
         ccl.remove('eq-dn');
         cl.add('eq-up');
         ccl.add('eq-up');
      }
      else {
         cl.remove('eq-up');
         ccl.remove('eq-up');
         cl.add('eq-dn');
         ccl.add('eq-dn');
      }
      cls.select(el, c, ci);
   }
};

/*------------------------------------------------------------------------
   static PopText.close()
   Close PopText item list.
------------------------------------------------------------------------*/

eq.c.PopText.close = function(el, c, commit) {
   var cl = el.classList, ccl = c.classList, ci;
   if (!cl.contains('eq-open'))
      throw new Error("PopText.close BUG: not open, id:" + el.id);
   el.appendChild(c);
   ccl.remove('eq-poptext');
   ccl.remove('eq-border');
   cl.remove('eq-open');
   cl.remove('eq-enter');
   if (commit && (ci = c.querySelector('.eq-column>.eq-selected'))) {
      var dm = eq.Dom;
      dm.copyContent(el.firstElementChild, ci);
      dm.fireEvent(el, 'input');
   }
};

/*------------------------------------------------------------------------
   static PopText.move()
   Handle PopText move capture.
------------------------------------------------------------------------*/

eq.c.PopText.move = function(e, m) {
   var c = m.c;
   if (c) {
      var
         cls = eq.c.PopText,
         t = e.target,
         el = m.el,
         co = {},
         ln = cls.line(t, co);
      if (el !== co.ct) {
         cls.select(el, c, ln);
         if (ln)
            m.close = m.commit = true;
      }
   }
};

eq.c.PopText.move.done = function(m) {
   if (m.close)
      eq.app.closeOverlay(m.el, m.commit);
   var a = m.a;
   if (a !== undefined)
      a.classList.remove('eq-click');
   return true;
};

/*------------------------------------------------------------------------
   PopText.setAttr()
   Set PopText attributes.
------------------------------------------------------------------------*/

eq.c.PopText.prototype.setAttr = function(dlg, id, el, rt) {
   var dm = eq.Dom, tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.content: {
            var
               v = rc.v,
               l = v.length,
               cl = el.classList,
               c, n, nl, ln;
            if (l)
               cl.remove('eq-empty');
            else
               cl.add('eq-empty');
            if (cl.contains('eq-open')) {
               c = document.body.querySelector('body>.eq-poptext.eq-column');
               if (!c)
                  throw new Error("PopText.setAttr BUG: open, no column");
            }
            else {
               if (!(c = el.querySelector('.eq-poptext>.eq-button'))) {
                  c = document.createElement('DIV');
                  c.className = 'eq-rt eq-button';
                  el.appendChild(c);
               }
               if (!(c = el.querySelector('.eq-poptext>.eq-column'))) {
                  c = document.createElement('DIV');
                  c.className = 'eq-rt eq-column';
                  el.appendChild(c);
               }
            }
            if (l)
               nl = (n = c.children).length;
            else {
               // Empty, delete children.
               c.textContent = '';
               n = c.children;
               nl = 0;
            }
            for (var i = 0; i < l; i++) {
               if (i < nl)
                  ln = n[i];
               else {
                  ln = document.createElement('DIV');
                  c.appendChild(ln);
               }
               dm.setContent(ln, v[i]);
            }
            while (n.length > l)
               c.removeChild(c.lastElementChild);
            dm.copyContent(el.firstElementChild,
               c.querySelector('.eq-selected'));
            break;
         }

         case tag.activeLine: {
            var
               c = el.querySelector('.eq-poptext>.eq-column'),
               n = c.children,
               v = rc.v,
               ci;
            if (v > 0 && v <= n.length)
               ci = n[v - 1];
            dm.copyContent(el.firstElementChild, ci);
            break;
         }

         case tag.boxSize: {
            var
               w = rc.w,
               h = rc.h,
               c, s;
            if (el.classList.contains('eq-open'))
               c = document.body.querySelector('body>.eq-poptext.eq-column');
            else
               c = el.querySelector('.eq-poptext>.eq-column');
            if (!c)
               throw new Error("PopText.setAttr BUG: boxSize, no column");
            (s = c.style).maxWidth = w ? w : '';
            s.maxHeight = h ? h : '';
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   PopText.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.PopText.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   var
      cls = this.constructor,
      dm = eq.Dom,
      c = el.querySelector('.eq-poptext>.eq-column'),
      d;
   switch (key) {
      case 'Enter':
         if (!c) {
            eq.app.closeOverlay(el, true);
            if (el.classList.contains('eq-noenter')) {
               // .tabonenter disabled.
               dm.consumeEvent(e);
               return true;
            }
         }
         return false;
      case 'Escape':
      case 'Esc': // IE
         if (!c)
            eq.app.closeOverlay(el, false);
         dm.consumeEvent(e);
         return true;

      // Navigation?
      case 'PageUp':
      case 'PageDown':
      case 'ArrowUp':
      case 'Up':
      case 'ArrowDown':
      case 'Down':
         if (c) {
            cls.open(el, c);
            d = 0;
         }
         break;
      default:
         // No.
         return false;
   }

   var n, ci;
   c = document.body.querySelector('body>.eq-poptext.eq-column');
   if (c && (n = c.children).length) {
      if (d === undefined)
         switch (key) {
            case 'PageUp':
               ci = n[0];
               break;
            case 'PageDown':
               ci = n[n.length - 1];
               break;
            case 'ArrowUp':
            case 'Up':
               d = -1;
               break;
            case 'ArrowDown':
            case 'Down':
               d = 1;
               break;
         }
      if (d !== undefined) {
         if (ci = c.querySelector('.eq-column>.eq-selected')) {
            var i = eq.Dom.parentIndex(ci), l = n.length;
            if ((i += d) === -1)
               i = 0;
            else if (i >= l)
               i = l - 1;
            ci = n[i];
         }
         else
            ci = n[0];
      }
      if (ci !== undefined)
         cls.select(el, c, ci);
   }

   dm.consumeEvent(e);
   return true;
};

/*------------------------------------------------------------------------
   PopText.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.PopText.prototype.onChange = function(dlg, id, el, e) {
   if (e.type === 'mousedown') {
      var
         cls = this.constructor,
         app = eq.app,
         dm = eq.Dom,
         t = e.target,
         ib = t && t.classList.contains('eq-button');
      if (!ib) {
         ib = el === dm.eqControl(t)
           && (t = el.querySelector('.eq-poptext>.eq-button'));
         if (el.classList.contains('eq-open')) {
            var
               m = { el : el },
               co = {},
               ln = cls.line(t, co);
            if (co.c) {
               cls.select(el, m.c = co.c, ln);
               m.close = m.commit = true;
            }
            else if (el === co.ct) {
               m.a = t;
               m.close = true;
               t.classList.add('eq-click');
            }
            app.moveCapture(m, cls.move, cls.move.done);
            if (el !== co.ct) {
               dm.consumeEvent(e);
               return false;
            }
            return true;
         }
      }

      if (ib) {
         var cl = el.classList;
         if (cl.contains('eq-disabled') || cl.contains('eq-empty'))
            return false;
         if (cl.contains('eq-focus') && app.setFocus(id, el)) {
            app.pendingFocus = {
               id : id,
               act : cls.open
            };
            return false;
         }
         var
            c = el.querySelector('.eq-poptext>.eq-column'),
            m = { el : el, a : t };
         t.classList.add('eq-click');
         if (c)
            cls.open(el, m.c = c);
         else
            m.close = true;
         app.moveCapture(m, cls.move, cls.move.done);
         dm.consumeEvent(e);
         return false;
      }
   }
   return true;
};

/*------------------------------------------------------------------------
   PopText.focusLost()
   PopText has lost focus.
------------------------------------------------------------------------*/

eq.c.PopText.prototype.focusLost = function(dlg, el) {
   if (el.classList.contains('eq-open'))
      eq.app.closeOverlay(el);
};

/*------------------------------------------------------------------------
   PopText.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.PopText.prototype.changed = function(dlg, id, el, ty) {
   for (var i = 0, l = ty.length; i < l; i++)
      if (ty[i] === 'input') {
         var
            ci = el.querySelector('.eq-poptext>.eq-column>.eq-selected'),
            ch = {
               id : id,
               ty : eq.RsValType.activeLine,
               iv : ci ? eq.Dom.parentIndex(ci) + 1 : 0
            };
         return ch;
      }
   return null;
};

/** @preserve $Id: 20-progressbar.js,v 28.7 2022/03/01 17:15:27 mike Exp $
*//*======================================================================
   final class ProgressBar extends DlgElement
   DLG ProgressBar element class.
========================================================================*/

eq.c.DlgElement.addClass('ProgressBar', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
});

delete eq.c.ProgressBar.subClass; // final

/*------------------------------------------------------------------------
   ProgressBar.setAttr()
   Set ProgressBar attributes.
------------------------------------------------------------------------*/

eq.c.ProgressBar.prototype.setAttr = function(dlg, id, el, rt) {
   var tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.vertical: {
            if (!el.firstElementChild) {
               var
                  d = document.createElement('DIV'),
                  s1 = document.createElement('SPAN'),
                  s2 = document.createElement('SPAN');
               s1.className = s2.className = 'eq-fn';
               d.appendChild(s2);
               el.appendChild(s1);
               el.appendChild(d);
            }
            var cl = el.classList;
            if (rc.v) {
               cl.remove('eq-horz');
               cl.add('eq-vert');
            }
            else {
               cl.remove('eq-vert');
               cl.add('eq-horz');
            }
            break;
         }

         case tag.value: {
            var
               cl = el.classList,
               d = el.querySelector('.eq-progressbar>div'),
               v = rc.v;
            if (v) {
               cl.remove('eq-noval');
               cl.add('eq-val');
               if (cl.contains('eq-horz'))
                  d.style.width = v;
               else
                  d.style.height = v;
            }
            else {
               cl.remove('eq-val');
               cl.add('eq-noval');
               d.style.width = '';
            }
            break;
         }

         case tag.text: {
            var
               dm = eq.Dom,
               s = el.getElementsByTagName('SPAN');
            dm.setContent(s[0], rc.v);
            dm.setContent(s[1], rc.v);
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/** @preserve $Id: 20-pushbutton.js,v 28.9 2023/10/18 14:36:09 mike Exp $
*//*======================================================================
   final class PushButton extends DlgElement
   DLG PushButton element class.
========================================================================*/

eq.c.DlgElement.addClass('PushButton', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
});

delete eq.c.PushButton.subClass; // final

/*------------------------------------------------------------------------
   PushButton.setSensitive()
   Set element-specific 'sensitive' properties.
------------------------------------------------------------------------*/

eq.c.PushButton.prototype.setSensitive = function(dlg, id, el, sensitive) {
   var n = el.getElementsByTagName('BUTTON');
   if (n.length !== 1)
      throw new Error("BUG: PushButton.setSensitive id:" + el.id +
         " elements:" + n.length + ", expected: 1");
   n[0].disabled = !sensitive;
   eq.c.DlgElement.prototype.setSensitive.call(this, dlg, id, el, sensitive);
};

/*------------------------------------------------------------------------
   PushButton.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.PushButton.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   var dm = eq.Dom, kc = dm.isEnterOrSpace(key);
   if (kc && (kc !== 10 || el.classList.contains('eq-noenter'))) {
      // SPACE key, or ENTER key and .tabonenter disabled.
      dm.fireClick(el, { key : kc });
      dm.consumeEvent(e);
      return true;
   }

   return false;
};

/*------------------------------------------------------------------------
   PushButton.onTypeAhead()
   Process type-ahead key stroke.
------------------------------------------------------------------------*/

eq.c.PushButton.prototype.onTypeAhead = function(dlg, id, el, key) {
   var kc;
   switch (key) {
      case 'Enter':
      case 'ShiftEnter':
         if (el.classList.contains('eq-noenter')) {
            // .tabonenter disabled.
            kc = 10;
         }
         break;
      case ' ':
         kc = 32;
         break;
   }

   if (kc !== undefined) {
      var dm = eq.Dom;
      dm.fireClick(el, { key : kc });
      return true;
   }

   return eq.c.DlgElement.prototype.onTypeAhead.call(this, dlg, id, el, key);
};

/*------------------------------------------------------------------------
   PushButton.focusElement()
   Obtain focus element.
------------------------------------------------------------------------*/

eq.c.PushButton.prototype.focusElement = function(dlg, el) {
   var n = el.getElementsByTagName('BUTTON');
   if (n.length !== 1)
      throw new Error("BUG: PushButton.focusElement id:" + el.id +
         " elements:" + n.length + ", expected: 1");
   return n[0];
};

/** @preserve $Id: 20-radiobutton.js,v 28.10 2022/10/19 13:06:30 mike Exp $
*//*======================================================================
   final class RadioButton extends DlgElement
   DLG RadioButton element class.
========================================================================*/

eq.c.DlgElement.addClass('RadioButton', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
});

delete eq.c.RadioButton.subClass; // final

/*------------------------------------------------------------------------
   RadioButton.setSensitive()
   Set element-specific 'sensitive' properties.
------------------------------------------------------------------------*/

eq.c.RadioButton.prototype.setSensitive = function(dlg, id, el, sensitive) {
   eq.Dom.inputElement(el).disabled = !sensitive;
   eq.c.DlgElement.prototype.setSensitive.call(this, dlg, id, el, sensitive);
};

/*------------------------------------------------------------------------
   RadioButton.onEvent()
   Filter and/or handle action event.
------------------------------------------------------------------------*/

eq.c.RadioButton.prototype.onEvent = function(dlg, id, el, e) {
   return !eq.Dom.inputElement(el).checked;
};

/*------------------------------------------------------------------------
   RadioButton.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.RadioButton.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   var dm = eq.Dom, kc = dm.isEnterOrSpace(key);
   if (kc) {
      if (kc !== 10 || el.classList.contains('eq-noenter')) {
         // SPACE key, or ENTER key and .tabonenter disabled.

         dm.fireClick(dm.inputElement(el), { key : kc });
         dm.consumeEvent(e);
         return true;
      }
      return false;
   }

   var
      t = e.target,
      n = el.parentNode.querySelectorAll('input[name="' + t.name + '"]'),
      d = 0;
   if (n.length) {
      // Navigation?
      switch (key) {
         case 'Home':
            el = n[0];
            kc = 36;
            break;
         case 'End':
            el = n[n.length - 1];
            kc = 35;
            break;
         case 'PageUp':
            d = -1;
            kc = 33;
            break;
         case 'PageDown':
            d = 1;
            kc = 34;
            break;
         default:
            // No.
            return false;
      }
      if (d !== 0) {
         el = null;
         for (var i = 0, l = n.length; i < l; i++)
            if (t === n[i]) {
               if ((i += d) === -1)
                  i = l - 1;
               else if (i >= l)
                  i = 0;
               el = n[i];
               break;
            }
      }
      if (el !== null) {
         dm.fireClick(el, { key : kc });
         dm.consumeEvent(e);
         return true;
      }
   }

   return false;
};

/*------------------------------------------------------------------------
   RadioButton.onTypeAhead()
   Process type-ahead key stroke.
------------------------------------------------------------------------*/

eq.c.RadioButton.prototype.onTypeAhead = function(dlg, id, el, key) {
   var kc;
   switch (key) {
      case 'Enter':
      case 'ShiftEnter':
         if (el.classList.contains('eq-noenter')) {
            // .tabonenter disabled.
            kc = 10;
         }
         break;
      case ' ':
         kc = 32;
         break;
   }

   if (kc !== undefined) {
      var dm = eq.Dom;
      dm.fireClick(dm.inputElement(el), { key : kc });
      return true;
   }

   return eq.c.DlgElement.prototype.onTypeAhead.call(this, dlg, id, el, key);
};

/*------------------------------------------------------------------------
   RadioButton.onSubmit()
   Prepare submission, enqueue change if not already in queue.
------------------------------------------------------------------------*/

eq.c.RadioButton.prototype.onSubmit = function(dlg, id, el, ev) {
   var app = eq.app;
   if (!app.changed.has(id))
      app.changed.set(id, 'input');
};

/*------------------------------------------------------------------------
   RadioButton.focusElement()
   Obtain focus element.
------------------------------------------------------------------------*/

eq.c.RadioButton.prototype.focusElement = function(dlg, el) {
   return eq.Dom.inputElement(el);
};

/*------------------------------------------------------------------------
   RadioButton.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.RadioButton.prototype.changed = function(dlg, id, el, ty) {
   if (!el.checked)
      return null;
   var ch = {
      id : id,
      ty : eq.RsValType.buttonChecked,
      iv : 1
   };
   return ch;
};

/** @preserve $Id: 20-splitter.js,v 28.12 2022/11/01 12:08:33 mike Exp $
*//*======================================================================
   final class Splitter extends DlgElement
   DLG Splitter element class.
========================================================================*/

eq.c.DlgElement.addClass('Splitter', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.panes = [];
   this.lastPos = null;
});

delete eq.c.Splitter.subClass; // final

/*------------------------------------------------------------------------
   Splitter.dispose()
------------------------------------------------------------------------*/

eq.c.Splitter.prototype.dispose = function(dlg, id) {
   var a;
   if ((a = this.panes) !== null) {
      a.length = 0;
      this.panes = null;
   }
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   static Splitter.move()
   Handle Splitter move capture.
------------------------------------------------------------------------*/

eq.c.Splitter.move = function(e, m) {
   if (!m.busy) {
      m.busy = true;
      eq.Dom.onNextFrame(m.el, {
         cb : eq.c.Splitter.move.exec,
         x : e.clientX,
         y : e.clientY,
         m : m
      });
   }
};

eq.c.Splitter.move.exec = function(el, qi) {
   var
      x = qi.x,
      y = qi.y,
      m = qi.m,
      pa = m.pa,
      rc = pa.getBoundingClientRect(),
      di;
   if (m.x !== undefined) {
      // Horizontal.
      if (Math.abs(di = x - m.x) > 2) {
         var w = rc.width + di;
         if (w >= 0) {
            pa.style.width = w + 'px';
            if (pa.getBoundingClientRect().width !== rc.width) {
               m.x = x;
               m.modified = true;
            }
         }
      }
   }
   else {
      // Vertical.
      if (Math.abs(di = y - m.y) > 2) {
         var h = rc.height + di;
         if (h >= 0) {
            pa.style.height = h + 'px';
            if (pa.getBoundingClientRect().height !== rc.height) {
               m.y = y;
               m.modified = true;
            }
         }
      }
   }

   m.busy = undefined;
};

eq.c.Splitter.move.done = function(m, e) {
   var el = m.el, cl = el.classList, app, id;
   if (m.modified) {
      // Pane sizes modified.
      (app = eq.app).setChanged((id = m.id), 'change');
      if (cl.contains('eq-rule'))
         app.submit(el, {
            id : id,
            type : 'change'
         });
   }
   else if (e !== undefined && cl.contains('eq-quick')) {
      // Quick-expand control clicked?
      var
         modified = false,
         pa = m.pa,
         ps = pa.style,
         rc, self;
      if (m.x !== undefined) {
         // Horizontal.
         var sw = ps.width;
         if (sw === '0%' || sw === '100%') {
            if ((self = m.self).lastPos !== null) {
               ps.width = self.lastPos + 'px';
               self.lastPos = null;
               modified = true;
            }
         }
         else if ((rc = pa.getBoundingClientRect()).height >= 64) {
            var y = e.clientY;
            if (y <= rc.top + 28 && y >= rc.top) {
               (self = m.self).lastPos = rc.width;
               ps.width = '0%';
               modified = true;
            }
            else if (y >= rc.bottom - 28 && y <= rc.bottom) {
               (self = m.self).lastPos = rc.width;
               ps.width = '100%';
               modified = true;
            }
         }
      }
      else {
         // Vertical.
         var sh = ps.height;
         if (sh === '0%' || sh === '100%') {
            if ((self = m.self).lastPos !== null) {
               ps.height = self.lastPos + 'px';
               self.lastPos = null;
               modified = true;
            }
         }
         else if ((rc = pa.getBoundingClientRect()).width >= 64) {
            var x = e.clientX;
            if (x <= rc.left + 28 && x >= rc.left) {
               (self = m.self).lastPos = rc.height;
               ps.height = '0%';
               modified = true;
            }
            else if (x >= rc.right - 28 && x <= rc.right) {
               (self = m.self).lastPos = rc.height;
               ps.height = '100%';
               modified = true;
            }
         }
      }
      if (modified) {
         (app = eq.app).setChanged((id = m.id), 'change');
         if (cl.contains('eq-rule'))
            app.submit(el, {
               id : id,
               type : 'change'
            });
      }
   }
   return true;
};

/*------------------------------------------------------------------------
   Splitter.setAttr()
   Set Splitter attributes.
------------------------------------------------------------------------*/

eq.c.Splitter.prototype.setAttr = function(dlg, id, el, rt) {
   var tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.panes: {
            var
               ps = this.panes,
               pa = el.querySelector('#' + id + '>.eq-pane'),
               n, removed;
            if (pa) {
               // Remove empty panes.
               n = pa.querySelectorAll('#' + id + '>.eq-pane>*>:only-child');
               for (var i = 0, l = n.length; i < l; i++) {
                  pa.removeChild(n[i].parentNode);
                  removed = true;
               }
               if (removed)
                  ps.length = 0;
               // Register existing panes, relink hidden children.
               n = pa.querySelectorAll('#' + id + '>.eq-pane>*>:first-child');
               for (var i = 0, l = n.length; i < l; i++) {
                  var sc = n[i];
                  if (sc.classList.contains('eq-hidden')) {
                     // Relink hidden child, remove pane.
                     pa.removeChild(sc.parentNode);
                     el.appendChild(sc);
                  }
                  if (removed)
                     ps.push(sc.id);
               }
            }
            else {
               pa = document.createElement('DIV');
               pa.className = 'eq-rt eq-pane';
               el.appendChild(pa);
            }
            // Add new or now visible panes.
            n = el.querySelectorAll('#' + id + '>:not(.eq-pane)');
            for (var i = 0, l = n.length; i < l; i++) {
               var sc = n[i];
               if (!sc.classList.contains('eq-hidden')) {
                  var
                     sp = document.createElement('DIV'),
                     ca = document.createElement('DIV'),
                     pi = ps.indexOf(sc.id);
                  pa.insertBefore(sp, pi === 0 ? pa.firstElementChild : null);
                  sp.appendChild(sc);
                  ca.className = 'eq-caption';
                  sp.appendChild(ca);
                  if (pi === -1)
                     ps.push(sc.id);
               }
            }
            break;
         }

         case tag.position: {
            var
               v = rc.v,
               n = el.querySelectorAll('#' + id + '>.eq-pane>*'),
               p, s;
            if (v >= 0 && n.length === 2) {
               s = (p = n[0]).style;
               if (el.classList.contains('eq-vert')) {
                  var ph = p.parentNode.getBoundingClientRect().height;
                  s.width = '';
                  if (v == 0) {
                     if (ph > 0) {
                        var h = p.getBoundingClientRect().height;
                        this.lastPos = h > 0 ? h : ph / 2;
                     }
                     else
                        this.lastPos = null;
                     s.height = '0%';
                  }
                  else if (ph > 0 && v >= ph) {
                     var h = p.getBoundingClientRect().height;
                     this.lastPos = h > 0 ? h : ph / 2;
                     s.height = '100%';
                  }
                  else {
                     this.lastPos = null;
                     s.height = v + 'px';
                  }
               }
               else {
                  var pw = p.parentNode.getBoundingClientRect().width;
                  s.height = '';
                  if (v == 0) {
                     if (pw > 0) {
                        var w = p.getBoundingClientRect().width;
                        this.lastPos = w > 0 ? w : pw / 2;
                     }
                     else
                        this.lastPos = null;
                     s.width = '0%';
                  }
                  else if (pw > 0 && v >= pw) {
                     var w = p.getBoundingClientRect().width;
                     this.lastPos = w > 0 ? w : pw / 2;
                     s.width = '100%';
                  }
                  else {
                     this.lastPos = null;
                     s.width = v + 'px';
                  }
               }
            }
            else if (n.length) {
               s = n[0].style;
               s.width = s.height = '';
            }
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   Splitter.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.Splitter.prototype.onChange = function(dlg, id, el, e) {
   var t = e.target;
   if (t && t.classList.contains('eq-caption')) {
      var
         cls = this.constructor,
         cl = el.classList,
         m = {
            self : this,
            id   : id,
            el   : el,
            pa   : t.parentNode
         };
      if (cl.contains('eq-vert'))
         m.y = e.clientY;
      else
         m.x = e.clientX;
      eq.app.moveCapture(m, cls.move, cls.move.done);
      e.stopPropagation();
   }
   return false;
};

/*------------------------------------------------------------------------
   Splitter.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.Splitter.prototype.changed = function(dlg, id, el, ty) {
   if (ty.indexOf('change') !== -1) {
      var n = el.querySelectorAll('#' + id + '>.eq-pane>*');
      if (n.length === 2) {
         var
            rc = n[0].getBoundingClientRect(),
            ch = {
               id : id,
               ty : eq.RsValType.splitterPos,
               iv : el.classList.contains('eq-vert') ? rc.height : rc.width
            };
         return ch;
      }
   }
   return null;
};

/** @preserve $Id: 20-statictext.js,v 28.3 2022/11/09 10:41:48 mike Exp $
*//*======================================================================
   final class StaticText extends DlgElement
   DLG StaticText element class.
========================================================================*/

eq.c.DlgElement.addClass('StaticText', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
});

delete eq.c.StaticText.subClass; // final

/*------------------------------------------------------------------------
   StaticText.onAccel()
   Handle accelerator key event.
------------------------------------------------------------------------*/

eq.c.StaticText.prototype.onAccel = function(dlg, id, el) {
   var cl, f;
   while (el = el.nextElementSibling)
      if (!(cl = el.classList).contains('eq-disabled')) {
         if (cl.contains('eq-focus'))
            f = el;
         else
            f = el.querySelector('.eq-focus:not(.eq-disabled)');
         if (f) {
            eq.app.setFocus(f.id, f);
            break;
         }
      }
};

/** @preserve $Id: 20-statusbar.js,v 28.1 2022/10/27 11:49:55 mike Exp $
*//*======================================================================
   final class StatusBar extends DlgElement
   DLG StatusBar element class.
========================================================================*/

eq.c.DlgElement.addClass('StatusBar', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.dropRule = 0;
   this.globalDropRule = 0;
});

delete eq.c.StatusBar.subClass; // final

/*------------------------------------------------------------------------
   StatusBar.setAttr()
   Set StatusBar attributes.
------------------------------------------------------------------------*/

eq.c.StatusBar.prototype.setAttr = function(dlg, id, el, rt) {
   var tag = eq.RqTag, brt, dlgCls = dlg.constructor;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.dropRule:
            if ((this.dropRule = rc.i) || this.globalDropRule) {
               el.ondragover = dlgCls.onDragOver;
               el.ondrop = dlgCls.onDrop;
            }
            else {
               el.ondragover = null;
               el.ondrop = null;
            }
            break;

         case tag.globalDropRule:
            if ((this.globalDropRule = rc.i) || this.dropRule) {
               el.ondragover = dlgCls.onDragOver;
               el.ondrop = dlgCls.onDrop;
            }
            else {
               el.ondragover = null;
               el.ondrop = null;
            }
            break;

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   StatusBar.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.StatusBar.prototype.onDrop = function(dlg, id, el, t, dnd) {
   return dnd.drag ? this.dropRule : this.globalDropRule;
};

/** @preserve $Id: 20-tabbox.js,v 28.23 2025/07/09 07:49:43 mike Exp $
*//*======================================================================
   final class TabBox extends DlgElement
   DLG TabBox element class.
========================================================================*/

eq.c.DlgElement.addClass('TabBox', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.ttTabs = null;
});

delete eq.c.TabBox.subClass; // final

/*------------------------------------------------------------------------
   TabBox.dispose()
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.dispose = function(dlg, id) {
   var a;
   if ((a = this.ttTabs) !== null) {
      a.length = 0;
      this.ttTabs = null;
   }
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   static TabBox.select()
   Select TabBox tab.
------------------------------------------------------------------------*/

eq.c.TabBox.select = function(dlg, el, ta, always) {
   var tp, cl;
   if (ta) {
      if (   (tp = ta.parentNode) && (cl = tp.classList)
          && cl.contains('eq-tabs') && (cl = ta.classList)) {
         if (   cl.contains('eq-selected')
             || cl.contains('eq-hidden')
             || (!always && cl.contains('eq-disabled'))) {
            // Already selected or invisible (or disabled).
            return null;
         }
      }
      else {
         // Not a tab.
         tp = undefined;
      }
   }
   // Deselect, select.
   var
      n = el.children,
      ti = tp === undefined ? -1 : eq.Dom.parentIndex(ta),
      sl = null;
   for (var ni = 0, nl = n.length; ni < nl; ni++) {
      var tn = n[ni].children;
      for (var i = 0, l = tn.length; i < l; i++) {
         cl = (ta = tn[i]).classList;
         if (i === ti && !cl.contains('eq-hidden')) {
            cl.add('eq-selected');
            sl = ta;
         }
         else
            cl.remove('eq-selected');
      }
   }
   return sl;
};

/*------------------------------------------------------------------------
   TabBox.setAttr()
   Set TabBox attributes.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.setAttr = function(dlg, id, el, rt) {
   var
      cls = this.constructor,
      app = eq.app, dm = eq.Dom,
      tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.tabs: {
            var
               base = eq.c.DlgElement,
               ets = eq.TabState,
               v = rc.v,
               tb = el.firstElementChild,
               ac = [],
               ta, tt, l, n, nl, cl, ts, vv, ic;
            if (!tb || !tb.classList.contains('eq-tabs')) {
               ta = document.createElement('DIV');
               ta.className = 'eq-rt eq-tabs';
               tb = el.insertBefore(ta, tb);
            }
            if ((tt = this.ttTabs) !== null)
               tt.length = 0;
            if (l = v.length) {
               this.ttTabs = tt = [];
               tt.length = l;
            }
            else
               this.ttTabs = tt = null;
            if (l)
               nl = (n = tb.children).length;
            else {
               // Empty, delete children.
               tb.textContent = '';
               n = tb.children;
               nl = 0;
            }
            ac.length = l;
            for (var i = 0; i < l; i++) {
               if (i < nl)
                  ta = n[i];
               else {
                  ta = document.createElement('DIV');
                  ta.className = 'eq-fn';
                  tb.appendChild(ta);
               }
               cl = ta.classList;
               if ((ts = (vv = v[i]).s) & ets.visible) {
                  cl.remove('eq-hidden');
                  if (ts & ets.sensitive)
                     cl.remove('eq-disabled');
                  else
                     cl.add('eq-disabled');
                  if (tt[i] = vv.tt)
                     app.ttAttach(ta);
                  else
                     app.ttDetach(ta);
                  base.setFont(ta, vv.f);
                  if (ic = vv.i) {
                     ta.textContent = ''; // Delete children.
                     var
                        im = document.createElement('IMG'),
                        sp = document.createElement('SPAN');
                     im.src = ic;
                     ta.appendChild(im);
                     ta = ta.appendChild(sp);
                  }
                  ac[i] = dm.setContent(ta, vv.t || ' ', true);
               }
               else {
                  cl.add('eq-hidden');
                  app.ttDetach(ta);
                  tt[i] = ac[i] = null;
               }
            }
            while (n.length > l)
               tb.removeChild(tb.lastElementChild);
            // Deselect, selectedTab always set afterwards.
            cls.select(dlg, el, null, false);
            if (l)
               dm.onNextCycle(cls.setTabAccel, {
                  dlg : dlg,
                  el  : el,
                  ac  : ac
               });
            break;
         }

         case tag.selectedTab: {
            var
               eds = dlg.eds,
               v = rc.v,
               n, ta;
            if (v && v <= (n = el.firstElementChild.children).length)
               ta = n[v-1];
            cls.select(dlg, el, ta, true);
            break;
         }

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

eq.c.TabBox.setTabAccel = function(arg) {
   var
      dlg = arg.dlg,
      eds = dlg.eds,
      el = arg.el,
      ac = arg.ac,
      n = el.lastElementChild.children,
      id, ed;
   for (var i = 0, l = n.length; i < l; i++) {
      if ((ed = eds.get(id = n[i].id)) === undefined)
         throw new Error("TabBox.setTabAccel(" +
            el.id + ") BUG: child " + id + " not registered");
      ed.setAccel(dlg, id, ac[i], eq.KeyMod.alt);
   }
};

/*------------------------------------------------------------------------
   TabBox.onEvent()
   Filter and/or handle action event.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.onEvent = function(dlg, id, el, e) {
   var t = e.target, cl, tp;
   while (t && (!(cl = t.classList) || !cl.contains('eq-tabbox'))) {
      if ((tp = t.parentNode) && (cl = tp.classList)
                              && cl.contains('eq-tabs')) {
         return el.classList.contains('eq-rule')
             || el.lastElementChild.children[
                  eq.Dom.parentIndex(t)].classList.contains('eq-rule');
      }
      t = tp;
   }
   return false;
};

/*------------------------------------------------------------------------
   TabBox.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   var
      n = el.parentNode.querySelectorAll(
            '.eq-tabs > :not(.eq-hidden):not(.eq-disabled)'),
      d = 0,
      kc;
   if (n.length) {
      // Navigation?
      switch (key) {
         case 'Home':
            el = n[0];
            kc = 36;
            break;
         case 'End':
            el = n[n.length - 1];
            kc = 35;
            break;
         case 'PageUp':
            d = -1;
            kc = 33;
            break;
         case 'PageDown':
            d = 1;
            kc = 34;
            break;
         default:
            // No.
            return false;
      }
      if (d !== 0) {
         el = null;
         for (var i = 0, l = n.length; i < l; i++)
            if (n[i].classList.contains('eq-selected')) {
               if ((i += d) === -1)
                  i = l - 1;
               else if (i >= l)
                  i = 0;
               el = n[i];
               break;
            }
      }
      if (el !== null) {
         var dm = eq.Dom;
         dm.fireClick(el, { key : kc });
         dm.consumeEvent(e);
         return true;
      }
   }

   return false;
};

/*------------------------------------------------------------------------
   TabBox.onTabAccel()
   Handle tab accelerator key event.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.onTabAccel = function(el, ta) {
   // Obtain tab index.
   var
      n = el.lastElementChild.children,
      tb, cl;
   for (var i = 0, l = n.length; i < l; i++) {
      if (ta === (tb = n[i])) {
         // Tab found, simulate click if visible and enabled.
         cl = (ta = (n = el.firstElementChild.children)[i]).classList;
         if (!cl.contains('eq-hidden') && !cl.contains('eq-disabled')) {
            eq.Dom.fireClick(ta);
            // Flash tab.
            ta.classList.add('eq-flash');
            window.setTimeout(this.constructor.flashTab, 100, ta);
         }
         break;
      }
   }
};

eq.c.TabBox.flashTab = function(ta) {
   ta.classList.remove('eq-flash');
};

/*------------------------------------------------------------------------
   TabBox.onSubmit()
   Prepare submission.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.onSubmit = function(dlg, id, el, ev) {
   var n = el.lastElementChild.children, ta, cl;
   for (var i = 0, l = n.length; i < l; i++)
      if ((cl = (ta = n[i]).classList).contains('eq-selected')) {
         if (cl.contains('eq-rule'))
            ev.id = ta.id;
         break;
      }
};

/*------------------------------------------------------------------------
   TabBox.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.onChange = function(dlg, id, el, e) {
   if (e.type === 'mousedown') {
      var dm = eq.Dom, t = e.target, cl, tp, ta;
      while (t && (!(cl = t.classList) || !cl.contains('eq-tabbox'))) {
         if ((tp = t.parentNode) && (cl = tp.classList)
                                 && cl.contains('eq-tabs')) {
            if (ta = this.constructor.select(dlg, el, t, false)) {
               if (   ta.classList.contains('eq-rule')
                   || el.classList.contains('eq-rule'))
               {
                  // Enable action event if necessary.
                  if (this.a === undefined)
                     dlg.updateControl(id, el, {
                        ev : {
                           // Element is focusable and can submit,
                           // use 'mousedown', not 'click'.
                           a : [ 'mousedown' ]
                        }
                     });
                  return true;
               }
               else if (this.a !== undefined) {
                  // Disable action event.
                  dlg.updateControl(id, el, { ev : { a : null } });
               }
               eq.app.apiUpdate(id, eq.UpdateTag.tabOrder, dm.parentIndex(t));
            }
            break;
         }
         t = tp;
      }
   }
   return false;
};

/*------------------------------------------------------------------------
   TabBox.onToolTip()
   Obtain tooltip text or null if undefined.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.onToolTip = function(el) {
   var cl, tp, tt, i, tc;
   while (el && (!(cl = el.classList) || !cl.contains('eq-tabbox'))) {
      if ((tp = el.parentNode) && (cl = tp.classList)
                               && cl.contains('eq-tabs')) {
         if ((tt = this.ttTabs) !== null) {
            i = eq.Dom.parentIndex(el);
            if (i < tt.length && (tc = tt[i]))
               return tc;
         }
         break;
      }
      el = tp;
   }

   return null;
};

/*------------------------------------------------------------------------
   TabBox.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.changed = function(dlg, id, el, ty) {
   if (   (el = el.firstElementChild) && el.classList.contains('eq-tabs')
       && (el = el.querySelector('.eq-tabs>.eq-selected'))) {
      var ch = {
         id : id,
         ty : eq.RsValType.selectedTab,
         iv : eq.Dom.parentIndex(el)
      };
      return ch;
   }

   return null;
};

/*------------------------------------------------------------------------
   TabBox.updated()
   Act upon API thread update response.
------------------------------------------------------------------------*/

eq.c.TabBox.prototype.updated = function(dlg, id, el, tag, arg) {
   var ut = eq.UpdateTag;
   switch (tag) {
      case ut.tabOrder:
         dlg.tabOrder = arg;
         break;

      default:
         eq.c.DlgElement.prototype.updated.call(this, dlg, id, el, tag, arg);
   }
};

/** @preserve $Id: 20-toolbar.js,v 28.1 2022/10/27 11:49:55 mike Exp $
*//*======================================================================
   final class ToolBar extends DlgElement
   DLG ToolBar element class.
========================================================================*/

eq.c.DlgElement.addClass('ToolBar', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.dropRule = 0;
   this.globalDropRule = 0;
});

delete eq.c.ToolBar.subClass; // final

/*------------------------------------------------------------------------
   ToolBar.setAttr()
   Set ToolBar attributes.
------------------------------------------------------------------------*/

eq.c.ToolBar.prototype.setAttr = function(dlg, id, el, rt) {
   var tag = eq.RqTag, brt, dlgCls = dlg.constructor;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.dropRule:
            if ((this.dropRule = rc.i) || this.globalDropRule) {
               el.ondragover = dlgCls.onDragOver;
               el.ondrop = dlgCls.onDrop;
            }
            else {
               el.ondragover = null;
               el.ondrop = null;
            }
            break;

         case tag.globalDropRule:
            if ((this.globalDropRule = rc.i) || this.dropRule) {
               el.ondragover = dlgCls.onDragOver;
               el.ondrop = dlgCls.onDrop;
            }
            else {
               el.ondragover = null;
               el.ondrop = null;
            }
            break;

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

/*------------------------------------------------------------------------
   ToolBar.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.ToolBar.prototype.onDrop = function(dlg, id, el, t, dnd) {
   return dnd.drag ? this.dropRule : this.globalDropRule;
};

/** @preserve $Id: 20-tree.js,v 28.16 2024/07/11 11:21:05 mike Exp $
*//*======================================================================
   final class Tree extends DlgElement
   DLG Tree element class.
========================================================================*/

eq.c.DlgElement.addClass('Tree', function(dlg, el) {
   eq.c.DlgElement.call(this, dlg, el);
   this.emptyNode = 0;
   this.dnd = null;
   this.dndMode = 0;
   this.dropRule = 0;
   this.globalDropRule = 0;
});

delete eq.c.Tree.subClass; // final

eq.c.Tree.itemFlags = {
   node : 1,
   open : 2,
   auto : 4
};

/*------------------------------------------------------------------------
   Tree.dispose()
------------------------------------------------------------------------*/

eq.c.Tree.prototype.dispose = function(dlg, id) {
   this.dnd = null;
   eq.c.DlgElement.prototype.dispose.call(this, dlg, id);
};

/*------------------------------------------------------------------------
   static Tree.select()
   Select Tree item.
------------------------------------------------------------------------*/

eq.c.Tree.select = function(el, i) {
   var ti, cl;
   if (i !== undefined) {
      var n = el.querySelectorAll('.eq-tree .eq-item');
      if (i >= 0 && i < n.length)
         cl = (ti = n[i]).classList;
   }
   if (ti) {
      // Expand parent nodes if necessary.
      var p = ti, pl;
      while ((p = p.parentNode) && (pl = p.classList)
                                && !pl.contains('eq-tree'))
         if (pl.contains('eq-node'))
            pl.add('eq-open');
      // Scroll into view if necessary.
      this.scrollIntoView(el, {
         cb : this.scrollIntoView,
         ti : ti
      });
   }
   // Deselect.
   var n = el.querySelectorAll('.eq-tree .eq-selected');
   for (var ni = 0, nl = n.length; ni < nl; ni++)
      n[ni].classList.remove('eq-selected');
   // Select.
   if (ti) {
      cl.add('eq-selected');
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   static Tree.scrollIntoView()
   Scroll Tree item into view if necessary.
------------------------------------------------------------------------*/

eq.c.Tree.scrollIntoView = function(el, qi) {
   var ti = qi.ti, bh, ih;
   if (   eq.app.idle
       || (bh = el.getBoundingClientRect().height) <= 0
       || (ih = ti.querySelector('.eq-item>span').getBoundingClientRect().height) <= 0) {
      // Application idle, or height not available yet.
      eq.Dom.onNextFrame(el, qi);
      return;
   }
   var pt = el.scrollTop, it = ti.offsetTop;
   if (it < pt)
      el.scrollTop = Math.floor(it - ih / 2);
   else if (it + ih > pt + bh)
      el.scrollTop = Math.ceil(it + ih * 1.5 - bh);
};

/*------------------------------------------------------------------------
   static Tree.selectedIndex()
   Obtain index of selected Tree item if any.
------------------------------------------------------------------------*/

eq.c.Tree.selectedIndex = function(el, sl) {
   if (sl === undefined)
      sl = el.querySelector('.eq-tree .eq-selected');
   if (sl) {
      var n = el.querySelectorAll('.eq-tree .eq-item');
      for (var i = 0, l = n.length; i < l; i++)
         if (sl === n[i])
            return i;
   }
   return -1;
};

/*------------------------------------------------------------------------
   static Tree.itemIndex()
   Obtain index of Tree item.
------------------------------------------------------------------------*/

eq.c.Tree.itemIndex = function(el, ti) {
   var n = el.querySelectorAll('.eq-tree .eq-item');
   for (var i = 0, l = n.length; i < l; i++)
      if (ti === n[i])
         return i;
   return -1;
};

/*------------------------------------------------------------------------
   static Tree.itemExpanded()
   Check whether Tree item is node and expanded.
------------------------------------------------------------------------*/

eq.c.Tree.itemExpanded = function(ti) {
   var cl;
   while (ti && (!(cl = ti.classList) || !cl.contains('eq-tree'))) {
      if (cl && (cl.contains('eq-node') || cl.contains('eq-leaf'))) {
         while ((ti = ti.parentNode) && (cl = ti.classList)
                                     && !cl.contains('eq-tree')) {
            if (cl.contains('eq-node') && !cl.contains('eq-open'))
               return false;
         }
         return true;
      }
      ti = ti.parentNode;
   }
   return false;
};

/*------------------------------------------------------------------------
   static Tree.moveDone()
   Handle Tree move capture.
------------------------------------------------------------------------*/

eq.c.Tree.moveDone = function(m) {
   m.ti.classList.remove('eq-click');
   eq.app.setChanged(m.id, 'status');
   return true;
};

/*------------------------------------------------------------------------
   Tree.setAttr()
   Set Tree attributes.
------------------------------------------------------------------------*/

eq.c.Tree.prototype.setAttr = function(dlg, id, el, rt) {
   var cls = this.constructor, tag = eq.RqTag, brt;
   for (var ri = 0, rl = rt.length; ri < rl; ri++) {
      var rc = rt[ri];
      switch (rc.t) {
         case tag.content:
            this.setContent(el.querySelector('.eq-tree>.eq-pane'), rc.v);
            break;

         case tag.activeLine: {
            var v = rc.v;
            if (v > 0)
               cls.select(el, v - 1);
            else
               cls.select(el);
            break;
         }

         case tag.status: {
            var
               n = el.querySelectorAll('.eq-tree .eq-node'),
               v = rc.v,
               vi = 0,
               vl = v.length;
            for (var i = 0, l = n.length; i < l; i++) {
               var cl = n[i].classList;
               if (vi < vl && i === v[vi] && !cl.contains('eq-empty')) {
                  cl.add('eq-open');
                  vi++;
               }
               else
                  cl.remove('eq-open');
            }
            break;
         }

         case tag.dndMode: {
            var
               n = el.querySelectorAll('.eq-tree .eq-item>span'),
               onDragStart, sp;
            if (this.dndMode = rc.i) {
               onDragStart = dlg.constructor.onDragStart;
               for (var i = 0, l = n.length; i < l; i++) {
                  (sp = n[i]).draggable = true;
                  sp.ondragstart = onDragStart;
               }
            }
            else
               for (var i = 0, l = n.length; i < l; i++) {
                  (sp = n[i]).draggable = false;
                  sp.ondragstart = null;
               }
            break;
         }

         case tag.dropRule:
            if ((this.dropRule = rc.i) || this.globalDropRule) {
               var dlgCls = dlg.constructor;
               el.ondragover = dlgCls.onDragOver;
               el.ondrop = dlgCls.onDrop;
            }
            else {
               el.ondragover = null;
               el.ondrop = null;
            }
            break;

         case tag.globalDropRule:
            if ((this.globalDropRule = rc.i) || this.dropRule) {
               var dlgCls = dlg.constructor;
               el.ondragover = dlgCls.onDragOver;
               el.ondrop = dlgCls.onDrop;
            }
            else {
               el.ondragover = null;
               el.ondrop = null;
            }
            break;

         default:
            if (brt)
               brt.push(rc);
            else
               brt = [ rc ];
      }
   }
   if (brt)
      eq.c.DlgElement.prototype.setAttr.call(this, dlg, id, el, brt);
};

eq.c.Tree.prototype.setContent = function(p, v, pn) {
   var
      tf = this.constructor.itemFlags,
      dm = eq.Dom,
      l = v.length,
      dnd = this.dndMode,
      dlgCls;
   if (dnd)
      dlgCls = eq.c.Dlg;
   p.textContent = ''; // Delete children.
   if (!l && pn !== undefined) {
      var cl = pn.classList;
      cl.add('eq-empty');
      cl.remove('eq-open');
      cl.remove('eq-auto');
   }
   for (var i = 0; i < l; i++) {
      var
         vv = v[i],
         ti = document.createElement('DIV'),
         sp = document.createElement('SPAN');
      if (typeof vv === 'object') {
         // Node.
         var
            bt = document.createElement('DIV'),
            ch = document.createElement('DIV'),
            cl = ti.classList;
         ti.className = 'eq-item eq-node';
         if (vv.f & tf.open)
            cl.add('eq-open');
         if (vv.f & tf.auto)
            cl.add('eq-auto');
         bt.className = 'eq-button';
         ti.appendChild(bt);
         dm.setContent(sp, vv.hc);
         if (dnd) {
            sp.draggable = true;
            sp.ondragstart = dlgCls.onDragStart;
         }
         ti.appendChild(sp);
         ch.className = 'eq-pane';
         this.setContent(ch, vv.ch, ti);
         ti.appendChild(ch);
      }
      else {
         // Leaf.
         ti.className = 'eq-item eq-leaf';
         dm.setContent(sp, vv);
         if (dnd) {
            sp.draggable = true;
            sp.ondragstart = dlgCls.onDragStart;
         }
         ti.appendChild(sp);
      }
      p.appendChild(ti);
   }
};

/*------------------------------------------------------------------------
   Tree.onEvent()
   Filter and/or handle action event.
------------------------------------------------------------------------*/

eq.c.Tree.prototype.onEvent = function(dlg, id, el, e) {
   if (e.type === 'keydown')
      return true;
   var t = e.target, cl;
   while (t && (!(cl = t.classList) || !cl.contains('eq-tree'))) {
      if (t.tagName === 'SPAN') {
         while (   (t = t.parentNode)
                && (!(cl = t.classList) || !cl.contains('eq-tree'))) {
            if (cl && (cl.contains('eq-node') || cl.contains('eq-leaf')))
               return true;
         }
         return false;
      }
      t = t.parentNode;
   }
   return false;
};

/*------------------------------------------------------------------------
   Tree.onKey()
   Filter and/or handle key event.
------------------------------------------------------------------------*/

eq.c.Tree.prototype.onKey = function(dlg, id, el, key, kmod, e) {
   if (kmod)
      return false;

   var
      cls = this.constructor,
      app = eq.app,
      dm = eq.Dom,
      kc = dm.isEnterOrSpace(key);
   if (kc) {
      var sl = el.querySelector('.eq-tree .eq-selected>span');
      if (sl) {
         e.eq = { key : kc };
         app.processEvent(dlg, e, id, el, this);
      }
      dm.consumeEvent(e);
      return true;
   }

   var d;
   switch (key) {
      case 'Escape':
      case 'Esc': // IE
         cls.select(el);
         dm.consumeEvent(e);
         return true;

      // Navigation?
      case 'Home':
      case 'PageUp':
      case 'End':
      case 'PageDown':
      case 'ArrowUp':
      case 'Up':
      case 'ArrowDown':
      case 'Down':
         break;

      // Collapse?
      case 'ArrowLeft':
      case 'Left':
         d = -1;
         break;

      // Expand?
      case 'ArrowRight':
      case 'Right':
         d = 1;
         break;
      default:
         // No.
         return false;
   }

   if (d !== undefined) {
      var sl = el.querySelector('.eq-tree .eq-node.eq-selected');
      if (sl) {
         var cl = sl.classList, pn;
         if (d === 1) {
            if (!cl.contains('eq-open')) {
               if (cl.contains('eq-empty')) {
                  var i;
                  if (   ('rule' in el.dataset)
                      && (i = cls.itemIndex(el, sl)) !== -1) {
                     var rule = el.dataset.rule;
                     if (+rule === +rule) {
                        this.emptyNode = i+1;
                        app.setChanged(id, 'emptynode');
                        app.onceRule = rule;
                        app.onceId = id;
                        app.submit(el, {
                           id : id,
                           type : 'click'
                        });
                     }
                  }
               }
               else {
                  cl.add('eq-open');
                  app.setChanged(id, 'status');
               }
            }
         }
         else {
            if (cl.contains('eq-open')) {
               cl.remove('eq-open');
               app.setChanged(id, 'status');
               if (   el.classList.contains('eq-auto')
                   && cl.contains('eq-auto')
                   && (pn = sl.querySelector('.eq-pane'))) {
                  pn.textContent = ''; // Delete children.
                  cl.remove('eq-auto');
                  cl.add('eq-empty');
               }
            }
         }
      }
      dm.consumeEvent(e);
      return true;
   }

   var
      n = el.querySelectorAll('.eq-tree .eq-item'),
      l = n.length, i, si;
   if (l) {
      switch (key) {
         case 'Home':
            i = 0;
            kc = 36;
            break;
         case 'End':
            i = l - 1;
            kc = 35;
            break;
         case 'PageUp':
            i = 0;
            kc = 33;
            break;
         case 'PageDown':
            i = l - 1;
            kc = 34;
            break;
         case 'ArrowUp':
         case 'Up':
            d = -1;
            kc = 38;
            break;
         case 'ArrowDown':
         case 'Down':
            d = 1;
            kc = 40;
            break;
      }
      si = cls.selectedIndex(el);
      if (d !== undefined) {
         if ((i = si) === -1)
            i = 0;
         else if ((i += d) === -1)
            i = 0;
         else if (i >= l)
            i = l - 1;
      }
      var j = i;
      while (!cls.itemExpanded(n[j])) {
         if (d < 0) {
            if (--j === -1) {
               j = i;
               while (!cls.itemExpanded(n[++j])) {}
               break;
            }
         }
         else {
            if (++j === l) {
               j = i;
               while (!cls.itemExpanded(n[--j])) {}
               break;
            }
         }
      }
      if (j !== si) {
         if (cls.select(el, j))
            app.setChanged(id, 'change');
         // Submit on selection if enabled.
         if (this.f) {
            e.eq = { key : kc };
            app.processEvent(dlg, e, id, el, this);
            dm.consumeEvent(e);
            return true;
         }
      }
   }

   dm.consumeEvent(e);
   return true;
};

/*------------------------------------------------------------------------
   Tree.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.Tree.prototype.onChange = function(dlg, id, el, e) {
   if (e.type === 'mousedown') {
      var cls = this.constructor, app = eq.app, t = e.target;
      if (t) {
         if (t.classList.contains('eq-button')) {
            var ti = t.parentNode, cl;
            if (ti && (cl = ti.classList) && cl.contains('eq-node')) {
               var ck = true, pn, i;
               if (cl.contains('eq-open')) {
                  cl.remove('eq-open');
                  if (   el.classList.contains('eq-auto')
                      && cl.contains('eq-auto')
                      && (pn = ti.querySelector('.eq-pane'))) {
                     pn.textContent = ''; // Delete children.
                     cl.remove('eq-auto');
                     cl.add('eq-empty');
                  }
               }
               else if (cl.contains('eq-empty')) {
                  ck = false;
                  if (   ('rule' in el.dataset)
                      && (i = cls.itemIndex(el, ti)) !== -1) {
                     var rule = el.dataset.rule;
                     if (+rule === +rule) {
                        this.emptyNode = i+1;
                        app.setChanged(id, 'emptynode');
                        app.onceRule = rule;
                        app.onceId = id;
                        app.submit(el, {
                           id : id,
                           type : 'click'
                        });
                     }
                  }
               }
               else
                  cl.add('eq-open');
               if (ck) {
                  cl.add('eq-click');
                  app.moveCapture({ id : id, ti : ti }, null, cls.moveDone);
               }
            }
         }
         else {
            var cl;
            while (t && (!(cl = t.classList) || !cl.contains('eq-tree'))) {
               if (cl && cl.contains('eq-item')) {
                  var i = cls.selectedIndex(el, t);
                  if (cls.select(el, i)) {
                     app.setChanged(id, 'change');
                     return true;
                  }
                  break;
               }
               t = t.parentNode;
            }
         }
      }
   }
   return false;
};

/*------------------------------------------------------------------------
   Tree.onDragStart()
   Start drag&drop action.
------------------------------------------------------------------------*/

eq.c.Tree.prototype.onDragStart = function(dlg, id, el, t, e, dnd) {
   var
      dndMode = this.dndMode,
      md = eq.Dnd;
   if (dndMode & md.local) {
      dnd.drag = { id : id };
      eq.c.DlgElement.setDropModes(e, dndMode);
      return true;
   }
   if (dndMode & md.global) {
      e.dataTransfer.setData('text/plain', t.textContent);
      eq.c.DlgElement.setDropModes(e, md.all);
      return true;
   }
   return false;
};

/*------------------------------------------------------------------------
   Tree.onDragOver()
   Return drop target when dragging over element.
------------------------------------------------------------------------*/

eq.c.Tree.prototype.onDragOver = function(dlg, id, el, t) {
   var p, cl;
   while (t && (p = t.parentNode)) {
      if ((cl = p.classList) && cl.contains('eq-item'))
         return t;
      if (p === el || (cl && cl.contains('eq-pane')))
         break;
      t = p;
   }
   return null;
};

/*------------------------------------------------------------------------
   Tree.onDrop()
   Finish drag&drop action.
------------------------------------------------------------------------*/

eq.c.Tree.prototype.onDrop = function(dlg, id, el, t, dnd) {
   while (t && (t = t.parentNode)) {
      var cl = t.classList;
      if (cl && cl.contains('eq-item'))
         break;
      if (t === el || (cl && cl.contains('eq-pane'))) {
         if (!(this.dndMode & eq.Dnd.dropExact)) {
            dnd.drop.line = 0;
            (this.dnd = dnd).ch.push(dnd.drop.id = id);
            return dnd.drag ? this.dropRule : this.globalDropRule;
         }
         return 0;
      }
   }
   if (t) {
      var n = el.querySelectorAll('.eq-tree .eq-item');
      for (var i = 0, l = n.length; i < l; i++)
         if (t === n[i]) {
            dnd.drop.line = i + 1;
            (this.dnd = dnd).ch.push(dnd.drop.id = id);
            return dnd.drag ? this.dropRule : this.globalDropRule;
         }
   }
   return 0;
};

/*------------------------------------------------------------------------
   Tree.changed()
   Collect element changes.
------------------------------------------------------------------------*/

eq.c.Tree.prototype.changed = function(dlg, id, el, ty) {
   var v = [];
   for (var i = 0, l = ty.length; i < l; i++)
      switch (ty[i]) {
         case 'change':
            v.push({
               ty : eq.RsValType.activeLine,
               iv : eq.c.Tree.selectedIndex(el) + 1
            });
            break;

         case 'emptynode':
            if (this.emptyNode > 0)
            {
               v.push({
                  ty : eq.RsValType.emptyNode,
                  iv : this.emptyNode
               });
               this.emptyNode = 0;
            }
            break;

         case 'status': {
            var
               n = el.querySelectorAll('.eq-tree .eq-node'),
               vv = [];
            for (var ni = 0, nl = n.length; ni < nl; ni++)
               if (n[ni].classList.contains('eq-open'))
                  vv.push(ni);
            v.splice(0, 0, { // Must come first!
               ty : eq.RsValType.status,
               vv : vv
            });
            break;
         }

         case 'dnd': {
            var dnd = this.dnd, drag, drop, iv;
            if (dnd) {
               if ((drop = dnd.drop) && drop.id === id) {
                  if ((iv = drop.line) !== undefined) {
                     v.push({
                        ty : eq.RsValType.dropLine,
                        iv : iv
                     });
                  }
               }
            }
            break;
         }
      }
   this.dnd = null;
   switch (v.length) {
      case 0:
         return null;
      case 1:
         v[0].id = id;
         return v[0];
      default:
         return {
            id : id,
            v : v
         };
   }
};

/** @preserve $Id: 20-windowmenu.js,v 28.9 2022/09/26 08:59:53 mike Exp $
*//*======================================================================
   final class WindowMenu extends Menu
   DLG WindowMenu element class.
========================================================================*/

eq.c.Menu.addClass('WindowMenu', function(dlg, el) {
   eq.c.Menu.call(this, dlg, el);
});

delete eq.c.WindowMenu.subClass; // final

/*------------------------------------------------------------------------
   WindowMenu.onChange()
   Filter and/or handle change event.
------------------------------------------------------------------------*/

eq.c.WindowMenu.prototype.onChange = function(dlg, id, el, e) {
   var
      dm = eq.Dom,
      o = el.classList.contains('eq-open');
   dm.closeMenu();
   if (o) {
      // WindowMenu clicked when open.
      var t = e.target;
      while ((t = t.parentNode) && t !== el)
         if (t.classList.contains('eq-item')) {
            eq.c.App.activateSession(eq.app.sessionByIndex(dm.parentIndex(t)));
            break;
         }
   }
   else {
      // Open WindowMenu.
      var
         app = eq.app,
         ml = el.lastElementChild,
         tl = app.sessionTitles();
      ml.textContent = ''; // Delete children.
      for (var i = 0, l = tl.length; i < l; i++) {
         var
            mi = document.createElement('LI'),
            mt = document.createElement('LABEL');
         mi.className = 'eq-item';
         mt.className = 'eq-fn';
         dm.setContent(mt, tl[i]);
         mi.appendChild(mt);
         ml.appendChild(mi);
      }
      el.classList.add('eq-open');
      app.menuOpen = id;
      // Close tooltip if any.
      app.constructor.ttClose();
   }
   return true;
};

/** @preserve $Id: 99-main.js,v 28.143 2025/09/16 10:27:56 mike Exp $
*//*======================================================================
   final class App extends Obj
   Main thread application class.
========================================================================*/

eq.c.Obj.subClass('App', function() {
   eq.c.Obj.call(this);

/* NOTYET

   // DLG anchor elements.
   // If not empty, Dialog is anchored on first element.
   // If first element is null, Dialog is anchored on body element.
   // Additional anchors are used to anchor Dialog children by 'cssid'.
   this.dlgAnchor = [];
*/

   // Initialize session list.
   this.initSessionList();

   // Application title.
   this.title = null;

   // Application CSS classes.
   this.cssClasses = null;

   // Dialog map.
   this.dlg = new Map();

   // Current Dialog.
   this.currDlg = null;
   this.pendingCurrDlgId = 0;

   // Pending Dialog synchronization.
   this.pendingSync = 0;

   // Focus.
   this.focus = null;
   this.pendingFocus = null;
   this.dlgFocus = false;

   // Changed elements.
   this.changed = new Map();

   // Timers.
   this.dialogTimers = new eq.c.Timers();
   this.mouseDownTimers = new eq.c.Timers();

   // Application is idle (not waiting for user input).
   this.idle = true;

   // Application locale.
   this.locale = null;

   // Application default font, size factor.
   this.fnDefault = null;
   this.fnSizeFactor = undefined;

   // Applicaton logo/icon.
   this.logoIcon = null;

   // Current drag&drop action.
   this.dnd = null;
   this.dndSerial = 0;

   // Decode script query string.
   var
      uri, host, port,
      re = new RegExp('^(http[s]?://)([^/]+)(/[^?#]+/)?[^?#]*[?#]?.*$', 'i');
   try {
      // Obtain script base URI from script URI.
      uri = re.exec(document.querySelector('script#eq-webdlg').src);
      this.scriptBase = uri[1] + uri[2] + (uri[3] || '/');
   }
   catch (e) {
      throw new Error("WEBDLG: <script id=\"eq-webdlg\"> not found");
   }

   try {
      // Optional query string app= argument.
      this.app = /[?&]app=([^&#]+)/i.exec(uri[0])[1];
   } catch (e) {}

   try {
      // Optional query string host= argument.
      host = /[?&]host=([^&#]+)/i.exec(uri[0])[1];
   } catch (e) {}

   try {
      // Optional query string port= argument.
      port = /[?&]port=([^&#]+)/i.exec(uri[0])[1];
   } catch (e) {}

   // Compose base and WebSocket URIs.
   uri = re.exec(location.href);
   host = host ? host : uri[2];
   if (port) {
      try {
         host = /^(.+?:)[^:]*$/.exec(host)[1] + port;
      }
      catch (e) {
         host += ':' + port;
      }
   }
   this.appBase = uri[1] + host + '/';
   this.appWS = 'ws' + uri[1].substring(4).toLowerCase() + host + '/';
});

delete eq.c.App.subClass; // final

/*------------------------------------------------------------------------
   static App.doubleClick
   Double-click timing (max. number of millseconds
   after 'mouseup' before next 'mousedown' on same target).
------------------------------------------------------------------------*/

eq.c.App.doubleClick = {
   tv : 200,     // dynamically adjusted, see: App.onGlobalMouseDoubleClick()
   md : [ 0, 0], // 'mousedown' last, next-to-last time stamps
   mu : [ 0, 0]  // 'mouseup' next-to-last, last time stamps
};

/*------------------------------------------------------------------------
   static App.toolTipTimeout
   Tooltip timing (millseconds).
------------------------------------------------------------------------*/

eq.c.App.toolTipTimeout = {
   show   : 750,
   hide   : 5000,
   update : 75
};

/*------------------------------------------------------------------------
   static App timeout constants
------------------------------------------------------------------------*/

eq.c.App.timeout = {
   pagehide    : 300,   // When stopped, until 'pagehide' event expected.
   debounceKey : 500,   // Debounce key.
   showIdle    : 2000,  // Visualize idle state.
   typeAhead   : 10000  // Type-ahead.
};

/*------------------------------------------------------------------------
   static App.start()
   Start application.
------------------------------------------------------------------------*/

eq.c.App.start = function(e) {
   var
      self = eq.app,
      app = self.constructor,
      wss = window.sessionStorage,
      appOverride, args, el;

   // Close message dialog if currently open.
   eq.c.MessageDialog.close();

   // Deactivate 'eq-start' elements.
   el = document.body.getElementsByClassName('eq-start');
   for (var i = 0, l = el.length; i < l; i++) {
      // Check for 'data-app' application override.
      if (e !== undefined && e.target === el[i] && ('app' in el[i].dataset))
         appOverride = el[i].dataset.app;

      // Remove 'click' handler.
      el[i].removeEventListener('click', app.start);

      // Make disabled.
      el[i].classList.add('eq-disabled');
      if ('disabled' in el[i])
         el[i].disabled = true;
   }

/* NOTYET

   // Find 'eq-anchor' elements if any.
   var
      el = document.body.getElementsByClassName('eq-anchor'),
      l = el.length;
   if (l) {
      // First, find 'eq-dialog' anchor if present.
      for (var i = 0, iDialog; i < l; i++)
         if (el[i].id === 'eq-dialog') {
            iDialog = i;
            break;
         }

      // Set first anchor.
      self.dlgAnchor.push(iDialog !== undefined ? 'eq-dialog' : null);

      // Next, find additional anchors if any.
      for (var i = 0; i < l; i++)
         if (i !== iDialog && el[i].id)
            self.dlgAnchor.push(el[i].id);
   }
*/

   // Notify API thread to start application.

   if (self.currApp === undefined)
   {
      if (appOverride !== undefined)
         self.currApp = appOverride;
      else {
         if (!self.app)
            throw new Error("Cannot start, application name undefined");
         self.currApp = self.app;
      }
   }

   // Persistently store current application
   // so it can be restarted if document is reloaded.
   wss.setItem('eq-webdlg.app', self.currApp);

   // Obtain application arguments if any.
   args = self.args || app.args;
   self.args = undefined;

   self.api.postMessage({
      o    : eq.ApiOp.openWebSocket,
      v    : eq.p.version,
      app  : self.currApp,
      url  : self.appWS,
      sid  : wss.getItem('eq-webdlg.sid'),
      args : args
   });

   // Add 'eq-webdlg' document class.
   (el = document.documentElement).classList.add('eq-webdlg');

   // If set, remove 'eq-restart' class
   // until first Dialog or POPUP BOX is made interactive.
   if (el.classList.contains('eq-restart')) {
      self.restart = true;
      el.classList.remove('eq-restart');
   }
};

/*------------------------------------------------------------------------
   static App.prepareStart()
   return: true if caller may immediately start application
------------------------------------------------------------------------*/

eq.c.App.prepareStart = function(restart) {
   var self = eq.app;

   // Restart application?
   if (restart === undefined) {
      // Invoked when DOM is ready.
      if (self.currApp = window.sessionStorage.getItem('eq-webdlg.app')) {
         // Yes, done.
         this.start();
         return false;
      }
   }
   if (restart) {
      // Invoked when application has stopped and may restart.
      if (   self.currApp !== undefined
          && document.documentElement.classList.contains('eq-restart')) {
         // Yes, clear session storage, done.
         var
            wss = window.sessionStorage,
            sid = wss.getItem('eq-webdlg.sid'),
            opn = wss.getItem('eq-webdlg.opn');
         self.updateSessionList(true);
         wss.removeItem('eq-webdlg.app');
         wss.removeItem('eq-webdlg.uat');
         wss.removeItem('eq-webdlg.sid');
         if (opn && opn === sid)
            wss.removeItem('eq-webdlg.opn');
         this.start();
         return false;
      }
   }
   // No, invalidate current application.
   self.currApp = undefined;

   // Find 'eq-start' elements if any.
   var
      el = document.body.getElementsByClassName('eq-start'),
      l = el.length;
   if (!l) {
      // No 'eq-start' elements,
      // caller may immediately start application.
      return true;
   }

   // Setup 'eq-start' click handlers.
   for (var i = 0; i < l; i++) {
      el[i].addEventListener('click', this.start);
      el[i].classList.remove('eq-disabled');
      if ('disabled' in el[i])
         el[i].disabled = false;
   }

   return false;
};

/*------------------------------------------------------------------------
   static App.stopped()
   Application stopped.
------------------------------------------------------------------------*/

eq.c.App.stopped = function() {
   var
      self = eq.app,
      app = self.constructor,
      dnd, drag, el, c, cc, wfs, cl, css;

   // Session still alive? Wait for subsequent 'pagehide' event.
   self.stopped = window.setTimeout(app.onStopped, app.timeout.pagehide);

   // Dispose tooltip if necessary.
   self.ttDispose();

   // Dispose timers.
   self.mouseDownTimers.dispose();
   self.dialogTimers.dispose();

   // Dispose type-ahead if necessary.
   if (self.typeAhead !== undefined) {
      app.cancelTypeAhead();
      self.typeAhead = undefined;
   }

   // Cancel current user action if any.
   self.cancelUserAction();

   // Close login dialog if currently open.
   eq.c.LoginDialog.close();

   // Close POPUP BOX if currently open.
   if (self.popupBox !== undefined) {
      self.popupBox.close();
      self.popupBox = undefined;
   }

   // Clear current drag&drop action.
   self.dndSerial = 0;
   if (dnd = self.dnd) {
      if ((drag = dnd.drag) && (el = drag.over))
         el.classList.remove('eq-dragover');
      self.dnd = null;
   }

   // Close Dialogs, clear current Dialog.
   self.dlg.forEach(app.cbCloseDlg);
   self.dlg.clear();
   self.currDlg = null;
   self.pendingCurrDlgId = 0;

   // Reset pending Dialog synchronization.
   self.pendingSync = 0;

   // Cleanup application styles.
   self.cleanupStyles();

   // Clear focus.
   self.focus = null
   self.pendingFocus = null;
   self.dlgFocus = false;

   // Clear changed elements.
   self.changed.clear();

/* NOTYET

   // Clear 'eq-anchor' elements.
   self.dlgAnchor.length = 0;
*/

   // Clear pending 'mousedown' and 'click' events.
   self.pendingMouseDown = undefined;
   self.pendingClick = undefined;

   // Clear pending tab mode.
   self.pendingTabMode = undefined;
   self.pendingTabModeId = undefined;

   // Clear pending 'once' rule.
   self.onceRule = undefined;
   self.onceId = undefined;

   // Clear layout mode.
   self.layoutMode = undefined;

   // Disable beep-notification when busy.
   self.notifyBusy = undefined;

   // Application is idle.
   self.idle = true;

   // Cleanup classes, dispose static properties if necessary.
   for (c in eq.c)
      if ((cc = eq.c[c]).dispose)
         cc.dispose();

   // Reset default font and size factor.
   self.fnDefault = null;
   self.fnSizeFactor = undefined;

   // Reset logo/icon.
   self.logoIcon = null;

   // Clear application title.
   self.title = null;

   // Clear popup window position.
   self.popupPos = undefined;

   // Clear window features.
   if (wfs = self.wfs) {
      wfs.clear();
      self.wfs = undefined;
   }

   // Remove 'eq-webdlg' document class.
   cl = document.documentElement.classList;
   cl.remove('eq-webdlg');

   // Disable warning when leaving page.
   app.warnWhenLeavingPage(false);
   self.warnLeavingPage = 0;

   // Remove application CSS classes.
   if (self.cssClasses !== null) {
      for (var i = 0, l = (css = self.cssClasses).length; i < l; i++)
         cl.remove(css[i]);
      self.cssClasses = null;
   }

   // Clear pending 'eq-restart' document element class if necessary.
   if (self.restart)
      self.restart = undefined;
};

eq.c.App.onStopped = function() {
   var
      self = eq.app,
      wss = window.sessionStorage,
      sid = wss.getItem('eq-webdlg.sid'),
      opn = wss.getItem('eq-webdlg.opn');
   self.stopped = undefined;
   // Application stopped, no subsequent 'pagehide' event.
   // Session probably closed by server, clear session storage.
   self.updateSessionList(true);
   wss.removeItem('eq-webdlg.app');
   wss.removeItem('eq-webdlg.uat');
   wss.removeItem('eq-webdlg.sid');
   if (opn) {
      if (opn === sid)
         wss.removeItem('eq-webdlg.opn');
      else if (sid) {
         // Secondary session, close window.
         window.close();
      }
   }
};

eq.c.App.cbCloseDlg = function(d) {
   d.close();
};

/*------------------------------------------------------------------------
   static App.domReady()
   Continue main thread initialization when DOM is ready.
------------------------------------------------------------------------*/

eq.c.App.domReady = function(e) {
   var self = eq.app, app = self.constructor, el;

   // Save original page title.
   app.title = document.title;

   // Obtain/create application style sheet.
   var el = document.getElementById('eq-style');
   if (!el) {
      el = document.createElement('STYLE');
      el.id = 'eq-style';
      document.head.appendChild(el);
   }
   self.stl = {
      sheet : el.sheet,
      owner : []
   };
   if (!self.stl.sheet)
      throw new Error("WEBDLG: <style id=\"eq-style\"> StyleSheet undefined");
   self.cleanupStyles();

   // IE: suppress default help action on F1 key.
   if (window.onhelp !== undefined) {
      window.addEventListener('help', app.onHelp, true);
      app.onHelpInstalled = true;
   }
   if (document.onhelp !== undefined) {
      document.addEventListener('help', app.onHelp, true);
      app.onHelpInstalled = true;
   }

   // Notify API thread when Document is complete.
   if (document.readyState === 'complete')
      app.documentComplete();
   else
      window.addEventListener('load', app.documentComplete);

   // Prepare starting application.
   if (app.prepareStart()) {
      // Immediately start application.
      app.start();
   }

   // This method/listener is now obsolete.
   if (e !== undefined)
      window.removeEventListener('DOMContentLoaded', app.domReady);
   delete app.domReady;
};

/*------------------------------------------------------------------------
   static App.documentComplete()
   Notify API thread when document is complete.
------------------------------------------------------------------------*/

eq.c.App.documentComplete = function(e) {
   var self = eq.app, app = self.constructor, eqp, p;

   // Notify API thread.
   self.api.postMessage({
      o : eq.ApiOp.documentComplete,
      v : eq.p.version,
      b : self.appBase
   });

   // Install plugins if any.
   if ((eqp = eq.plugin) !== undefined && (p = eqp.install()).length) {
      // Transfer plugin descriptors to API thread.
      self.api.postMessage({
         o : eq.ApiOp.installPlugins,
         p : p
      });
   }

   // This method/listener is now obsolete.
   if (e !== undefined)
      window.removeEventListener('load', app.documentComplete);
   delete app.documentComplete;
};

/*------------------------------------------------------------------------
   static App.apiStart()
   Start API thread.
------------------------------------------------------------------------*/

eq.c.App.apiStart = function() {
   var
      self = eq.app,
      app = self.constructor,
      re = new RegExp('^(http[s]?://[^/]+)', 'i');
   if (re.exec(location.href)[1] === re.exec(self.scriptBase)[1]) {
      // Same origin.
      self.api = new Worker(self.scriptBase + "eq-webdlg-api.js");
   }
   else {
      // HACK: Enable cross-origin worker script.
      // eslint-disable-next-line
      self.apiUrl = (window.URL || window.webkitURL).createObjectURL(
         new Blob(
            ["importScripts('" + self.scriptBase + "eq-webdlg-api.js');"],
            { "type" : 'application/javascript' }));
      self.api = new Worker(self.apiUrl);
   }
   self.api.onmessage = app.apiReceived;
   self.api.onerror = app.apiError;
};

/*------------------------------------------------------------------------
   static App.apiReceived()
   Process message from API thread.
------------------------------------------------------------------------*/

eq.c.App.apiReceived = function(m) {
   var self = eq.app, app = self.constructor, op = eq.MainOp, resume = false;
   if (self.dlgListen)
      app.dlgListener.close();
   for (var di = 0, dl = m.data.length; di < dl; di++) {
      var ddi = m.data[di], o = ddi.o, d = ddi.d;
      switch (o) {
         case op.apiThreadStarted:
            if (self.apiUrl !== undefined) {
               // eslint-disable-next-line
               (window.URL || window.webkitURL).revokeObjectURL(self.apiUrl);
               self.apiUrl = undefined;
            }
            continue;

         case op.ssnNewOrResume: {
            var
               wss = window.sessionStorage,
               prevSid = wss.getItem('eq-webdlg.sid'),
               prevOpn = wss.getItem('eq-webdlg.opn'),
               css, cl;
            if (!d.uat)
               throw new Error("apiReceived ssnNewOrResume:" +
                  " invalid, user agent tag undefined");
            wss.setItem('eq-webdlg.uat', d.uat);
            if (!d.sid)
               throw new Error("apiReceived ssnNewOrResume:" +
                  " invalid, session id undefined");
            wss.setItem('eq-webdlg.sid', d.sid);
            if (prevSid && prevSid === prevOpn)
               prevSid = prevOpn = null;
            if (!prevOpn)
               wss.setItem('eq-webdlg.opn', d.sid);
            window.name = 'eq-webdlg.' + d.sid;
            self.updateSessionList();
            self.title = d.title,
            self.locale = d.locale;
            self.fnDefault = d.fn;
            self.fnSizeFactor = d.sf;
            self.logoIcon = d.icon;
            self.cssClasses = css = d.css;
            // Add application CSS classes.
            cl = document.documentElement.classList;
            for (var i = 0, l = css.length; i < l; i++)
               cl.add(css[i]);
            // Enable warning when leaving page.
            app.warnWhenLeavingPage(true);
            if (!prevSid) {
               // New session, show DLG listener message if necessary.
               app.dlgListener.message();
            }
            continue;
         }

         case op.inject: {
            var inj = ddi.inj, done = true;
            for (var i = 0, l = inj.length; i < l; i++) {
               if (app.inject(inj[i]))
                  done = false;
            }
            if (done) {
               // Done, notify API thread.
               self.api.postMessage({ o : eq.ApiOp.injectDone });
            }
            continue;
         }

         case op.message:
            new eq.c.MessageDialog(ddi.s, 10000, null);
            continue;

         case op.login:
            new eq.c.LoginDialog(ddi.login, ddi.save);
            continue;

         case op.applicationStopped: {
            // Application has stopped.
            app.stopped();
            if (d) {
               // Restart or prepare restarting.
               app.prepareStart(true);
            }
            continue;
         }

         case op.applicationFailed:
            // Prepare restarting if necessary.
            app.stopped();
            self.currApp = undefined;
            app.prepareStart(false);
            continue;

         case op.popupPos:
            self.popupPos = d;
            continue;

         case op.warnLeavingPage:
            self.warnLeavingPage = d > 1 ? d : (app.dlgListener() ? 0 : d);
            continue;

         case op.windowFeature: {
            var wfs = self.wfs, k = ddi.k.toUpperCase(), v = ddi.v;
            if (!wfs)
               wfs = new Map();
            if (v.length)
               wfs.set(k, v);
            else
               wfs.delete(k);
            self.wfs = wfs;
            continue;
         }

         case op.windowFeatures: {
            var wfs = self.wfs, wi;
            if (wfs)
               wfs.clear();
            else
               wfs = new Map();
            for (var i = 0, l = d.length; i < l; i++) {
               wi = d[i];
               wfs.set(wi.k.toUpperCase(), wi.v);
            }
            self.wfs = wfs;
            continue;
         }

         case op.layout:
            self.layoutMode = d;
            continue;

         case op.notifyBusy:
            self.notifyBusy = d !== 0;
            continue;

         case op.typeAhead: {
            if (d) {
               if (self.typeAhead === undefined)
                  self.typeAhead = {
                     active : false,
                     dlg    : null,
                     focus  : null,
                     buf    : [],
                     timer  : 0
                  };
            }
            else if (self.typeAhead !== undefined) {
               app.cancelTypeAhead();
               self.typeAhead = undefined;
            }
            continue;
         }

         case op.cancelCurrentCall: {
            var id = ddi.i, dlg;
            if (!id)
               dlg = self.currDlg;
            else if (   (dlg = self.dlg.get(id)) === undefined
                     || (id !== self.pendingCurrDlgId && dlg !== self.currDlg))
               continue;
            if (dlg !== null) {
               self.mouseDownTimers.dispose();
               self.dialogTimers.dispose();
               self.cancelUserAction();
               self.startTypeAhead();
               if (dlg = self.currDlg)
                  dlg.resetCurrDlg();
               self.pendingSync = 0;
               self.pendingCurrDlgId = 0;
            }
            else if (self.popupBox !== undefined) {
               self.popupBox.close();
               self.popupBox = undefined;
            }
            if (id)
               self.api.postMessage({
                  o : eq.ApiOp.canceled,
                  i : id
               });
            continue;
         }

         case op.update: {
            var
               id = ddi.i,
               dlg = self.dlg.get(id),
               ctid, ed, el, val;
            if (dlg === undefined)
               throw new Error("apiReceived update: DLG " +
                  id + " does not exist");
            if ((ed = dlg.eds.get(ctid = ddi.c)) === undefined)
               throw new Error("apiReceived update: id " + ctid +
                  " not registered");
            el = document.getElementById(ctid);
            val = ed.onUpdate(dlg, ctid, el, ddi.t, ddi.a, ddi.x);
            if (val === null) {
               // Prepare for asynchronous return.
               ed.ddi = ddi;
               continue;
            }
            ddi.o = eq.ApiOp.updated;
            ddi.d = val;
            self.api.postMessage(ddi);
            if (dlg.temp)
               self.closeDialog(dlg);
            continue;
         }

         case op.updated: {
            var
               dlg = self.currDlg,
               ctid, el, ed;
            if (   dlg !== null && d === dlg.id
                && (el = document.getElementById(ctid = ddi.i))
                && (ed = dlg.eds.get(ctid)) !== undefined)
               ed.updated(dlg, ctid, el, ddi.t, ddi.a);
            continue;
         }

         case op.synced:
            if (self.pendingSync !== 2)
               throw new Error("apiReceived synced BUG: " +
                  "pendingSync=" + self.pendingSync);
            self.pendingSync = 0;
            continue;

         case op.openUrl: {
            var
               target = d.t,
               key = d.k,
               wfs = self.wfs,
               wf = wfs ? wfs.get(key.toUpperCase()) : undefined;
            if (wf)
               wf = eq.Dom.resolveWindowFeatures(wf);
            if (!window.open(d.u, target.lastIndexOf('_', 0) === 0
                                ? target : 'eq-webdlg.' + target, wf))
               console.error("Failed to open window '" + key +
                  "' for URL '" + d.u + "'");
            continue;
         }

         case op.beep:
            app.beep();
            continue;

         case op.playSound:
            app.playSound(d.url);
            continue;

         case op.startApp: {
            // Open new window, set query string app= argument.
            var name = d.app, wf = d.wf, wfs;
            if (!wf)
               wf = (wfs = self.wfs) ? wfs.get(name.toUpperCase()) : undefined;
            if (wf)
               wf = eq.Dom.resolveWindowFeatures(wf);
            if (!window.open(eq.Dom.updateQueryString(
                  window.location.href, 'app', name), '_blank', wf))
               console.error("Failed to open window for application '"
                  + name + "'");
            continue;
         }

         case op.activate: {
            app.activateSession(self.sessionByTitle(d));
            continue;
         }

         case op.invalidate: {
            for (var i = d.length; --i >= 0;) {
               var dlg = self.dlg.get(d[i]);
               if (dlg !== undefined)
                  self.closeDialog(dlg);
            }
            continue;
         }
      }

      if (self.currDlg === null || !self.currDlg.ddo) {
         // Sanity check except when Dialog.do currently active.
         if (!self.idle)
            throw new Error("apiReceived op " + o + ": invalid, not idle");
         if (self.currDlg !== null)
            throw new Error("apiReceived op " + o +
               ": invalid, current Dialog pending");
         if (self.focus !== null)
            throw new Error("apiReceived op " + o + ": invalid, focus pending");
         if (self.changed.size)
            throw new Error("apiReceived op " + o + ": invalid, " +
               self.changed.size + " changed elements pending");
      }
      if (self.popupBox !== undefined)
         throw new Error("apiReceived op " + o +
            ": invalid, POPUP BOX open");
      if (self.pendingSync !== 0)
         throw new Error("apiReceived op " + o +
            ": invalid, pendingSync=" + self.pendingSync);
      if (self.pendingCurrDlgId !== 0)
         throw new Error("apiReceived op " + o +
            ": invalid, pendingCurrDlgId=" + self.pendingCurrDlgId);

      switch (o) {
         case op.resume:
            resume = true;
            break;

         case op.grid: {
            var
               id = ddi.i,
               fn = ddi.f,
               dlg = self.dlg.get(id);
            if (dlg === undefined)
               dlg = self.newDialog(id);
            dlg.calcGrid(dlg.fn = fn);
            self.api.postMessage({
               o : eq.ApiOp.grid,
               d : id,
               g : dlg.grid
            });
            break;
         }

         case op.addControl: {
            var
               id = ddi.i,
               dlg = self.dlg.get(id);
            if (dlg === undefined) {
               if (d.pa !== 'eq-temp')
                  throw new Error("apiReceived addControl: DLG " +
                     id + " does not exist");
               dlg = self.newTempDialog(id);
            }
            if (d.pa !== undefined) {
               // Add to specified parent.
               var
                  p = document.getElementById(d.pa),
                  cl, el;
               if (!p)
                  throw new Error("apiReceived addControl: parent '" +
                     d.pa + "' does not exist");
               if (!(cl = p.classList).contains('eq-container')) {
                  if (cl.contains('eq-dialog'))
                     p = p.querySelector(
                        '.eq-dialog>.eq-pane>.eq-view>.eq-container');
                  else
                     p = p.querySelector('.eq-container');
               }
               if (!p)
                  throw new Error("apiReceived addControl: parent '" +
                     d.pa + "' container not found");
               el = dlg.addControl(d, p);
/* NOTYET
               // TODO: dlgAnchor
               if (self.dlgAnchor.indexOf(d.pa) !== -1) {
                  el.classList.add('eq-root');
                  dlg.root.push(el);
                  app.addRootListeners(el);
                  self.setIdle(dlg);
               }
*/
            }
            else {
               // Add to root.
               dlg.close();
               dlg.addRoot(dlg.addControl(d));
               self.setIdle(dlg);
            }
            break;
         }

         case op.modifyControl: {
            var
               id = ddi.i,
               dlg = self.dlg.get(id);
            if (dlg === undefined)
               throw new Error("apiReceived modifyControl: DLG " +
                  id + " does not exist");
            if (!d.id)
               throw new Error("apiReceived modifyControl: id not set");
            if (!(el = document.getElementById(d.id)))
               throw new Error("apiReceived modifyControl: id '" +
                  d.id + "' does not exist");
            dlg.modifyControl(d.id, el, d);
            break;
         }

         case op.deleteControl: {
            var
               id = ddi.i,
               dlg = self.dlg.get(id),
               el = document.getElementById(d);
            if (dlg === undefined)
               throw new Error("apiReceived deleteControl: DLG " +
                  id + " does not exist");
            if (!el)
               throw new Error("apiReceived deleteControl: id '" +
                  d + "' does not exist");
            if (dlg.removeElement(el))
               self.closeDialog(dlg);
            break;
         }

         case op.dlg_DRAW:
            if (resume) {
               resume = false;
               self.dialogsVisible();
            }
            self.pendingSync = 1;
            eq.Dom.onNextCycle(app.dlg_DRAW, ddi);
            break;

         case op.dlg_DO: {
            if (resume) {
               resume = false;
               self.dialogsVisible();
            }
            if (self.restart) {
               self.restart = undefined;
               document.documentElement.classList.add('eq-restart');
            }
            var
               id = ddi.i,
               dlg = self.dlg.get(id);
            if (dlg === undefined)
               throw new Error("apiReceived dlg_DO: DLG " + id +
                  " does not exist");
            if (dlg.ddo) {
               // Dialog.do implicit DLG DRAW.
               self.pendingSync = 1;
            }
            self.pendingCurrDlgId = id;
            eq.Dom.onNextCycle(app.dlg_DO, ddi);
            break;
         }

         case op.dlg_POPUP_BOX:
            if (resume) {
               resume = false;
               self.dialogsVisible();
            }
            if (self.restart) {
               self.restart = undefined;
               document.documentElement.classList.add('eq-restart');
            }
            self.popupBox = new eq.c.PopupBox(d);
            // POPUP BOX has focus.
            self.dlgFocus = true;
            break;

         default:
            throw new Error("apiReceived invalid op: " + o);
      }
   }

   if (resume) {
      self.dialogsVisible();
      var dlg = self.topDialog();
      if (dlg && dlg.ddo) {
         // Resuming Dialog.do, activate Dialog.
         if (self.currDlg !== null)
            throw new Error("apiReceived resume: " +
               "invalid, current Dialog pending");
         self.setCurrDialog(dlg);
         if (dlg.ddo.f)
            self.setFocus(dlg.ddo.f);
         if (dlg.ddo.t)
            dlg.tabOrder = dlg.ddo.t;
         self.setIdle();
      }
   }
};

eq.c.App.dlg_DO = function(ddi) {
   var
      self = eq.app,
      app = self.constructor,
      id = ddi.i,
      idPending = self.pendingCurrDlgId,
      dlg = self.dlg.get(id),
      dt;
   self.pendingCurrDlgId = 0;
   if (dlg === undefined)
      throw new Error("App.dlg_DO: DLG " + id + " does not exist");
   // Make Dialog top and visible.
   if (dlg.whenReady && dlg.ddo)
      throw new Error("App.dlg_DO BUG: whenReady pending");
   if (idPending !== id) {
      if (idPending !== 0)
         throw new Error("App.dlg_DO: unexpected pending DLG " +
            idPending + ", expected: " + id + " or zero");
      return;
   }
   dlg.whenReady = app.dlg_DO.ready;
   dlg.whenReadyArg = ddi.d.f;
   self.setCurrDialog(dlg);
   if ((dt = ddi.d.t) !== undefined)
      dlg.tabOrder = dt;
};

eq.c.App.dlg_DO.ready = function(dlg, focus) {
   var
      self = eq.app,
      app = self.constructor;
   if (dlg !== self.currDlg) {
      // Current Dialog has changed or been reset.
      if (self.currDlg)
         throw new Error("App.dlg_DO.ready: unexpected DLG " +
            self.currDlg.id + ", expected: " + dlg.id);
      // Dialog.do, notify API thread: synchronized.
      if (dlg.ddo) {
         if (self.pendingSync !== 1)
            throw new Error("App.dlg_DO.ready BUG: " +
               "Dialog.do pendingSync=" + self.pendingSync);
         self.pendingSync = 2;
         self.api.postMessage({ o : eq.ApiOp.onSynced });
      }
      return;
   }
   if (focus)
      self.setFocus(focus);
   else
      app.resetFocus();
   // Activate Dialog.
   self.setIdle();
   // Execute pending focus action if any.
   if (self.pendingFocus !== null) {
      var pf = self.pendingFocus, act, el;
      self.pendingFocus = null;
      if (   pf.id === self.focus
          && (act = pf.act)
          && (el = document.getElementById(pf.id)))
         act(el);
   }
   if (dlg.ddo) {
      // Dialog.do implicit DLG DRAW,
      // notify API thread: synchronized.
      if (self.pendingSync !== 1)
         throw new Error("App.dlg_DO.ready BUG: " +
            "Dialog.do pendingSync=" + self.pendingSync);
      self.pendingSync = 2;
      self.api.postMessage({ o : eq.ApiOp.onSynced });
   }
   else {
      var w = dlg.w;
      if (w !== null) {
         // Reset Window close button state if necessary,
         // act upon pending Window close request.
         var cl, el = w.root.querySelector(
            '.eq-window>.eq-pane>.eq-caption>.eq-close');
         if (el && (cl = el.classList).contains('eq-click'))
            cl.remove('eq-click');
         if (dlg.pendingClose) {
            eq.Dom.onNextCycle(app.submitWindowClose);
            return;
         }
         // Enable warning when leaving page
         // if Window does not have active close button.
         app.warnWhenLeavingPage(self.warnLeavingPage > 1 || !w.canClose());
      }
      // Dialog has focus.
      self.dlgFocus = true;
      // Start Dialog timer if configured.
      if (dlg.timer !== undefined)
         self.dialogTimers.add(dlg.timer.delay,
            app.submitDialogTimer, null, dlg.timer.rule);
   }
};

eq.c.App.dlg_DRAW = function(ddi) {
   var
      self = eq.app,
      id = ddi.i,
      dlg = self.dlg.get(id);
   if (dlg === undefined)
      throw new Error("App.dlg_DRAW: DLG " + id + " does not exist");
   // Make Dialog top and visible.
   if (dlg.whenReady)
      throw new Error("App.dlg_DRAW BUG: whenReady pending");
   dlg.whenReady = self.constructor.dlg_DRAW.ready;
   self.setCurrDialog(dlg);
};

eq.c.App.dlg_DRAW.ready = function(dlg) {
   var self = eq.app;
   if (dlg !== self.currDlg) {
      // Current Dialog has changed or been reset.
      if (self.currDlg)
         throw new Error("App.dlg_DRAW.ready: unexpected DLG " +
            self.currDlg.id + ", expected: " + dlg.id);
   }
   else if (!dlg.ddo) {
      // Not Dialog.do, reset current Dialog.
      self.currDlg = null;
   }
   // Notify API thread: synchronized.
   if (self.pendingSync !== 1)
      throw new Error("App.dlg_DRAW.ready BUG: " +
         "Dialog.do pendingSync=" + self.pendingSync);
   self.pendingSync = 2;
   self.api.postMessage({ o : eq.ApiOp.onSynced });
};

eq.c.App.submitWindowClose = function() {
   var self = eq.app, d;
   if (!self.idle && (d = self.currDlg) !== null && d.pendingClose) {
      d.pendingClose = undefined;
      self.submit(document.body, { type : 'close' });
   }
};

eq.c.App.submitDialogTimer = function(rule) {
   var self = eq.app;
   if (!self.idle && self.currDlg !== null) {
      self.onceRule = rule;
      self.submit(document.body, { type : 'timer' });
   }
};

/*------------------------------------------------------------------------
   App.apiUpdate()
   Post update request to API thread.
------------------------------------------------------------------------*/

eq.c.App.prototype.apiUpdate = function(id, tag, arg) {
   var dlg = this.currDlg;
   if (!dlg)
      throw new Error("BUG: apiUpdate no current Dialog");
   this.api.postMessage({
      o : eq.ApiOp.update,
      d : dlg.id,
      i : id,
      t : tag,
      a : arg
   });
};

/*------------------------------------------------------------------------
   static App.apiError()
   Handle API thread error.
------------------------------------------------------------------------*/

eq.c.App.apiError = function() {
   var self = eq.app;
   if (self.apiUrl !== undefined) {
      // eslint-disable-next-line
      (window.URL || window.webkitURL).revokeObjectURL(self.apiUrl);
      self.apiUrl = undefined;
   }
};

/*------------------------------------------------------------------------
   static App.onEvent()
   Root element event handler.
------------------------------------------------------------------------*/

eq.c.App.onEvent = function(e) {
   var
      self = eq.app,
      dm = eq.Dom,
      t = e.target,
      focusIn, mouseDown;

   switch (e.type) {
      case 'focusin':
         focusIn = true;
         break;
      case 'mousedown':
         if (e.button !== 0) {
            // Not the main button.
            return;
         }
         mouseDown = true;
         break;
   }

   // Dialog has focus.
   self.dlgFocus = true;

   if (t.classList.contains('eq-idle')) {
      // Event on idle pane.
      dm.consumeEvent(e);
      if (mouseDown) {
         // 'mousedown', set wait cursor, issue beep notification.
         t.style.cursor = 'wait';
         if (self.notifyBusy)
            self.constructor.beep();
      }
      return;
   }
   if (self.idle)
      return;

   var
      el = dm.eqControl(t), // Obtain 'eq-control' element.
      cl, mdFocus, moEvent;
   if (el) {
      if ((cl = el.classList).contains('eq-disabled')) {
         if (!mouseDown || !cl.contains('eq-md'))
            dm.consumeEvent(e);
         if (self.menuOpen)
            dm.onNextCycle(dm.closeMenu);
         return;
      }

      var id = el.id, dlg = self.currDlg, ed;
      if (!dlg)
         throw new Error("BUG: onEvent no current Dialog");
      if ((ed = dlg.eds.get(id)) !== undefined) {
         if (mouseDown) {
            mdFocus = cl.contains('eq-focus') || cl.contains('eq-md');
            self.pendingMouseDown = { id : id, t : t };
         }
         else if (focusIn) {
            // About to receive focus.
            if (cl.contains('eq-focus')) {
               // Set focus if different from current focus.
               if (id !== self.focus)
                  self.setFocus(id, el);
            }
            else {
               // Target not focusable, ignore event.
               dm.consumeEvent(e);
            }
            return;
         }

         self.processEvent(dlg, e, id, el, ed);
      }

      moEvent = self.menuOpen && dm.isMenu(el);
   }

   if (mouseDown) {
      if (!mdFocus) {
         // Target not focusable.
         dm.consumeEvent(e);
      }
      if (self.menuOpen && !moEvent) {
         // Menu open, not a menu event, close menu on next event cycle.
         dm.onNextCycle(dm.closeMenu);
      }
   }
};

/*------------------------------------------------------------------------
   App.processEvent()
   Process change and action event.
------------------------------------------------------------------------*/

eq.c.App.prototype.processEvent = function(dlg, e, id, el, ed) {
   var type = e.type;

   // Change event?
   if (ed.c !== undefined && ed.c.indexOf(type) !== -1) {
      // Yes, enqueue change if not already in queue.
      var ch = this.changed.get(id);
      if (ch === undefined)
         this.changed.set(id, [ type ]);
      else if (ch.indexOf(type) === -1)
         ch.push(type);
      if (!ed.onChange(dlg, id, el, e))
         return false;
   }

   // Action event?
   if (ed.a !== undefined && ed.a.indexOf(type) !== -1) {
      // Yes.
      if (!ed.onEvent(dlg, id, el, e))
         return false;
      var pn;
      if (type === 'mousedown') {
         // 'mousedown', register pending 'click'.
         if ((pn = this.pendingClick) !== undefined) {
            this.stopPendingClickTimer();
            if (ed.m === undefined)
               return false;
            if (id !== pn.id) {
               // Target different from already pending 'click' target,
               // submit previous event, ignore this event.
               eq.c.App.commitPendingClick();
            }
            return true;
         }
         var ea = { btn : e.buttons };
         if (ea.btn === undefined) {
            switch (e.button) {
               case 0: // Main button, usually left button.
                  ea.btn = 1;
                  break;
               case 1: // Auxiliary button, usually wheel/middle button.
                  ea.btn = 4;
                  break;
               case 2: // Secondary button, usually right button.
                  ea.btn = 2;
                  break;
               default:
                  ea.btn = 0;
            }
         }
         this.pendingClick = {
            id : id,
            type : 'click',
            cnt : 0,
            ea : ea
         };
      }
      else if ((pn = this.pendingClick) !== undefined) {
         // 'click' pending, append event.
         if (pn.ev !== undefined)
            throw new Error("BUG: onEvent pending click id:" + pn.id +
               " and id:" + pn.ev.id + " type:" + pn.ev.type);
         pn.ev = {
            id : id,
            type : type,
            ea : e.eq
         };
      }
      else {
         // Submit event.
         this.submit(el, {
            id : id,
            type : type,
            ea : e.eq
         });
      }

      return true;
   }

   return false;
};

/*------------------------------------------------------------------------
   App.cancelUserAction()
   Cancel current user action if any.
------------------------------------------------------------------------*/

eq.c.App.prototype.cancelUserAction = function() {
   if (this.capture !== undefined) {
      // Clear pending capture.
      this.endCapture();
   }
   // Close overlay if currently present.
   this.closeOverlay();
   // Close menu if currently open.
   eq.Dom.closeMenu();
};

eq.c.App.onCancelUserAction = function(e) {
   if (e.type == 'focus') {
      if (eq.Dom.isRootChild(e.target) || eq.c.PopupBox.setFocus())
         eq.app.dlgFocus = true;
      else if (!eq.c.LoginDialog.setFocus(e))
         eq.c.MessageDialog.setFocus();
   }
   else if (e.target === window) {
      var app = eq.app;
      app.cancelUserAction();
      if (e.type == 'blur')
         app.dlgFocus = false;
   }
};

/*------------------------------------------------------------------------
   App.setChanged()
   Set element changed.
------------------------------------------------------------------------*/

eq.c.App.prototype.setChanged = function(id, ty) {
   if (!this.idle) {
      var dlg = this.currDlg, ed;
      if (!dlg)
         throw new Error("BUG: setChanged no current Dialog");
      if ((ed = dlg.eds.get(id)) !== undefined) {
         if (ed.c !== undefined && ed.c.indexOf(ty) !== -1) {
            var ch = this.changed.get(id);
            if (ch === undefined)
               this.changed.set(id, [ ty ]);
            else if (ch.indexOf(ty) === -1)
               ch.push(ty);
         }
      }
   }
};

/*------------------------------------------------------------------------
   App.elementsChanged()
   Collect and return element changes.
------------------------------------------------------------------------*/

eq.c.App.prototype.elementsChanged = function(ea) {
   var
      app = this.constructor,
      dlg = this.currDlg,
      w, r, dv, m,
      changed = [{
         ty : eq.RsValType.idFocus,
         sv : this.focus || ""
      }];
   if (!dlg)
      throw new Error("BUG: elementsChanged no current Dialog");
   if (ea !== undefined) {
      // Dialog-specific event attributes.
      if (ea.key !== undefined) {
         changed.push({
            ty : eq.RsValType.ruleKey,
            iv : ea.key
         });
      }
      else {
         if (ea.btn !== undefined) {
            changed.push({
               ty : eq.RsValType.mouseBtns,
               iv : ea.btn
            });
         }
         if (ea.cnt !== undefined) {
            changed.push({
               ty : eq.RsValType.mouseCnt,
               iv : ea.cnt
            });
         }
         if (ea.mod !== undefined) {
            changed.push({
               ty : eq.RsValType.mouseMods,
               iv : ea.mod
            });
         }
      }
   }
   if ((w = dlg.w) !== null && (dv = (r = w.root).querySelector(
         '.eq-window>.eq-pane>.eq-dialog>.eq-pane>.eq-view'))) {
      if ((m = w.modified).maximized) {
         m.maximized = undefined;
         changed.push({
            ty : eq.RsValType.dialogMax,
            iv : r.classList.contains('eq-max') ? 1 : 0
         });
      }
      if (m.pos) {
         m.pos = undefined;
         changed.push({
            ty : eq.RsValType.dialogPos,
            x : r.offsetLeft,
            y : r.offsetTop
         });
      }
      if (m.size) {
         m.size = undefined;
         changed.push({
            ty : eq.RsValType.dialogSize,
            w : dv.offsetWidth,
            h : dv.offsetHeight
         });
      }
   }
   if (this.onceRule !== undefined) {
      changed.push({
         id : this.onceId,
         ty : eq.RsValType.ruleOnce,
         iv : this.onceRule
      });
      this.onceRule = undefined;
      this.onceId = undefined;
   }
   this.changed.forEach(app.cbElementsChanged, { d: dlg, c : changed });
   this.changed.clear();
   return changed;
};

eq.c.App.prototype.updateElementsChanged = function() {
   var
      app = this.constructor,
      dlg = this.currDlg,
      changed = [];
   if (!dlg)
      throw new Error("BUG: updateElementsChanged no current Dialog");
   this.changed.forEach(app.cbElementsChanged, { d: dlg, c : changed });
   this.changed.clear();
   return changed;
};

eq.c.App.cbElementsChanged = function(ty, id) {
   var
      dlg = this.d,
      ed, el, v;
   if ((ed = dlg.eds.get(id)) === undefined)
      throw new Error("BUG: elementsChanged id:" + id +
         " not registered, type:" + ty);
   el = document.getElementById(id);
   if (!el.classList.contains('eq-event'))
      el = el.querySelector('.eq-event');
   if (!el)
      throw new Error("BUG: cbElementsChanged id:" + id +
         " event element not found");
   if ((v = ed.changed(dlg, id, el, ty)) !== null)
      this.c.push(v);
};

/*------------------------------------------------------------------------
   App.submit()
   Submit event.
------------------------------------------------------------------------*/

eq.c.App.prototype.submit = function(el, ev) {
   if (   this.onceRule === -1
       || (this.onceRule === undefined && el.classList.contains('eq-help'))) {
      this.api.postMessage({
         o : eq.ApiOp.onHelp,
         d : this.onceRule === -1 && this.onceId !== undefined
           ? this.onceId : el.id
      });
      return;
   }
   var dlg, ddo, id, ed;
   this.mouseDownTimers.dispose();
   this.dialogTimers.dispose();
   if (ev.type !== 'timer')
      this.cancelUserAction();
   if ((ddo = (dlg = this.currDlg).ddo)) {
      // Dialog.do event.
      var
         f = this.focus || "",
         r;
      if (ddo.hide || ddo.show) {
         // Hide and/or show as specified.
         if (ddo.hide && (el = document.getElementById(ddo.hide)))
            el.style.display = 'none';
         if (ddo.show && (el = document.getElementById(ddo.show)))
            el.style.display = '';
      }
      else {
         // Hide Dialog.
         for (var i = dlg.root.length; --i >= 0;) {
            if ((r = dlg.root[i]).classList.contains('eq-dialog'))
               dlg.w.root.style.display = 'none';
            else
               r.style.display = 'none';
         }
      }
      this.currDlg = null;
      this.focus = null;
      this.changed.clear();
      this.setIdle(dlg);
      // Notify API thread.
      this.api.postMessage({
         o : eq.ApiOp.onDo,
         i : dlg.id,
         f : f
      });
      return;
   }
   if (dlg.whenReady) {
      // Call pending whenReady action.
      dlg.callWhenReady();
   }
   // Prepare submission if necessary.
   if (   this.onceRule === undefined
       && el !== document.body
       && (ed = dlg.eds.get(id = el.id)) !== undefined)
      ed.onSubmit(dlg, id, el, ev);
   // Notify API thread on next event cycle.
   this.setIdle(dlg);
   eq.Dom.onNextCycle(this.constructor.apiSubmit, ev);
};

eq.c.App.apiSubmit = function(ev) {
   var self = eq.app, dnd, drag, el;
   if (self.currDlg !== null) {
      ev.changed = self.elementsChanged(ev.ea);
      self.api.postMessage({
         o : eq.ApiOp.onEvent,
         d : ev
      });
      self.startTypeAhead();
      self.currDlg = null;
      self.focus = null;
      if (dnd = self.dnd) {
         if ((drag = dnd.drag) && (el = drag.over))
            el.classList.remove('eq-dragover');
         self.dnd = null;
      }
   }
};

/*------------------------------------------------------------------------
   static App.onGlobalKeyDown() .onGlobalKeyUp()
   Global keyboard event handler.
------------------------------------------------------------------------*/

eq.c.App.onGlobalKeyDown = function(e) {
   var key = e.key;
   switch (key) {
      case "Alt":
      case "AltGraph":
      case "CapsLock":
      case "Control":
      case "Fn":
      case "FnLock":
      case "Hyper":
      case "Meta":
      case "NumLock":
      case "OS": //IE
      case "Scroll":
      case "ScrollLock":
      case "Shift":
      case "Super":
      case "Symbol":
      case "SymbolLock":
         return;
   }

   var self = eq.app, app = self.constructor, dm = eq.Dom;
   if (!self.dlgFocus) {
      // Focus is somewhere else, ignore event.
      if (self.debounceKeyTimer !== undefined) {
         window.clearTimeout(self.debounceKeyTimer);
         self.debounceKeyTimer = undefined;
         self.debounceKey = undefined;
      }
      return;
   }

   if (key === self.debounceKey) {
      // Continue debouncing key.
      app.debounceKey(key);
      dm.consumeEvent(e);
      return;
   }

   if (self.popupBox !== undefined) {
      // POPUP BOX is open.
      if (self.popupBox.onKey(e))
         dm.consumeEvent(e);
      return;
   }

   if (self.addTypeAhead(e)) {
      // Key stored in type-ahead buffer.
      dm.consumeEvent(e);
      return;
   }

   if (self.idle) {
      dm.consumeEvent(e);
      if (self.notifyBusy)
         app.beep();
      return;
   }

   // Current Dialog?
   var dlg = self.currDlg;
   if (dlg) {
      // Modifier keys.
      var
         km = eq.KeyMod,
         kmod = e.shiftKey ? km.shift : 0;
      if (e.ctrlKey)
         kmod |= km.ctrl;
      if (e.altKey)
         kmod |= km.alt;
      if (e.metaKey)
         kmod |= km.meta;
      if (kmod === km.shift && key.length === 1)
         kmod = 0;

      // Menu?
      var menuOpen = self.menuOpen, el, mo, l, id, ed;
      if (menuOpen && (el = document.getElementById(menuOpen))) {
         if (l = (mo = el.getElementsByClassName('eq-open')).length)
            el = mo[l-1];
         if ((ed = dlg.eds.get(id = el.id)) !== undefined) {
            if (ed.onKey(dlg, menuOpen, el, key, kmod, e))
               return;
            el = id = ed = undefined;
         }
      }

      // Obtain target element, overlay or 'eq-control' or focus.
      var cl, t = e.target;
      if (self.overlay !== undefined)
         el = self.overlay.el;
      else if (self.focus !== null)
         if (t === document.body || dm.eqControl(t))
            el = document.getElementById(self.focus);
      if (el) {
         if ((cl = el.classList).contains('eq-disabled')) {
            dm.consumeEvent(e);
            return;
         }
         // DLG element?
         if ((ed = dlg.eds.get(id = el.id)) === undefined)
            return;
      }

      // Function key?
      var fkey;
      switch (key) {
         case 'Enter':
            // eq-enter: Enter key has special function.
            if (   !kmod && (!cl || !cl.contains('eq-enter'))
                && dlg.fkey && dlg.fkey.length > 0 && dlg.fkey[0]) {
               // Dialog.cr function key.
               fkey = 0;
            }
            break;
         case 'F1':
            fkey = 1;
            if (app.onHelpInstalled) {
               // IE: suppress default help action on F1 key.
               self.onHelpIgnore = true;
            }
            break;
         case 'F2':
            fkey = 2;
            break;
         case 'F3':
            fkey = 3;
            break;
         case 'F4':
            fkey = 4;
            break;
         case 'F5':
            fkey = 5;
            break;
         case 'F6':
            fkey = 6;
            break;
         case 'F7':
            fkey = 7;
            break;
         case 'F8':
            fkey = 8;
            break;
         case 'F9':
            fkey = 9;
            break;
         case 'F10':
            fkey = 10;
            break;
         case 'F11':
            fkey = 11;
            break;
         case 'F12':
            fkey = 12;
      }

      if (fkey !== undefined) {
         // Function key, check accelerator first.
         if (dlg.isAccel(key, kmod, e)) {
            dm.consumeEvent(e);
            if (menuOpen === self.menuOpen)
               dm.closeMenu();
            return;
         }
         if (!kmod && dlg.fkey && fkey < dlg.fkey.length) {
            var r = dlg.fkey[fkey];
            if (r) {
               dm.consumeEvent(e);

               if (r > 0) {
                  // Submit function key rule.
                  self.onceRule = r;
                  if (   self.focus !== null
                      && (el = document.getElementById(self.focus))) {
                     self.onceId = self.focus;
                     self.submit(el, { id : self.onceId, type : 'key' });
                  }
                  else
                     self.submit(document.body, { type : 'key' });
               }
               else if (r === -1)
                  self.api.postMessage({
                     o : eq.ApiOp.onHelp,
                     d : el ? el.id : null
                  });

               if (menuOpen === self.menuOpen)
                  dm.closeMenu();
               return;
            }
         }

         self.onHelpIgnore = undefined;
      }
      else if (el) {
         if (ed.onKey(dlg, id, el, key, kmod, e)) {
            if (menuOpen === self.menuOpen)
               dm.closeMenu();
            return;
         }

         // Accelerator?
         if (dlg.isAccel(key, kmod, e)) {
            dm.consumeEvent(e);
            if (menuOpen === self.menuOpen)
               dm.closeMenu();
            return;
         }

         // Navigation?
         var tab;
         if (kmod <= km.shift) // No modifier or shift.
            switch (key) {
               case 'Enter':
                  // eq-enter: Enter key has special function,
                  // eq-noenter: .tabonenter disabled.
                  if (cl.contains('eq-enter') || cl.contains('eq-noenter'))
                     break;
                  // FALLTHROUGH
               case 'Tab':
                  tab = kmod ? -1 : 1; // Tab:1 Shift-Tab:-1
                  break;
               case 'ArrowUp':
               case 'Up':
               case 'ArrowLeft':
               case 'Left':
                  if (!kmod)
                     tab = -1;
                  break;
               case 'ArrowDown':
               case 'Down':
               case 'ArrowRight':
               case 'Right':
                  if (!kmod)
                     tab = 1;
            }

         if (tab) {
            dm.consumeEvent(e);

            if (!self.inSetFocus) {
               // Navigate to next/previous element in tab order.
               var elNext = dlg.nextTab(id, tab === -1), idNext;
               if (elNext) {
                  self.pendingTabMode = tab;
                  self.pendingTabModeId = idNext = elNext.id;
                  self.setFocus(idNext, elNext);
               }
               else {
                  // No other element found in tab order,
                  // submit 'blur' event if registered.
                  self.processEvent(dlg, {
                     type : 'blur',
                     target : ed.focusElement(dlg, el)
                  }, id, el, ed);
               }
            }
         }
      }
      else if (dlg.isAccel(key, kmod, e)) {
         // Accelerator.
         dm.consumeEvent(e);
      }

      if (menuOpen === self.menuOpen)
         dm.closeMenu();
   }
};

eq.c.App.onGlobalKeyUp = function(e) {
   var self = eq.app;
   if (e.key === self.debounceKey) {
      window.clearTimeout(self.debounceKeyTimer);
      self.debounceKeyTimer = undefined;
      self.debounceKey = undefined;
      eq.Dom.consumeEvent(e);
   }
};

/*------------------------------------------------------------------------
   static App.debounceKey()
   Debounce key stroke.
------------------------------------------------------------------------*/

eq.c.App.debounceKey = function(key) {
   var self = eq.app;
   self.debounceKey = key;
   if (self.debounceKeyTimer !== undefined)
      window.clearTimeout(self.debounceKeyTimer);
   self.debounceKeyTimer = window.setTimeout(
      this.onDebounceKeyTimer, this.timeout.debounceKey);
};

eq.c.App.onDebounceKeyTimer = function() {
   var self = eq.app;
   // Timer has elapsed.
   self.debounceKeyTimer = undefined;
   self.debounceKey = undefined;
};

/*------------------------------------------------------------------------
   static App.help()
   IE: Selectively filter F1 function key default action.
------------------------------------------------------------------------*/

eq.c.App.onHelp = function(e) {
   var self = eq.app;
   if (self.onHelpIgnore) {
      // IE: suppress default help action on F1 key.
      self.onHelpIgnore = undefined;
      eq.Dom.consumeEvent(e);
      return false;
   }
   return true;
};

/*------------------------------------------------------------------------
   static App.onGlobalMouseDownOrClick()
   Global 'mousedown' or 'click' event handler.
------------------------------------------------------------------------*/

eq.c.App.onGlobalMouseDownOrClick = function(e) {
   // Prevent repeated invocation on same event (Window frame, overlay).
   var ea = e.eq;
   if (ea !== undefined) {
      if (ea.onGlobalMouseDownOrClick)
         return;
      ea.onGlobalMouseDownOrClick = true;
   }
   else
      e.eq = { onGlobalMouseDownOrClick : true };

   var
      self = eq.app,
      target = e.target,
      el = target,
      md = e.type === 'mousedown',
      ts, cl, ci, id, dc;

   if (md)
      ts = performance.now();

   // Close tooltip if any.
   if (self.toolTip && self.toolTip.tt)
      self.constructor.ttClose();

   // Check for context menu button.
   while (el && el !== document) {
      if (cl = el.classList) {
         if (   !cl.contains('eq-disabled')
             && cl.contains('eq-control') && (id = el.id))
            break;
         // Context menu child element?
         if (!ci && cl.contains('eq-cmc'))
            ci = el;
      }
      el = el.parentNode;
   }
   if (id) {
      var
         btn = e.buttons,
         mask = el.dataset.mask || 2;
      if (btn === undefined) {
         switch (e.button) {
            case 0: // Main button, usually left button.
               btn = 1;
               break;
            case 1: // Auxiliary button, usually wheel/middle button.
               btn = 4;
               break;
            case 2: // Secondary button, usually right button.
               btn = 2;
               break;
            default:
               btn = 0;
         }
      }
      if (btn & mask) {
         // Context menu button.
         while (el && el !== document) {
            if (cl = el.classList) {
               var ignore = cl.contains('eq-idle') || cl.contains('eq-overlay');
               if (ignore || cl.contains('eq-root')
                          || cl.contains('eq-window')) {
                  self.cancelUserAction();
                  e.preventDefault();
                  e.stopImmediatePropagation();
                  if (!ignore && self.currDlg) {
                     var changed = self.updateElementsChanged();
                     if (changed.length)
                        self.apiUpdate(null, eq.UpdateTag.changed, {
                           c : changed
                        });
                     self.apiUpdate(id, eq.UpdateTag.mouseButton2, {
                        x : e.clientX,
                        y : e.clientY,
                        c : ci ? eq.Dom.parentIndex(ci) + 1 : 0
                     });
                  }
                  return;
               }
            }
            el = el.parentNode;
         }
         return;
      }
   }

   if (e.button !== 0) {
      // Not the main button.
      self.cancelUserAction();
      return;
   }

   if (md) {
      // Register main button 'mousedown' time stamp.
      dc = eq.c.App.doubleClick;
      dc.md[1] = dc.md[0];
      dc.md[0] = Math.ceil(ts);
   }

   if (self.capture !== undefined) {
      // End capture.
      e.stopPropagation();
      self.endCapture();
   }

   // 'mousedown' on overlay?
   var dm = eq.Dom;
   if (md && self.overlay !== undefined) {
      if (dm.eqOverlay(target)) {
         var
            dlg = self.currDlg,
            ed;
         if (dlg && (el = dm.eqControl(target) || self.overlay.el)) {
            if ((cl = el.classList) && cl.contains('eq-disabled')) {
               // Clicked disabled control, close.
               self.closeOverlay();
               dm.consumeEvent(e);
            }
            else if ((ed = dlg.eds.get(id = el.id)) !== undefined) {
               self.processEvent(dlg, e, id, el, ed);
               dm.consumeEvent(e);
            }
         }
      }
      else if (dm.eqControl(target) !== self.overlay.el) {
         // Clicked outside overlay, close.
         self.closeOverlay();
         dm.consumeEvent(e);
      }
   }

   // Close menu if clicked outside Dialog or MenuBar.
   if (self.menuOpen) {
      var found;
      el = target;
      while (el && el !== document) {
         if (cl = el.classList)
            if (cl.contains('eq-dialog') || cl.contains('eq-menubar')) {
               found = true;
               break;
            }
         el = el.parentNode;
      }
      if (!found)
         dm.onNextCycle(dm.closeMenu);
   }

   if (self.idle && self.popupBox !== undefined) {
      // POPUP BOX.
      if (self.popupBox.isEvent(e)) {
         if (self.popupBox.close(e)) {
            self.popupBox = undefined;
            dm.consumeEvent(e);
            return;
         }
      }
      else if (!target.classList.contains('eq-idle'))
         return;

      if (md) {
         // 'mousedown', target not focusable.
         e.preventDefault();
      }
   }
   else {
      // Window frame element?
      var w = eq.c.Window.fromTarget(target);
      if (w && w.onMouseDownOrClick(e) && md) {
         // Yes, 'mousedown', target not focusable.
         dm.consumeEvent(e);
      }
   }
};

/*------------------------------------------------------------------------
   static App.onGlobalMouseDoubleClick()
   Global 'dblclick' event handler.
------------------------------------------------------------------------*/

eq.c.App.onGlobalMouseDoubleClick = function(e) {
   // Adjust max. double-click time if necessary.
   var dc = eq.c.App.doubleClick, dt = dc.md[0] - dc.mu[0];
   if (dt > dc.tv)
      dc.tv = dt;
};

/*------------------------------------------------------------------------
   static App.onGlobalMouseUp()
   Global 'mouseup' event handler.
------------------------------------------------------------------------*/

eq.c.App.onGlobalMouseUp = function(e) {
   var self = eq.app;

   if (e.button === 0) {
      // Register main button 'mouseup' time stamp.
      var
         ts = performance.now(),
         dc = eq.c.App.doubleClick;
      dc.mu[0] = dc.mu[1];
      dc.mu[1] = Math.ceil(ts);
   }

   // Cancel mousedown timers if any.
   self.mouseDownTimers.cancelAll();

   if (self.capture !== undefined) {
      // End capture.
      e.stopPropagation();
      if (!self.endCapture(e)) {
         self.pendingMouseDown = undefined;
         self.pendingClick = undefined;
         self.stopPendingClickTimer();
         return;
      }
   }

   if (self.idle) {
      // 'mouseup' while idle.
      self.pendingMouseDown = undefined;
      self.pendingClick = undefined;
      self.stopPendingClickTimer();
      return;
   }

   var dm = eq.Dom, pn;
   if ((pn = self.pendingMouseDown) !== undefined) {
      // 'mouseup' while 'mousedown' pending.
      var t = pn.t;
      if (t === e.target) {
         // 'mouseup' target same as pending 'mousedown' target,
         // obtain 'eq-control' element.
         var
            id = pn.id,
            dlg = self.currDlg,
            el = dm.eqControl(t);
         if (!dlg)
            throw new Error("BUG: onGlobalMouseUp no current Dialog");
         // 'click' committed.
         self.processEvent(dlg, {
            type : 'click',
            target : t
         }, id, el, dlg.eds.get(id));
         if (el.classList.contains('eq-focus')) {
            // Element focusable, set focus.
            self.setFocus(id, el);
         }
      }
      else if (self.menuOpen && dm.isMenu(t = e.target)) {
         // menu is open, 'mouseup' target is menu element,
         // obtain 'eq-control' element.
         var
            dlg = self.currDlg,
            el = dm.eqControl(t),
            id = el.id;
         if (!dlg)
            throw new Error("BUG: onGlobalMouseUp no current Dialog");
         // 'click' committed.
         self.processEvent(dlg, {
            type : 'click',
            target : t,
            menuMouseUp : true
         }, id, el, dlg.eds.get(id));
      }

      self.pendingMouseDown = undefined;

      // Close menu if item click not submitted.
      if (self.menuOpen && dm.isMenu(t = e.target)) {
         var el = dm.eqControl(t);
         if (el && el.classList.contains('eq-item'))
            dm.onNextCycle(dm.closeMenu);
      }
   }

   if ((pn = self.pendingClick) !== undefined) {
      // 'mouseup' while 'click' pending.
      self.stopPendingClickTimer();
      // Obtain 'eq-control' element.
      var
         dlg = self.currDlg,
         el = dm.eqControl(e.target),
         id, ed;
      if (!dlg)
         throw new Error("BUG: onGlobalMouseUp pendingClick no current Dialog");
      if (el && (id = el.id) === pn.id) {
         // 'mouseup' target same as pending 'click' target,
         // obtain event attributes btn, cnt, mod.
         if (e.eq !== undefined)
            pn.ea = e.eq;
         else {
            var
               ea = pn.ea = pn.ea || {},
               mod = 0;
            ea.cnt = pn.cnt ? pn.cnt + 1 : 1;
            if (e.shiftKey)
               mod += 1;
            if (e.ctrlKey)
               mod += 2;
            if (e.metaKey)
               mod += 4;
            if (e.altKey)
               mod += 8;
            if (mod)
               ea.mod = mod;
         }
         if ((ed = dlg.eds.get(id)) !== undefined && ed.m !== undefined)
         {
            // Check number of mouse clicks.
            if (pn.cnt === 0) {
               // First click, wait for second click.
               pn.cnt = 1;
               var app = self.constructor;
               self.commitPendingClickTimer =
                  window.setTimeout(app.commitPendingClick,
                                    app.doubleClick.tv);
               return;
            }
         }
         // 'click' committed, submit event.
         self.submit(el, pn);
      }
      else if ((pn = pn.ev) !== undefined) {
         // 'mouseup' target different from pending 'click' target,
         // submit pending 2nd event.
         if (el = document.getElementById(pn.id))
            self.submit(el, pn);
      }

      self.pendingClick = undefined;
   }
};

eq.c.App.commitPendingClick = function() {
   var self = eq.app, pn;
   self.commitPendingClickTimer = undefined;
   if ((pn = self.pendingClick) !== undefined) {
      var id = pn.id, dlg, ed, el;
      if ((dlg = self.currDlg) && (ed = dlg.eds.get(id)) !== undefined
                               && ed.m !== undefined) {
         if (pn.cnt >= ed.m) {
            // 'click' committed, submit event.
            if (el = document.getElementById(id))
               self.submit(el, pn);
         }
         else if ((pn = pn.ev) !== undefined) {
            // submit pending 2nd event.
            if (el = document.getElementById(pn.id))
               self.submit(el, pn);
         }
      }
      self.pendingClick = undefined;
   }
};

eq.c.App.prototype.stopPendingClickTimer = function() {
   if (this.commitPendingClickTimer !== undefined) {
      window.clearTimeout(this.commitPendingClickTimer);
      this.commitPendingClickTimer = undefined;
   }
};

/*------------------------------------------------------------------------
   App.moveCapture()
   Begin 'mousemove' capture.
------------------------------------------------------------------------*/

eq.c.App.prototype.moveCapture = function(arg, cb, done) {
   var app = this.constructor;
   if (this.capture !== undefined)
      this.endCapture();
   app.ttClose();
   this.capture = { arg : arg, cb : cb, done : done };
   window.addEventListener('mousemove', app.onCapture);
};

eq.c.App.prototype.endCapture = function(e) {
   var app = this.constructor, c = this.capture;
   window.removeEventListener('mousemove', app.onCapture);
   this.capture = undefined;
   if (c.done)
      return c.done(c.arg, e);
   return true;
};

eq.c.App.onCapture = function(e) {
   e.stopPropagation();
   var self = eq.app, app = self.constructor, c = self.capture;
   if (!c)
      window.removeEventListener('mousemove', app.onCapture);
   else if (c.cb)
      c.cb(e, c.arg);
};

/*------------------------------------------------------------------------
   static App.onGlobalContextMenu()
   Global 'contextmenu' event handler, typically invoked after
   secondary mouse button 'mousedown' event.
------------------------------------------------------------------------*/

eq.c.App.onGlobalContextMenu = function(e) {
   var t = e.target, cl;
   while (t && t !== document) {
      if (cl = t.classList) {
         if (   cl.contains('eq-idle')
             || cl.contains('eq-overlay')
             || cl.contains('eq-tip')
             || cl.contains('eq-root')
             || cl.contains('eq-window')) {
            e.preventDefault();
            e.stopImmediatePropagation();
            return false;
         }
      }
      t = t.parentNode;
   }
   return true;
};

/*------------------------------------------------------------------------
   static App.onGlobalPageHide()
   Global 'pagehide' event handler, fired when leaving current page.
------------------------------------------------------------------------*/

eq.c.App.onGlobalPageHide = function(e) {
   var self = eq.app;
   if (self.stopped !== undefined) {
      // Application stopped because user closed window or moved away.
      // Restart session later if possible, see eq.c.App.onStopped().
      window.clearTimeout(self.stopped);
      self.stopped = undefined;
   }
   self.updateSessionList(true);
};

/*------------------------------------------------------------------------
   static App.onGlobalDragEnd()
   Current drag&drop action has finished.
------------------------------------------------------------------------*/

eq.c.App.onGlobalDragEnd = function(e) {
   // Remove 'el-drag-over' class if present.
   var self = eq.app, dnd, drag, el;
   if ((dnd = self.dnd) && (drag = dnd.drag) && (el = drag.over)) {
      drag.over = null;
      el.classList.remove('eq-dragover');
   }
};

/*------------------------------------------------------------------------
   static App.onStorageModified()
   Global 'storage' event handler, storage modified in other document.
   Updates session list from localStorage if necessary.
------------------------------------------------------------------------*/

eq.c.App.onStorageModified = function(e) {
   if (e.storageArea === window.localStorage) {
      switch (e.key) {
         case 'eq-webdlg.sls': {
            // Session list.
            var self = eq.app;
            if (!e.newValue) {
               // Session list deleted.
               self.sls = undefined;
            }
            else {
               // Session list modified.
               try {
                  var
                     nsls = JSON.parse(e.newValue),
                     csls = self.sls;
                  if (!csls || nsls.ts >= csls.ts) {
                     // No current session list,
                     // or modified list is newer, take modified list.
                     self.sls = nsls;
                  }
               }
               catch (e) {
                  console.log("Failed to parse session list, " + e.message);
                  self.sls = undefined;
                  window.localStorage.removeItem('eq-webdlg.sls');
               }
            }
            self.updateSessionList();
            break;
         }

         case 'eq-webdlg.act': {
            // Activate session.
            var sid;
            if (   e.newValue
                && (sid = window.sessionStorage.getItem('eq-webdlg.sid'))
                && e.newValue === sid) {
               window.focus();
               window.localStorage.removeItem('eq-webdlg.act');
            }
            break;
         }
      }
   }
};

/*------------------------------------------------------------------------
   static App.activateSession()
   Activate other session.
------------------------------------------------------------------------*/

eq.c.App.activateSession = function(s) {
   var sid, w;
   if (   s
       && (sid = window.sessionStorage.getItem('eq-webdlg.sid'))
       && s !== sid
       && (w = window.open('', 'eq-webdlg.' + s))) {
      // Try to activate session window.
      w.focus();
      window.localStorage.setItem('eq-webdlg.act', s);
   }
};

/*------------------------------------------------------------------------
   App.sessionTitles()
   Obtain titles of all other sessions.
------------------------------------------------------------------------*/

eq.c.App.prototype.sessionTitles = function() {
   var
      t = [],
      sls = this.sls,
      wss = window.sessionStorage,
      uat, opn, sn, s;
   if (   sls !== undefined
       && (uat = wss.getItem('eq-webdlg.uat'))
       && sls.ua == uat
       && (opn = wss.getItem('eq-webdlg.opn'))) {
      for (var i = 0, l = (sn = sls.sn).length; i < l; i++)
         if (opn === (s = sn[i]).o && s.t)
            t.push(s.t);
   }
   return t;
};

/*------------------------------------------------------------------------
   App.sessionByIndex()
   Obtain other session by index.
------------------------------------------------------------------------*/

eq.c.App.prototype.sessionByIndex = function(si) {
   var
      sls = this.sls,
      wss = window.sessionStorage,
      uat, opn, sn, s;
   if (   sls !== undefined
       && (uat = wss.getItem('eq-webdlg.uat'))
       && sls.ua == uat
       && (opn = wss.getItem('eq-webdlg.opn'))) {
      for (var i = 0, l = (sn = sls.sn).length; i < l; i++)
         if (opn === (s = sn[i]).o && s.t && si-- === 0)
            return s.s;
   }
   return null;
};

/*------------------------------------------------------------------------
   App.sessionByTitle()
   Obtain other session by title.
------------------------------------------------------------------------*/

eq.c.App.prototype.sessionByTitle = function(t) {
   var
      sls = this.sls,
      wss = window.sessionStorage,
      uat, sid, opn, sn, s;
   if (   sls !== undefined
       && (uat = wss.getItem('eq-webdlg.uat'))
       && sls.ua == uat
       && (sid = wss.getItem('eq-webdlg.sid'))
       && (opn = wss.getItem('eq-webdlg.opn'))) {
      for (var i = 0, l = (sn = sls.sn).length; i < l; i++)
         if (opn === (s = sn[i]).o && sid !== s.s && t === s.t)
            return s.s;
   }
   return null;
};

/*------------------------------------------------------------------------
   App.initSessionList()
   Initialize session list.
------------------------------------------------------------------------*/

eq.c.App.prototype.initSessionList = function() {
   var
      wss = window.sessionStorage,
      opn, sls, sn;
   if (opn = wss.getItem('eq-webdlg.opn')) {
      if ((sls = this.sls) === undefined) {
         try {
            if (sls = JSON.parse(window.localStorage.getItem('eq-webdlg.sls')))
               this.sls = sls;
         } catch (e) {}
      }
      if (!sls) {
         // No session list, done.
         return;
      }
      for (var i = 0, l = (sn = sls.sn).length; i < l; i++)
         if (sn[i].o === opn) {
            // Opener exists, done.
            return;
         }
      // Opener does not (aka. no longer) exist.
      wss.removeItem('eq-webdlg.opn');
   }
};

/*------------------------------------------------------------------------
   App.updateSessionList()
   Update session list.
------------------------------------------------------------------------*/

eq.c.App.prototype.updateSessionList = function(closing) {
   var
      wss = window.sessionStorage,
      uat, sid, opn, sls, sn;
   if (   !closing
       && (uat = wss.getItem('eq-webdlg.uat'))
       && (sid = wss.getItem('eq-webdlg.sid'))
       && (opn = wss.getItem('eq-webdlg.opn'))) {
      if ((sls = this.sls) === undefined) {
         try {
            if (sls = JSON.parse(window.localStorage.getItem('eq-webdlg.sls')))
               this.sls = sls;
         } catch (e) {}
      }
      if (!sls || sls.ua !== uat) {
         // No session list or different user agent tag.
         this.sls = sls = { ua: uat, sn : [] };
      }
      var i, l = (sn = sls.sn).length, t = document.title, s, modified;
      for (i = 0; i < l; i++) {
         if (sid === (s = sn[i]).s) {
            if (s.o !== opn || s.t !== t) {
               s.o = opn;
               s.t = t;
               modified = true;
            }
            break;
         }
      }
      if (i === l) {
         sn.push({
            s : sid,
            o : opn,
            t : t
         });
         modified = true;
      }
      if (modified) {
         sls.ts = Date.now();
         window.localStorage.setItem('eq-webdlg.sls', JSON.stringify(sls));
      }
   }
   else {
      this.sls = undefined;
      if (   (uat = wss.getItem('eq-webdlg.uat'))
          && (sid = wss.getItem('eq-webdlg.sid'))) {
         try {
            sls = JSON.parse(window.localStorage.getItem('eq-webdlg.sls'));
            if (sls.ua === uat)
               for (var i = 0, l = (sn = sls.sn).length; i < l; i++)
                  if (sid === sn[i].s) {
                     sn.splice(i, 1);
                     sls.ts = Date.now();
                     window.localStorage.setItem(
                        'eq-webdlg.sls', JSON.stringify(sls));
                     break;
                  }
         } catch (e) {}
      }
   }
};

/*------------------------------------------------------------------------
   static App.addRootListeners() .removeRootListeners()
   Add/remove root element event listeners.
------------------------------------------------------------------------*/

eq.c.App.rootEventTypes = [
   'change',
   'focusin',
   'input',
   'mousedown'
];

eq.c.App.addRootListeners = function(el) {
   var ln = this.onEvent, types = this.rootEventTypes;
   for (var i = 0, l = types.length; i < l; i++)
      el.addEventListener(types[i], ln, true);
};

eq.c.App.removeRootListeners = function(el) {
   var ln = this.onEvent, types = this.rootEventTypes;
   for (var i = 0, l = types.length; i < l; i++)
      el.removeEventListener(types[i], ln, true);
};

/*------------------------------------------------------------------------
   App.setFocus()
   Set focus by element id.
------------------------------------------------------------------------*/

eq.c.App.prototype.setFocus = function(id, el) {
   // Focus changed?
   if (id !== this.focus) {
      var
         app = this.constructor,
         dlg = this.currDlg,
         n, f, ed, fe;
      if (!dlg)
         throw new Error("BUG: setFocus no current Dialog");

      // Remove current focus indicator.
      n = document.body.getElementsByClassName('eq-focused');
      for (var i = 0, l = n.length; i < l; i++)
         n[i].classList.remove('eq-focused');

      if ((f = this.focus) !== null) {
         // Focus change, check 'blur' event on current focus element.
         if ((ed = dlg.eds.get(f)) === undefined)
            throw new Error("setFocus: from '" + f + "' not registered");
         var from = document.getElementById(f);
         if (!from)
            throw new Error("setFocus: from '" + f + "' does not exist");
         if (this.pendingTabModeId === f) {
            this.pendingTabMode = undefined;
            this.pendingTabModeId = undefined;
         }
         ed.focusLost(dlg, from);
         if (this.processEvent(dlg, {
            type : 'blur',
            target : ed.focusElement(dlg, from)
         }, f, from, ed)) {
            // Action event submitted, done.
            this.focus = id;
            return true;
         }
      }

      // Set new focus.
      if ((ed = dlg.eds.get(id)) === undefined)
         throw new Error("setFocus: to '" + id + "' not registered");
      if (el === undefined && !(el = document.getElementById(id)))
         throw new Error("setFocus: to '" + id + "' does not exist");
      this.focus = id; // Set before focusGained()!
      el.classList.add('eq-focused');
      if (this.pendingTabModeId !== id) {
         this.pendingTabMode = undefined;
         this.pendingTabModeId = undefined;
      }
      ed.focusGained(dlg, fe = ed.focusElement(dlg, el));
      this.pendingTabMode = undefined;
      this.pendingTabModeId = undefined;
      // Set element focus on next event cycle.
      this.inSetFocus = true;
      eq.Dom.onNextCycle(app.setElementFocus, {
         d : dlg,
         e : el,
         f : fe
      });
   }
   return false;
};

eq.c.App.setElementFocus = function(arg) {
   arg.f.focus();
   // Scroll into view if necessary.
   var dm = eq.Dom;
   dm.scrollIntoView(arg.d, arg.e);
   // Verify element focus on next event cycle.
   dm.onNextCycle(eq.c.App.verifyElementFocus);
};

eq.c.App.verifyElementFocus = function() {
   var
      self = eq.app,
      f = self.focus,
      typeAhead = self.typeAhead,
      el;
   if (f && (!(el = eq.Dom.eqControl(document.activeElement)) || el.id !== f))
      // Focus could not be set, blur if not body element.
      if ((el = document.activeElement) && el !== document.body)
         el.blur();

   // Element focus verified.
   self.inSetFocus = undefined;

   // Process type-ahead key strokes if necessary.
   if (typeAhead !== undefined && typeAhead.active) {
      var app = self.constructor, d = self.currDlg, ed, key;
      if (   d !== null && !d.ddo
          && d === typeAhead.dlg && f === typeAhead.focus) {
         el = document.getElementById(f);
         for (var i = 0, k = typeAhead.buf, l = k.length; i < l;) {
            if (!(ed = d.eds.get(f)))
               break;
            if ((key = k[i++]) === 'Enter' || key === 'ShiftEnter')
               if (d.fkey && d.fkey.length > 0 && d.fkey[0]) {
                  // Invalid: Enter key, Dialog.cr function key is configured.
                  break;
               }
            if (!ed.onTypeAhead(d, f, el, key))
               break;
            if (self.focus !== f) {
               // Focus has changed,
               // continue when new focus has been verified.
               if ((typeAhead.focus = self.focus) !== null) {
                  k.splice(0, i);
                  return;
               }
               break;
            }
         }
      }
      app.cancelTypeAhead();
   }
};

/*------------------------------------------------------------------------
   App.resetFocus()
   Reset focus if present.
------------------------------------------------------------------------*/

eq.c.App.resetFocus = function(dlg) {
   var n = document.body.getElementsByClassName('eq-focused');
   for (var i = 0, l = n.length; i < l; i++)
      n[i].classList.remove('eq-focused');
   // Set focus to html document element.
   document.documentElement.focus();
};

/*------------------------------------------------------------------------
   App.setIdle()
   Set application idle or active.
------------------------------------------------------------------------*/

eq.c.App.prototype.setIdle = function(dlg) {
   var app = this.constructor, dm = eq.Dom;
   if (dlg === undefined) {
      // Delay setting active state until next event cycle.
      dm.onNextCycle(app.setActive);
   }
   else {
      // Idle, cancel setActive if pending.
      this.idle = true;
      dm.cancelNextCycle(app.setActive);
      // Add idle pane(s) if necessary.
      var pane;
      for (var i = 0, l = dlg.root.length; i < l; i++) {
         var el = dlg.root[i];
         if (!el.querySelector('.eq-root>.eq-idle')) {
            pane = document.createElement('DIV');
            pane.className = 'eq-idle';
            pane.style.zIndex = dm.zIndex(el) + 2;
            el.appendChild(pane);
         }
      }
      // Visualize idle state on newly added panes after delay.
      if (pane) {
         if (this.idleTimer !== undefined)
            window.clearTimeout(this.idleTimer);
         this.idleTimer = window.setTimeout(
            app.showIdle, app.timeout.showIdle);
      }
      // Enable warning when leaving page.
      app.warnWhenLeavingPage(true);
   }
};

// Set active state.
eq.c.App.setActive = function() {
   var
      self = eq.app,
      dlg = self.currDlg,
      el;
   if (dlg !== null) {
      self.idle = false;
      // Remove idle pane(s) if any.
      for (var i = 0, l = dlg.root.length; i < l; i++)
         if (el = dlg.root[i].querySelector('.eq-root>.eq-idle'))
            el.parentNode.removeChild(el);
      if (this.idleTimer !== undefined) {
         window.clearTimeout(this.idleTimer);
         this.idleTimer = undefined;
      }
   }
};

// Visualize idle state.
eq.c.App.showIdle = function() {
   // Timer has elapsed.
   eq.app.idleTimer = undefined;
   // Add 'eq-fog' class to idle pane(s).
   var n = document.body.getElementsByClassName('eq-idle');
   for (var i = 0, l = n.length; i < l; i++)
      n[i].classList.add('eq-fog');
};

/*------------------------------------------------------------------------
   App.makeOverlay() .closeOverlay()
   Overlay management.
------------------------------------------------------------------------*/

eq.c.App.prototype.makeOverlay = function(el, ov, onClose) {
   if (this.overlay !== undefined)
      throw new Error("makeOverlay BUG: pending overlay id:" +
         this.overlay.el.id);
   if (document.body.querySelector('body>.eq-overlay'))
      throw new Error("makeOverlay BUG: orphaned overlay");
   this.overlay = {
      el : el,
      overlay : ov,
      onClose : onClose
   };
   var
      b = document.body,
      app = this.constructor;
   ov.classList.add('eq-overlay');
   ov.style.zIndex = eq.Dom.zIndexMax(b) + 1;
   b.appendChild(ov);
   // Set 'mousedown' handler,
   // global handler might not trigger on new body children (Safari).
   ov.onmousedown = this.constructor.onGlobalMouseDownOrClick;
   // Close tooltip if any.
   app.ttClose();
};

eq.c.App.prototype.closeOverlay = function(el, arg) {
   var od = this.overlay;
   if (od === undefined) {
      if (document.body.querySelector('body>.eq-overlay'))
         throw new Error("closeOverlay BUG: orphaned overlay");
      return;
   }
   var ov = od.overlay;
   if (el !== undefined && el !== od.el)
      throw new Error("closeOverlay BUG: wrong id:" + el.id +
         ", expected:" + od.el.id);
   if (!ov.classList.contains('eq-overlay'))
      throw new Error("closeOverlay BUG: no overlay id:" + od.el.id);
   ov.onmousedown = null;
   this.overlay = undefined;
   od.onClose(od.el, ov, arg);
   ov.style.zIndex = '';
   ov.classList.remove('eq-overlay');
};

/*------------------------------------------------------------------------
   App.cleanupStyles() .removeStyles()
   Cleanup application styles, remove styles by id.
------------------------------------------------------------------------*/

eq.c.App.prototype.cleanupStyles = function() {
   var
      stl = this.stl,
      sheet = stl.sheet,
      rules = sheet.cssRules,
      l;
   if (l = rules.length) {
      if (l !== stl.owner.length)
         throw new Error("cleanupStyles BUG: " + l +
            " stale rules, inconsistent owner.length:" + stl.owner.length);
      else
         console.warn("cleanupStyles: " + l + " stale rules");
      do
         sheet.deleteRule(l - 1);
      while (l = rules.length);
      stl.owner.length = 0;
   }
   else if (stl.owner.length)
      throw new Error("cleanupStyles BUG: inconsistent owner.length:" +
         stl.owner.length);
};

eq.c.App.prototype.removeStyles = function(id) {
   var
      stl = this.stl,
      sheet = stl.sheet,
      owner = stl.owner,
      rl = owner.length,
      i = 0, ri, ci;
   if ((ri = ci = owner.indexOf(id)) !== -1) {
      do {
         sheet.deleteRule(ci);
         i++;
      } while (++ri < rl && owner[ri] === id);
      owner.splice(ci, i);
   }
};

/*------------------------------------------------------------------------
   App.newDialog()
   Create new Dialog.
------------------------------------------------------------------------*/

eq.c.App.prototype.newDialog = function(id) {
   var m = eq.LayoutMode, lm = this.layoutMode, dlg;
   if (lm !== undefined && (lm & m.inline) && !(lm & m.dialog)) {
      // Single inline Dialog, forced by layout mode,
      // invalidate and close existing Dialogs.
      this.dlg.forEach(eq.c.App.cbInvalidateCloseDlg, this);
      this.dlg.clear();
   }

   dlg = new eq.c.Dlg(id);
   this.dlg.set(id, dlg);
   return dlg;
};

eq.c.App.cbInvalidateCloseDlg = function(d) {
   this.api.postMessage({
      o : eq.ApiOp.invalidate,
      d : d.id
   });
   d.close();
};

/*------------------------------------------------------------------------
   App.newTempDialog()
   Create new temporary Dialog.
------------------------------------------------------------------------*/

eq.c.App.prototype.newTempDialog = function(id) {
   var
      t = 'eq-temp',
      p = document.getElementById(t),
      dlg;
   if (p)
      throw new Error("newTempDialog BUG: '" + t + "' already exists");
   p = document.createElement('DIV');
   p.id = t;
   p.className = 'eq-dialog eq-hidden eq-container';
   document.body.appendChild(p);

   dlg = new eq.c.Dlg(id);
   this.dlg.set(id, dlg);
   dlg.temp = true;
   return dlg;
};

/*------------------------------------------------------------------------
   App.closeDialog()
   Close Dialog.
------------------------------------------------------------------------*/

eq.c.App.prototype.closeDialog = function(dlg) {
   this.dlg.delete(dlg.id);
   dlg.close();
};

/*------------------------------------------------------------------------
   App.topDialog()
   Get topmost Dialog.
------------------------------------------------------------------------*/

eq.c.App.prototype.topDialog = function() {
   var wl, i, d;
   if ((wl = this.wl) !== undefined)
      for (i = wl.length; --i >= 0;)
         if ((d = wl[i].dlg) !== undefined)
            return d;

   return null;
};

/*------------------------------------------------------------------------
   App.setCurrDialog()
   Set current Dialog, make it top and visible.
------------------------------------------------------------------------*/

eq.c.App.prototype.setCurrDialog = function(dlg) {
   var app = this.constructor, d = this.currDlg, t;
   if (d !== null && d !== dlg && d.ddo) {
      // Cancel pending Dialog.do on previous Dialog.
      this.currDlg = null;
      this.focus = null;
      this.changed.clear();
      this.setIdle(d);
      d.ddo = undefined;
   }

   if (dlg.w !== null && dlg.w.activate()) {
      // Close tooltip if any.
      app.ttClose();
   }

   dlg.visible();
   this.currDlg = dlg;

   // Set Dialog title.
   if ((t = dlg.title || app.title) !== document.title) {
      document.title = t;
      this.updateSessionList();
   }
};

/*------------------------------------------------------------------------
   App.dialogsVisible()
   Make Dialogs visible.
------------------------------------------------------------------------*/

eq.c.App.prototype.dialogsVisible = function() {
   var app = this.constructor;
   this.dlg.forEach(app.cbDialogsVisible);
};

eq.c.App.cbDialogsVisible = function(d) {
   d.visible();
};

/*------------------------------------------------------------------------
   Type-ahead management.
------------------------------------------------------------------------*/

eq.c.App.prototype.startTypeAhead = function() {
   var typeAhead = this.typeAhead, f, d;
   if (typeAhead !== undefined && !typeAhead.active) {
      if ((f = this.focus) !== null) {
         if ((d = this.currDlg) === null)
            throw new Error("App.startTypeAhead BUG: no current Dialog");
         typeAhead.active = true;
         typeAhead.dlg = d;
         typeAhead.focus = f;
      }
   }
};

eq.c.App.prototype.addTypeAhead = function(e) {
   var app = this.constructor, typeAhead = this.typeAhead, key;
   if (typeAhead !== undefined && typeAhead.active) {
      switch (key = e.key) {
         case 'Tab':
         case 'Enter':
            if (e.shiftKey)
               key = 'Shift' + key;
            break;

         case ' ':
         case 'Spacebar': // IE
            key = ' ';
            break;

         case 'Shift':
         case 'CapsLock':
            // Allowed modifier keys, ignore.
            return false;

         default:
            if (key.length !== 1 || e.ctrlKey || e.altKey || e.metaKey) {
               // Invalid key.
               app.cancelTypeAhead();
               return false;
            }
      }

      typeAhead.buf.push(key);
      if (typeAhead.timer !== 0)
         window.clearTimeout(typeAhead.timer);
      typeAhead.timer = window.setTimeout(
         app.cancelTypeAhead, app.timeout.typeAhead);
      return true;
   }

   return false;
};

eq.c.App.cancelTypeAhead = function() {
   var typeAhead = eq.app.typeAhead;
   if (typeAhead !== undefined) {
      typeAhead.active = false;
      typeAhead.dlg = null;
      typeAhead.focus = null;
      typeAhead.buf.length = 0;
      if (typeAhead.timer !== 0) {
         window.clearTimeout(typeAhead.timer);
         typeAhead.timer = 0;
      }
   }
};

/*------------------------------------------------------------------------
   Tooltip management.
------------------------------------------------------------------------*/

eq.c.App.prototype.ttAttach = function(el) {
   if (!el.onmouseenter) {
      var cls = this.constructor;
      if (el.onmouseleave)
         throw new Error("BUG: App.ttAttach id:" +
            eq.Dom.eqControl(el).id + " event handler in use");
      el.onmouseenter = cls.ttOnMouseEnter;
      el.onmouseleave = cls.ttOnMouseLeave;
      if (this.toolTip === undefined)
         this.toolTip = {
            dlg : null,  // Dialog
            el  : null,  // DLG element
            ed  : null,  // DLG element descriptor
            dd  : false, // DLG element is disabled
            tt  : null,  // Tooltip
            ti  : 0,     // Timer
            x   : 0,     // Pointer position relative to document origin
            y   : 0,
            upd : false  // Tooltip visible, update pending
         };
   }
   else if (!el.onmouseleave)
      throw new Error("BUG: App.ttAttach id:" +
         eq.Dom.eqControl(el).id + " event handler not set");
};

eq.c.App.prototype.ttDetach = function(el) {
   if (el.onmouseenter) {
      var tt = this.toolTip;
      if (!el.onmouseleave)
         throw new Error("BUG: App.ttDetach id:" +
            eq.Dom.eqControl(el).id + " event handler not set");
      el.onmouseenter = null;
      el.onmouseleave = null;
      el.onmousemove = null;
      if (tt && tt.el === el)
         this.constructor.ttClose();
   }
   else if (el.onmouseleave)
      throw new Error("BUG: App.ttDetach id:" +
         eq.Dom.eqControl(el).id + " event handler in use");
};

eq.c.App.ttShow = function() {
   var
      self = eq.app,
      app, dm, cn, tt, ed, el, b, s, dlg, fn, vw, rc, x, y;
   if (tt = self.toolTip) {
      app = self.constructor;
      if (!(dlg = tt.dlg) || !(ed = tt.ed) || !(cn = ed.onToolTip(el = tt.el)))
         app.ttHide();
      else {
         dm = eq.Dom;
         if (el = tt.tt) {
            s = el.style;
            if (tt.dd)
               el.classList.add('eq-disabled');
            else
               el.classList.remove('eq-disabled');
         }
         else {
            tt.tt = el = document.createElement('DIV');
            el.className = 'eq-tip';
            if (tt.dd)
               el.classList.add('eq-disabled');
            (s = el.style).zIndex = dm.zIndexMax(b = document.body) + 1;
            // Derive font from Dialog or set default font.
            if (!dlg.copyFontToElement(el))
               if ((fn = self.fnDefault) && dm.setFont(el, fn))
                  dm.adjustElementFontSize(el);
            b.appendChild(el);
         }
         el.onmouseenter = app.ttOnMouseEnter;
         el.onmouseleave = app.ttOnMouseLeave;
         el.onmousemove = app.ttOnMouseMove;
         dm.setContent(el, cn);
         vw = dm.viewRect();
         rc = el.getBoundingClientRect();
         s.maxWidth = '';
         s.maxHeight = '';
         if ((x = tt.x + 12) + rc.width >= vw.x + vw.w)
            if ((x = vw.x + vw.w - rc.width - 1) < vw.x) {
               x = vw.x + 1;
               s.maxWidth = (vw.w - 2) + 'px';
            }
         if ((y = tt.y + 12) + rc.height >= vw.y + vw.h)
            if ((y = vw.y + vw.h - rc.height - 1) < vw.y) {
               y = vw.y + 1;
               s.maxHeight = (vw.h - 2) + 'px';
            }
         s.left = x + 'px';
         s.top = y + 'px';
         tt.ti = window.setTimeout(app.ttHide, app.toolTipTimeout.hide);
         tt.upd = false;
      }
   }
};

eq.c.App.ttHide = function() {
   var
      self = eq.app,
      tt, el, ti;
   if (tt = self.toolTip) {
      if (el = tt.el)
         el.onmousemove = null;
      if (el = tt.tt) {
         el.parentNode.removeChild(el);
         tt.tt = null;
      }
      if (ti = tt.ti) {
         window.clearTimeout(ti);
         tt.ti = 0;
         tt.upd = false;
      }
   }
};

eq.c.App.ttClose = function() {
   var
      self = eq.app,
      tt, el, ti;
   if (tt = self.toolTip) {
      tt.dlg = null;
      tt.ed = null;
      if (el = tt.el) {
         el.classList.remove('eq-tt');
         el.onmousemove = null;
         tt.el = null;
      }
      if (el = tt.tt) {
         el.parentNode.removeChild(el);
         tt.tt = null;
      }
      if (ti = tt.ti) {
         window.clearTimeout(ti);
         tt.ti = 0;
         tt.upd = false;
      }
   }
}

eq.c.App.prototype.ttDispose = function() {
   this.constructor.ttClose();
   this.toolTip = undefined;
};

eq.c.App.prototype.ttMouseEnter = function(el, x, y, btn) {
   var app, dlg, id, tt, ti, ec, upd;
   if (el.classList.contains('eq-tip')) {
      // Entered current tooltip, keep open.
      if (!(tt = this.toolTip))
         throw new Error("BUG: App.ttOnMouseEnter no current tooltip");
      if (el !== tt.tt)
         throw new Error("BUG: App.ttOnMouseEnter tooltip element mismatch");
      if (ti = tt.ti) {
         window.clearTimeout(ti);
         tt.ti = 0;
         tt.upd = false;
      }
      tt.x = x;
      tt.y = y;
      return;
   }
   if ((tt = this.toolTip)) {
      app = this.constructor;
      tt.x = x;
      tt.y = y;
      if (dlg = this.currDlg) {
         if (ti = tt.ti) {
            window.clearTimeout(ti);
            tt.ti = 0;
            tt.upd = false;
         }
         if (el !== tt.el) {
            if (!(ec = eq.Dom.eqControl(el))) {
               // Temporarily not a control (probably overlay),
               // close immediately.
               app.ttClose();
               return;
            }
            tt.dlg = dlg;
            tt.el = el;
            if ((tt.ed = dlg.eds.get(id = ec.id)) === undefined)
               throw new Error("BUG: App.ttOnMouseEnter id:" +
                  id + " not registered");
            tt.dd = ec.classList.contains('eq-disabled');
            el.classList.add('eq-tt');
         }
         el.onmousemove = app.ttOnMouseMove;
         if (btn === 0) {
            // No buttons pressed, start 'show tooltip' timer.
            upd = tt.upd = !!tt.tt;
            tt.ti = window.setTimeout(app.ttShow,
               upd ? app.toolTipTimeout.update : app.toolTipTimeout.show);
         }
      }
   }
};

eq.c.App.ttOnMouseEnter = function(e) {
   var self = eq.app;
   if (self.capture === undefined) {
      e.stopImmediatePropagation();
      self.ttMouseEnter(e.target, e.pageX, e.pageY,
         e.buttons !== undefined ? e.buttons : e.button);
   }
};

eq.c.App.ttOnMouseLeave = function(e) {
   var
      self = eq.app,
      el = e.target,
      tt, ti, app, upd, cl;
   el.classList.remove('eq-tt');
   if (self.capture === undefined) {
      e.stopImmediatePropagation();
      if (tt = self.toolTip) {
         tt.x = e.pageX;
         tt.y = e.pageY;
         if (ti = tt.ti) {
            window.clearTimeout(ti);
            tt.ti = 0;
            tt.upd = false;
         }
         if (tt.tt) {
            app = self.constructor;
            upd = tt.upd = !self.idle;
            tt.ti = window.setTimeout(app.ttClose,
               upd ? app.toolTipTimeout.update : app.toolTipTimeout.hide);
         }
      }
      // Check if parent element entered.
      while ((el = el.parentNode) && el !== document)
         if ((cl = el.classList) && cl.contains('eq-tt')) {
            self.ttMouseEnter(el, tt.x, tt.y,
               e.buttons !== undefined ? e.buttons : e.button);
            break;
         }
   }
};

eq.c.App.ttOnMouseMove = function(e) {
   var
      self = eq.app,
      app, tt, ti;
   if (self.capture === undefined) {
      e.stopImmediatePropagation();
      if (tt = self.toolTip) {
         tt.x = e.pageX;
         tt.y = e.pageY;
         if (tt.tt) {
            if (!tt.upd) {
               if (ti = tt.ti) {
                  window.clearTimeout(ti);
                  tt.ti = 0;
                  tt.upd = false;
               }
               if (!self.idle) {
                  app = self.constructor;
                  tt.ti = window.setTimeout(app.ttHide, app.toolTipTimeout.hide);
               }
            }
         }
         else {
            if (ti = tt.ti) {
               window.clearTimeout(ti);
               tt.ti = 0;
               tt.upd = false;
            }
            if ((e.buttons !== undefined ? e.buttons : e.button) === 0) {
               // Tooltip not shown, no buttons pressed.
               app = self.constructor;
               tt.ti = window.setTimeout(app.ttShow, app.toolTipTimeout.show);
            }
         }
      }
   }
};

/*------------------------------------------------------------------------
   Inject script or style into page.
------------------------------------------------------------------------*/

eq.c.App.inject = function(s) {
   var sl = s.length;
   if (!sl) {
      console.error("App.inject() failed, empty URL.");
      return false;
   }
   var
      self = this.inject,
      h = document.head,
      n = h.getElementsByClassName('eq-inject');
   // Check duplicate.
   for (var i = 0, l = n.length; i < l; i++) {
      var t = n[i], u, ul;
      if (t.tagName === 'SCRIPT')
         u = t.src;
      else if (t.tagName === 'LINK')
         u = t.href;
      if ((ul = u.length) < sl)
         continue;
      if (ul > sl)
          u = u.substring(ul - sl);
      if (u.toLowerCase() === s.toLowerCase())
         return false;
   }
   if (/\.js$/i.test(s)) {
      // Inject script.
      var el = document.createElement('SCRIPT');
      el.src = s;
      el.className = 'eq-inject';
      self.pending++;
      el.onload = self.ready;
      el.onerror = self.failed;
      h.appendChild(el);
      return true;
   }
   if (/\.css$/i.test(s)) {
      // Inject style sheet.
      var el = document.createElement('LINK');
      el.href = s;
      el.className = 'eq-inject';
      el.rel = 'stylesheet';
      el.type = 'text/css';
      self.pending++;
      el.onload = self.ready;
      el.onerror = self.failed;
      h.appendChild(el);
      return true;
   }
   console.error("App.inject(" + s + ") failed, unknown type.");
   return false;
};

eq.c.App.inject.pending = 0;

eq.c.App.inject.ready = function(e) {
   var
      el = e.target,
      pending = --eq.c.App.inject.pending,
      api, op, eqp, p;
   el.onload = el.onerror = null;
   if (pending === 0) {
      // Inject finished, install new plugins if any.
      api = eq.app.api;
      op = eq.ApiOp;
      if ((eqp = eq.plugin) !== undefined) {
         // Install plugins if any.
         if ((p = eqp.install()).length) {
            // Transfer plugin descriptors to API thread.
            api.postMessage({ o : op.installPlugins, p : p });
         }
      }
      // Notify API thread.
      api.postMessage({ o : op.injectDone });
   }
   else if (pending < 0)
      throw new Error("BUG App.inject.ready pending:" + pending);
};

eq.c.App.inject.failed = function(e) {
   var el = e.target;
   el.parentNode.removeChild(el);
   eq.c.App.inject.ready(e);
};

/*------------------------------------------------------------------------
   DLG listener utilities.
------------------------------------------------------------------------*/

eq.c.App.dlgListener = function() {
   var a = eq.app.currApp, r;
   if (a && (r = /^.+?:([0-9]+)$/.exec(a)))
      return r[1];
   return null;
};

eq.c.App.dlgListener.message = function() {
   var p = this();
   if (p) {
      eq.app.dlgListen = true;
      new eq.c.MessageDialog(eq.strings.get('message.dlgListen') +
         " <b>" + p + "</b>", 0, this.canceled);
   }
   else
      eq.app.dlgListen = true;
};

eq.c.App.dlgListener.close = function() {
   var self = eq.app;
   if (self.dlgListen) {
      self.dlgListen = false;
      eq.c.MessageDialog.close();
   }
};

eq.c.App.dlgListener.canceled = function() {
   var self = eq.app;
   if (self.dlgListen)
      self.api.postMessage({ o : eq.ApiOp.closeWebSocket });
};

/*------------------------------------------------------------------------
   Beep notification.
------------------------------------------------------------------------*/

eq.c.App.beep = function() {
   try {
      var el = document.getElementById('eq-beep');
      if (!el) {
         el = document.createElement('AUDIO');
         el.id = 'eq-beep';
         el.src = 'eq-webdlg-beep.mp3';
         document.body.appendChild(el);
      }
      el.play();
   }
   catch (e) {
      console.error("beep " + e.name + ": " + e.message);
   }
};

/*------------------------------------------------------------------------
   Play sound URL.
------------------------------------------------------------------------*/

eq.c.App.playSound = function(url) {
   try {
      var el = document.getElementById('eq-sound');
      if (!el) {
         el = document.createElement('AUDIO');
         el.id = 'eq-sound';
         document.body.appendChild(el);
      }
      el.src = url;
      el.play();
   }
   catch (e) {
      console.error("playSound '" + url + "' " +
         e.name + ": " + e.message);
   }
};

/*------------------------------------------------------------------------
   static App.warnWhenLeavingPage()
   Enable/disable warning when leaving page.
   Note: Never enabled when application was started via DLG listener.
------------------------------------------------------------------------*/

eq.c.App.warnWhenLeavingPage = function(enable) {
   if (enable && eq.app.warnLeavingPage)
      window.addEventListener('beforeunload', this.onBeforeUnload, true);
   else
      window.removeEventListener('beforeunload', this.onBeforeUnload, true);
};

eq.c.App.onBeforeUnload = function(e) {
   e.preventDefault();
   return e.returnValue = "Do you want to leave this page? "
                        + "This will terminate the running program.";
};

/*------------------------------------------------------------------------
   static App.onWindowMessage()
   Window 'message' event handler.
------------------------------------------------------------------------*/

eq.c.App.onWindowMessage = function(e) {
   var self = eq.app, dlg, d, id, m, el, ed;
   if (   !self.idle && (dlg = self.currDlg)
       && (Array.isArray(d = e.data)) && d.length === 2
       && (id = d[0]) && (m = d[1])
       && (el = document.getElementById(id))
       && (ed = dlg.eds.get(id)) !== undefined)
      ed.onMessage(dlg, id, el, m);
};

/*========================================================================
   Start main thread
========================================================================*/

if (!eq.redirected) (function() {
   var app = eq.c.App;
   eq.app = new app();

   // Obtain URL query string arguments.
   try {
      var q = /(\?[^#]+)/.exec(window.location.href)[1];
      if (q) {
         var
            re = new RegExp('[?&]([^=]+)=([^&]*)', 'g'),
            r, a;
         while (r = re.exec(q)) {
            if (a === undefined)
               a = {};
            a[r[1]] = r[2];
         }
         app.args = a;
      }
   } catch (e) {}

   // Install global event listeners.
   window.addEventListener('blur', app.onCancelUserAction, true);
   window.addEventListener('click', app.onGlobalMouseDownOrClick, true);
   window.addEventListener('contextmenu', app.onGlobalContextMenu, true);
   window.addEventListener('dblclick', app.onGlobalMouseDoubleClick, true);
   window.addEventListener('dragend', app.onGlobalDragEnd, true);
   window.addEventListener('focus', app.onCancelUserAction, true);
   window.addEventListener('keydown', app.onGlobalKeyDown, true);
   window.addEventListener('keyup', app.onGlobalKeyUp, true);
   window.addEventListener('message', app.onWindowMessage, true);
   window.addEventListener('mousedown', app.onGlobalMouseDownOrClick, true);
   window.addEventListener('mouseup', app.onGlobalMouseUp, true);
   window.addEventListener('pagehide', app.onGlobalPageHide, true);
   window.addEventListener('resize', app.onCancelUserAction, true);
   window.addEventListener('storage', app.onStorageModified);

   // Start API thread.
   app.apiStart();

   // Continue initialization when DOM is ready.
   if (document.readyState === 'loading')
      window.addEventListener('DOMContentLoaded', app.domReady);
   else
      app.domReady();
})();

/*========================================================================
   Public API: Start application.
========================================================================*/

eq.start = function(app, args) {
   if (document.documentElement.classList.contains('eq-webdlg')) {
      // Application running, ignore.
      return false;
   }
   var cls = eq.c.App, self;
   if (cls.documentComplete) {
      // Document not complete, ignore.
      return false;
   }
   (self = eq.app).currApp = app;
   self.args = args;
   cls.start();
   return true;
};

