@litejs/ui 25.10.0 → 26.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +14 -2
  2. package/load.js +13 -9
  3. package/package.json +3 -3
  4. package/shim.js +33 -20
  5. package/ui.js +52 -19
package/README.md CHANGED
@@ -13,18 +13,30 @@ used on heavy-traffic websites since 2006.
13
13
 
14
14
  - Dependency-free, weighs around 25kB (+8kB polyfills for old browsers).
15
15
  - Written in ES5, compatible with all browsers (including IE5.5).
16
- - No transpiling/compiling/bundling headache, just write a working code.
16
+ Works seamlessly in ESM projects too.
17
+ - No transpiling/compiling/bundling headache, just write working code.
18
+
19
+ Includes templates, view routing, data binding, i18n, keyboard shortcuts, and touch gestures.
17
20
 
18
21
  For usage instructions, see [Quick-Start](https://github.com/litejs/litejs/wiki/Quick-Start) guide
19
22
  and [wiki](https://github.com/litejs/ui/wiki).
20
23
 
24
+ ## Examples
25
+
26
+ - [Simplest example](https://litejs.github.io/ui/simplest.html) —
27
+ [source](/litejs/ui/blob/main/test/html/simplest.html)
28
+ - [Built-in routing](https://litejs.github.io/ui/routed.html) —
29
+ [source](/litejs/ui/blob/main/test/html/routed.html)
30
+ - [Full SVG SPA](https://litejs.github.io/ui/svg-spa.html) —
31
+ [source](/litejs/ui/blob/main/test/html/svg-spa.html)
32
+
21
33
 
22
34
  ## Contributing
23
35
 
24
36
  Follow [Coding Style Guide](https://github.com/litejs/litejs/wiki/Style-Guide),
25
37
  run tests `npm install; npm test`.
26
38
 
27
- > Copyright (c) 2006-2024 Lauri Rooden <lauri@rooden.ee>
39
+ > Copyright (c) 2006-2026 Lauri Rooden <lauri@rooden.ee>
28
40
  [MIT License](https://litejs.com/MIT-LICENSE.txt) |
29
41
  [GitHub repo](https://github.com/litejs/ui) |
30
42
  [npm package](https://npmjs.org/package/@litejs/ui) |
package/load.js CHANGED
@@ -56,6 +56,7 @@
56
56
  /*** log ***/
57
57
  , unsentLog = xhr._l = []
58
58
  , lastError
59
+ // load.js is expected to be the first script to run and no prior window.onerror exists.
59
60
  , onerror = window.onerror = function(message, file, line, col, error) {
60
61
  // Do not send multiple copies of the same error.
61
62
  // file = document.currentScript.src || import.meta.url
@@ -77,12 +78,14 @@
77
78
 
78
79
 
79
80
  /*** theme ***/
81
+ , savedTheme
80
82
  , ALT_THEME = "dark"
81
- , matchMedia = window.matchMedia
82
- , localStorage = window.localStorage
83
+ try {
84
+ savedTheme = window.localStorage.theme
85
+ } catch(e){}
83
86
  if (ALT_THEME == (
84
- localStorage && localStorage.theme ||
85
- matchMedia && matchMedia("(prefers-color-scheme:dark)").matches && ALT_THEME
87
+ savedTheme ||
88
+ (savedTheme = window.matchMedia) && savedTheme("(prefers-color-scheme:dark)").matches && ALT_THEME
86
89
  )) {
87
90
  document.documentElement.className = "is-" + ALT_THEME
88
91
  }
@@ -212,10 +215,7 @@
212
215
  var execResult = (xhr[files[pos].replace(/[^?]+\.|\?.*/g, "")] || execScript)(res[pos], files[pos])
213
216
  if (execResult && execResult.then) {
214
217
  res[pos] = 0
215
- return execResult.then(function() {
216
- res[pos] = ""
217
- exec()
218
- })
218
+ return execResult.then(advanceExec, advanceExec)
219
219
  }
220
220
  } catch(e) {
221
221
  /*** log ***/
@@ -239,6 +239,11 @@
239
239
  }
240
240
  }
241
241
  }
242
+ function advanceExec(err) {
243
+ if (err) onerror(err, files[pos])
244
+ res[pos] = ""
245
+ exec()
246
+ }
242
247
  }
243
248
 
244
249
  load([
@@ -247,4 +252,3 @@
247
252
  /**/
248
253
 
249
254
  }(this) // jshint ignore:line
250
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litejs/ui",
3
- "version": "25.10.0",
3
+ "version": "26.2.0",
4
4
  "description": "UI engine for LiteJS full-stack framework",
5
5
  "license": "MIT",
6
6
  "author": "Lauri Rooden <lauri@rooden.ee>",
@@ -24,9 +24,9 @@
24
24
  "lj-extract-lang": "./bin/extract-lang.js"
25
25
  },
26
26
  "scripts": {
27
- "test": "lj t"
27
+ "test": "lj t test/index.js"
28
28
  },
29
29
  "devDependencies": {
30
- "@litejs/cli": "25.5.0"
30
+ "@litejs/cli": "26.2.1"
31
31
  }
32
32
  }
package/shim.js CHANGED
@@ -14,7 +14,6 @@
14
14
 
15
15
 
16
16
  /* global El, xhr, escape */
17
- /* c8 ignore start */
18
17
  !function(window, Date, Function, Infinity, P) {
19
18
 
20
19
  // Array#flat() - Chrome69, Firefox62, Safari12
@@ -26,6 +25,7 @@
26
25
 
27
26
  var UNDEF, canCapture, isArray, oKeys
28
27
  , O = window
28
+ , NULL = null
29
29
  , patched = (window.xhr || window)._p = []
30
30
  , jsonRe = /[\x00-\x1f\x22\x5c]/g
31
31
  , JSONmap = {"\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r","\t":"\\t","\"":"\\\"","\\":"\\\\"}
@@ -49,6 +49,7 @@
49
49
  , b = "setInterval"
50
50
  , setInterval = (window[b] = window[b])
51
51
  , c
52
+ /* node:coverage ignore next 19 */
52
53
  , ie678 = !+"\v1" && a < 9 // jshint ignore:line
53
54
  , ie6789 = ie678 || a == 9
54
55
  , ie67 = ie678 && a < 8
@@ -71,6 +72,7 @@
71
72
  wheel: wheelEv
72
73
  }
73
74
  , fixFn = Event.fixFn = {
75
+ /* node:coverage ignore next 17 */
74
76
  wheel: wheelEv !== "wheel" && function(el, fn) {
75
77
  // DOMMouseScroll Firefox 1 MouseScrollEvent.detail - number of lines to scroll (-32768/+32768 = page up/down)
76
78
  return function(e) {
@@ -119,8 +121,9 @@
119
121
  patch("cancel" + a, "clearTimeout(a)")
120
122
 
121
123
 
124
+ /* node:coverage ignore next 8 */
122
125
  if (!IS_NODE && !(onhashchange in window) || ie67) {
123
- patch(onhashchange, null)
126
+ patch(onhashchange, NULL)
124
127
  setInterval(function() {
125
128
  if (lastHash !== (lastHash = location.href.split("#")[1]) && isFn(window[onhashchange])) {
126
129
  window[onhashchange]()
@@ -131,6 +134,7 @@
131
134
  // Missing PointerEvents with Scribble enable on Safari 14
132
135
  // https://mikepk.com/2020/10/iOS-safari-scribble-bug/
133
136
  // https://bugs.webkit.org/show_bug.cgi?id=217430
137
+ /* node:coverage ignore next 50 */
134
138
  if (!window.PointerEvent) {
135
139
  // IE10
136
140
  if (window[MS + EV]) {
@@ -176,6 +180,7 @@
176
180
  }
177
181
  _fn.call(el, e)
178
182
  }
183
+ /* node:coverage ignore next 19 */
179
184
  function touchToPointer(e) {
180
185
  var touch
181
186
  , touches = e.changedTouches
@@ -234,7 +239,7 @@
234
239
  return (data[id] = "" + val)
235
240
  },
236
241
  getItem: function(id) {
237
- return data[id]
242
+ return data[id] || NULL
238
243
  },
239
244
  removeItem: function(id) {
240
245
  delete data[id]
@@ -270,7 +275,7 @@
270
275
  // IE 8 serializes `undefined` as `"undefined"`
271
276
  return (
272
277
  isStr(o) ? "\"" + o.replace(jsonRe, jsonFn) + "\"" :
273
- o !== o || o == null || o === Infinity || o === -Infinity ? "null" :
278
+ o !== o || o == NULL || o === Infinity || o === -Infinity ? "null" :
274
279
  typeof o == "object" ? (
275
280
  isFn(o.toJSON) ? stringify(o.toJSON()) :
276
281
  isArray(o) ? "[" + o.map(stringify) + "]" :
@@ -290,11 +295,14 @@
290
295
  // Since Chrome23/Firefox21 parseInt parses leading-zero strings as decimal, not octal
291
296
  b = patch("g:parseInt", "return X(a,(b>>>0)||(Y.test(''+a)?16:10))", _parseInt("08") !== 8, _parseInt, /^\s*[-+]?0[xX]/)
292
297
 
298
+ O = Math
299
+ patch("log10", "return X(a)/Y", 0, O.log, O.LN10)
300
+
301
+ a = O.pow
293
302
  O = Number
294
303
  patch("parseInt", b)
295
304
  patch("parseFloat", parseFloat)
296
305
  patch("isNaN", "return a!==a")
297
- a = Math.pow
298
306
  c = "_SAFE_INTEGER"
299
307
  patch("EPSILON", a(2, -52))
300
308
  patch(
@@ -310,6 +318,7 @@
310
318
  O = O[P]
311
319
  // IE8 toJSON does not return milliseconds
312
320
  // FF37 returns invalid extended ISO-8601, `29349-01-26T00:00:00.000Z` instead of `+029349-01-26T00:00:00.000Z`
321
+ /* node:coverage ignore next */
313
322
  b = O[a = "toISOString"] && new Date(8e14)[a]().length < 27 || ie678
314
323
  patch(a, patch("toJSON", [
315
324
  "a=t.getUTCFullYear();if(a!==a)throw RangeError('Invalid time');return(b=a<0?'-':a>9999?'+':'')+X(a<0?-a:a,'-',b?6:4", "Month()+1,'-'", "Date(),'T'",
@@ -352,7 +361,7 @@
352
361
 
353
362
  // TODO:2021-02-25:lauri:Accept iterable objects
354
363
  //patch("from", "a=S.call(a);return b?a.map(b,c):a")
355
- patch("from", "a=X(a)?a.split(''):b?a:S.call(a);return b?a.map(b,c):a", 0, isStr)
364
+ patch("from", "a=X(a)?a.split(''):S.call(a);return b?a.map(b,c):a", 0, isStr)
356
365
  patch("of", "return S.call(A)")
357
366
 
358
367
  O = O[P]
@@ -406,9 +415,13 @@
406
415
  patch("sendBeacon", function(url, data) {
407
416
  // The synchronous XMLHttpRequest blocks the process of unloading the document,
408
417
  // which in turn causes the next navigation appear to be slower.
409
- url = xhr("POST", url, xhr.unload)
410
- url.setRequestHeader("Content-Type", "text/plain;charset=UTF-8")
411
- url.send(data)
418
+ try {
419
+ url = xhr("POST", url, xhr.unload)
420
+ url.setRequestHeader("Content-Type", "text/plain;charset=UTF-8")
421
+ url.send(data)
422
+ return true
423
+ } catch(e){}
424
+ return false
412
425
  })
413
426
 
414
427
  // The HTML5 document.head DOM tree accessor
@@ -441,8 +454,9 @@
441
454
  , closest = patch("closest", walk.bind(window, "parentNode", 1))
442
455
  , matches = patch("matches", "return!!X(a)(t)", 0, selectorFn)
443
456
 
457
+ /* node:coverage ignore next 13 */
444
458
  try {
445
- O[a = "addEventListener"]("t", null, Object.defineProperties({}, {
459
+ O[a = "addEventListener"]("t", NULL, Object.defineProperties({}, {
446
460
  capture: { get: function() { canCapture = 1 } }
447
461
  }))
448
462
  b = "removeEventListener"
@@ -456,6 +470,7 @@
456
470
  // https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel
457
471
  // - IE8 always prevents the default of the mousewheel event.
458
472
  patch(a, "return(t.attachEvent('on'+a,b=X(t,a,b)),b)", 0, function(el, ev, fn) {
473
+ /* node:coverage ignore next 8 */
459
474
  return function() {
460
475
  var e = new Event(ev)
461
476
  if (e.clientX !== UNDEF) {
@@ -474,8 +489,8 @@
474
489
 
475
490
 
476
491
  function selectorFn(str) {
477
- if (str != null && !isStr(str)) throw Error("Invalid selector")
478
- return selectorCache[str || ""] ||
492
+ if (!str || !isStr(str)) throw Error("Invalid selector")
493
+ return selectorCache[str] ||
479
494
  (selectorCache[str] = Function("m,c", "return function(_,v,a,b){return " +
480
495
  str.split(selectorSplitRe).map(function(sel) {
481
496
  var relation, from
@@ -510,7 +525,7 @@
510
525
  if (first) return el
511
526
  out.push(el)
512
527
  }
513
- return first ? null : out
528
+ return first ? NULL : out
514
529
  }
515
530
 
516
531
  function find(node, sel, first) {
@@ -523,6 +538,7 @@
523
538
 
524
539
  // ie6789
525
540
  // The documentMode is an IE only property, supported from IE8.
541
+ /* node:coverage ignore next 8 */
526
542
  if (ie678) {
527
543
  try {
528
544
  // Remove background image flickers on hover in IE6
@@ -535,12 +551,13 @@
535
551
  function isFn(value) {
536
552
  return typeof value === "function"
537
553
  }
538
- function isStr(value) {
539
- return typeof value === "string"
540
- }
554
+ /* node:coverage ignore next 3 */
541
555
  function isObj(obj) {
542
556
  return !!obj && obj.constructor === Object
543
557
  }
558
+ function isStr(value) {
559
+ return typeof value === "string"
560
+ }
544
561
  function nop() {}
545
562
 
546
563
  function patch(key_, src, force, arg1, arg2) {
@@ -552,7 +569,3 @@
552
569
  ))
553
570
  }
554
571
  }(this, Date, Function, Infinity, "prototype") // jshint ignore:line
555
- /* c8 ignore stop */
556
-
557
-
558
-
package/ui.js CHANGED
@@ -3,6 +3,10 @@
3
3
 
4
4
  /* global escape, navigator, xhr */
5
5
 
6
+ // Conditional compilation via toggle comments (processed by build tool):
7
+ // /*** name ***/ code /**/ - `code` active in source; build can strip it
8
+ // /*** name ***/ code1 /*/ code2 /**/ - `code1` active in source, `code2` commented out; build can swap
9
+
6
10
  /*** debug ***/
7
11
  console.log("LiteJS is in debug mode, but it's fine for production")
8
12
  /**/
@@ -13,7 +17,7 @@ console.log("LiteJS is in debug mode, but it's fine for production")
13
17
 
14
18
  var UNDEF, parser, pushBase, styleNode
15
19
  , NUL = null
16
- // THIS will be undefined in strict mode and window in sloppy mode
20
+ // THIS will be `undefined` in strict mode and `window` in sloppy mode
17
21
  , THIS = this
18
22
  , html = document.documentElement
19
23
  , body = document.body
@@ -22,11 +26,13 @@ console.log("LiteJS is in debug mode, but it's fine for production")
22
26
  , plugins = {}
23
27
  , sources = []
24
28
  , assign = Object.assign
29
+ // bind(fn, ctx, ...args)() calls fn.call(ctx, ...args); closureless partial application
25
30
  , bind = El.bind.bind(El.call)
26
31
  , create = Object.create
27
32
  , hasOwn = bind(plugins.hasOwnProperty)
28
33
  , isArr = Array.isArray
29
34
  , slice = emptyArr.slice
35
+ // Closureless utilities via Function() to avoid capturing outer scope
30
36
  , elReplace = Function("a,b,c", "a&&b&&(c=a.parentNode)&&c.replaceChild(b,a)")
31
37
  , elRm = Function("a,b", "a&&(b=a.parentNode)&&b.removeChild(a)")
32
38
  , getAttr = Function("a,b", "return a&&a.getAttribute&&a.getAttribute(b)")
@@ -39,7 +45,7 @@ console.log("LiteJS is in debug mode, but it's fine for production")
39
45
  , ie678 = !+"\v1" // jshint ignore:line
40
46
  // innerText is implemented in IE4, textContent in IE9, Node.text in Opera 9-10
41
47
  // Safari 2.x innerText results an empty string when style.display=="none" or Node is not in DOM
42
- , txtAttr = "textContent" in html ? "textContent" : "innerText"
48
+ , txtAttr = El.T = "textContent" in html ? "textContent" : "innerText"
43
49
  , elTxt = function(el, txt) {
44
50
  if (el[txtAttr] !== txt) el[txtAttr] = txt
45
51
  }
@@ -51,9 +57,12 @@ console.log("LiteJS is in debug mode, but it's fine for production")
51
57
 
52
58
  , elSeq = 0
53
59
  , elCache = {}
60
+ // Parses ";name! args" binding expressions from _b attribute
54
61
  , renderRe = /[;\s]*([-.\w$]+)(?:(!?)[ :]*((?:(["'\/])(?:\\.|[^\\])*?\4|[^;])*))?/g
62
+ // Parses CSS selectors: .class #id [attr=val] :pseudo
55
63
  , selectorRe = /([.#:[])([-\w]+)(?:([~^$*|]?)=(("|')(?:\\.|[^\\])*?\5|[-\w]+))?]?/g
56
64
  , fnCache = {}
65
+ // Matches tokens to exclude from scope variable extraction: strings, keywords, member access, labels
57
66
  , fnRe = /('|")(?:\\.|[^\\])*?\1|\/(?:\\.|[^\\])+?\/[gim]*|\$el\b|\$[aorsS]\b|\b(?:false|in|if|new|null|this|true|typeof|void|function|var|else|return)\b|\.\w+|\w+:/g
58
67
  , wordRe = /[a-z_$][\w$]*/ig
59
68
  , bindingsCss = acceptMany(function(el, key, val, current) {
@@ -75,11 +84,9 @@ console.log("LiteJS is in debug mode, but it's fine for production")
75
84
  set: acceptMany(setAttr),
76
85
  txt: elTxt,
77
86
  /*** form ***/
78
- val: function elVal(el, val, input) {
79
- try {
80
- if (!el || !input && document.activeElement === el) return
81
- } catch (e) {}
82
- var step, key, value
87
+ val: function elVal(el, val, ignoreFocus) {
88
+ if (!el) return
89
+ var input, step, key, value
83
90
  , i = 0
84
91
  , type = el.type
85
92
  , opts = el.options
@@ -95,7 +102,7 @@ console.log("LiteJS is in debug mode, but it's fine for production")
95
102
  // Read-only checkboxes can be changed by the user
96
103
 
97
104
  for (opts = {}; (input = el.elements[i++]); ) if (!input.disabled && (key = input.name || input.id)) {
98
- value = elVal(input, val != UNDEF ? val[key] : UNDEF)
105
+ value = elVal(input, val != UNDEF ? val[key] : UNDEF, ignoreFocus)
99
106
  if (value !== UNDEF) {
100
107
  step = opts
101
108
  replace(/\[(.*?)\]/g, replacer, key)
@@ -106,6 +113,9 @@ console.log("LiteJS is in debug mode, but it's fine for production")
106
113
  }
107
114
 
108
115
  if (val !== UNDEF) {
116
+ try {
117
+ if (!ignoreFocus && document.activeElement === el) return
118
+ } catch (e) {}
109
119
  if (opts) {
110
120
  for (value = (isArr(val) ? val : [ val ]).map(String); (input = opts[i++]); ) {
111
121
  input.selected = value.indexOf(input.value) > -1
@@ -146,11 +156,13 @@ console.log("LiteJS is in debug mode, but it's fine for production")
146
156
  }
147
157
  /**/
148
158
  }
159
+ // Stores "!" once-bindings; index used in compiled fn to strip from _b after first run
149
160
  , bindOnce = []
150
161
  , globalScope = {
151
162
  El: El,
152
163
  $b: bindings
153
164
  }
165
+ // Array-like wrapper methods for multi-element collections (mixed into arrays by ElWrap)
154
166
  , elArr = {
155
167
  append: function(el) {
156
168
  var elWrap = this
@@ -168,6 +180,8 @@ console.log("LiteJS is in debug mode, but it's fine for production")
168
180
  }
169
181
  }
170
182
 
183
+ // fixEv: maps custom event names to native (e.g., touch→"" for non-DOM events)
184
+ // fixFn: transforms event handlers for browser compat (e.g., touch→pointer init)
171
185
  , Event = window.Event || window
172
186
  , fixEv = Event.fixEv || (Event.fixEv = {})
173
187
  , fixFn = Event.fixFn || (Event.fixFn = {})
@@ -260,6 +274,9 @@ console.log("LiteJS is in debug mode, but it's fine for production")
260
274
  }
261
275
  }
262
276
 
277
+ // Events stored as triplets [scope, _origin, fn] in emitter._e[type]
278
+ // _origin tracks the unwrapped fn before fixFn (for rmEvent lookup)
279
+ // emptyArr substitutes window as emitter (can't safely add _e property to window)
263
280
  function on(emitter, type, fn, scope, _origin) {
264
281
  if (emitter && type && fn) {
265
282
  if (emitter === window) emitter = emptyArr
@@ -343,6 +360,10 @@ console.log("LiteJS is in debug mode, but it's fine for production")
343
360
  root: body
344
361
  }, opts)
345
362
 
363
+ // View properties:
364
+ // .r route pattern .e template element .p parent view
365
+ // .c active child .o rendered clone .f file dependencies (csv)
366
+ // .s route sequence# .kb keyboard shortcuts
346
367
  function View(route, el, parent) {
347
368
  var view = views[route]
348
369
  if (view) {
@@ -423,6 +444,7 @@ console.log("LiteJS is in debug mode, but it's fine for production")
423
444
  }
424
445
  })
425
446
 
447
+ // params._p pending async count; ._v current view in traversal; ._c view to close; ._t navigation timestamp
426
448
  function bubbleUp(params) {
427
449
  var parent
428
450
  , view = lastView
@@ -614,6 +636,11 @@ console.log("LiteJS is in debug mode, but it's fine for production")
614
636
  }
615
637
  }
616
638
 
639
+ // Plugin properties:
640
+ // .n name .x parent view name .u parent DOM element
641
+ // .e container el .d done callback .c saved elCache (for %el/%view)
642
+ // When proto is a function, plugin accumulates raw text:
643
+ // .r raw handler .t accumulated text .o original op+text .s separator
617
644
  function addPlugin(name, proto, expectContent) {
618
645
  plugins[name] = Plugin
619
646
  function Plugin(parent, op, sep) {
@@ -1179,8 +1206,8 @@ console.log("LiteJS is in debug mode, but it's fine for production")
1179
1206
  rate: rate,
1180
1207
  replace: elReplace,
1181
1208
  scope: elScope,
1182
- scrollLeft: scrollLeft,
1183
- scrollTop: scrollTop,
1209
+ scrollLeft: bind(scrollPos, NUL, "pageXOffset", "scrollLeft"),
1210
+ scrollTop: bind(scrollPos, NUL, "pageYOffset", "scrollTop"),
1184
1211
  step: step,
1185
1212
  stop: eventStop
1186
1213
  })
@@ -1242,7 +1269,7 @@ console.log("LiteJS is in debug mode, but it's fine for production")
1242
1269
  el = before.parentNode
1243
1270
  }
1244
1271
  el.insertBefore(child, (
1245
- isNum(before) ? el.childNodes[before < 0 ? el.childNodes.length - before - 2 : before] :
1272
+ isNum(before) ? el.childNodes[before < 0 ? el.childNodes.length + before : before] :
1246
1273
  isArr(before) ? before[0] :
1247
1274
  before
1248
1275
  ) || NUL)
@@ -1371,6 +1398,9 @@ console.log("LiteJS is in debug mode, but it's fine for production")
1371
1398
  hydrate(node, "data-out", scope)
1372
1399
  }
1373
1400
 
1401
+ // Reads binding expression from DOM attr (_b or data-out), compiles via makeFn, executes.
1402
+ // Caches expr on node[attr] to avoid re-reading DOM; true = no bindings (already processed).
1403
+ // Returns truthy if binding replaced the element (if/each), so render() skips children.
1374
1404
  function hydrate(node, attr, scope) {
1375
1405
  var fn
1376
1406
  , expr = node[attr] || (node[attr] = setAttr(node, attr, "") || true)
@@ -1381,6 +1411,9 @@ console.log("LiteJS is in debug mode, but it's fine for production")
1381
1411
  throw e + "\n" + attr + ": " + expr
1382
1412
  }
1383
1413
  }
1414
+ // Compiles binding expression string (e.g. ";txt foo;cls 'active',bar") into a Function.
1415
+ // Extracts free variable names and aliases them from scope ($s.varName).
1416
+ // raw parameter bypasses the $s guard wrapper (used by i18n getExt).
1384
1417
  function makeFn(fn, raw, i) {
1385
1418
  fn = raw || "$s&&(" + replace(renderRe, function(match, name, op, args) {
1386
1419
  return (
@@ -1398,7 +1431,7 @@ console.log("LiteJS is in debug mode, but it's fine for production")
1398
1431
 
1399
1432
  /*** kb ***/
1400
1433
  var kbMaps = []
1401
- , kbMod = LiteJS.kbMod = /^(Mac|iP)/.test(navigator.platform) ? "metaKey" : "ctrlKey"
1434
+ , kbMod = LiteJS.kbMod = /\bMac|\biP/.test(navigator.userAgent) ? "metaKey" : "ctrlKey"
1402
1435
  , kbCodes = LiteJS.kbCodes = ",,,,,,,,backspace,tab,,,,enter,,,shift,ctrl,alt,pause,caps,,,,,,,esc,,,,,,pgup,pgdown,end,home,left,up,right,down,,,,,ins,del,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,cmd,,,,,,,,,,,,,,,,,,,,,f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12".split(splitRe)
1403
1436
 
1404
1437
  El.addKb = addKb
@@ -1610,6 +1643,9 @@ console.log("LiteJS is in debug mode, but it's fine for production")
1610
1643
  function nearest(el, sel) {
1611
1644
  return el ? find(el, sel) || nearest(el.parentNode, sel) : NUL
1612
1645
  }
1646
+ // Wraps fn to accept: space-separated names, object maps {name:val}, CSS selectors, delays.
1647
+ // prepareVal=1: wraps val as event delegate (string val→emit on view, fn+selector→delegation)
1648
+ // After arg normalization, selector is reused as element array, delay as loop counter.
1613
1649
  function acceptMany(fn, prepareVal) {
1614
1650
  return function f(el, name, val, selector, delay, data) {
1615
1651
  if (el && name) {
@@ -1634,9 +1670,9 @@ console.log("LiteJS is in debug mode, but it's fine for production")
1634
1670
  if (prepareVal) val = delegate(el, val, selector, data)
1635
1671
  selector = !prepareVal && selector ? findAll(el, selector) : isArr(el) ? el : [ el ]
1636
1672
  for (delay = 0; (el = selector[delay++]); ) {
1637
- for (var result, arr = ("" + name).split(splitRe), i = 0, len = arr.length; i < len; ) {
1673
+ for (var result, arr = ("" + name).split(splitRe), i = 0, len = arr.length; i < len; i++) {
1638
1674
  if (arr[i]) {
1639
- result = fn(el, arr[i++], isArr(val) ? val[i - 1] : val, data)
1675
+ result = fn(el, arr[i], isArr(val) ? val[i] : val, data)
1640
1676
  if (!prepareVal && data > 0) f(el, name, result, "", data)
1641
1677
  }
1642
1678
  }
@@ -1729,11 +1765,8 @@ console.log("LiteJS is in debug mode, but it's fine for production")
1729
1765
  }
1730
1766
  }
1731
1767
  }
1732
- function scrollLeft() {
1733
- return window.pageXOffset || html.scrollLeft || body.scrollLeft || 0
1734
- }
1735
- function scrollTop() {
1736
- return window.pageYOffset || html.scrollTop || body.scrollTop || 0
1768
+ function scrollPos(page, key) {
1769
+ return window[page] || html[key] || body[key] || 0
1737
1770
  }
1738
1771
  function step(num, factor, mid) {
1739
1772
  var x = ("" + factor).split(".")