lsd_rails 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/Packages/Sheet.js/Source/SheetParser.Value.js +1 -1
  2. data/Packages/lsd/Source/Action/Clone.js +2 -12
  3. data/Packages/lsd/Source/Action/Delete.js +1 -7
  4. data/Packages/lsd/Source/Action/Display.js +7 -7
  5. data/Packages/lsd/Source/Action/Edit.js +2 -2
  6. data/Packages/lsd/Source/Action/Invoke.js +2 -2
  7. data/Packages/lsd/Source/Action/Submit.js +2 -0
  8. data/Packages/lsd/Source/Action/Toggle.js +4 -0
  9. data/Packages/lsd/Source/Action/Update.js +2 -1
  10. data/Packages/lsd/Source/Action.js +4 -5
  11. data/Packages/lsd/Source/Document.js +10 -7
  12. data/Packages/lsd/Source/LSD.js +22 -127
  13. data/Packages/lsd/Source/Layer.js +4 -4
  14. data/Packages/lsd/Source/Layout.js +1077 -259
  15. data/Packages/lsd/Source/Mixin/Animation.js +1 -1
  16. data/Packages/lsd/Source/Mixin/Choice.js +2 -2
  17. data/Packages/lsd/Source/Mixin/Command.js +26 -8
  18. data/Packages/lsd/Source/Mixin/ContentEditable.js +2 -1
  19. data/Packages/lsd/Source/{Trait → Mixin}/Date.js +21 -11
  20. data/Packages/lsd/Source/{Trait/Menu.js → Mixin/Details.js} +6 -1
  21. data/Packages/lsd/Source/Mixin/Draggable.js +1 -1
  22. data/Packages/lsd/Source/Mixin/Fieldset.js +51 -49
  23. data/Packages/lsd/Source/Mixin/Focusable.js +38 -39
  24. data/Packages/lsd/Source/Mixin/Invokable.js +13 -14
  25. data/Packages/lsd/Source/Mixin/List.js +9 -17
  26. data/Packages/lsd/Source/Mixin/Placeholder.js +2 -7
  27. data/Packages/lsd/Source/Mixin/Request.js +5 -3
  28. data/Packages/lsd/Source/Mixin/Resizable.js +3 -3
  29. data/Packages/lsd/Source/Mixin/Resource.js +1 -1
  30. data/Packages/lsd/Source/Mixin/Root.js +17 -1
  31. data/Packages/lsd/Source/Mixin/Scrollable.js +1 -1
  32. data/Packages/lsd/Source/{Trait → Mixin}/Slider.js +0 -0
  33. data/Packages/lsd/Source/Mixin/Sortable.js +2 -2
  34. data/Packages/lsd/Source/Mixin/Submittable.js +2 -1
  35. data/Packages/lsd/Source/Mixin/Target.js +11 -6
  36. data/Packages/lsd/Source/Mixin/Touchable.js +2 -2
  37. data/Packages/lsd/Source/Mixin/Unselectable.js +1 -1
  38. data/Packages/lsd/Source/Mixin/Uploader.js +11 -13
  39. data/Packages/lsd/Source/Mixin/Validity.js +35 -9
  40. data/Packages/lsd/Source/Mixin/Value.js +5 -3
  41. data/Packages/lsd/Source/Module/Accessories/Attributes.js +90 -89
  42. data/Packages/lsd/Source/Module/Accessories/Chain.js +40 -25
  43. data/Packages/lsd/Source/Module/Accessories/Element.js +59 -58
  44. data/Packages/lsd/Source/Module/Accessories/Events.js +25 -10
  45. data/Packages/lsd/Source/Module/Accessories/Options.js +11 -13
  46. data/Packages/lsd/Source/Module/Accessories/States.js +42 -5
  47. data/Packages/lsd/Source/Module/Accessories/Styles.js +1 -1
  48. data/Packages/lsd/Source/Module/Accessories/Tag.js +21 -7
  49. data/Packages/lsd/Source/Module/Ambient/Allocations.js +178 -64
  50. data/Packages/lsd/Source/Module/Ambient/DOM.js +98 -55
  51. data/Packages/lsd/Source/Module/Ambient/Expectations.js +57 -8
  52. data/Packages/lsd/Source/Module/Ambient/Interpolations.js +147 -0
  53. data/Packages/lsd/Source/Module/Ambient/Layout.js +52 -15
  54. data/Packages/lsd/Source/Module/Ambient/Proxies.js +44 -36
  55. data/Packages/lsd/Source/Module/Ambient/Relations.js +3 -1
  56. data/Packages/lsd/Source/Module/Ambient/Selectors.js +3 -3
  57. data/Packages/lsd/Source/Module/Ambient.js +2 -2
  58. data/Packages/lsd/Source/Module/Graphics/Layers.js +1 -1
  59. data/Packages/lsd/Source/Module/Graphics/Render.js +1 -1
  60. data/Packages/lsd/Source/Relation.js +19 -20
  61. data/Packages/lsd/Source/Sheet.js +4 -5
  62. data/Packages/lsd/Source/{Behavior.js → Tools/Behavior.js} +0 -0
  63. data/Packages/lsd/Source/Tools/Command.js +190 -0
  64. data/Packages/lsd/Source/Tools/Helpers.js +109 -0
  65. data/Packages/lsd/Source/Tools/Interpolation.js +351 -0
  66. data/Packages/lsd/Source/Tools/Microdata.js +75 -0
  67. data/Packages/lsd/Source/Tools/Object.js +192 -0
  68. data/Packages/lsd/Source/Tools/Position.js +208 -0
  69. data/Packages/lsd/Source/Tools/Require.js +76 -0
  70. data/Packages/lsd/Source/Type.js +2 -2
  71. data/Packages/lsd/package.yml +11 -7
  72. data/Packages/lsd-mobile/Source/Body/Dialog.js +2 -2
  73. data/Packages/lsd-mobile/Source/Input/Date.js +9 -84
  74. data/Packages/lsd-native/Source/Input/Checkbox.js +4 -4
  75. data/Packages/lsd-native/Source/Input/Date.js +46 -6
  76. data/Packages/lsd-native/Source/Input/Radio.js +1 -4
  77. data/Packages/lsd-native/Source/Input.js +5 -6
  78. data/Packages/lsd-widgets/Source/Body/Dialog.js +9 -6
  79. data/Packages/lsd-widgets/Source/Body/Page.js +1 -1
  80. data/Packages/lsd-widgets/Source/Body.js +1 -1
  81. data/Packages/lsd-widgets/Source/Button.js +1 -1
  82. data/Packages/lsd-widgets/Source/Form.js +1 -1
  83. data/Packages/lsd-widgets/Source/Input/Checkbox.js +2 -2
  84. data/Packages/lsd-widgets/Source/Input/Date.js +45 -16
  85. data/Packages/lsd-widgets/Source/Input/File.js +2 -2
  86. data/Packages/lsd-widgets/Source/Input/HTML.js +2 -2
  87. data/Packages/lsd-widgets/Source/Input/Radio.js +1 -1
  88. data/Packages/lsd-widgets/Source/Input/Range.js +2 -2
  89. data/Packages/lsd-widgets/Source/Input/Submit.js +1 -1
  90. data/Packages/lsd-widgets/Source/Input.js +2 -11
  91. data/Packages/lsd-widgets/Source/Label.js +4 -8
  92. data/Packages/lsd-widgets/Source/Menu/List.js +3 -3
  93. data/Packages/lsd-widgets/Source/Menu.js +2 -2
  94. data/Packages/lsd-widgets/Source/Progress.js +1 -1
  95. data/Packages/lsd-widgets/Source/Select.js +7 -6
  96. data/Packages/lsd-widgets/Source/Table/Calendar.js +41 -15
  97. data/Packages/lsd-widgets/Source/Table.js +7 -2
  98. data/Packages/mootools-ext/Source/Core/Class.Mixin.js +43 -38
  99. data/Packages/mootools-ext/Source/Core/Class.States.js +26 -22
  100. data/Packages/mootools-ext/Source/Element/Element.from.js +1 -1
  101. data/Packages/mootools-ext/Source/Element/Properties/Item.js +1 -2
  102. data/Packages/mootools-ext/Source/Request/Request.Statuses.js +9 -7
  103. data/Packages/mootools-ext/Source/Types/{FastArray.js → Object.Array.js} +8 -14
  104. data/Packages/mootools-ext/package.yml +1 -2
  105. data/lib/lsd.rake +28 -0
  106. metadata +15 -11
  107. data/Packages/lsd/Source/Command.js +0 -135
  108. data/Packages/lsd/Source/Interpolation.js +0 -103
  109. data/Packages/lsd/Source/Module/Ambient/Container.js +0 -56
  110. data/Packages/mootools-ext/Source/Element/Element.onDispose.js +0 -36
@@ -11,9 +11,10 @@ authors: Yaroslaff Fedin
11
11
 
12
12
  requires:
13
13
  - LSD
14
- - More/Object.Extras
15
14
  - LSD.Interpolation
16
- - Sheet/SheetParser.Value
15
+ - LSD.Helpers
16
+ - LSD.Microdata
17
+ - More/Object.Extras
17
18
 
18
19
  provides:
19
20
  - LSD.Layout
@@ -32,242 +33,322 @@ provides:
32
33
  extract attributes and classes from elements.
33
34
  */
34
35
 
35
- LSD.Layout = function(widget, layout, options) {
36
+ LSD.Layout = function(widget, layout, memo) {
36
37
  this.origin = widget;
37
- this.setOptions(options);
38
- this.context = LSD[this.options.context.capitalize()];
39
38
  if (widget) if (!layout && !widget.lsd) {
40
39
  layout = widget;
41
40
  widget = null;
42
41
  } else if (!widget.lsd) widget = this.convert(widget);
43
- if (layout) this.render(layout, widget);
42
+ if (layout) this.render(layout, widget, memo);
44
43
  };
45
44
 
46
- LSD.Layout.prototype = Object.append(new Options, {
47
-
48
- options: {
49
- clone: false,
50
- context: 'element',
51
- interpolate: null
52
- },
53
-
45
+ LSD.Layout.context = 'element';
46
+
47
+ LSD.Layout.prototype = Object.append({
54
48
  $family: Function.from('layout'),
55
49
 
56
- render: function(layout, parent, opts) {
57
- var type = layout.push ? 'array' : layout.nodeType ? LSD.Layout.NodeTypes[layout.nodeType] : layout.indexOf ? 'string' : 'object';
58
- if (type) {
59
- var result = this[type](layout, parent, opts);
60
- if (!this.result) this.result = result;
61
- return result;
62
- }
50
+ render: function(layout, parent, memo) {
51
+ if (layout.getLayout) layout = layout.getLayout();
52
+ var type = (layout.push) ? 'array' : (layout.item && ('length' in layout)) ? 'children' :
53
+ layout.nodeType ? LSD.Layout.NodeTypes[layout.nodeType] : layout.indexOf ? 'string' : 'object';
54
+ var result = this[type](layout, parent, memo);
55
+ if (!this.result) this.result = result;
56
+ return result;
63
57
  },
64
58
 
65
59
  // type handlers
66
60
 
67
- array: function(array, parent, opts) {
61
+ array: function(array, parent, memo) {
68
62
  for (var i = 0, result = [], length = array.length; i < length; i++)
69
- result[i] = this.render(array[i], parent, opts)
63
+ result[i] = this.render(array[i], parent, memo)
70
64
  return result;
71
65
  },
72
66
 
73
- element: function(element, parent, opts, stack, last) {
74
- var converted = element.uid && Element.retrieve(element, 'widget');
75
- var children = LSD.slice(element.childNodes);
76
- var cloning = (opts && opts.clone) || this.options.clone, group;
67
+ /*
68
+ Process single element. The ultimate goal is to create a widget to be used with the given
69
+ element. But if the widget is not found, the element may still be used as a source of
70
+ microdata, or as an intermediate node in a complex mutation. It also handles cloning of nodes.
71
+
72
+ If a method is invoked on an element directly, it postpones a processing and enters a `walk`
73
+ loop instead. The walk loop takes care about all the child node iteration and prepares the
74
+ meta information to process an element in correct context of parents and siblings. Then it
75
+ calls `element` method back with all the data in `memo`.
76
+
77
+ An element then goes through a `match` routine, which iterates through things on the stack
78
+ and matches selectors against a node. There are two results of this action:
79
+
80
+ * A node may be mutated into a widget, if the mutation selector matches. A mutation provides
81
+ options for the widget, to be initialized shortly after. If multiple mutations match, their
82
+ options are merged together.
83
+ * A node may advance complex mutation selectors. If a node is a `<li>` element, and there's a
84
+ `li > a` selector on the stack, the match creates a new mutation `> a` to be used for
85
+ childnodes of the `<li>` element.
86
+
87
+ If there was a succesful mutation a widget is created. Otherwise, function attempts to convert
88
+ the node into widget (if the node is `<input type=text>` it will try to find `Input.Text` and
89
+ `Input` widgets). If there was no widget, but the layout is set to clone with `memo.clone`,
90
+ it clones the node.
91
+
92
+ Then, it appends the result (widget or node) into a parent node.
93
+ */
94
+
95
+ element: function(element, parent, memo) {
96
+ // Prepare options and run walker (once per element tree)
97
+ if (!memo || !memo.walking) return this.walk(element, parent, memo);
98
+ this.match(element, memo);
99
+ var group, converted = element.uid && Element.retrieve(element, 'widget');
77
100
  var ascendant = parent[0] || parent, container = parent[1] || parent.toElement();
78
- // Retrieve the stack if the render was not triggered from the root of the layout
79
- if (!stack) {
80
- stack = [];
81
- if ((group = ascendant.mutations['>'])) stack.push(group);
82
- for (var node = ascendant; node; node = node.parentNode)
83
- if ((group = node.mutations[' '])) stack.push(group);
84
- //for (var node = ascendant; node; node = node.previousSibling) {
85
- // if ((group = node.mutations['+'])) stack.push(group);
86
- // if ((group = node.mutations['-'])) stack.push(group);
87
- //}
88
- }
89
- // Match all selectors in the stack and find a right mutation
90
- var index = stack.length;
91
- if (index) {
92
- var mutation, advanced, tagName = LSD.toLowerCase(element.tagName);
93
- for (var i = index, item, result, ary = ['*', tagName]; item = stack[--i];)
94
- for (var j = 0, value = item[1] || item, tag; tag = ary[j++];)
95
- if ((group = value[tag]))
96
- for (var k = 0, possibility, sel; possibility = group[k++];) {
97
- var result = possibility[1];
98
- if ((!mutation || (result && !result.indexOf)) && (sel = possibility[0])) {
99
- if ((!sel.id && !sel.classes && !sel.attributes && !sel.pseudos) ? (tagName == sel.tag || j == 0) :
100
- (Slick.matchSelector(element, sel.tag, sel.id, sel.classes, sel.attributes, sel.pseudos)))
101
- if (!result || !result.call || (result = result(element)))
102
- if (!result || !result.push) {
103
- mutation = result || true;
104
- } else (advanced || (advanced = [])).push(result);
105
- }
106
- }
107
- }
108
- // prepare options (once)
109
- if (!opts || !opts.lazy) {
110
- opts = Object.append({lazy: true}, opts);
111
- if (this.options.context && LSD.Widget.prototype.options.context != this.options.context)
112
- opts.context = this.options.context;
113
- if (this.options.interpolation) opts.interpolation = this.options.interpolation;
114
- var prepared = true;
115
- }
116
101
  // Create, clone or reuse a widget.
117
102
  if (!converted) {
118
- if (mutation) {
119
- var options = Object.append({}, opts, mutation.indexOf ? LSD.Layout.parse(mutation) : mutation);
120
- var widget = this.context.create(element, options);
103
+ if (!memo.defaults) memo.defaults = this.getOptions(memo, parent);
104
+ // Retrieve the widget type object that finds the appropriate classes for widgets
105
+ if (!memo.type) memo.type = this.getType(memo, parent);
106
+ if (memo.options) {
107
+ // If there are options produced by the selector matching routine, it's a widget
108
+ var widget = memo.type.create(element, memo.options);
121
109
  } else {
122
- var widget = this.context.convert(element, opts);
110
+ // Otherwise, try converting the element (will turn <input type=date> into Input.Date)
111
+ var widget = memo.type.convert(element, memo.defaults);
123
112
  }
124
113
  } else {
125
- var widget = cloning ? converted.cloneNode(false, opts) : converted;
114
+ var widget = memo.clone ? converted.cloneNode(false) : converted;
126
115
  }
127
116
  // Append widget into parent widget without moving elements
128
117
  if (widget) {
129
- if (!widget.parentNode) {
130
- var override = function() {
131
- if (widget.toElement().parentNode == container) return;
132
- if (cloning)
133
- this.appendChild(container, widget.element)
134
- else if (widget.origin == element && element.parentNode && element.parentNode == container)
135
- element.parentNode.replaceChild(widget.element, element);
136
- }.bind(this);
137
- this.appendChild(ascendant, widget, opts, override)
138
- }
118
+ this.appendChild(ascendant, widget, memo, function(container, child) {
119
+ if (widget.origin == element && element.parentNode && element.parentNode == container) {
120
+ element.parentNode.replaceChild(widget.element, element);
121
+ } else if (!child.parentNode) return parent[1] || container;
122
+ return false;
123
+ });
139
124
  } else {
140
- if (cloning) var clone = element.cloneNode(false);
141
- if (cloning || (ascendant.origin == element.parentNode)) this.appendChild(container, clone || element, opts);
142
- }
143
- var newParent = [widget || ascendant, clone || (widget && widget.element) || element];
144
- // Put away selectors in the stack that should not be matched again widget children
145
- var group, direct, following;
146
- for (var i = stack.length; group = stack[--i];) {
147
- switch (group[0]) {
148
- case '+':
149
- stack.pop();
150
- break;
151
- case '~':
152
- (following || (following = [])).push(stack.pop());
153
- break;
154
- case '>':
155
- (direct || (direct = [])).push(stack.pop());
156
- }
157
- }
158
- if (opts && opts.before) {
159
- var before = opts.before;
160
- delete opts.before;
125
+ if (memo.clone) var clone = element.cloneNode(false);
126
+ this.appendChild(container, clone || element, memo);
161
127
  }
162
- // Collect mutations from a widget
163
- if (widget) {
164
- if ((group = widget.mutations[' '])) stack.push([' ', group]);
165
- if ((group = widget.mutations['>'])) stack.push(['>', group]);
166
- }
167
- // Collect mutations that advanced with this element AND are looking for children
168
- if (advanced) for (var i = 0, group; group = advanced[i]; i++) {
169
- switch (group[0]) {
170
- case ' ': case '>':
171
- advanced.splice(i--, 1);
172
- stack.push(group);
173
- break;
174
- }
175
- }
176
- // Cleanup options if they prepared on this iteration
177
- if (prepared) {
178
- options = {};
179
- for (var option in opts) if (LSD.Layout.Inheritable[option]) options[option] = opts[option];
180
- opts = options;
181
- }
182
- // Render children
128
+ return widget || clone || element;
129
+ },
130
+
131
+ /*
132
+ Process child nodes of an element. Child node collection is actually an array,
133
+ and should be given to the function as array. What makes `children` different from
134
+ `array` is that it keeps track of sibling combinators like `~` and `+` and maintains
135
+ the mutation stack, by collecting mutations and proxies from parent widget via `push`
136
+ & `pop`
137
+ */
138
+
139
+ children: function(children, parent, memo) {
140
+ if (!memo) memo = {};
141
+ if (!memo.type) memo.type = this.getType(memo, parent);
142
+ var widget = (!parent[1] || parent[1] == parent[0].element);
143
+ if (widget) this.push(parent, memo);
183
144
  for (var j = children.length - 1, child; j > -1 && (child = children[j]) && child.nodeType != 1; j--);
184
- for (var i = 0, child, previous, result, following; child = children[i]; i++) {
185
- // Pick up selectors targetting on a node's next siblings
145
+ var args = [null, parent, memo];
146
+ for (var i = 0, child, previous, result = []; child = children[i]; i++) {
147
+ /*
148
+ Pick up selectors targetting on a node's next siblings
149
+ */
186
150
  if (previous && i) {
187
- if ((group = previous.mutations['~'])) stack.push(['~', group]);
188
- if ((group = previous.mutations['+'])) stack.push(['+', group]);
151
+ if ((group = previous.mutations['~'])) memo.stack.push(['~', group]);
152
+ if ((group = previous.mutations['+'])) memo.stack.push(['+', group]);
153
+ }
154
+ memo.last = (i == j);
155
+ memo.first = (i == 0);
156
+ args[0] = child;
157
+ /*
158
+ If the child is element, walk it again and render it there, otherwise render it right away
159
+ */
160
+ if (child.nodeType == 1) {
161
+ previous = this.walk.apply(this, args);
162
+ if (!previous.lsd) previous = null;
163
+ } else {
164
+ this[LSD.Layout.NodeTypes[child.nodeType]].apply(this, args);
165
+ previous = null;
189
166
  }
190
- previous = this[LSD.Layout.NodeTypes[child.nodeType]](child, newParent, opts, stack, i == j);
191
- if (!previous.lsd) previous = null;
192
- }
193
- // Put advanced selectors back to the stack
194
- if (advanced) for (var i = 0; group = advanced[i++];)
195
- if (group[0] != '+' || !last) stack.push(group);
196
- // Put back selectors for next siblings
197
- if (!last) {
198
- if (following) for (var i = 0; group = following[i++];) stack.push(group);
199
- if (direct) for (var i = 0; group = direct[i++];) stack.push(group);
200
167
  }
201
- if (before) opts.before = before;
202
-
203
- return widget || clone || element;
168
+ delete memo.last; delete memo.first;
169
+ if (widget) this.pop(parent, memo)
170
+ return children;
204
171
  },
205
172
 
206
- textnode: function(element, parent, opts) {
207
- var value = element.textContent;
208
- if (this.options.interpolate) var interpolated = this.interpolate(value);
209
- var cloning = (opts && opts.clone || this.options.clone);
210
- if (interpolated != null || cloning) {
211
- var textnode = element.ownerDocument.createTextNode(interpolated || value);
212
- if (!cloning) element.parentNode.replaceChild(textnode, element);
213
- this.appendChild(parent[1] || parent.toElement(), textnode || element)
214
- }
215
- return textnode || element;
173
+ /*
174
+ Process a single textnode. It may be cloned and/or interpolated. Interpolation is a process
175
+ of finding variable expressions in text content. LSD uses `{hello}` syntax to embed variables
176
+ in text. There are multiple ways of adding values for those variables in widgets - selectors,
177
+ field values, microdata and custom dictionaries.
178
+ */
179
+
180
+ textnode: function(element, parent, memo) {
181
+ if (memo && memo.clone) var clone = element.cloneNode(false);
182
+ this.appendChild(parent, clone || element, memo);
183
+ LSD.Interpolation.textnode(clone || element, parent[0] || parent);
184
+ return clone || element;
216
185
  },
217
186
 
218
- comment: function(element, parent) {
219
- //var text = element.innerText;
187
+ /*
188
+ Process a single comment node. LSD uses comments as boundaries of conditional blocks of HTML.
189
+ A comment like `<!-- if hello -->` will make all contents after the comment definition on that
190
+ level be displayed only if `hello` expression evaluates to true. Expressions are in fact
191
+ interpolations that are bound to show or hide parts of layout.
192
+ */
193
+
194
+ comment: function(comment, parent, memo) {
195
+ var keyword = Element.retrieve(comment, 'keyword');
196
+ this.appendChild(parent, comment, memo);
197
+ if (keyword) return keyword === true ? comment : keyword;
198
+ else keyword = this.keyword(comment.nodeValue, parent, memo, comment);
199
+ if (keyword) {
200
+ Element.store(comment, 'keyword', keyword);
201
+ if (keyword !== true) (memo.branches || (memo.branches = [])).push(keyword);
202
+ return keyword;
203
+ } else return comment;
220
204
  },
221
205
 
222
- fragment: function(element, parent, opts) {
223
- for (var nodes = LSD.slice(element.childNodes, 0), i = 0, node; node = nodes[i++];)
224
- this[LSD.Layout.NodeTypes[node.nodeType]](node, parent, opts);
206
+ /*
207
+ Process a document fragment. The best way to build and process HTML DOM elements is to do
208
+ all the things in memory first, and then inject the already-prepared element tree into the
209
+ document. Fragments are used to create a virtual DOM tree from a HTML string via
210
+ document.createFragment(html). They are also useful, because they can operate on multiple nodes
211
+ coming from a fragment. So there may be more than one root node parsed from HTML, but they
212
+ are processed as a single fragment node.
213
+ */
214
+
215
+ fragment: function(element, parent, memo) {
216
+ return this.children(LSD.slice(element.childNodes), parent, memo);
225
217
  },
226
218
 
227
- string: function(string, parent, opts) {
228
- var element = parent[1];
229
- var interpolated = this.interpolate(string);
230
- if (interpolated != null) string = interpolated;
231
- if (!element || element == parent[0].element) {
232
- var textnode = parent[0].write(string);
233
- } else {
234
- var textnode = element.ownerDocument.createTextNode(element);
235
- element.appendChild(textnode);
236
- }
219
+ /*
220
+ Creates a text node from a string and try to interpolate it.
221
+ */
222
+
223
+ string: function(string, parent, memo) {
224
+ var element = parent[1] || parent.toElement();
225
+ var textnode = element.ownerDocument.createTextNode(string);
226
+ this.appendChild(element, textnode, memo);
227
+ LSD.Interpolation.textnode(textnode, parent[0] || parent);
237
228
  return textnode;
238
229
  },
239
230
 
240
- object: function(object, parent, opts) {
241
- var result = {}, parsed, layout, branch;
231
+ /*
232
+ Create from a javascript object layout template.
233
+ */
234
+
235
+ object: function(object, parent, memo) {
236
+ var result = {}, layout, branch;
242
237
  for (var selector in object) {
243
- layout = object[selector] === true ? null : object[selector];
244
- if ((parsed = LSD.Layout.extractKeyword(selector))) {
245
- branch = result[selector] = this.keyword(parsed.keyword, parsed.expressions, branch, layout, parent, opts);
238
+ layout = object[selector] === true || !object[selector] ? null : object[selector];
239
+ if (!memo) memo = {};
240
+ if ((branch = this.keyword(selector, parent, memo))) {
241
+ (memo.branches || (memo.branches = [])).push(branch);
242
+ branch.setLayout(layout, true);
243
+ result[selector] = [branch, layout];
246
244
  } else {
247
- branch = null;
248
- var result = this.selector(selector, parent, opts);
249
- result[selector] = !layout || [result, this.render(layout, result.lsd ? result : [parent[0] || parent, result], null, opts)];
245
+ if (branch && memo && memo.branches && memo.branches[memo.branches.length - 1] == branch) {
246
+ memo.branches.pop();
247
+ branch = null;
248
+ }
249
+ var rendered = this.selector(selector, parent, memo);
250
+ if (rendered.promise) {
251
+ rendered.children = layout;
252
+ result[selector] = [rendered, layout];
253
+ } else {
254
+ result[selector] = [rendered, !layout || this.render(layout, rendered.lsd ? rendered : [parent[0] || parent, rendered], null, memo)];
255
+ }
250
256
  }
251
257
  }
258
+ if (memo.predecessors) delete memo.predecessors;
252
259
  return result;
253
260
  },
254
261
 
255
- selector: function(string, parent, opts) {
256
- var options = Object.append({context: this.options.context}, opts, LSD.Layout.parse(string, parent[0] || parent));
257
- if (options.tag != '*' && (options.source || this.context.find(options.tag) || !LSD.Layout.NodeNames[options.tag])) {
262
+ /*
263
+ Builds a single node from a selector. It accepts raw selectors in form of strings
264
+ and also options set. The result may be either element or widget.
265
+
266
+ The method creates widget if:
267
+
268
+ * If a tag name is not a standart HTML element tag name. A custom tag means widget.
269
+ * A tag name and attributes combination matches a known widget class name
270
+ in current scope. e.g. `input[type=text][kind=colorful]` matches
271
+ `Input.Text.Colorful` class name. Only `type` and `kind` attribute have the
272
+ subclassing semantics.
273
+ * Selector is given as parsed options object and `source` option is given.
274
+ It makes the method skip all tests and build the widget with that source.
275
+ It often happens after element mutation and results in a source expression,
276
+ which is basically another selector that has enough information about the
277
+ widget class to be used.
278
+
279
+ If a tag name in selector has dashes which makes the method treat it like a `source`
280
+ option described above. For example, `p-more[name=signin]` does not imply `p` tag,
281
+ but `p-more` source which translates to P.More class name. If the actual class
282
+ object is not found by that name, a widget is created without specific role.
283
+
284
+ If a `memo.lazy` flag is set, the node is not rendered immediately. Function
285
+ returns a `Promise` object that keeps all options around. Those options later
286
+ may be passed to this `selector` method again to be finally built. Promise object
287
+ is compatible with Proxy options object and is used that way by an `object` layout
288
+ method. Proxies set expectation of an element that was meant to be rendered and
289
+ lets other parts of layout to be rendered. While those parts are rendered,
290
+ the elements being processed and matched against all promise expectations.
291
+ That allows layout engine to reuse DOM nodes that happen to match the selector
292
+ instead of building its own new DOM nodes.
293
+ */
294
+
295
+ selector: function(string, parent, memo) {
296
+ if (string.match) var options = LSD.Layout.parse(string, parent);
297
+ else var options = string;
298
+ if (!memo) memo = {};
299
+ if (!memo.type) memo.type = this.getType(memo, parent);
300
+ var source = options.source;
301
+ if (source && source.match && !source.match(LSD.Layout.rSimpleSource)) {
302
+ delete options.source;
303
+ Object.merge(options, LSD.Layout.parse(source, parent));
304
+ }
305
+ if (options.allocation || options.source || (options.tag && (memo.type.find(options.tag) || !LSD.Layout.NodeNames[options.tag]))) {
258
306
  var allocation = options.allocation;
259
- if (allocation) (parent[0] || parent).allocate(allocation.type, allocation.kind, allocation.options);
260
- var widget = this.context.create(options), self = this;
261
- if (widget.element && widget.element.childNodes.length) var nodes = widget.element.childNodes;
262
- this.appendChild(parent[0] || parent, widget, opts, function() {
263
- self.appendChild(parent[1] || parent.toElement(), widget.toElement());
264
- });
265
- options = {};
266
- for (var option in opts) if (LSD.Layout.Inheritable[option]) options[option] = opts[option];
267
- opts = options;
268
- if (nodes) this.array(nodes, [widget.element, widget], opts);
307
+ // If a pseudo class is recognized as allocatable widget,
308
+ // it needs to retrieve the selectors that gives additional options
309
+ if (allocation) {
310
+ if (memo.lazy) {
311
+ var allocated = (parent[0] || parent).preallocate(allocation);
312
+ // The node is prematurely built, because allocation is not lazy
313
+ if (allocated.nodeType) return allocated;
314
+ if (!allocated.source) throw "Allocation of type " + allocation.type + " did not provide `source` selector to build nodes"
315
+ memo.options = allocated;
316
+ var selector = allocated.selector || allocated.source;
317
+ if (options.order) selector += options.order;
318
+ if (options.combinator) selector = options.order + selector;
319
+ // Parse selector coming from allocation and pass the promise
320
+ return this.selector(selector, parent, memo);
321
+ } else {
322
+ var widget = (parent[0] || parent).allocate(allocation.type, allocation.kind, allocation.options);
323
+ if (widget.parentNode) {
324
+ if (widget.lsd) parent = [widget.parentNode, (widget.element && widget.element.parentNode) || widget.parentNode.element]
325
+ else parent = [parent[0] || parent, widget.parentNode];
326
+ }
327
+ }
328
+ } else {
329
+ if (!memo.defaults) memo.defaults = this.getOptions(memo, parent);
330
+ var opts = Object.append({}, memo.defaults, memo.options, options);
331
+ if (memo.lazy) return new LSD.Layout.Promise(this, string, true, parent, opts, memo);
332
+ if (options.combinator) {
333
+ memo.combinator = options.combinator;
334
+ delete options.combinator;
335
+ }
336
+ var widget = memo.type.create(opts);
337
+ }
338
+ if (widget.element && widget.element.childNodes.length) var nodes = LSD.slice(widget.element.childNodes);
339
+ this.appendChild(parent, widget, memo, parent[1]);
340
+ if (nodes && nodes.length) this.children(nodes, [widget, widget.element], memo);
269
341
  } else {
270
- var props = {}, tag = (options.tag == '*') ? 'div' : options.tag;
342
+ if (memo.options) {
343
+ options = Object.merge(memo.options, options);
344
+ delete memo.options;
345
+ }
346
+ if (options.combinator) {
347
+ memo.combinator = options.combinator;
348
+ delete options.combinator;
349
+ }
350
+ if (memo.lazy) return new LSD.Layout.Promise(this, string, false, parent, options, memo);
351
+ var props = {}, tag = (!options.tag || options.tag == '*') ? 'div' : options.tag;
271
352
  if (options.id) props.id = options.id;
272
353
  var attributes = options.attributes;
273
354
  if (attributes) for (var attr, i = 0, l = attributes.length; i < l; i++){
@@ -278,116 +359,839 @@ LSD.Layout.prototype = Object.append(new Options, {
278
359
  }
279
360
  var element = document.createElement(tag);
280
361
  for (var name in props) element.setAttribute(name, props[name]);
281
- if (options.classes) element.className = options.classes.join(' ');
282
- if (parent) this.appendChild(parent[1] || parent.toElement(), element);
362
+ if (options.classes) element.className = LSD.Object.prototype.join.call(options.classes, ' ');
363
+ if (parent) this.appendChild(parent, element, memo);
364
+ if (options.content) element.innerHTML = options.content;
283
365
  }
284
366
  return widget || element;
285
367
  },
286
368
 
287
- interpolate: function(string, object) {
288
- if (!object) object = this.options.interpolate;
289
- var interpolated = LSD.Interpolation.attempt(string, object);
290
- if (interpolated !== false) {
291
- this.interpolated = true;
292
- return interpolated;
369
+ keyword: function(text, parent, memo, element) {
370
+ var parsed = LSD.Layout.extractKeyword(text);
371
+ if (!parsed) return;
372
+ var keyword = LSD.Layout.Keyword[parsed.keyword];
373
+ if (!keyword) return;
374
+ var options = keyword(parsed.expression);
375
+ var parentbranch = memo.branches && memo.branches[memo.branches.length - 1];
376
+ if (options.ends || options.link) {
377
+ if (!(options.superbranch = (memo.branches && memo.branches.pop())))
378
+ throw "Alternative branch is missing its original branch";
379
+ var node = options.superbranch.options.element;
380
+ if (node) {
381
+ for (var layout = []; (node = node.nextSibling) != element;) layout.push(node);
382
+ options.superbranch.setLayout(layout);
383
+ }
384
+ if (element && options.superbranch.options.clean) {
385
+ element.parentNode.removeChild(element);
386
+ element = options.superbranch.options.element;
387
+ if (element) element.parentNode.removeChild(element);
388
+ }
389
+ if (options.ends) return true;
390
+ }
391
+ if (options.branch) {
392
+ options.keyword = parsed.keyword;
393
+ options.parent = parentbranch;
394
+ options.widget = parent[0] || parent;
395
+ options.element = element;
396
+ return new LSD.Layout.Branch(options);
397
+ } else {
398
+ if (options.layout) {
399
+ return this.render(options.layout);
400
+ }
293
401
  }
294
402
  },
295
403
 
296
- keyword: function(keyword, expression, holder, layout, parent, opts) {
297
- var options = {keyword: keyword, holder: holder, layout: layout, parent: parent, options: opts}
298
- switch (keyword) {
299
- case "if":
300
- options.expression = expression;
301
- break;
302
- case "unless":
303
- options.expression = expression;
304
- options.inversed = true;
404
+ /*
405
+ Remove rendered content from DOM. It only removes argument from DOM, keeping
406
+ all of its contents untouched.
407
+
408
+ The function accepts all kind of arguments, but it works best when paired with
409
+ results of `render()`. This way the meta-constructs like conditions and loops
410
+ will be preserved and gracefully removed.
411
+ */
412
+ remove: function(layout, parent, memo) {
413
+ return this.set(layout, parent, memo, false)
414
+ },
415
+
416
+ /*
417
+ Re-add once rendered content that was removed. When a `render`ed chunk of layout
418
+ was removed, `add` is the function that will gracefully add it back.
419
+ */
420
+ add: function(layout, parent, memo) {
421
+ return this.set(layout, parent, memo, true)
422
+ },
423
+
424
+ set: function(layout, parent, memo, state) {
425
+ switch (typeOf(layout)) {
426
+ case "array": case "collection":
427
+ for (var i = 0, j = layout.length, value; i < j; i++)
428
+ if ((value = layout[i])) this.manipulate(state, parent, value, memo);
305
429
  break;
306
- case "elsif": case "elseif": case "else if":
307
- options.expression = expression;
308
- options.link = true;
430
+ case "object":
431
+ for (var key in layout) {
432
+ var value = layout[key];
433
+ if (value && (value = value[0])) this.manipulate(state, parent, value, memo);
434
+ }
309
435
  break;
310
- case "else":
311
- options.link = true;
436
+ case "widget": case "string":
437
+ return this.manipulate(state, parent, layout);
312
438
  }
313
- return new LSD.Layout.Branch(options);
439
+ return layout;
314
440
  },
315
441
 
316
- appendChild: function(parent, child, opts, override) {
317
- if (child.parentNode != parent) {
318
- if (opts && opts.before) {
319
- var before = !parent.lsd && opts.before.toElement ? opts.before.toElement() : opts.before;
320
- parent.insertBefore(child, before, override);
321
- } else {
322
- parent.appendChild(child, override);
442
+ manipulate: function(state, parent, child, memo) {
443
+ if (state) {
444
+ this.appendChild(parent, child, memo);
445
+ } else {
446
+ this.removeChild(child.parentNode || parent, child, memo)
447
+ }
448
+ if (child.nodeType == 8) {
449
+ var keyword = Element.retrieve(child, 'keyword');
450
+ if (keyword && keyword !== true) keyword[state ? 'attach' : 'detach']();
451
+ }
452
+ return child;
453
+ },
454
+
455
+ /*
456
+ Places a node in a parent node. The function appends node in the end of
457
+ the parent node by default. Alternatively, a node given with `memo.before`
458
+ will be used as a place to insert before.
459
+
460
+ `override` function may be used when a widget is inserted into another widget
461
+ but DOM manipulation should be altered in some way.
462
+
463
+ A function may be given proxies stack with `memo.proxies`. The stack is then
464
+ be iterated and proxies are matched against the child node. Upon a succesful
465
+ match, a proxy function returns an object with values that may specify a new
466
+ `parent`, `override` or `before` variables for manipulation.
467
+ */
468
+
469
+ appendChild: function(parents, child, memo, override) {
470
+ if (parents.push) var widget = parents[0], element = parents[1];
471
+ else if (parents.lsd) var widget = parents, element = widget.element || widget.toElement();
472
+ else var element = parents;
473
+ var parent = child.lsd ? widget : element, before;
474
+ if (memo && memo.combinator) {
475
+ var combinator = LSD.Layout.Combinators[memo.combinator];
476
+ if (combinator) {
477
+ var result = combinator(parent, child);
478
+ if (result) {
479
+ if (result.parent) parent = result.parent;
480
+ if (result.before) before = result.before;
481
+ }
323
482
  }
324
- if (child.lsd) {
325
- var doc = child.parentNode.document;
326
- if (child.document != doc) child.setDocument(doc);
483
+ delete memo.combinator;
484
+ }
485
+ if (memo && memo.proxies)
486
+ proxy: for (var i = 0, group, proxies, close; group = memo.proxies[i++];) {
487
+ proxies = group[1];
488
+ close = element == group[0];
489
+ for (var j = 0, proxy; proxy = proxies[j++];) {
490
+ if (close || proxy.deep || proxy.element == element) {
491
+ if (LSD.Module.Proxies.match(child, proxy, group[0])) {
492
+ var result = LSD.Module.Proxies.invoke(parent, child, proxy, memo);
493
+ if (result) {
494
+ if (result.parent) parent = result.parent;
495
+ if (result.override) override = result.override;
496
+ if (result.before) before = result.before;
497
+ break proxy
498
+ } else if (result === false) return false;
499
+ }
500
+ }
501
+ }
327
502
  }
328
- return true;
503
+ if (before || (memo && (before = memo.before))) {
504
+ if (!parent.lsd) {
505
+ if (before.lsd) before = before.toElement();
506
+ parent = before.parentNode;
507
+ };
508
+ parent.insertBefore(child, before, override);
509
+ } else {
510
+ if (child.parentNode == parent) return;
511
+ parent.appendChild(child, override);
512
+ }
513
+ if (child.lsd) {
514
+ var doc = parent.document;
515
+ if (child.document != doc) child.setDocument(doc);
329
516
  }
517
+ return true;
330
518
  },
331
519
 
332
520
  removeChild: function(parent, child, override) {
333
- if (child.parentNode != parent) {
334
- parent.removeChild(child, override);
335
- return true;
521
+ if (parent.push) parent = parent[child.lsd ? 0 : 1] || parent;
522
+ if (!child.lsd && parent.lsd) parent = parent.toElement();
523
+ if (child.parentNode != parent) return;
524
+ parent.removeChild(child, override);
525
+ return true;
526
+ },
527
+
528
+ /*
529
+ Realize layout promises, reuse found elements and build
530
+ the ones that are missing
531
+ */
532
+ realize: function(layout, memo) {
533
+ if (!memo) memo = {};
534
+ if (layout.hasOwnProperty('length')) {
535
+ for (var i = 0, j = layout.length, value; i < j; i++)
536
+ if ((value = layout[i])) layout[i] = this.realize(value, memo);
537
+ } else if (layout.nodeType) {
538
+ return;
539
+ } else if (!layout.promise) {
540
+ for (var key in layout) {
541
+ var row = layout[key];
542
+ var value = row[0]
543
+ var children = row[1];
544
+ if (value) {
545
+ if (value.promise) {
546
+ row[0] = value.result || value.realize(memo);
547
+ row[1] = children = value.advanced;
548
+ }
549
+ if (children && children !== true) this.realize(children, memo);
550
+ }
551
+ }
552
+ } else {
553
+ return layout.result || layout.realize(memo);
554
+ }
555
+ },
556
+
557
+ /*
558
+ Match all selectors in the stack and finds a right mutation
559
+ */
560
+ match: function(element, memo, soft) {
561
+ var stack = memo.stack
562
+ var options, advanced, tagName = LSD.toLowerCase(element.tagName);
563
+ /*
564
+ Matching process happens twice. First time, it matches against
565
+ selectors on the stack with no regard to tag name (as `*`),
566
+ and the other time it takes tag name into account
567
+ */
568
+ for (var i = stack.length, item, result, ary = ['*', tagName]; item = stack[--i];)
569
+ for (var j = 0, value = item[1] || item, tag; tag = ary[j++];)
570
+ if ((group = value[tag]))
571
+ for (var k = 0, possibility, exp; possibility = group[k++];) {
572
+ var exp = possibility[0], result = possibility[1];
573
+ if ((!exp.classes && !exp.attributes && !exp.pseudos)
574
+ // Quickly match tag and id, if other things dont matter
575
+ ? ((j == 0 || tagName == exp.tag) && (!exp.id || element.id == exp.id))
576
+ // Or do a full match
577
+ : (Slick.matchSelector(element, exp.tag, exp.id, exp.classes, exp.attributes, exp.pseudos)))
578
+ /*
579
+ If selector matches, proceed and execute callback
580
+ A callback may be a:
581
+
582
+ * **string**, to be evaluated as a mutation selector
583
+ and parsed into options
584
+ * **object**, with options for widget
585
+ * **a function**, that may return a group of mutations
586
+ that should be applied to the following elements
587
+ * **true**, to initialize widget on that element with
588
+ no specific options.
589
+
590
+ An element may match more than one mutation. In
591
+ that case options extracted from parsing selectors
592
+ will be merged together.
593
+
594
+ If callbacks produced options, the widget will
595
+ be initialized on that element with those options.
596
+
597
+ `soft` parameter tells matcher to skip mutations
598
+ and only advance selectors instead.
599
+ */
600
+ if (!result || !result.call || (result = result(element))) {
601
+ if (!result) result = true;
602
+ if (result.push) {
603
+ (advanced || (advanced = [])).push(result);
604
+ } else if (!soft) {
605
+ if (!options) options = this.getOptions(memo);
606
+ if (result !== true)
607
+ options = LSD.reverseMerge(options, result.match ? LSD.Layout.parse(result) : result);
608
+ }
609
+ }
610
+ }
611
+ if (advanced) memo.advanced = advanced;
612
+ if (options) memo.options = options;
613
+ },
614
+
615
+ /*
616
+ Collect mutations and proxies from the widget. When it is first called
617
+ on a memo that doesnt have things collected from parent widgets,
618
+ it attempts to walk up and restore the context. Restored mutation stack
619
+ is limited to parents, so ~ and + combinators are not processed on parent
620
+ nodes for speed. So only > and <space> combiantors will be collected.
621
+ */
622
+
623
+ push: function(parent, memo) {
624
+ var group, stack = memo.stack;
625
+ if (stack) {
626
+ var widget = parent[0] || parent;
627
+ /*
628
+ Collect mutations from a widget
629
+ */
630
+ if ((group = widget.mutations[' '])) stack.push([' ', group]);
631
+ if ((group = widget.mutations['>'])) stack.push(['>', group]);
632
+ if (widget.proxies) (memo.proxies || (memo.proxies = [])).push([parent[1] || parent.element, widget.proxies]);
633
+ } else {
634
+ var element = parent[1] || parent.element;
635
+ var stack = memo.stack = [], direct;
636
+ for (var parents = [], node = element; node && node.nodeType == 1; node = node.parentNode)
637
+ parents.push(node);
638
+ for (var i = parents.length, node, widget; node = parents[--i];) {
639
+ this.match(node, memo, true);
640
+ if (direct) {
641
+ for (var j = 0, index; index = direct[j++];) stack.splice(index, 1);
642
+ direct = null;
643
+ }
644
+ if (memo.advanced) {
645
+ stack.push.apply(stack, memo.advanced);
646
+ reduce: for (var j = stack.length; group = stack[--j];) {
647
+ switch (group[0]) {
648
+ case '+': case '~':
649
+ stack.splice(j, 1);
650
+ break;
651
+ case '>':
652
+ (direct || (direct = [])).push(j);
653
+ break;
654
+ default:
655
+ break reduce;
656
+ }
657
+ }
658
+ delete memo.advanced;
659
+ }
660
+ widget = node.uid && LSD.Module.DOM.find(node, true);
661
+ if (widget) {
662
+ if ((group = widget.mutations[' '])) stack.push([' ', group]);
663
+ if (i == 0 && (group = widget.mutations['>'])) stack.push(['>', group]);
664
+ if (widget.proxies) (memo.proxies || (memo.proxies = [])).push([node, widget.proxies]);
665
+ }
666
+ }
667
+ }
668
+ },
669
+
670
+ /*
671
+ Remove proxies that were collected from given widget from
672
+ current stacks.
673
+ */
674
+
675
+ pop: function(parent, memo) {
676
+ var group, widget = parent[0] || parent, stack = memo.stack;
677
+ if (stack) {
678
+ if ((group = widget.mutations[' ']))
679
+ for (var j = stack.length; --j > -1;)
680
+ if (stack[j][1] == group) {
681
+ stack.splice(j, 1)
682
+ break;
683
+ }
684
+ if ((group = widget.mutations['>']))
685
+ for (var j = stack.length; --j > -1;)
686
+ if (stack[j][1] == group) {
687
+ stack.splice(j, 1)
688
+ break;
689
+ }
690
+ }
691
+ if (widget.proxies && memo.proxies) {
692
+ for (var j = memo.proxies.length, group; group = memo.proxies[--j];)
693
+ if (group[1] == widget.proxies) {
694
+ memo.proxies.splice(j, 1);
695
+ break;
696
+ }
697
+ }
698
+ },
699
+
700
+ walk: function(element, parent, memo) {
701
+ var ascendant = parent[0] || parent;
702
+ /*
703
+ Retrieve the stack if the render was not triggered from the root of the layout
704
+ */
705
+ if (!memo) memo = {};
706
+ memo.walking = true;
707
+ var stack = memo.stack;
708
+ if (!stack) {
709
+ this.push(parent, memo);
710
+ stack = memo.stack;
711
+ }
712
+ /*
713
+ Render the given element (may clone element or translate it to widget)
714
+ */
715
+ var children = LSD.slice(element.childNodes);
716
+ var ret = this.element(element, parent, memo);
717
+ if (ret.lsd) var widget = ret;
718
+ else if (ret.branch) {
719
+ (memo.branches || (memo.branches = [])).push(ret);
720
+ } else if (memo.clone) var clone = ret;
721
+ /*
722
+ Put away selectors in the stack that should not be matched against element's child nodes
723
+ */
724
+ var group, direct, following;
725
+ reduce: for (var i = stack.length; group = stack[--i];) {
726
+ switch (group[0]) {
727
+ case '+':
728
+ stack.pop();
729
+ break;
730
+ case '~':
731
+ (following || (following = [])).push(stack.pop());
732
+ break;
733
+ case '>':
734
+ (direct || (direct = [])).push(stack.pop());
735
+ break;
736
+ default:
737
+ break reduce;
738
+ }
739
+ }
740
+ /*
741
+ Collect mutations that advanced with this element AND are looking for children
742
+ */
743
+ var advanced = memo.advanced;
744
+ if (advanced) {
745
+ map: for (var i = 0, group; group = advanced[i]; i++) {
746
+ switch (group[0]) {
747
+ case ' ': case '>':
748
+ advanced.splice(i--, 1);
749
+ stack.push(group);
750
+ break;
751
+ default:
752
+ break map;
753
+ }
754
+ }
755
+ delete memo.advanced;
336
756
  }
757
+ delete memo.options;
758
+ /*
759
+ Put away reversed insertion direction option, because it does not affect child nodes
760
+ */
761
+ var before = memo.before;
762
+ if (before) delete memo.before;
763
+ /*
764
+ Scan element for microdata
765
+ */
766
+ var itempath = memo.itempath;
767
+ var scope = LSD.Microdata.element(element, widget || ascendant, itempath && itempath[itempath.length - 1]);
768
+ if (scope) (itempath || (itempath = memo.itempath = [])).push(scope);
769
+ if (widget && itempath) widget.itempath = itempath;
770
+ /*
771
+ Prepare parent array - first item is a nearest parent widget and second is a direct parent element
772
+ */
773
+ var newParent = [widget || ascendant, clone || (widget && widget.element) || element];
774
+ var ascendant = parent[0] || parent;
775
+ /*
776
+ Iterate children
777
+ */
778
+ var first = memo.first, last = memo.last;
779
+ if (children.length) this.children(children, newParent, memo);
780
+ /*
781
+ Restore reversed insertion direction
782
+ */
783
+ if (before) memo.before = before;
784
+ /*
785
+ Put advanced selectors back to the stack
786
+ */
787
+ if (advanced) for (var i = 0; group = advanced[i++];)
788
+ if (group[0] != '+' || !last) stack.push(group);
789
+ /*
790
+ Put back selectors for next siblings
791
+ */
792
+ if (!last) {
793
+ if (direct) for (var i = 0; group = direct[i++];) stack.push(group);
794
+ if (following) for (var i = 0; group = following[i++];) stack.push(group);
795
+ }
796
+ /*
797
+ Reduce the microdata path
798
+ */
799
+ if (scope) itempath.pop();
800
+ /*
801
+ Notify widget that children are processed
802
+ */
803
+ if (widget) widget.fireEvent('DOMChildNodesRendered')
804
+ return ret;
805
+ },
806
+
807
+ getOptions: function(memo, parent) {
808
+ return {
809
+ lazy: true,
810
+ context: memo.context || (parent && (parent[0] || parent).options.context) || LSD.Layout.context
811
+ };
812
+ },
813
+
814
+ getType: function(memo, parent) {
815
+ var context = memo.context || (parent && (parent[0] || parent).options.context) || LSD.Layout.context;
816
+ return LSD[LSD.toClassName(context)];
337
817
  }
338
818
  });
339
819
 
820
+ LSD.Layout.rSimpleSource = /^[a-z-_0-9]+$/;
821
+ /*
822
+ Combinator methods are used in appendChild/removeChild manipulations.
823
+ All of those (and more) are also implemented for matching in Slick.
824
+
825
+ When the layout is pattern matched, Slick first looks for nodes that
826
+ satisfy selector expressions with combinator to reuse. But if it does
827
+ not find those, LSD builds them there.
828
+
829
+ Combinators preceeded with exclaimation mark, are Slick's invention.
830
+ It logically inverts the meaning of combinator, so where a `+` means
831
+ next node, `!+` means previous node.
832
+ */
833
+ LSD.Layout.Combinators = {
834
+ /*
835
+ Insert a node next to the parent
836
+ */
837
+ '+': function(parent, child) {
838
+ return {parent: parent.parentNode, before: parent.nextSibling};
839
+ },
840
+ /*
841
+ Insert a node previous to the parent
842
+ */
843
+ '!+': function(parent, child) {
844
+ return {parent: parent.parentNode, before: parent};
845
+ },
846
+ /*
847
+ Insert node as a last node at parent's level
848
+ */
849
+ '~': function(parent, child) {
850
+ return {parent: parent.parentNode};
851
+ },
852
+ /*
853
+ Inwert node as a first node at parent's level
854
+ */
855
+ '!~': function(parent, child) {
856
+ return {parent: parent.parentNode, before: parent.parentNode.firstChild};
857
+ },
858
+ /*
859
+ Insert node as a first child
860
+ */
861
+ '^': function(parent, child) {
862
+ return {before: parent.firstChild}
863
+ }
864
+ };
865
+
866
+ /*
867
+ ++ and ~~ combinators act as their + and ~ counterparts, but they also look
868
+ back too. So `++` reads as `next child or previous child`. It makes sense
869
+ for matching phase.
870
+
871
+ Although, when a node is built, there's only one node that can not be inserted
872
+ previously to the node and next to the same node simultaneously. So they insert
873
+ ahead, just like `+` and `~`.
874
+ */
875
+ LSD.Layout.Combinators['++'] = LSD.Layout.Combinators['+'];
876
+ LSD.Layout.Combinators['~~'] = LSD.Layout.Combinators['~'];
877
+
878
+ /*
879
+ When a lazy layout is set to render selector, it creates a promise object first.
880
+ The promise object holds all variables required to really render the piece,
881
+ but it postpones the rendering to future. The promise object is used as a proxy
882
+ definition on widget, giving layout a chance to render other chunks of layout that
883
+ are not lazy. When other chunks are rendered, they are matched against current
884
+ proxies on a widget. And when there's a match, proxy (which is really a promise object)
885
+ uses the matched node to and does not build a new one.
886
+
887
+ Then, a `layout.realize()` call sets all promised objects to render if they didnt
888
+ match any nodes built in other chunks of layout.
889
+ */
890
+
891
+ LSD.Layout.Promise = function(layout, selector, lsd, parent, options, memo) {
892
+ selector = selector.replace(LSD.Layout.Promise.rOrderCombinator, function(whole, match) {
893
+ this.order = match;
894
+ return "";
895
+ }.bind(this))
896
+ var parsed = Slick.parse(selector), expressions = parsed.expressions[0];
897
+ // makes proxy deep - look into elements
898
+ this.deep = true;
899
+ this.layout = layout;
900
+ this.options = options;
901
+ this.promise = true;
902
+ this.lsd = lsd;
903
+ this[lsd ? 'selector' : 'mutation'] = selector;
904
+ this.widget = parent[0] || parent;
905
+ this.element = parent[1] || parent.element || parent.toElement();
906
+ this.parent = parent;
907
+ this.attach(memo);
908
+ };
909
+ LSD.Layout.Promise.rOrderCombinator = /\s*([+~])\s*$/;
910
+
911
+
912
+ LSD.Layout.Promise.prototype = {
913
+ attach: function(memo) {
914
+ this.memo = memo;
915
+ if ((this.combinator = memo.combinator)) delete memo.combiantor;
916
+ memo.promised = true;
917
+ var predecessors = memo.predecessors
918
+ if (predecessors) {
919
+ this.predecessors = predecessors.slice(0);
920
+ this.index = this.predecessors.length;
921
+ // Look for previous promises that had + combinator and pair them with this promise
922
+ for (var i = predecessors.length, predecessor; predecessor = predecessors[--i];) {
923
+ if (predecessor.order == '+' && !predecessor.next) {
924
+ this.previous = predecessor;
925
+ this.previous.next = this;
926
+ break;
927
+ }
928
+ }
929
+ } else this.index = 0;
930
+ if (this.order) {
931
+ if (!predecessors) predecessors = memo.predecessors = [];
932
+ predecessors.push(this);
933
+ }
934
+ // promise adds itself to the widget as proxy
935
+ // so its properties are treated as proxy options.
936
+ // `callback` method is called like if it was a proxy setting
937
+ this.widget.addProxy('promise', this);
938
+ },
939
+
940
+ detach: function() {
941
+ if (this.order) {
942
+ var predecessors = this.memo && this.memo.predecessors;
943
+ if (predecessors)
944
+ for (var i = predecessors.length, predecessor; predecessor = predecessors[--i];)
945
+ if (predecessor == this) predecessors.splice(i, 1);
946
+ }
947
+ this.widget.removeProxy('promise', this)
948
+ },
949
+
950
+ advance: function(memo) {
951
+ this.advanced = this.layout.render(this.children, this.result.lsd ? this.result : [this.widget, this.result], this.memo)
952
+ },
953
+
954
+ callback: function(child, proxy, memo) {
955
+ proxy.realize(memo, child)
956
+ },
957
+
958
+ realize: function(memo, result) {
959
+ this.detach();
960
+ var stored = this.options.stored;
961
+ if (stored) delete this.options.stored;
962
+ if (!result) {
963
+ if (this.previous && this.previous.result) this.before = this.previous.result.nextSibling;
964
+ var before = this.before;
965
+ if (before) {
966
+ var old = memo.before;
967
+ memo.before = before;
968
+ }
969
+ result = this.layout.selector(this.options, this.parent, memo);
970
+ if (this.before) memo.before = old;
971
+ }
972
+ this.result = result;
973
+ var predecessors = this.predecessors;
974
+ if (predecessors)
975
+ for (var i = predecessors.length, predecessor; predecessor = predecessors[--i];) {
976
+ var before = predecessor.before;
977
+ if ((predecessor.order == '+' && predecessor.next == this) || !predecessor.following || predecessor.following.index > this.index) {
978
+ predecessor.before = result;
979
+ predecessor.following = this;
980
+ }
981
+ }
982
+ if (this.next) this.next.after = result;
983
+ if (this.children) this.advance(memo);
984
+ if (stored && result.store) {
985
+ for (var name in stored) stored[name].call(this, result);
986
+ result.store('allocation', stored);
987
+ delete this.options.stored;
988
+ }
989
+ }
990
+ };
991
+
340
992
  LSD.Layout.Branch = function(options) {
341
993
  this.options = options;
342
- if (options.link) {
343
- if (!options.holder) throw "Alternative branch is missing its original branch"
344
- options.holder.addEvents({
994
+ this.id = ++LSD.Layout.Branch.UID;
995
+ this.$events = Object.clone(this.$events);
996
+ if (options.superbranch) {
997
+ options.superbranch.addEvents({
345
998
  check: this.unmatch.bind(this),
346
999
  uncheck: this.match.bind(this)
347
1000
  });
1001
+ if (!options.superbranch.checked ^ this.options.invert) this.match();
1002
+ } else if (options.expression || options.show) {
1003
+ this.match();
1004
+ } else if (options.template) {
1005
+ LSD.Template[options.template] = this;
348
1006
  }
349
- this.addEvents({
350
- check: this.show.bind(this),
351
- uncheck: this.hide.bind(this)
352
- });
353
1007
  };
1008
+ LSD.Layout.Branch.UID = 0;
1009
+ LSD.Template = {};
354
1010
 
355
1011
  LSD.Layout.Branch.prototype = Object.append({
1012
+ branch: true,
1013
+ getInterpolation: function() {
1014
+ if (this.interpolation) return this.interpolation;
1015
+ this.interpolation = LSD.Interpolation.compile(this.options.expression, this, this.options.widget, true);
1016
+ return this.interpolation;
1017
+ },
356
1018
  match: function() {
357
- if (!this.checked && this.condition()) this.fireEvent('check', arguments);
1019
+ if (this.options.expression) {
1020
+ var value = this.getInterpolation().attach().value;
1021
+ if ((value == null) ^ this.options.invert) return;
1022
+ }
1023
+ this.check();
1024
+ },
1025
+ unmatch: function(lazy) {
1026
+ if (this.options.expression) {
1027
+ var value = this.interpolation.value;
1028
+ this.interpolation.detach()
1029
+ if ((value == null) ^ this.options.invert) return;
1030
+ }
1031
+ this.uncheck(lazy);
358
1032
  },
359
- unmatch: function() {
360
- if (this.checked) this.fireEvent('uncheck', arguments);
1033
+ check: function(lazy) {
1034
+ if (!this.checked) {
1035
+ this.checked = true;
1036
+ this.show();
1037
+ if (!lazy) this.fireEvent('check', arguments);
1038
+ }
1039
+ },
1040
+ uncheck: function(lazy) {
1041
+ if (this.checked || this.checked == null) {
1042
+ this.checked = false;
1043
+ this.hide();
1044
+ if (!lazy) this.fireEvent('uncheck', arguments);
1045
+ }
361
1046
  },
362
- condition: function() {
363
- return (this.options.expression) ^ this.options.inverse;
1047
+ set: function(value) {
1048
+ this[((value != false && value != null) ^ this.options.invert) ? 'check' : 'uncheck']();
364
1049
  },
365
1050
  show: function() {
366
- return this.layout.add(this.options.layout, this.options.parent, this.options.options);
1051
+ var layout = this.layout;
1052
+ if (!layout) return;
1053
+ if (layout.length) for (var i = 0, child, keyword, depth = 0; child = layout[i]; i++) {
1054
+ if (child.call) {
1055
+ if (layout === this.layout) layout = layout.slice(0);
1056
+ var result = child.call(this);
1057
+ if (result) {
1058
+ for (var branch = this; branch; branch = branch.options.parent) branch.dirty = true;
1059
+ if (result.length) layout.splice.apply(layout, [i, 1].concat(result))
1060
+ else layout[i] = result;
1061
+ }
1062
+ }
1063
+ }
1064
+ var before = this.options.element && this.options.element.nextSibling;
1065
+ var rendered = this.options.widget.addLayout(this.id, layout, this.options.widget, this.options.options, {before: before});
1066
+ if (result) this.validate(rendered)
1067
+ if (rendered) this.layout = rendered;
367
1068
  },
368
1069
  hide: function() {
369
- return this.layout.remove(this.options.layout, this.options.parent, this.options.options);
1070
+ var layout = this.layout;
1071
+ if (!layout) return;
1072
+ this.options.widget.removeLayout(this.id, layout, this.options.widget, this.options.options);
1073
+ },
1074
+ splice: function(branch, layout, baseline) {
1075
+ var offset = 0;
1076
+ if (branch.layout) {
1077
+ if (!layout) layout = this.layout;
1078
+ for (var i = 0, child, keyword; child = branch.layout[i]; i++) {
1079
+ var index = layout.indexOf(child);
1080
+ if (index > -1) {
1081
+ var keyword = Element.retrieve(child, 'keyword');
1082
+ if (keyword && keyword !== true) {
1083
+ offset += this.splice(keyword, layout);
1084
+ }
1085
+ layout.splice(index, 1);
1086
+ i--;
1087
+ }
1088
+ }
1089
+ }
1090
+ return offset;
1091
+ },
1092
+ validate: function(layout) {
1093
+ var validate = true;
1094
+ for (var index, child, i = 0; child = layout[i]; i++) {
1095
+ switch ((child = layout[i]).nodeType) {
1096
+ case 8:
1097
+ if (validate)
1098
+ if (index != null) validate = index = null;
1099
+ else index = i;
1100
+ var keyword = Element.retrieve(child, 'keyword');
1101
+ if (keyword && keyword !== true) i += this.splice(keyword, layout, i)
1102
+ break;
1103
+ case 3:
1104
+ if (validate && child.textContent.match(LSD.Layout.rWhitespace)) break;
1105
+ default:
1106
+ index = validate = null;
1107
+ }
1108
+ }
1109
+ if (index != null) {
1110
+ var comment = layout[index];
1111
+ layout[index] = function() {
1112
+ return LSD.slice(document.createFragment(this.expand(comment.nodeValue)).childNodes);
1113
+ };
1114
+ comment.parentNode.removeChild(comment);
1115
+ if (this.options.clean) layout = layout[index];
1116
+ }
1117
+ return layout;
1118
+ },
1119
+ setLayout: function(layout, soft) {
1120
+ this.layout = layout.push ? this.validate(layout) : layout;
1121
+ if (this.checked) {
1122
+ this.show();
1123
+ } else if (!soft) this.hide();
1124
+ },
1125
+ getLayout: function(layout) {
1126
+ return this.layout;
1127
+ },
1128
+ attach: function() {
1129
+ if ((this.options.expression && !this.options.link) || !this.options.superbranch.checked) this.match(true);
1130
+ //this.show();
1131
+ },
1132
+ detach: function() {
1133
+ if (this.options.expression && !this.options.link) this.unmatch(true);
1134
+ this.hide()
1135
+ },
1136
+ expand: function(text) {
1137
+ var depth = 0;
1138
+ text = text.replace(LSD.Layout.rComment, function(whole, start, end) {
1139
+ depth += (start ? 1 : -1);
1140
+ if (depth == !!start) return start ? '<!--' : '-->'
1141
+ return whole;
1142
+ });
1143
+ if (depth) throw "The lazy branch is unbalanced"
1144
+ return text;
370
1145
  }
371
1146
  }, Events.prototype);
372
1147
 
1148
+ LSD.Layout.rComment = /(\<\!-)|(-\>)/g
1149
+ LSD.Layout.rWhitespace = /^\s*$/;
1150
+
373
1151
  LSD.Layout.NodeTypes = {1: 'element', 3: 'textnode', 8: 'comment', 11: 'fragment'};
374
- LSD.Layout.NodeNames = Array.fast('!doctype', 'a', 'abbr', 'acronym', 'address', 'applet', 'area',
1152
+ LSD.Layout.NodeNames = Array.object('!doctype', 'a', 'abbr', 'acronym', 'address', 'applet', 'area',
375
1153
  'article', 'aside', 'audio', 'b', 'base', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas',
376
1154
  'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details',
377
1155
  'dfn', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frame',
378
- 'frameset', 'h1', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins',
379
- 'keygen', 'kbd', 'label', 'legend', 'li', 'link', 'map', 'mark', 'menu', 'meta', 'meter', 'nav',
380
- 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q',
381
- 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strike',
1156
+ 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe',
1157
+ 'img', 'input', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li', 'link', 'map', 'mark', 'menu', 'meta',
1158
+ 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress',
1159
+ 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strike',
382
1160
  'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead',
383
1161
  'time', 'title', 'tr', 'ul', 'var', 'video', 'wbr');
384
- LSD.Layout.Inheritable = Array.fast('context', 'interpolation', 'clone', 'lazy');
385
1162
 
386
- LSD.Layout.rExpressions = /^\s*(if|else|call|els[e ]*if|unless)(?:$|\s+(.*?)\s*$)/
1163
+ LSD.Layout.Keyword = {
1164
+ 'if': function(expression) {
1165
+ return {branch: true, expression: expression};
1166
+ },
1167
+ 'unless': function(expression) {
1168
+ return {branch: true, expression: expression, invert: true};
1169
+ },
1170
+ 'elsif': function(expression) {
1171
+ return {branch: true, expression: expression, link: true};
1172
+ },
1173
+ 'else': function() {
1174
+ return {branch: true, link: true};
1175
+ },
1176
+ 'build': function(expression) {
1177
+ var options = {layout: {}};
1178
+ options.layout[parsed.expression] = true;
1179
+ return options;
1180
+ },
1181
+ 'template': function(expression) {
1182
+ return {branch: true, template: expression, clean: true}
1183
+ },
1184
+ 'end': function(expression) {
1185
+ return {ends: true}
1186
+ }
1187
+ };
1188
+
1189
+ LSD.Layout.rExpression = /^\s*([a-z]+)(?:\s(.*?))?\s*$/;
387
1190
  Object.append(LSD.Layout, {
388
1191
  extractKeyword: function(input) {
389
- var match = input.match(LSD.Layout.rExpressions);
390
- if (match) return {keyword: match[1], expression: SheetParser.Value.translate(match[2])};
1192
+ var match = input.match(LSD.Layout.rExpression);
1193
+ if (match && LSD.Layout.Keyword[match[1]])
1194
+ return {keyword: match[1], expression: match[2]};
391
1195
  },
392
1196
 
393
1197
  /*
@@ -397,31 +1201,45 @@ Object.append(LSD.Layout, {
397
1201
  var options = {};
398
1202
  var expressions = (selector.Slick ? selector : Slick.parse(selector)).expressions[0];
399
1203
  var parsed = expressions[0];
1204
+ if (expressions.length > 1) var order = expressions[expressions.length - 1].combinator;
400
1205
  if (parsed.combinator != ' ') {
401
1206
  if (parsed.combinator == '::') {
402
1207
  if (LSD.Allocations[parsed.tag]) {
403
- options.allocation = LSD.Module.Allocations.prepare(options, parsed.tag, parsed.classes, parsed.attributes, parsed.pseudos);
1208
+ var allocation = options.allocation = LSD.Module.Allocations.compile(parsed.tag, parsed.classes, parsed.attributes, parsed.pseudos);
1209
+ if (allocation.options && allocation.options.source) {
1210
+ var source = allocation.options.source;
1211
+ delete allocation.options.source
1212
+ }
404
1213
  } else {
405
1214
  var relation = (parent[0] || parent).relations[parsed.tag];
406
1215
  if (!relation) throw "Unknown pseudo element ::" + parsed.tag;
407
1216
  var source = relation.getSource();
408
- if (source) Object.append(options, LSD.Layout.parse(source, parent[0] || parent));
409
- }
1217
+ }
1218
+ if (source && (!source.match || !source.match(rSIMPLE_SOURCE)))
1219
+ Object.merge(options, LSD.Layout.parse(source, parent));
410
1220
  } else options.combinator = parsed.combinator;
411
- }
1221
+ }
1222
+ if (order && order != ' ') options.order = order;
1223
+ if (parsed.id) (options.attributes || (options.attributes = {})).id = parsed.id
1224
+ if (parsed.attributes)
1225
+ for (var all = parsed.attributes, attribute, i = 0; attribute = all[i++];) {
1226
+ var value = attribute.value || LSD.Attributes[attribute.key] == 'number' || "";
1227
+ (options.attributes || (options.attributes = {}))[attribute.key] = value;
1228
+ }
412
1229
  if (parsed.tag != '*' && parsed.combinator != '::')
413
1230
  if (parsed.tag.indexOf('-') > -1)
414
1231
  options.source = parsed.tag.split('-');
415
- else
1232
+ else {
416
1233
  options.tag = parsed.tag;
417
- if (parsed.id) (options.attributes || (options.attributes = {})).id = parsed.id
418
- if (parsed.attributes) for (var all = parsed.attributes, attribute, i = 0; attribute = all[i++];) {
419
- var value = attribute.value || LSD.Attributes.Boolean[attribute.key] || "";
420
- (options.attributes || (options.attributes = {}))[attribute.key] = value;
421
- }
422
- if (parsed.classes) options.classes = parsed.classes.map(Macro.map('value'));
423
- if (parsed.pseudos) for (var all = parsed.pseudos, pseudo, i = 0; pseudo = all[i++];)
424
- (options.pseudos || (options.pseudos = {})).push(pseudo.key);
1234
+ var source = LSD.Layout.getSource(options, options.tag);
1235
+ if (source.push) options.source = source;
1236
+ }
1237
+ if (parsed.classes)
1238
+ for (var all = parsed.classes, pseudo, i = 0; klass = all[i++];)
1239
+ (options.classes || (options.classes = {}))[klass.value] = true;
1240
+ if (parsed.pseudos)
1241
+ for (var all = parsed.pseudos, pseudo, i = 0; pseudo = all[i++];)
1242
+ (options.pseudos || (options.pseudos = {}))[pseudo.key] = true;
425
1243
  return options;
426
1244
  },
427
1245