terminus 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.rdoc +27 -89
  2. data/bin/terminus +7 -23
  3. data/lib/capybara/driver/terminus.rb +30 -11
  4. data/lib/terminus.rb +33 -22
  5. data/lib/terminus/application.rb +13 -7
  6. data/lib/terminus/browser.rb +110 -28
  7. data/lib/terminus/controller.rb +51 -20
  8. data/lib/terminus/host.rb +22 -0
  9. data/lib/terminus/node.rb +23 -4
  10. data/lib/terminus/proxy.rb +62 -0
  11. data/lib/terminus/proxy/driver_body.rb +63 -0
  12. data/lib/terminus/proxy/external.rb +25 -0
  13. data/lib/terminus/proxy/rewrite.rb +20 -0
  14. data/lib/terminus/public/icon.png +0 -0
  15. data/lib/terminus/public/loader.js +1 -0
  16. data/lib/terminus/public/style.css +43 -0
  17. data/lib/terminus/public/syn/browsers.js +150 -0
  18. data/lib/terminus/public/syn/drag/drag.js +322 -0
  19. data/lib/terminus/public/syn/key.js +905 -0
  20. data/lib/terminus/public/syn/mouse.js +284 -0
  21. data/lib/terminus/public/syn/synthetic.js +830 -0
  22. data/lib/{public → terminus/public}/terminus.js +109 -40
  23. data/lib/terminus/server.rb +5 -12
  24. data/lib/terminus/timeouts.rb +2 -2
  25. data/lib/{views/bookmarklet.erb → terminus/views/bootstrap.erb} +17 -12
  26. data/lib/terminus/views/index.erb +21 -0
  27. data/lib/terminus/views/infinite.html +16 -0
  28. data/spec/reports/chrome.txt +748 -0
  29. data/spec/reports/firefox.txt +748 -0
  30. data/spec/reports/opera.txt +748 -0
  31. data/spec/reports/safari.txt +748 -0
  32. data/spec/spec_helper.rb +18 -14
  33. data/spec/terminus_driver_spec.rb +7 -5
  34. data/spec/terminus_session_spec.rb +5 -18
  35. metadata +71 -57
  36. data/lib/public/loader.js +0 -1
  37. data/lib/public/style.css +0 -49
  38. data/lib/public/syn.js +0 -2355
  39. data/lib/views/index.erb +0 -32
@@ -0,0 +1,284 @@
1
+ steal.then(function(){
2
+ //handles mosue events
3
+
4
+ var h = Syn.helpers,
5
+ getWin = h.getWindow;
6
+
7
+ Syn.mouse = {};
8
+ h.extend(Syn.defaults, {
9
+ mousedown: function( options ) {
10
+ Syn.trigger("focus", {}, this)
11
+ },
12
+ click: function() {
13
+ // prevents the access denied issue in IE if the click causes the element to be destroyed
14
+ var element = this;
15
+ try {
16
+ element.nodeType;
17
+ } catch (e) {
18
+ return;
19
+ }
20
+ //get old values
21
+ var href, radioChanged = Syn.data(element, "radioChanged"),
22
+ scope = getWin(element),
23
+ nodeName = element.nodeName.toLowerCase();
24
+
25
+ //this code was for restoring the href attribute to prevent popup opening
26
+ //if ((href = Syn.data(element, "href"))) {
27
+ // element.setAttribute('href', href)
28
+ //}
29
+
30
+ //run href javascript
31
+ if (!Syn.support.linkHrefJS && /^\s*javascript:/.test(element.href) ) {
32
+ //eval js
33
+ var code = element.href.replace(/^\s*javascript:/, "")
34
+
35
+ //try{
36
+ if ( code != "//" && code.indexOf("void(0)") == -1 ) {
37
+ if ( window.selenium ) {
38
+ eval("with(selenium.browserbot.getCurrentWindow()){" + code + "}")
39
+ } else {
40
+ eval("with(scope){" + code + "}")
41
+ }
42
+ }
43
+ }
44
+
45
+ //submit a form
46
+ if (!(Syn.support.clickSubmits) && (nodeName == "input" && element.type == "submit") || nodeName == 'button' ) {
47
+
48
+ var form = Syn.closest(element, "form");
49
+ if ( form ) {
50
+ Syn.trigger("submit", {}, form)
51
+ }
52
+
53
+ }
54
+ //follow a link, probably needs to check if in an a.
55
+ if ( nodeName == "a" && element.href && !/^\s*javascript:/.test(element.href) ) {
56
+
57
+ scope.location.href = element.href;
58
+
59
+ }
60
+
61
+ //change a checkbox
62
+ if ( nodeName == "input" && element.type == "checkbox" ) {
63
+
64
+ //if(!Syn.support.clickChecks && !Syn.support.changeChecks){
65
+ // element.checked = !element.checked;
66
+ //}
67
+ if (!Syn.support.clickChanges ) {
68
+ Syn.trigger("change", {}, element);
69
+ }
70
+ }
71
+
72
+ //change a radio button
73
+ if ( nodeName == "input" && element.type == "radio" ) { // need to uncheck others if not checked
74
+ if ( radioChanged && !Syn.support.radioClickChanges ) {
75
+ Syn.trigger("change", {}, element);
76
+ }
77
+ }
78
+ // change options
79
+ if ( nodeName == "option" && Syn.data(element, "createChange") ) {
80
+ Syn.trigger("change", {}, element.parentNode); //does not bubble
81
+ Syn.data(element, "createChange", false)
82
+ }
83
+ }
84
+ })
85
+
86
+ //add create and setup behavior for mosue events
87
+ h.extend(Syn.create, {
88
+ mouse: {
89
+ options: function( type, options, element ) {
90
+ var doc = document.documentElement,
91
+ body = document.body,
92
+ center = [options.pageX || 0, options.pageY || 0],
93
+ //browser might not be loaded yet (doing support code)
94
+ left = Syn.mouse.browser && Syn.mouse.browser.left[type],
95
+ right = Syn.mouse.browser && Syn.mouse.browser.right[type];
96
+ return h.extend({
97
+ bubbles: true,
98
+ cancelable: true,
99
+ view: window,
100
+ detail: 1,
101
+ screenX: 1,
102
+ screenY: 1,
103
+ clientX: options.clientX || center[0] - (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0),
104
+ clientY: options.clientY || center[1] - (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0),
105
+ ctrlKey: !! Syn.key.ctrlKey,
106
+ altKey: !! Syn.key.altKey,
107
+ shiftKey: !! Syn.key.shiftKey,
108
+ metaKey: !! Syn.key.metaKey,
109
+ button: left && left.button != null ? left.button : right && right.button || (type == 'contextmenu' ? 2 : 0),
110
+ relatedTarget: document.documentElement
111
+ }, options);
112
+ },
113
+ event: function( type, defaults, element ) { //Everyone Else
114
+ var doc = getWin(element).document || document
115
+ if ( doc.createEvent ) {
116
+ var event;
117
+
118
+ try {
119
+ event = doc.createEvent('MouseEvents');
120
+ event.initMouseEvent(type, defaults.bubbles, defaults.cancelable, defaults.view, defaults.detail, defaults.screenX, defaults.screenY, defaults.clientX, defaults.clientY, defaults.ctrlKey, defaults.altKey, defaults.shiftKey, defaults.metaKey, defaults.button, defaults.relatedTarget);
121
+ } catch (e) {
122
+ event = h.createBasicStandardEvent(type, defaults, doc)
123
+ }
124
+ event.synthetic = true;
125
+ return event;
126
+ } else {
127
+ var event;
128
+ try {
129
+ event = h.createEventObject(type, defaults, element)
130
+ }
131
+ catch (e) {}
132
+
133
+ return event;
134
+ }
135
+
136
+ }
137
+ },
138
+ click: {
139
+ setup: function( type, options, element ) {
140
+ var nodeName = element.nodeName.toLowerCase(),
141
+ type;
142
+
143
+ //we need to manually 'check' in browser that can't check
144
+ //so checked has the right value
145
+ if (!Syn.support.clickChecks && !Syn.support.changeChecks && nodeName === "input" ) {
146
+ type = element.type.toLowerCase(); //pretty sure lowercase isn't needed
147
+ if ( type === 'checkbox' ) {
148
+ element.checked = !element.checked;
149
+ }
150
+ if ( type === "radio" ) {
151
+ //do the checks manually
152
+ if (!element.checked ) { //do nothing, no change
153
+ try {
154
+ Syn.data(element, "radioChanged", true);
155
+ } catch (e) {}
156
+ element.checked = true;
157
+ }
158
+ }
159
+ }
160
+
161
+ if ( nodeName == "a" && element.href && !/^\s*javascript:/.test(element.href) ) {
162
+
163
+ //save href
164
+ Syn.data(element, "href", element.href)
165
+
166
+ //remove b/c safari/opera will open a new tab instead of changing the page
167
+ // this has been removed because newer versions don't have this problem
168
+ //element.setAttribute('href', 'javascript://')
169
+ //however this breaks scripts using the href
170
+ //we need to listen to this and prevent the default behavior
171
+ //and run the default behavior ourselves. Boo!
172
+ }
173
+ //if select or option, save old value and mark to change
174
+ if (/option/i.test(element.nodeName) ) {
175
+ var child = element.parentNode.firstChild,
176
+ i = -1;
177
+ while ( child ) {
178
+ if ( child.nodeType == 1 ) {
179
+ i++;
180
+ if ( child == element ) break;
181
+ }
182
+ child = child.nextSibling;
183
+ }
184
+ if ( i !== element.parentNode.selectedIndex ) {
185
+ //shouldn't this wait on triggering
186
+ //change?
187
+ element.parentNode.selectedIndex = i;
188
+ Syn.data(element, "createChange", true)
189
+ }
190
+ }
191
+
192
+ }
193
+ },
194
+ mousedown: {
195
+ setup: function( type, options, element ) {
196
+ var nn = element.nodeName.toLowerCase();
197
+ //we have to auto prevent default to prevent freezing error in safari
198
+ if ( Syn.browser.safari && (nn == "select" || nn == "option") ) {
199
+ options._autoPrevent = true;
200
+ }
201
+ }
202
+ }
203
+ });
204
+ //do support code
205
+ (function() {
206
+ if (!document.body ) {
207
+ setTimeout(arguments.callee, 1)
208
+ return;
209
+ }
210
+ var oldSynth = window.__synthTest;
211
+ window.__synthTest = function() {
212
+ Syn.support.linkHrefJS = true;
213
+ }
214
+ var div = document.createElement("div"),
215
+ checkbox, submit, form, input, select;
216
+
217
+ div.innerHTML = "<form id='outer'>" + "<input name='checkbox' type='checkbox'/>" + "<input name='radio' type='radio' />" + "<input type='submit' name='submitter'/>" + "<input type='input' name='inputter'/>" + "<input name='one'>" + "<input name='two'/>" + "<a href='javascript:__synthTest()' id='synlink'></a>" + "<select><option></option></select>" + "</form>";
218
+ document.documentElement.appendChild(div);
219
+ form = div.firstChild
220
+ checkbox = form.childNodes[0];
221
+ submit = form.childNodes[2];
222
+ select = form.getElementsByTagName('select')[0]
223
+
224
+ checkbox.checked = false;
225
+ checkbox.onchange = function() {
226
+ Syn.support.clickChanges = true;
227
+ }
228
+
229
+ Syn.trigger("click", {}, checkbox)
230
+ Syn.support.clickChecks = checkbox.checked;
231
+
232
+ checkbox.checked = false;
233
+
234
+ Syn.trigger("change", {}, checkbox);
235
+
236
+ Syn.support.changeChecks = checkbox.checked;
237
+
238
+ form.onsubmit = function( ev ) {
239
+ if ( ev.preventDefault ) ev.preventDefault();
240
+ Syn.support.clickSubmits = true;
241
+ return false;
242
+ }
243
+ Syn.trigger("click", {}, submit)
244
+
245
+
246
+
247
+ form.childNodes[1].onchange = function() {
248
+ Syn.support.radioClickChanges = true;
249
+ }
250
+ Syn.trigger("click", {}, form.childNodes[1])
251
+
252
+
253
+ Syn.bind(div, 'click', function() {
254
+ Syn.support.optionClickBubbles = true;
255
+ Syn.unbind(div, 'click', arguments.callee)
256
+ })
257
+ Syn.trigger("click", {}, select.firstChild)
258
+
259
+
260
+ Syn.support.changeBubbles = Syn.eventSupported('change');
261
+
262
+ //test if mousedown followed by mouseup causes click (opera), make sure there are no clicks after this
263
+ var clicksCount = 0
264
+ div.onclick = function() {
265
+ Syn.support.mouseDownUpClicks = true;
266
+ //we should use this to check for opera potentially, but would
267
+ //be difficult to remove element correctly
268
+ //Syn.support.mouseDownUpRepeatClicks = (2 == (++clicksCount))
269
+ }
270
+ Syn.trigger("mousedown", {}, div)
271
+ Syn.trigger("mouseup", {}, div)
272
+
273
+ //setTimeout(function(){
274
+ // Syn.trigger("mousedown",{},div)
275
+ // Syn.trigger("mouseup",{},div)
276
+ //},1)
277
+
278
+ document.documentElement.removeChild(div);
279
+
280
+ //check stuff
281
+ window.__synthTest = oldSynth;
282
+ Syn.support.ready++;
283
+ })();
284
+ })
@@ -0,0 +1,830 @@
1
+ steal.then(function(){
2
+ var extend = function( d, s ) {
3
+ var p;
4
+ for (p in s) {
5
+ d[p] = s[p];
6
+ }
7
+ return d;
8
+ },
9
+ // only uses browser detection for key events
10
+ browser = {
11
+ msie: !! (window.attachEvent && !window.opera),
12
+ opera: !! window.opera,
13
+ webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
14
+ safari: navigator.userAgent.indexOf('AppleWebKit/') > -1 && navigator.userAgent.indexOf('Chrome/') === -1,
15
+ gecko: navigator.userAgent.indexOf('Gecko') > -1,
16
+ mobilesafari: !! navigator.userAgent.match(/Apple.*Mobile.*Safari/),
17
+ rhino: navigator.userAgent.match(/Rhino/) && true
18
+ },
19
+ createEventObject = function( type, options, element ) {
20
+ var event = element.ownerDocument.createEventObject();
21
+ return extend(event, options);
22
+ },
23
+ data = {},
24
+ id = 1,
25
+ expando = "_synthetic" + new Date().getTime(),
26
+ bind, unbind, key = /keypress|keyup|keydown/,
27
+ page = /load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll/,
28
+ //this is maintained so we can click on html and blur the active element
29
+ activeElement,
30
+
31
+ /**
32
+ * @class Syn
33
+ * @download funcunit/dist/syn.js
34
+ * @test funcunit/synthetic/qunit.html
35
+ * Syn is used to simulate user actions. It creates synthetic events and
36
+ * performs their default behaviors.
37
+ *
38
+ * <h2>Basic Use</h2>
39
+ * The following clicks an input element with <code>id='description'</code>
40
+ * and then types <code>'Hello World'</code>.
41
+ *
42
+ @codestart
43
+ Syn.click({},'description')
44
+ .type("Hello World")
45
+ @codeend
46
+ * <h2>User Actions and Events</h2>
47
+ * <p>Syn is typically used to simulate user actions as opposed to triggering events. Typing characters
48
+ * is an example of a user action. The keypress that represents an <code>'a'</code>
49
+ * character being typed is an example of an event.
50
+ * </p>
51
+ * <p>
52
+ * While triggering events is supported, it's much more useful to simulate actual user behavior. The
53
+ * following actions are supported by Syn:
54
+ * </p>
55
+ * <ul>
56
+ * <li><code>[Syn.prototype.click click]</code> - a mousedown, focus, mouseup, and click.</li>
57
+ * <li><code>[Syn.prototype.dblclick dblclick]</code> - two <code>click!</code> events followed by a <code>dblclick</code>.</li>
58
+ * <li><code>[Syn.prototype.key key]</code> - types a single character (keydown, keypress, keyup).</li>
59
+ * <li><code>[Syn.prototype.type type]</code> - types multiple characters into an element.</li>
60
+ * <li><code>[Syn.prototype.move move]</code> - moves the mouse from one position to another (triggering mouseover / mouseouts).</li>
61
+ * <li><code>[Syn.prototype.drag drag]</code> - a mousedown, followed by mousemoves, and a mouseup.</li>
62
+ * </ul>
63
+ * All actions run asynchronously.
64
+ * Click on the links above for more
65
+ * information on how to use the specific action.
66
+ * <h2>Asynchronous Callbacks</h2>
67
+ * Actions don't complete immediately. This is almost
68
+ * entirely because <code>focus()</code>
69
+ * doesn't run immediately in IE.
70
+ * If you provide a callback function to Syn, it will
71
+ * be called after the action is completed.
72
+ * <br/>The following checks that "Hello World" was entered correctly:
73
+ @codestart
74
+ Syn.click({},'description')
75
+ .type("Hello World", function(){
76
+
77
+ ok("Hello World" == document.getElementById('description').value)
78
+ })
79
+ @codeend
80
+ <h2>Asynchronous Chaining</h2>
81
+ <p>You might have noticed the [Syn.prototype.then then] method. It provides chaining
82
+ so you can do a sequence of events with a single (final) callback.
83
+ </p><p>
84
+ If an element isn't provided to then, it uses the previous Syn's element.
85
+ </p>
86
+ The following does a lot of stuff before checking the result:
87
+ @codestart
88
+ Syn.type('ice water','title')
89
+ .type('ice and water','description')
90
+ .click({},'create')
91
+ .drag({to: 'favorites'},'newRecipe',
92
+ function(){
93
+ ok($('#newRecipe').parents('#favorites').length);
94
+ })
95
+ @codeend
96
+
97
+ <h2>jQuery Helper</h2>
98
+ If jQuery is present, Syn adds a triggerSyn helper you can use like:
99
+ @codestart
100
+ $("#description").triggerSyn("type","Hello World");
101
+ @codeend
102
+ * <h2>Key Event Recording</h2>
103
+ * <p>Every browser has very different rules for dispatching key events.
104
+ * As there is no way to feature detect how a browser handles key events,
105
+ * synthetic uses a description of how the browser behaves generated
106
+ * by a recording application. </p>
107
+ * <p>
108
+ * If you want to support a browser not currently supported, you can
109
+ * record that browser's key event description and add it to
110
+ * <code>Syn.key.browsers</code> by it's navigator agent.
111
+ * </p>
112
+ @codestart
113
+ Syn.key.browsers["Envjs\ Resig/20070309 PilotFish/1.2.0.10\1.6"] = {
114
+ 'prevent':
115
+ {"keyup":[],"keydown":["char","keypress"],"keypress":["char"]},
116
+ 'character':
117
+ { ... }
118
+ }
119
+ @codeend
120
+ * <h2>Limitations</h2>
121
+ * Syn fully supports IE 6+, FF 3+, Chrome, Safari, Opera 10+.
122
+ * With FF 1+, drag / move events are only partially supported. They will
123
+ * not trigger mouseover / mouseout events.<br/>
124
+ * Safari crashes when a mousedown is triggered on a select. Syn will not
125
+ * create this event.
126
+ * <h2>Contributing to Syn</h2>
127
+ * Have we missed something? We happily accept patches. The following are
128
+ * important objects and properties of Syn:
129
+ * <ul>
130
+ * <li><code>Syn.create</code> - contains methods to setup, convert options, and create an event of a specific type.</li>
131
+ * <li><code>Syn.defaults</code> - default behavior by event type (except for keys).</li>
132
+ * <li><code>Syn.key.defaults</code> - default behavior by key.</li>
133
+ * <li><code>Syn.keycodes</code> - supported keys you can type.</li>
134
+ * </ul>
135
+ * <h2>Roll Your Own Functional Test Framework</h2>
136
+ * <p>Syn is really the foundation of JavaScriptMVC's functional testing framework - [FuncUnit].
137
+ * But, we've purposely made Syn work without any dependencies in the hopes that other frameworks or
138
+ * testing solutions can use it as well.
139
+ * </p>
140
+ * @constructor
141
+ * Creates a synthetic event on the element.
142
+ * @param {Object} type
143
+ * @param {Object} options
144
+ * @param {Object} element
145
+ * @param {Object} callback
146
+ * @return Syn
147
+ */
148
+ Syn = function( type, options, element, callback ) {
149
+ return (new Syn.init(type, options, element, callback));
150
+ };
151
+
152
+ bind = function( el, ev, f ) {
153
+ return el.addEventListener ? el.addEventListener(ev, f, false) : el.attachEvent("on" + ev, f);
154
+ };
155
+ unbind = function( el, ev, f ) {
156
+ return el.addEventListener ? el.removeEventListener(ev, f, false) : el.detachEvent("on" + ev, f);
157
+ };
158
+
159
+ /**
160
+ * @Static
161
+ */
162
+ extend(Syn, {
163
+ /**
164
+ * Creates a new synthetic event instance
165
+ * @hide
166
+ * @param {Object} type
167
+ * @param {Object} options
168
+ * @param {Object} element
169
+ * @param {Object} callback
170
+ */
171
+ init: function( type, options, element, callback ) {
172
+ var args = Syn.args(options, element, callback),
173
+ self = this;
174
+ this.queue = [];
175
+ this.element = args.element;
176
+
177
+ //run event
178
+ if ( typeof this[type] === "function" ) {
179
+ this[type](args.options, args.element, function( defaults, el ) {
180
+ args.callback && args.callback.apply(self, arguments);
181
+ self.done.apply(self, arguments);
182
+ });
183
+ } else {
184
+ this.result = Syn.trigger(type, args.options, args.element);
185
+ args.callback && args.callback.call(this, args.element, this.result);
186
+ }
187
+ },
188
+ jquery: function( el, fast ) {
189
+ if ( window.FuncUnit && window.FuncUnit.jquery ) {
190
+ return window.FuncUnit.jquery;
191
+ }
192
+ if ( el ) {
193
+ return Syn.helpers.getWindow(el).jQuery || window.jQuery;
194
+ }
195
+ else {
196
+ return window.jQuery;
197
+ }
198
+ },
199
+ /**
200
+ * Returns an object with the args for a Syn.
201
+ * @hide
202
+ * @return {Object}
203
+ */
204
+ args: function() {
205
+ var res = {},
206
+ i = 0;
207
+ for ( ; i < arguments.length; i++ ) {
208
+ if ( typeof arguments[i] === 'function' ) {
209
+ res.callback = arguments[i];
210
+ } else if ( arguments[i] && arguments[i].jquery ) {
211
+ res.element = arguments[i][0];
212
+ } else if ( arguments[i] && arguments[i].nodeName ) {
213
+ res.element = arguments[i];
214
+ } else if ( res.options && typeof arguments[i] === 'string' ) { //we can get by id
215
+ res.element = document.getElementById(arguments[i]);
216
+ }
217
+ else if ( arguments[i] ) {
218
+ res.options = arguments[i];
219
+ }
220
+ }
221
+ return res;
222
+ },
223
+ click: function( options, element, callback ) {
224
+ Syn('click!', options, element, callback);
225
+ },
226
+ /**
227
+ * @attribute defaults
228
+ * Default actions for events. Each default function is called with this as its
229
+ * element. It should return true if a timeout
230
+ * should happen after it. If it returns an element, a timeout will happen
231
+ * and the next event will happen on that element.
232
+ */
233
+ defaults: {
234
+ focus: function() {
235
+ if (!Syn.support.focusChanges ) {
236
+ var element = this,
237
+ nodeName = element.nodeName.toLowerCase();
238
+ Syn.data(element, "syntheticvalue", element.value);
239
+
240
+ //TODO, this should be textarea too
241
+ //and this might be for only text style inputs ... hmmmmm ....
242
+ if ( nodeName === "input" || nodeName === "textarea" ) {
243
+ bind(element, "blur", function() {
244
+ if ( Syn.data(element, "syntheticvalue") != element.value ) {
245
+
246
+ Syn.trigger("change", {}, element);
247
+ }
248
+ unbind(element, "blur", arguments.callee);
249
+ });
250
+
251
+ }
252
+ }
253
+ },
254
+ submit: function() {
255
+ Syn.onParents(this, function( el ) {
256
+ if ( el.nodeName.toLowerCase() === 'form' ) {
257
+ el.submit();
258
+ return false;
259
+ }
260
+ });
261
+ }
262
+ },
263
+ changeOnBlur: function( element, prop, value ) {
264
+
265
+ bind(element, "blur", function() {
266
+ if ( value !== element[prop] ) {
267
+ Syn.trigger("change", {}, element);
268
+ }
269
+ unbind(element, "blur", arguments.callee);
270
+ });
271
+
272
+ },
273
+ /**
274
+ * Returns the closest element of a particular type.
275
+ * @hide
276
+ * @param {Object} el
277
+ * @param {Object} type
278
+ */
279
+ closest: function( el, type ) {
280
+ while ( el && el.nodeName.toLowerCase() !== type.toLowerCase() ) {
281
+ el = el.parentNode;
282
+ }
283
+ return el;
284
+ },
285
+ /**
286
+ * adds jQuery like data (adds an expando) and data exists FOREVER :)
287
+ * @hide
288
+ * @param {Object} el
289
+ * @param {Object} key
290
+ * @param {Object} value
291
+ */
292
+ data: function( el, key, value ) {
293
+ var d;
294
+ if (!el[expando] ) {
295
+ el[expando] = id++;
296
+ }
297
+ if (!data[el[expando]] ) {
298
+ data[el[expando]] = {};
299
+ }
300
+ d = data[el[expando]];
301
+ if ( value ) {
302
+ data[el[expando]][key] = value;
303
+ } else {
304
+ return data[el[expando]][key];
305
+ }
306
+ },
307
+ /**
308
+ * Calls a function on the element and all parents of the element until the function returns
309
+ * false.
310
+ * @hide
311
+ * @param {Object} el
312
+ * @param {Object} func
313
+ */
314
+ onParents: function( el, func ) {
315
+ var res;
316
+ while ( el && res !== false ) {
317
+ res = func(el);
318
+ el = el.parentNode;
319
+ }
320
+ return el;
321
+ },
322
+ //regex to match focusable elements
323
+ focusable: /^(a|area|frame|iframe|label|input|select|textarea|button|html|object)$/i,
324
+ /**
325
+ * Returns if an element is focusable
326
+ * @hide
327
+ * @param {Object} elem
328
+ */
329
+ isFocusable: function( elem ) {
330
+ var attributeNode;
331
+ return (this.focusable.test(elem.nodeName) ||
332
+ ((attributeNode = elem.getAttributeNode("tabIndex"))
333
+ && attributeNode.specified)) && Syn.isVisible(elem);
334
+ },
335
+ /**
336
+ * Returns if an element is visible or not
337
+ * @hide
338
+ * @param {Object} elem
339
+ */
340
+ isVisible: function( elem ) {
341
+ return (elem.offsetWidth && elem.offsetHeight) || (elem.clientWidth && elem.clientHeight);
342
+ },
343
+ /**
344
+ * Gets the tabIndex as a number or null
345
+ * @hide
346
+ * @param {Object} elem
347
+ */
348
+ tabIndex: function( elem ) {
349
+ var attributeNode = elem.getAttributeNode("tabIndex");
350
+ return attributeNode && attributeNode.specified && (parseInt(elem.getAttribute('tabIndex')) || 0);
351
+ },
352
+ bind: bind,
353
+ unbind: unbind,
354
+ browser: browser,
355
+ //some generic helpers
356
+ helpers: {
357
+ createEventObject: createEventObject,
358
+ createBasicStandardEvent: function( type, defaults, doc ) {
359
+ var event;
360
+ try {
361
+ event = doc.createEvent("Events");
362
+ } catch (e2) {
363
+ event = doc.createEvent("UIEvents");
364
+ } finally {
365
+ event.initEvent(type, true, true);
366
+ extend(event, defaults);
367
+ }
368
+ return event;
369
+ },
370
+ inArray: function( item, array ) {
371
+ var i =0;
372
+ for ( ; i < array.length; i++ ) {
373
+ if ( array[i] === item ) {
374
+ return i;
375
+ }
376
+ }
377
+ return -1;
378
+ },
379
+ getWindow: function( element ) {
380
+ return element.ownerDocument.defaultView || element.ownerDocument.parentWindow;
381
+ },
382
+ extend: extend,
383
+ scrollOffset: function( win , set) {
384
+ var doc = win.document.documentElement,
385
+ body = win.document.body;
386
+ if(set){
387
+ window.scrollTo(set.left, set.top);
388
+
389
+ } else {
390
+ return {
391
+ left: (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0),
392
+ top: (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0)
393
+ };
394
+ }
395
+
396
+ },
397
+ scrollDimensions: function(win){
398
+ var doc = win.document.documentElement,
399
+ body = win.document.body,
400
+ docWidth = doc.clientWidth,
401
+ docHeight = doc.clientHeight,
402
+ compat = win.document.compatMode === "CSS1Compat";
403
+
404
+ return {
405
+ height: compat && docHeight ||
406
+ body.clientHeight || docHeight,
407
+ width: compat && docWidth ||
408
+ body.clientWidth || docWidth
409
+ };
410
+ },
411
+ addOffset: function( options, el ) {
412
+ var jq = Syn.jquery(el),
413
+ off;
414
+ if ( typeof options === 'object' && options.clientX === undefined && options.clientY === undefined && options.pageX === undefined && options.pageY === undefined && jq ) {
415
+ el = jq(el);
416
+ off = el.offset();
417
+ options.pageX = off.left + el.width() / 2;
418
+ options.pageY = off.top + el.height() / 2;
419
+ }
420
+ }
421
+ },
422
+ // place for key data
423
+ key: {
424
+ ctrlKey: null,
425
+ altKey: null,
426
+ shiftKey: null,
427
+ metaKey: null
428
+ },
429
+ //triggers an event on an element, returns true if default events should be run
430
+ /**
431
+ * Dispatches an event and returns true if default events should be run.
432
+ * @hide
433
+ * @param {Object} event
434
+ * @param {Object} element
435
+ * @param {Object} type
436
+ * @param {Object} autoPrevent
437
+ */
438
+ dispatch: function( event, element, type, autoPrevent ) {
439
+
440
+ // dispatchEvent doesn't always work in IE (mostly in a popup)
441
+ if ( element.dispatchEvent && event ) {
442
+ var preventDefault = event.preventDefault,
443
+ prevents = autoPrevent ? -1 : 0;
444
+
445
+ //automatically prevents the default behavior for this event
446
+ //this is to protect agianst nasty browser freezing bug in safari
447
+ if ( autoPrevent ) {
448
+ bind(element, type, function( ev ) {
449
+ ev.preventDefault();
450
+ unbind(this, type, arguments.callee);
451
+ });
452
+ }
453
+
454
+
455
+ event.preventDefault = function() {
456
+ prevents++;
457
+ if (++prevents > 0 ) {
458
+ preventDefault.apply(this, []);
459
+ }
460
+ };
461
+ element.dispatchEvent(event);
462
+ return prevents <= 0;
463
+ } else {
464
+ try {
465
+ window.event = event;
466
+ } catch (e) {}
467
+ //source element makes sure element is still in the document
468
+ return element.sourceIndex <= 0 || (element.fireEvent && element.fireEvent('on' + type, event));
469
+ }
470
+ },
471
+ /**
472
+ * @attribute
473
+ * @hide
474
+ * An object of eventType -> function that create that event.
475
+ */
476
+ create: {
477
+ //-------- PAGE EVENTS ---------------------
478
+ page: {
479
+ event: function( type, options, element ) {
480
+ var doc = Syn.helpers.getWindow(element).document || document,
481
+ event;
482
+ if ( doc.createEvent ) {
483
+ event = doc.createEvent("Events");
484
+
485
+ event.initEvent(type, true, true);
486
+ return event;
487
+ }
488
+ else {
489
+ try {
490
+ event = createEventObject(type, options, element);
491
+ }
492
+ catch (e) {}
493
+ return event;
494
+ }
495
+ }
496
+ },
497
+ // unique events
498
+ focus: {
499
+ event: function( type, options, element ) {
500
+ Syn.onParents(element, function( el ) {
501
+ if ( Syn.isFocusable(el) ) {
502
+ if ( el.nodeName.toLowerCase() !== 'html' ) {
503
+ el.focus();
504
+ activeElement = el;
505
+ }
506
+ else if ( activeElement ) {
507
+ // TODO: The HTML element isn't focasable in IE, but it is
508
+ // in FF. We should detect this and do a true focus instead
509
+ // of just a blur
510
+ var doc = Syn.helpers.getWindow(element).document;
511
+ if ( doc !== window.document ) {
512
+ return false;
513
+ } else if ( doc.activeElement ) {
514
+ doc.activeElement.blur();
515
+ activeElement = null;
516
+ } else {
517
+ activeElement.blur();
518
+ activeElement = null;
519
+ }
520
+
521
+
522
+ }
523
+ return false;
524
+ }
525
+ });
526
+ return true;
527
+ }
528
+ }
529
+ },
530
+ /**
531
+ * @attribute support
532
+ * Feature detected properties of a browser's event system.
533
+ * Support has the following properties:
534
+ * <ul>
535
+ * <li><code>clickChanges</code> - clicking on an option element creates a change event.</li>
536
+ * <li><code>clickSubmits</code> - clicking on a form button submits the form.</li>
537
+ * <li><code>mouseupSubmits</code> - a mouseup on a form button submits the form.</li>
538
+ * <li><code>radioClickChanges</code> - clicking a radio button changes the radio.</li>
539
+ * <li><code>focusChanges</code> - focus/blur creates a change event.</li>
540
+ * <li><code>linkHrefJS</code> - An achor's href JavaScript is run.</li>
541
+ * <li><code>mouseDownUpClicks</code> - A mousedown followed by mouseup creates a click event.</li>
542
+ * <li><code>tabKeyTabs</code> - A tab key changes tabs.</li>
543
+ * <li><code>keypressOnAnchorClicks</code> - Keying enter on an anchor triggers a click.</li>
544
+ * </ul>
545
+ */
546
+ support: {
547
+ clickChanges: false,
548
+ clickSubmits: false,
549
+ keypressSubmits: false,
550
+ mouseupSubmits: false,
551
+ radioClickChanges: false,
552
+ focusChanges: false,
553
+ linkHrefJS: false,
554
+ keyCharacters: false,
555
+ backspaceWorks: false,
556
+ mouseDownUpClicks: false,
557
+ tabKeyTabs: false,
558
+ keypressOnAnchorClicks: false,
559
+ optionClickBubbles: false,
560
+ ready: 0
561
+ },
562
+ /**
563
+ * Creates a synthetic event and dispatches it on the element.
564
+ * This will run any default actions for the element.
565
+ * Typically you want to use Syn, but if you want the return value, use this.
566
+ * @param {String} type
567
+ * @param {Object} options
568
+ * @param {HTMLElement} element
569
+ * @return {Boolean} true if default events were run, false if otherwise.
570
+ */
571
+ trigger: function( type, options, element ) {
572
+ options || (options = {});
573
+
574
+ var create = Syn.create,
575
+ setup = create[type] && create[type].setup,
576
+ kind = key.test(type) ? 'key' : (page.test(type) ? "page" : "mouse"),
577
+ createType = create[type] || {},
578
+ createKind = create[kind],
579
+ event, ret, autoPrevent, dispatchEl = element;
580
+
581
+ //any setup code?
582
+ Syn.support.ready === 2 && setup && setup(type, options, element);
583
+
584
+ autoPrevent = options._autoPrevent;
585
+ //get kind
586
+ delete options._autoPrevent;
587
+
588
+ if ( createType.event ) {
589
+ ret = createType.event(type, options, element);
590
+ } else {
591
+ //convert options
592
+ options = createKind.options ? createKind.options(type, options, element) : options;
593
+
594
+ if (!Syn.support.changeBubbles && /option/i.test(element.nodeName) ) {
595
+ dispatchEl = element.parentNode; //jQuery expects clicks on select
596
+ }
597
+
598
+ //create the event
599
+ event = createKind.event(type, options, dispatchEl);
600
+
601
+ //send the event
602
+ ret = Syn.dispatch(event, dispatchEl, type, autoPrevent);
603
+ }
604
+
605
+ //run default behavior
606
+ ret && Syn.support.ready === 2 && Syn.defaults[type] && Syn.defaults[type].call(element, options, autoPrevent);
607
+ return ret;
608
+ },
609
+ eventSupported: function( eventName ) {
610
+ var el = document.createElement("div");
611
+ eventName = "on" + eventName;
612
+
613
+ var isSupported = (eventName in el);
614
+ if (!isSupported ) {
615
+ el.setAttribute(eventName, "return;");
616
+ isSupported = typeof el[eventName] === "function";
617
+ }
618
+ el = null;
619
+
620
+ return isSupported;
621
+ }
622
+
623
+ });
624
+ /**
625
+ * @Prototype
626
+ */
627
+ extend(Syn.init.prototype, {
628
+ /**
629
+ * @function then
630
+ * <p>
631
+ * Then is used to chain a sequence of actions to be run one after the other.
632
+ * This is useful when many asynchronous actions need to be performed before some
633
+ * final check needs to be made.
634
+ * </p>
635
+ * <p>The following clicks and types into the <code>id='age'</code> element and then checks that only numeric characters can be entered.</p>
636
+ * <h3>Example</h3>
637
+ * @codestart
638
+ * Syn('click',{},'age')
639
+ * .then('type','I am 12',function(){
640
+ * equals($('#age').val(),"12")
641
+ * })
642
+ * @codeend
643
+ * If the element argument is undefined, then the last element is used.
644
+ *
645
+ * @param {String} type The type of event or action to create: "_click", "_dblclick", "_drag", "_type".
646
+ * @param {Object} options Optiosn to pass to the event.
647
+ * @param {String|HTMLElement} [element] A element's id or an element. If undefined, defaults to the previous element.
648
+ * @param {Function} [callback] A function to callback after the action has run, but before any future chained actions are run.
649
+ */
650
+ then: function( type, options, element, callback ) {
651
+ if ( Syn.autoDelay ) {
652
+ this.delay();
653
+ }
654
+ var args = Syn.args(options, element, callback),
655
+ self = this;
656
+
657
+
658
+ //if stack is empty run right away
659
+ //otherwise ... unshift it
660
+ this.queue.unshift(function( el, prevented ) {
661
+
662
+ if ( typeof this[type] === "function" ) {
663
+ this.element = args.element || el;
664
+ this[type](args.options, this.element, function( defaults, el ) {
665
+ args.callback && args.callback.apply(self, arguments);
666
+ self.done.apply(self, arguments);
667
+ });
668
+ } else {
669
+ this.result = Syn.trigger(type, args.options, args.element);
670
+ args.callback && args.callback.call(this, args.element, this.result);
671
+ return this;
672
+ }
673
+ })
674
+ return this;
675
+ },
676
+ /**
677
+ * Delays the next command a set timeout.
678
+ * @param {Number} [timeout]
679
+ * @param {Function} [callback]
680
+ */
681
+ delay: function( timeout, callback ) {
682
+ if ( typeof timeout === 'function' ) {
683
+ callback = timeout;
684
+ timeout = null;
685
+ }
686
+ timeout = timeout || 600;
687
+ var self = this;
688
+ this.queue.unshift(function() {
689
+ setTimeout(function() {
690
+ callback && callback.apply(self, [])
691
+ self.done.apply(self, arguments);
692
+ }, timeout);
693
+ });
694
+ return this;
695
+ },
696
+ done: function( defaults, el ) {
697
+ el && (this.element = el);
698
+ if ( this.queue.length ) {
699
+ this.queue.pop().call(this, this.element, defaults);
700
+ }
701
+
702
+ },
703
+ /**
704
+ * @function click
705
+ * Clicks an element by triggering a mousedown,
706
+ * mouseup,
707
+ * and a click event.
708
+ * <h3>Example</h3>
709
+ * @codestart
710
+ * Syn.click({},'create',function(){
711
+ * //check something
712
+ * })
713
+ * @codeend
714
+ * You can also provide the coordinates of the click.
715
+ * If jQuery is present, it will set clientX and clientY
716
+ * for you. Here's how to set it yourself:
717
+ * @codestart
718
+ * Syn.click(
719
+ * {clientX: 20, clientY: 100},
720
+ * 'create',
721
+ * function(){
722
+ * //check something
723
+ * })
724
+ * @codeend
725
+ * You can also provide pageX and pageY and Syn will convert it for you.
726
+ * @param {Object} options
727
+ * @param {HTMLElement} element
728
+ * @param {Function} callback
729
+ */
730
+ "_click": function( options, element, callback, force ) {
731
+ Syn.helpers.addOffset(options, element);
732
+ Syn.trigger("mousedown", options, element);
733
+
734
+ //timeout is b/c IE is stupid and won't call focus handlers
735
+ setTimeout(function() {
736
+ Syn.trigger("mouseup", options, element);
737
+ if (!Syn.support.mouseDownUpClicks || force ) {
738
+ Syn.trigger("click", options, element);
739
+ callback(true);
740
+ } else {
741
+ //we still have to run the default (presumably)
742
+ Syn.create.click.setup('click', options, element);
743
+ Syn.defaults.click.call(element);
744
+ //must give time for callback
745
+ setTimeout(function() {
746
+ callback(true);
747
+ }, 1);
748
+ }
749
+
750
+ }, 1);
751
+ },
752
+ /**
753
+ * Right clicks in browsers that support it (everyone but opera).
754
+ * @param {Object} options
755
+ * @param {Object} element
756
+ * @param {Object} callback
757
+ */
758
+ "_rightClick": function( options, element, callback ) {
759
+ Syn.helpers.addOffset(options, element);
760
+ var mouseopts = extend(extend({}, Syn.mouse.browser.right.mouseup), options);
761
+
762
+ Syn.trigger("mousedown", mouseopts, element);
763
+
764
+ //timeout is b/c IE is stupid and won't call focus handlers
765
+ setTimeout(function() {
766
+ Syn.trigger("mouseup", mouseopts, element);
767
+ if ( Syn.mouse.browser.right.contextmenu ) {
768
+ Syn.trigger("contextmenu", extend(extend({}, Syn.mouse.browser.right.contextmenu), options), element);
769
+ }
770
+ callback(true);
771
+ }, 1);
772
+ },
773
+ /**
774
+ * @function dblclick
775
+ * Dblclicks an element. This runs two [Syn.prototype.click click] events followed by
776
+ * a dblclick on the element.
777
+ * <h3>Example</h3>
778
+ * @codestart
779
+ * Syn.dblclick({},'open')
780
+ * @codeend
781
+ * @param {Object} options
782
+ * @param {HTMLElement} element
783
+ * @param {Function} callback
784
+ */
785
+ "_dblclick": function( options, element, callback ) {
786
+ Syn.helpers.addOffset(options, element);
787
+ var self = this;
788
+ this._click(options, element, function() {
789
+ setTimeout(function() {
790
+ self._click(options, element, function() {
791
+ Syn.trigger("dblclick", options, element);
792
+ callback(true);
793
+ }, true);
794
+ }, 2);
795
+
796
+ });
797
+ }
798
+ });
799
+
800
+ var actions = ["click", "dblclick", "move", "drag", "key", "type", 'rightClick'],
801
+ makeAction = function( name ) {
802
+ Syn[name] = function( options, element, callback ) {
803
+ return Syn("_" + name, options, element, callback);
804
+ };
805
+ Syn.init.prototype[name] = function( options, element, callback ) {
806
+ return this.then("_" + name, options, element, callback);
807
+ };
808
+ },
809
+ i = 0;
810
+ for ( ; i < actions.length; i++ ) {
811
+ makeAction(actions[i]);
812
+ }
813
+ /**
814
+ * Used for creating and dispatching synthetic events.
815
+ * @codestart
816
+ * new MVC.Syn('click').send(MVC.$E('id'))
817
+ * @codeend
818
+ * @constructor Sets up a synthetic event.
819
+ * @param {String} type type of event, ex: 'click'
820
+ * @param {optional:Object} options
821
+ */
822
+ if ( window.jQuery || (window.FuncUnit && window.FuncUnit.jquery) ) {
823
+ ((window.FuncUnit && window.FuncUnit.jquery) || window.jQuery).fn.triggerSyn = function( type, options, callback ) {
824
+ Syn(type, options, this[0], callback);
825
+ return this;
826
+ };
827
+ }
828
+
829
+ window.Syn = Syn;
830
+ })