terminus 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,36 +2,87 @@
2
2
 
3
3
  * http://github.com/jcoglan/terminus
4
4
 
5
- Terminus is a distributed JavaScript terminal. It lets you connect
6
- web browsers you have open to a command-line console and run code
7
- on all connected browsers at once. It's currently a toy, but it
8
- might turn into a useful debugging and testing tool. We'll see.
5
+ Terminus is an experimental Capybara driver implemented in client-side
6
+ JavaScript. It lets you script your application in any browser on any
7
+ device, without needing browser plugins. This allows several types of
8
+ testing to be automated:
9
9
 
10
+ * Cross-browser testing
11
+ * Multi-browser interaction e.g. messaging apps
12
+ * Run tests on remote machines, phones, iPads etc.
10
13
 
11
- == Installation
12
-
13
- sudo gem install terminus
14
+ It is also a remote scripting tool, giving you a REPL that lets you
15
+ run JavaScript in any number of browsers at once.
14
16
 
15
17
 
16
18
  == Usage
17
19
 
18
- In your terminal, run the +terminus+ command. You can optionally
19
- specify a port for it to open up like this:
20
+ Terminus is a Capybara driver. For the most part, you will not use
21
+ it directly: you will use the Capybara API and it will send instructions
22
+ to Terminus for execution. To set Terminus as your driver:
23
+
24
+ require 'capybara'
25
+ require 'terminus'
26
+
27
+ Capybara.current_driver = :terminus
28
+
29
+ Terminus does require some extra setup before you can use it to control
30
+ your app. First up, you need to start the Terminus server on the machine
31
+ where your application will be running:
32
+
33
+ $ terminus
34
+
35
+ This starts the server on port 7004. Now open a browser at
36
+ http://127.0.0.1:7004/. (I recommend using the IP address of the Terminus
37
+ host; Chrome has bugs that can stop WebSockets working if you use the
38
+ hostname.) This is the 'holding page'. A browser is said to be 'docked'
39
+ while it is visiting this page, meaning it is ready and waiting to run
40
+ some tests for you.
41
+
42
+ To let Terminus control your app's pages, you need to include this
43
+ just before the closing +body+ tag when in testing mode:
44
+
45
+ <!-- For example if you're using Rails -->
46
+
47
+ <% if Rails.env.test? %>
48
+ <%= Terminus.driver_script '127.0.0.1' %>
49
+ <% end %>
50
+
51
+ If the browser you're using is on a different machine to the Terminus
52
+ server, replace <tt>127.0.0.1</tt> with the Terminus machine's IP as
53
+ seen by the browser. For example if I'm running the browser in VirtualBox
54
+ and the Terminus server on the host OS, then I set the IP to <tt>10.0.2.2</tt>.
55
+ You could also use <tt>request.host</tt> to get the right IP automatically
56
+ if you're running your app and your Terminus server on the same machine.
57
+
58
+ Finally, in your tests you need to make sure there's a docked browser
59
+ and select it. In a 'before' block, run the following:
60
+
61
+ Terminus.ensure_docked_browser
62
+ Terminus.browser = :docked
63
+
64
+ After each test is finished, you need to return the browser to the
65
+ holding page to make it ready to accept new work. In an 'after' block:
66
+
67
+ Terminus.return_to_dock
20
68
 
21
- terminus --port 7004
69
+ This returns all currently connected browsers to the holding page.
22
70
 
23
- You should see something like this:
24
71
 
25
- Terminus running at http://0.0.0.0:7004
26
- >>
72
+ == Notes / to-do
27
73
 
28
- This is the Terminus prompt - it's where you type in JavaScript
29
- commands. Before you do that, though, visit http://localhost:7004
30
- in your browser. From there you can grab a bookmarklet that lets
31
- you connect any web page to the Terminus server. When you click
32
- the bookmarklet, the current page will open a connection with
33
- Terminus so that any commands you type into the shell are executed
34
- on that page.
74
+ * Support IE, which has no built-in XPath engine for querying the DOM.
75
+ I'm working on Pathology (see http://github.com/jcoglan/pathology) to
76
+ try and fix this but it's currently not fast enough.
77
+
78
+ * Allow <tt>Terminus.browser=</tt> to select browsers by name and
79
+ version so we can control multiple browsers at once.
80
+
81
+ * It's slow, especially at filling out forms. Use it to sanity-check
82
+ your cross-browser code and JavaScript, not to test your whole app.
83
+
84
+ * It can be a little brittle, and occasionally there seem to be race
85
+ conditions when running the Capybara specs.
35
86
 
36
87
 
37
88
  == License
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rubygems'
4
- require 'readline'
5
4
  require 'oyster'
6
- require File.dirname(__FILE__) + '/../lib/terminus'
5
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/terminus')
7
6
 
8
7
  spec = Oyster.spec do
9
8
  name "terminus -- controll web browsers from the command line"
10
9
  synopsis "terminus [--port PORT]"
11
10
 
12
- integer :port, :default => 7004
11
+ integer :port, :default => Terminus::DEFAULT_PORT
13
12
  end
14
13
 
15
14
  begin
@@ -17,16 +16,29 @@ begin
17
16
  app = Terminus.create(options)
18
17
 
19
18
  app.run!
20
- puts "Terminus running at http://0.0.0.0:#{options[:port]}"
19
+ puts "Terminus server running on port #{options[:port]}"
21
20
  puts "Press CTRL-C to exit"
21
+ puts ""
22
22
 
23
23
  trap("INT") { app.stop! ; exit }
24
24
 
25
- loop {
26
- script, result = Readline.readline('>> '), nil
27
- Readline::HISTORY.push(script)
28
- app.execute(script) { |r| result = r }
29
- }
25
+ begin
26
+ require 'readline'
27
+
28
+ puts "This is the Terminus console. You can type JavaScript"
29
+ puts "in here and it will be executed on all connected pages."
30
+ puts ""
31
+
32
+ loop {
33
+ script, result = Readline.readline('>> '), nil
34
+ Readline::HISTORY.push(script)
35
+ app.execute(script) { |r| result = r }
36
+ }
37
+ rescue LoadError
38
+ puts "If you install the readline library, you'll get a console"
39
+ puts "that lets you execute JavaScript in connected browsers."
40
+ puts ""
41
+ end
30
42
 
31
43
  rescue Oyster::HelpRendered
32
44
  end
@@ -0,0 +1,47 @@
1
+ require 'forwardable'
2
+
3
+ class Capybara::Driver::Terminus < Capybara::Driver::Base
4
+ def initialize(app = nil)
5
+ raise ArgumentError.new if app.nil?
6
+ @app = app
7
+ @rack_server = Capybara::Server.new(@app)
8
+ @rack_server.boot if Capybara.run_server
9
+ end
10
+
11
+ def visit(path)
12
+ browser.visit @rack_server.url(path)
13
+ end
14
+
15
+ extend Forwardable
16
+ def_delegators :browser, :body,
17
+ :current_path,
18
+ :current_url,
19
+ :evaluate_script,
20
+ :execute_script,
21
+ :find,
22
+ :reset!,
23
+ :source
24
+
25
+ def within_window(name)
26
+ current_browser = browser
27
+ Terminus.browser = {:name => name}
28
+ yield
29
+ Terminus.browser = current_browser
30
+ end
31
+
32
+ private
33
+
34
+ def browser
35
+ Terminus.ensure_browser
36
+ Terminus.browser
37
+ end
38
+ end
39
+
40
+ # Capybara 0.3.9 looks in the Capybara::Driver namespace for
41
+ # appropriate drivers. 0.4 uses this registration API instead.
42
+ if Capybara.respond_to?(:register_driver)
43
+ Capybara.register_driver :terminus do |app|
44
+ Capybara::Driver::Terminus.new(app)
45
+ end
46
+ end
47
+
@@ -0,0 +1 @@
1
+ JS=(typeof JS==='undefined')?{}:JS;JS.Package=function(a){var b=JS.Package.OrderedSet;JS.Package._5(this);this._0=a;this._2=new b();this._6=new b();this._b=new b();this._3={};this._7={}};(function(g){var m=g.OrderedSet=function(a,b){this._c=this.list=[];this._5={};if(!a)return;for(var c=0,d=a.length;c<d;c++)this.push(b?b(a[c]):a[c])};m.prototype.push=function(a){var b=(a.id!==undefined)?a.id:a,c=this._5;if(c.hasOwnProperty(b))return;c[b]=this._c.length;this._c.push(a)};g._4=this;if((this.document||{}).getElementsByTagName){var n=document.getElementsByTagName('script')[0];g._k=(n.readyState!==undefined)}var j=g.prototype;j.addDependency=function(a){this._6.push(a)};j.addSoftDependency=function(a){this._b.push(a)};j.addName=function(a){this._2.push(a);g.getFromCache(a).pkg=this};j.onload=function(a){this._d=a};j.on=function(a,b,c){if(this._7[a])return b.call(c);var d=this._3[a]=this._3[a]||[];d.push([b,c])};j.fire=function(a){if(this._7[a])return false;this._7[a]=true;var b=this._3[a];if(!b)return true;delete this._3[a];for(var c=0,d=b.length;c<d;c++)b[c][0].call(b[c][1]);return true};j.isLoaded=function(a){if(!a&&this._8!==undefined)return this._8;var b=this._2.list,c=b.length,d,f;while(c--){d=b[c];f=g.getObject(d);if(f!==undefined)continue;if(a)throw new Error('Expected package at '+this._0+' to define '+d);else return this._8=false}return this._8=true};j.load=function(){if(!this.fire('request'))return;var c=this._6.list.concat(this._b.list),d='load',f={};f[d]=this._6.list;g.when(f,function(){g.when({complete:c,load:[this]},function(){this.fire('complete')},this);var a=this,b=function(){if(a._d)a._d();a.isLoaded(true);a.fire('load')};if(this.isLoaded()){this.fire('download');return this.fire('load')}if(this._0===undefined)throw new Error('No load path found for '+this._2.list[0]);typeof this._0==='function'?this._0(b):g.Loader.loadFile(this._0,b);this.fire('download')},this)};j.toString=function(){return'Package:'+this._2.list.join(',')};g.when=function(b,c,d){var f=[],h,k,i;for(h in b){if(!b.hasOwnProperty(h))continue;k=new g.OrderedSet(b[h],function(a){return g.getByName(a)});i=k.list.length;while(i--)f.push([h,k.list[i]])}var l=i=f.length;if(l===0)return c&&c.call(d);while(i--){f[i][1].on(f[i][0],function(){l-=1;if(l===0&&c)c.call(d)});f[i][1].load()}};g._e=1;g._f={};g._g={};g._h=[];g._5=function(a){a.id=this._e;this._e+=1};g.getByPath=function(a){var b=a.toString();return this._f[b]=this._f[b]||new this(a)};g.getByName=function(a){if(typeof a!=='string')return a;var b=this.getFromCache(a);if(b.pkg)return b.pkg;var c=this._i(a);if(c)return c;var d=new this();d.addName(a);return d};g.autoload=function(a,b){this._h.push([a,b])};g._i=function(d){var f=this._h,h=f.length,k,i,l;for(k=0;k<h;k++){i=f[k];if(!i[0].test(d))continue;l=i[1].from+'/'+d.replace(/([a-z])([A-Z])/g,function(a,b,c){return b+'_'+c}).replace(/\./g,'/').toLowerCase()+'.js';pkg=new this(l);pkg.addName(d);if(l=i[1].require)pkg.addDependency(d.replace(i[0],l));return pkg}return null};g.getFromCache=function(a){return this._g[a]=this._g[a]||{}};g.getObject=function(a){var b=this.getFromCache(a);if(b.obj!==undefined)return b.obj;var c=this._4,d=a.split('.'),f;while(f=d.shift())c=c&&c[f];return this.getFromCache(a).obj=c}})(JS.Package);JS.Package.DomLoader={usable:function(){return!!JS.Package.getObject('window.document.getElementsByTagName')},__FILE__:function(){var a=document.getElementsByTagName('script');return a[a.length-1].src},loadFile:function(c,d){var f=this,h=document.createElement('script');h.type='text/javascript';h.src=c;h.onload=h.onreadystatechange=function(){var a=h.readyState,b=h.status;if(!a||a==='loaded'||a==='complete'||(a===4&&b===200)){d();h.onload=h.onreadystatechange=f._j;h=null}};if(window.console&&console.info)console.info('Loading '+c);document.getElementsByTagName('head')[0].appendChild(h)},_j:function(){}};JS.Package.CommonJSLoader={usable:function(){return typeof require==='function'&&typeof exports==='object'},setup:function(){var b=this;require=(function(a){return function(){b._1=arguments[0]+'.js';return a.apply(JS.Package._4,arguments)}})(require)},__FILE__:function(){return this._1},loadFile:function(a,b){var c=(typeof process==='object'),d=c?process.cwd():require('file').cwd(),f=a.replace(/\.[^\.]+$/g,''),h=c?require('path'):require('file');require(h.join(d,f));b()}};JS.Package.ServerLoader={usable:function(){return typeof JS.Package.getObject('load')==='function'&&typeof JS.Package.getObject('version')==='function'},setup:function(){var b=this;load=(function(a){return function(){b._1=arguments[0];return a.apply(JS.Package._4,arguments)}})(load)},__FILE__:function(){return this._1},loadFile:function(a,b){load(a);b()}};JS.Package.WshLoader={usable:function(){return!!JS.Package.getObject('ActiveXObject')&&!!JS.Package.getObject('WScript')},__FILE__:function(){return this._1},loadFile:function(a,b){this._1=a;var c=new ActiveXObject('Scripting.FileSystemObject'),d,f;try{d=c.OpenTextFile(a);f=function(){eval(d.ReadAll())};f();b()}finally{try{if(d)d.Close()}catch(e){}}}};(function(){var a=[JS.Package.DomLoader,JS.Package.CommonJSLoader,JS.Package.ServerLoader,JS.Package.WshLoader],b=a.length,c,d;for(c=0;c<b;c++){d=a[c];if(d.usable()){JS.Package.Loader=d;if(d.setup)d.setup();break}}})();JS.Package.DSL={__FILE__:function(){return JS.Package.Loader.__FILE__()},pkg:function(a,b){var c=b?JS.Package.getByPath(b):JS.Package.getByName(a);c.addName(a);return new JS.Package.Description(c)},file:function(a){var b=JS.Package.getByPath(a);return new JS.Package.Description(b)},load:function(a,b){JS.Package.Loader.loadFile(a,b)},autoload:function(a,b){JS.Package.autoload(a,b)}};JS.Package.Description=function(a){this._9=a};(function(f){f._a=function(a,b){var c=b.length,a=this._9[a],d;for(d=0;d<c;d++)a.call(this._9,b[d]);return this};f.provides=function(){return this._a('addName',arguments)};f.requires=function(){return this._a('addDependency',arguments)};f.uses=function(){return this._a('addSoftDependency',arguments)};f.setup=function(a){this._9.onload(a);return this}})(JS.Package.Description.prototype);JS.Package.DSL.loader=JS.Package.DSL.file;JS.Packages=function(a){a.call(JS.Package.DSL)};JS.require=function(){var a=[],b=0;while(typeof arguments[b]==='string'){a.push(arguments[b]);b+=1}JS.Package.when({complete:a},arguments[b],arguments[b+1])};JS.Packages(function(){with(this){var b=JS.Package._4.JSCLASS_PATH||__FILE__().replace(/[^\/]*$/g,'');if(!/\/$/.test(b))b=b+'/';var c=function(a){return file(b+a+'.js')};c('core').provides('JS.Module','JS.Class','JS.Kernel','JS.Singleton','JS.Interface');c('comparable').provides('JS.Comparable').requires('JS.Module');c('constant_scope').provides('JS.ConstantScope').requires('JS.Module');c('forwardable').provides('JS.Forwardable').requires('JS.Module');c('enumerable').provides('JS.Enumerable').requires('JS.Module','JS.Class');c('observable').provides('JS.Observable').requires('JS.Module');c('hash').provides('JS.Hash').requires('JS.Class','JS.Enumerable','JS.Comparable');c('set').provides('JS.Set','JS.HashSet','JS.SortedSet').requires('JS.Class','JS.Enumerable').uses('JS.Hash');c('linked_list').provides('JS.LinkedList','JS.LinkedList.Doubly','JS.LinkedList.Doubly.Circular').requires('JS.Class','JS.Enumerable');c('command').provides('JS.Command','JS.Command.Stack').requires('JS.Class','JS.Enumerable','JS.Observable');c('decorator').provides('JS.Decorator').requires('JS.Module','JS.Class');c('method_chain').provides('JS.MethodChain').requires('JS.Module','JS.Kernel');c('proxy').provides('JS.Proxy','JS.Proxy.Virtual').requires('JS.Module','JS.Class');c('ruby').provides('JS.Ruby').requires('JS.Class');c('stack_trace').provides('JS.StackTrace').requires('JS.Module','JS.Singleton');c('state').provides('JS.State').requires('JS.Module','JS.Class')}});
@@ -21,8 +21,13 @@ body {
21
21
  font: 13px Helvetica, Arial, sans-serif;
22
22
  }
23
23
 
24
+ .footer p {
25
+ margin: 0;
26
+ line-height: 1.5em;
27
+ }
28
+
24
29
  h1 {
25
- font: bold 64px Helvetica, Arial, sans-serif;
30
+ font: bold 64px FreeSans, Helvetica, Arial, sans-serif;
26
31
  letter-spacing: -0.03em;
27
32
  margin: 0 0 32px;
28
33
  color: #f00846;
@@ -39,6 +44,6 @@ a:hover {
39
44
  }
40
45
 
41
46
  p {
42
- margin: 0;
47
+ margin: 1.5em 0 0;
43
48
  }
44
49
 
@@ -0,0 +1,2355 @@
1
+ // funcunit/synthetic/synthetic.js
2
+
3
+ (function($){
4
+
5
+
6
+
7
+ var extend = function(d, s) { for (var p in s) d[p] = s[p]; return d;},
8
+ // only uses browser detection for key events
9
+ browser = {
10
+ msie: !!(window.attachEvent && !window.opera),
11
+ opera: !!window.opera,
12
+ webkit : navigator.userAgent.indexOf('AppleWebKit/') > -1,
13
+ safari: navigator.userAgent.indexOf('AppleWebKit/') > -1 && navigator.userAgent.indexOf('Chrome/') == -1,
14
+ gecko: navigator.userAgent.indexOf('Gecko') > -1,
15
+ mobilesafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/),
16
+ rhino : navigator.userAgent.match(/Rhino/) && true
17
+ },
18
+ createEventObject = function(type, options, element){
19
+ var event = element.ownerDocument.createEventObject();
20
+ return extend(event, options);
21
+ },
22
+ data = {},
23
+ id = 0,
24
+ expando = "_synthetic"+(new Date() - 0),
25
+ bind,
26
+ unbind,
27
+ key = /keypress|keyup|keydown/,
28
+ page = /load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll/,
29
+
30
+ /**
31
+ * @constructor Syn
32
+ * @download funcunit/dist/syn.js
33
+ * @test funcunit/synthetic/qunit.html
34
+ * Syn is used to simulate user actions. It creates synthetic events and
35
+ * performs their default behaviors.
36
+ *
37
+ * <h2>Basic Use</h2>
38
+ * The following clicks an input element with <code>id='description'</code>
39
+ * and then types <code>'Hello World'</code>.
40
+ *
41
+ @codestart
42
+ Syn.click({},'description')
43
+ .type("Hello World")
44
+ @codeend
45
+ * <h2>User Actions and Events</h2>
46
+ * <p>Syn is typically used to simulate user actions as opposed to triggering events. Typing characters
47
+ * is an example of a user action. The keypress that represents an <code>'a'</code>
48
+ * character being typed is an example of an event.
49
+ * </p>
50
+ * <p>
51
+ * While triggering events is supported, it's much more useful to simulate actual user behavior. The
52
+ * following actions are supported by Syn:
53
+ * </p>
54
+ * <ul>
55
+ * <li><code>[Syn.prototype.click click]</code> - a mousedown, focus, mouseup, and click.</li>
56
+ * <li><code>[Syn.prototype.dblclick dblclick]</code> - two <code>click!</code> events followed by a <code>dblclick</code>.</li>
57
+ * <li><code>[Syn.prototype.key key]</code> - types a single character (keydown, keypress, keyup).</li>
58
+ * <li><code>[Syn.prototype.type type]</code> - types multiple characters into an element.</li>
59
+ * <li><code>[Syn.prototype.move move]</code> - moves the mouse from one position to another (triggering mouseover / mouseouts).</li>
60
+ * <li><code>[Syn.prototype.drag drag]</code> - a mousedown, followed by mousemoves, and a mouseup.</li>
61
+ * </ul>
62
+ * All actions run asynchronously.
63
+ * Click on the links above for more
64
+ * information on how to use the specific action.
65
+ * <h2>Asynchronous Callbacks</h2>
66
+ * Actions don't complete immediately. This is almost
67
+ * entirely because <code>focus()</code>
68
+ * doesn't run immediately in IE.
69
+ * If you provide a callback function to Syn, it will
70
+ * be called after the action is completed.
71
+ * <br/>The following checks that "Hello World" was entered correctly:
72
+ @codestart
73
+ Syn.click({},'description')
74
+ .type("Hello World", function(){
75
+
76
+ ok("Hello World" == document.getElementById('description').value)
77
+ })
78
+ @codeend
79
+ <h2>Asynchronous Chaining</h2>
80
+ <p>You might have noticed the [Syn.prototype.then then] method. It provides chaining
81
+ so you can do a sequence of events with a single (final) callback.
82
+ </p><p>
83
+ If an element isn't provided to then, it uses the previous Syn's element.
84
+ </p>
85
+ The following does a lot of stuff before checking the result:
86
+ @codestart
87
+ Syn.type('ice water','title')
88
+ .type('ice and water','description')
89
+ .click({},'create')
90
+ .drag({to: 'favorites'},'newRecipe',
91
+ function(){
92
+ ok($('#newRecipe').parents('#favorites').length);
93
+ })
94
+ @codeend
95
+
96
+ <h2>jQuery Helper</h2>
97
+ If jQuery is present, Syn adds a triggerSyn helper you can use like:
98
+ @codestart
99
+ $("#description").triggerSyn("type","Hello World");
100
+ @codeend
101
+ * <h2>Key Event Recording</h2>
102
+ * <p>Every browser has very different rules for dispatching key events.
103
+ * As there is no way to feature detect how a browser handles key events,
104
+ * synthetic uses a description of how the browser behaves generated
105
+ * by a recording application. </p>
106
+ * <p>
107
+ * If you want to support a browser not currently supported, you can
108
+ * record that browser's key event description and add it to
109
+ * <code>Syn.key.browsers</code> by it's navigator agent.
110
+ * </p>
111
+ @codestart
112
+ Syn.key.browsers["Envjs\ Resig/20070309 PilotFish/1.2.0.10\1.6"] = {
113
+ 'prevent':
114
+ {"keyup":[],"keydown":["char","keypress"],"keypress":["char"]},
115
+ 'character':
116
+ { ... }
117
+ }
118
+ @codeend
119
+ * <h2>Limitations</h2>
120
+ * Syn fully supports IE 6+, FF 3+, Chrome, Safari, Opera 10+.
121
+ * With FF 1+, drag / move events are only partially supported. They will
122
+ * not trigger mouseover / mouseout events.<br/>
123
+ * Safari crashes when a mousedown is triggered on a select. Syn will not
124
+ * create this event.
125
+ * <h2>Contributing to Syn</h2>
126
+ * Have we missed something? We happily accept patches. The following are
127
+ * important objects and properties of Syn:
128
+ * <ul>
129
+ * <li><code>Syn.create</code> - contains methods to setup, convert options, and create an event of a specific type.</li>
130
+ * <li><code>Syn.defaults</code> - default behavior by event type (except for keys).</li>
131
+ * <li><code>Syn.key.defaults</code> - default behavior by key.</li>
132
+ * <li><code>Syn.keycodes</code> - supported keys you can type.</li>
133
+ * </ul>
134
+ * <h2>Roll Your Own Functional Test Framework</h2>
135
+ * <p>Syn is really the foundation of JavaScriptMVC's functional testing framework - [FuncUnit].
136
+ * But, we've purposely made Syn work without any dependencies in the hopes that other frameworks or
137
+ * testing solutions can use it as well.
138
+ * </p>
139
+ * @init
140
+ * Creates a synthetic event on the element.
141
+ * @param {Object} type
142
+ * @param {Object} options
143
+ * @param {Object} element
144
+ * @param {Object} callback
145
+ * @return Syn
146
+ */
147
+ Syn = function(type, options, element, callback){
148
+ return ( new Syn.init(type, options, element, callback) )
149
+ }
150
+
151
+ if(window.addEventListener){ // Mozilla, Netscape, Firefox
152
+ bind = function(el, ev, f){
153
+ el.addEventListener(ev, f, false)
154
+ }
155
+ unbind = function(el, ev, f){
156
+ el.removeEventListener(ev, f, false)
157
+ }
158
+ }else{
159
+ bind = function(el, ev, f){
160
+ el.attachEvent("on"+ev, f)
161
+ }
162
+ unbind = function(el, ev, f){
163
+ el.detachEvent("on"+ev, f)
164
+ }
165
+ }
166
+ /**
167
+ * @Static
168
+ */
169
+ extend(Syn,{
170
+ /**
171
+ * Creates a new synthetic event instance
172
+ * @hide
173
+ * @param {Object} type
174
+ * @param {Object} options
175
+ * @param {Object} element
176
+ * @param {Object} callback
177
+ */
178
+ init : function(type, options, element, callback){
179
+ var args = Syn.args(options,element, callback),
180
+ self = this;
181
+ this.queue = [];
182
+ this.element = args.element;
183
+
184
+ //run event
185
+ if(typeof this[type] == "function") {
186
+ this[type](args.options, args.element, function(defaults,el ){
187
+ args.callback && args.callback.apply(self, arguments);
188
+ self.done.apply(self, arguments)
189
+ })
190
+ }else{
191
+ this.result = Syn.trigger(type, args.options, args.element);
192
+ args.callback && args.callback.call(this, args.element, this.result);
193
+ }
194
+ },
195
+ jquery : function(el, fast){
196
+ if(window.FuncUnit && window.FuncUnit.jquery){
197
+ return window.FuncUnit.jquery
198
+ } if (el){
199
+ return Syn.helpers.getWindow(el).jQuery || window.jQuery
200
+ }
201
+ else{
202
+ return window.jQuery
203
+ }
204
+ },
205
+ /**
206
+ * Returns an object with the args for a Syn.
207
+ * @hide
208
+ * @return {Object}
209
+ */
210
+ args : function(){
211
+ var res = {}
212
+ for(var i=0; i < arguments.length; i++){
213
+ if(typeof arguments[i] == 'function'){
214
+ res.callback = arguments[i]
215
+ }else if(arguments[i] && arguments[i].jquery){
216
+ res.element = arguments[i][0];
217
+ }else if(arguments[i] && arguments[i].nodeName){
218
+ res.element = arguments[i];
219
+ }else if(res.options && typeof arguments[i] == 'string'){ //we can get by id
220
+ res.element = document.getElementById(arguments[i])
221
+ }
222
+ else if(arguments[i]){
223
+ res.options = arguments[i];
224
+ }
225
+ }
226
+ return res;
227
+ },
228
+ click : function( options, element, callback){
229
+ Syn('click!',options,element, callback);
230
+ },
231
+ /**
232
+ * @attribute defaults
233
+ * Default actions for events. Each default function is called with this as its
234
+ * element. It should return true if a timeout
235
+ * should happen after it. If it returns an element, a timeout will happen
236
+ * and the next event will happen on that element.
237
+ */
238
+ defaults : {
239
+ focus : function(){
240
+ if(!Syn.support.focusChanges){
241
+ var element = this,
242
+ nodeName = element.nodeName.toLowerCase();
243
+ Syn.data(element,"syntheticvalue", element.value)
244
+
245
+ if(nodeName == "input"){
246
+
247
+ bind(element, "blur", function(){
248
+
249
+ if( Syn.data(element,"syntheticvalue") != element.value){
250
+
251
+ Syn.trigger("change", {}, element);
252
+ }
253
+ unbind(element,"blur", arguments.callee)
254
+ })
255
+
256
+ }
257
+ }
258
+ },
259
+ submit : function(){
260
+ Syn.onParents(this, function(el){
261
+ if( el.nodeName.toLowerCase() == 'form'){
262
+ el.submit()
263
+ return false;
264
+ }
265
+ });
266
+ }
267
+ },
268
+ changeOnBlur : function(element, prop, value){
269
+
270
+ bind(element, "blur", function(){
271
+ if( value != element[prop]){
272
+ Syn.trigger("change", {}, element);
273
+ }
274
+ unbind(element,"blur", arguments.callee)
275
+ })
276
+
277
+ },
278
+ /**
279
+ * Returns the closest element of a particular type.
280
+ * @hide
281
+ * @param {Object} el
282
+ * @param {Object} type
283
+ */
284
+ closest : function(el, type){
285
+ while(el && el.nodeName.toLowerCase() != type.toLowerCase()){
286
+ el = el.parentNode
287
+ }
288
+ return el;
289
+ },
290
+ /**
291
+ * adds jQuery like data (adds an expando) and data exists FOREVER :)
292
+ * @hide
293
+ * @param {Object} el
294
+ * @param {Object} key
295
+ * @param {Object} value
296
+ */
297
+ data : function(el, key, value){
298
+ var d;
299
+ if(!el[expando]){
300
+ el[expando] = id++;
301
+ }
302
+ if(!data[el[expando]]){
303
+ data[el[expando]] = {};
304
+ }
305
+ d = data[el[expando]]
306
+ if(value){
307
+ data[el[expando]][key] = value;
308
+ }else{
309
+ return data[el[expando]][key];
310
+ }
311
+ },
312
+ /**
313
+ * Calls a function on the element and all parents of the element until the function returns
314
+ * false.
315
+ * @hide
316
+ * @param {Object} el
317
+ * @param {Object} func
318
+ */
319
+ onParents : function(el, func){
320
+ var res;
321
+ while(el && res !== false){
322
+ res = func(el)
323
+ el = el.parentNode
324
+ }
325
+ return el;
326
+ },
327
+ //regex to match focusable elements
328
+ focusable : /^(a|area|frame|iframe|label|input|select|textarea|button|html|object)$/i,
329
+ /**
330
+ * Returns if an element is focusable
331
+ * @hide
332
+ * @param {Object} elem
333
+ */
334
+ isFocusable : function(elem){
335
+ var attributeNode;
336
+ return ( this.focusable.test(elem.nodeName) || (
337
+ (attributeNode = elem.getAttributeNode( "tabIndex" )) && attributeNode.specified ) )
338
+ && Syn.isVisible(elem)
339
+ },
340
+ /**
341
+ * Returns if an element is visible or not
342
+ * @hide
343
+ * @param {Object} elem
344
+ */
345
+ isVisible : function(elem){
346
+ return (elem.offsetWidth && elem.offsetHeight) || (elem.clientWidth && elem.clientHeight)
347
+ },
348
+ /**
349
+ * Gets the tabIndex as a number or null
350
+ * @hide
351
+ * @param {Object} elem
352
+ */
353
+ tabIndex : function(elem){
354
+ var attributeNode = elem.getAttributeNode( "tabIndex" );
355
+ return attributeNode && attributeNode.specified && ( parseInt( elem.getAttribute('tabIndex') ) || 0 )
356
+ },
357
+ bind : bind,
358
+ unbind : unbind,
359
+ browser: browser,
360
+ //some generic helpers
361
+ helpers : {
362
+ createEventObject : createEventObject,
363
+ createBasicStandardEvent : function(type, defaults){
364
+ var event;
365
+ try {
366
+ event = document.createEvent("Events");
367
+ } catch(e2) {
368
+ event = document.createEvent("UIEvents");
369
+ } finally {
370
+ event.initEvent(type, true, true);
371
+ extend(event, defaults);
372
+ }
373
+ return event;
374
+ },
375
+ inArray : function(item, array){
376
+ for(var i =0; i < array.length; i++){
377
+ if(array[i] == item){
378
+ return i;
379
+ }
380
+ }
381
+ return -1;
382
+ },
383
+ getWindow : function(element){
384
+ return element.ownerDocument.defaultView || element.ownerDocument.parentWindow
385
+ },
386
+ extend: extend,
387
+ scrollOffset : function(win){
388
+ var doc = win.document.documentElement,
389
+ body = win.document.body;
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
+ addOffset : function(options, el){
397
+ var jq = Syn.jquery(el)
398
+ if(typeof options == 'object' &&
399
+ options.clientX === undefined &&
400
+ options.clientY === undefined &&
401
+ options.pageX === undefined &&
402
+ options.pageY === undefined && jq){
403
+ var el = jq(el)
404
+ off = el.offset();
405
+ options.pageX = off.left + el.width() /2 ;
406
+ options.pageY = off.top + el.height() /2 ;
407
+ }
408
+ }
409
+ },
410
+ // place for key data
411
+ key : {
412
+ ctrlKey : null,
413
+ altKey : null,
414
+ shiftKey : null,
415
+ metaKey : null
416
+ },
417
+ //triggers an event on an element, returns true if default events should be run
418
+ /**
419
+ * Dispatches an event and returns true if default events should be run.
420
+ * @hide
421
+ * @param {Object} event
422
+ * @param {Object} element
423
+ * @param {Object} type
424
+ * @param {Object} autoPrevent
425
+ */
426
+ dispatch : (document.documentElement.dispatchEvent ?
427
+ function(event, element, type, autoPrevent){
428
+ var preventDefault = event.preventDefault,
429
+ prevents = autoPrevent ? -1 : 0;
430
+
431
+ //automatically prevents the default behavior for this event
432
+ //this is to protect agianst nasty browser freezing bug in safari
433
+ if(autoPrevent){
434
+ bind(element, type, function(ev){
435
+ ev.preventDefault()
436
+ unbind(this, type, arguments.callee)
437
+ })
438
+ }
439
+
440
+
441
+ event.preventDefault = function(){
442
+ prevents++;
443
+ if(++prevents > 0){
444
+ preventDefault.apply(this,[]);
445
+ }
446
+ }
447
+ element.dispatchEvent(event)
448
+ return prevents <= 0;
449
+ } :
450
+ function(event, element, type){
451
+ try {window.event = event;}catch(e) {}
452
+ //source element makes sure element is still in the document
453
+ return element.sourceIndex <= 0 || element.fireEvent('on'+type, event)
454
+ }
455
+ ),
456
+ /**
457
+ * @attribute
458
+ * @hide
459
+ * An object of eventType -> function that create that event.
460
+ */
461
+ create : {
462
+ //-------- PAGE EVENTS ---------------------
463
+ page : {
464
+ event : document.createEvent ? function(type, options, element){
465
+ var event = element.ownerDocument.createEvent("Events");
466
+ event.initEvent(type, true, true );
467
+ return event;
468
+ } : createEventObject
469
+ },
470
+ // unique events
471
+ focus : {
472
+ event : function(type, options, element){
473
+ Syn.onParents(element, function(el){
474
+ if( Syn.isFocusable(el)){
475
+ if(el.nodeName.toLowerCase() != 'html'){
476
+ el.focus();
477
+ }
478
+ return false
479
+ }
480
+ });
481
+ return true;
482
+ }
483
+ }
484
+ },
485
+ /**
486
+ * @attribute support
487
+ * Feature detected properties of a browser's event system.
488
+ * Support has the following properties:
489
+ * <ul>
490
+ * <li><code>clickChanges</code> - clicking on an option element creates a change event.</li>
491
+ * <li><code>clickSubmits</code> - clicking on a form button submits the form.</li>
492
+ * <li><code>mouseupSubmits</code> - a mouseup on a form button submits the form.</li>
493
+ * <li><code>radioClickChanges</code> - clicking a radio button changes the radio.</li>
494
+ * <li><code>focusChanges</code> - focus/blur creates a change event.</li>
495
+ * <li><code>linkHrefJS</code> - An achor's href JavaScript is run.</li>
496
+ * <li><code>mouseDownUpClicks</code> - A mousedown followed by mouseup creates a click event.</li>
497
+ * <li><code>tabKeyTabs</code> - A tab key changes tabs.</li>
498
+ * <li><code>keypressOnAnchorClicks</code> - Keying enter on an anchor triggers a click.</li>
499
+ * </ul>
500
+ */
501
+ support : {
502
+ clickChanges : false,
503
+ clickSubmits : false,
504
+ keypressSubmits : false,
505
+ mouseupSubmits: false,
506
+ radioClickChanges : false,
507
+ focusChanges : false,
508
+ linkHrefJS : false,
509
+ keyCharacters : false,
510
+ backspaceWorks : false,
511
+ mouseDownUpClicks : false,
512
+ tabKeyTabs : false,
513
+ keypressOnAnchorClicks : false,
514
+ optionClickBubbles : false
515
+ },
516
+ /**
517
+ * Creates a synthetic event and dispatches it on the element.
518
+ * This will run any default actions for the element.
519
+ * Typically you want to use Syn, but if you want the return value, use this.
520
+ * @param {String} type
521
+ * @param {Object} options
522
+ * @param {HTMLElement} element
523
+ * @return {Boolean} true if default events were run, false if otherwise.
524
+ */
525
+ trigger : function(type, options, element){
526
+ options || (options = {});
527
+
528
+ var create = Syn.create,
529
+ setup = create[type] && create[type].setup,
530
+ kind = key.test(type) ?
531
+ 'key' :
532
+ ( page.test(type) ?
533
+ "page" : "mouse" ),
534
+ createType = create[type] || {},
535
+ createKind = create[kind],
536
+ event,
537
+ ret,
538
+ autoPrevent = options._autoPrevent,
539
+ dispatchEl = element;
540
+
541
+ //any setup code?
542
+ Syn.support.ready && setup && setup(type, options, element);
543
+
544
+
545
+ //get kind
546
+
547
+ delete options._autoPrevent;
548
+
549
+ if(createType.event){
550
+ ret = createType.event(type, options, element)
551
+ }else{
552
+ //convert options
553
+ options = createKind.options ? createKind.options(type,options,element) : options;
554
+
555
+ if(!Syn.support.changeBubbles && /option/i.test(element.nodeName)){
556
+ dispatchEl = element.parentNode; //jQuery expects clicks on select
557
+ }
558
+
559
+ //create the event
560
+ event = createKind.event(type,options,dispatchEl)
561
+
562
+ //send the event
563
+ ret = Syn.dispatch(event, dispatchEl, type, autoPrevent)
564
+ }
565
+
566
+ //run default behavior
567
+ ret && Syn.support.ready
568
+ && Syn.defaults[type]
569
+ && Syn.defaults[type].call(element, options, autoPrevent);
570
+ return ret;
571
+ },
572
+ eventSupported: function( eventName ) {
573
+ var el = document.createElement("div");
574
+ eventName = "on" + eventName;
575
+
576
+ var isSupported = (eventName in el);
577
+ if ( !isSupported ) {
578
+ el.setAttribute(eventName, "return;");
579
+ isSupported = typeof el[eventName] === "function";
580
+ }
581
+ el = null;
582
+
583
+ return isSupported;
584
+ }
585
+
586
+ });
587
+ var h = Syn.helpers;
588
+ /**
589
+ * @Prototype
590
+ */
591
+ extend(Syn.init.prototype,{
592
+ /**
593
+ * @function then
594
+ * <p>
595
+ * Then is used to chain a sequence of actions to be run one after the other.
596
+ * This is useful when many asynchronous actions need to be performed before some
597
+ * final check needs to be made.
598
+ * </p>
599
+ * <p>The following clicks and types into the <code>id='age'</code> element and then checks that only numeric characters can be entered.</p>
600
+ * <h3>Example</h3>
601
+ * @codestart
602
+ * Syn('click',{},'age')
603
+ * .then('type','I am 12',function(){
604
+ * equals($('#age').val(),"12")
605
+ * })
606
+ * @codeend
607
+ * If the element argument is undefined, then the last element is used.
608
+ *
609
+ * @param {String} type The type of event or action to create: "_click", "_dblclick", "_drag", "_type".
610
+ * @param {Object} options Optiosn to pass to the event.
611
+ * @param {String|HTMLElement} [element] A element's id or an element. If undefined, defaults to the previous element.
612
+ * @param {Function} [callback] A function to callback after the action has run, but before any future chained actions are run.
613
+ */
614
+ then : function(type, options, element, callback){
615
+ if(Syn.autoDelay){
616
+ this.delay();
617
+ }
618
+ var args = Syn.args(options,element, callback),
619
+ self = this;
620
+
621
+
622
+ //if stack is empty run right away
623
+
624
+ //otherwise ... unshift it
625
+ this.queue.unshift(function(el, prevented){
626
+
627
+ if(typeof this[type] == "function") {
628
+ this.element = args.element || el;
629
+ this[type](args.options, this.element, function(defaults, el){
630
+ args.callback && args.callback.apply(self, arguments);
631
+ self.done.apply(self, arguments)
632
+ })
633
+ }else{
634
+ this.result = Syn.trigger(type, args.options, args.element);
635
+ args.callback && args.callback.call(this, args.element, this.result);
636
+ return this;
637
+ }
638
+ })
639
+ return this;
640
+ },
641
+ /**
642
+ * Delays the next command a set timeout.
643
+ * @param {Number} [timeout]
644
+ * @param {Function} [callback]
645
+ */
646
+ delay : function(timeout, callback){
647
+ if(typeof timeout == 'function'){
648
+ callback = timeout;
649
+ timeout = null;
650
+ }
651
+ timeout = timeout || 600
652
+ var self = this;
653
+ this.queue.unshift(function(){
654
+ setTimeout(function(){
655
+ callback && callback.apply(self,[])
656
+ self.done.apply(self, arguments)
657
+ },timeout)
658
+ })
659
+ return this;
660
+ },
661
+ done : function( defaults, el){
662
+ el && (this.element = el);;
663
+ if(this.queue.length){
664
+ this.queue.pop().call(this, this.element, defaults);
665
+ }
666
+
667
+ },
668
+ /**
669
+ * @function click
670
+ * Clicks an element by triggering a mousedown,
671
+ * mouseup,
672
+ * and a click event.
673
+ * <h3>Example</h3>
674
+ * @codestart
675
+ * Syn.click({},'create',function(){
676
+ * //check something
677
+ * })
678
+ * @codeend
679
+ * You can also provide the coordinates of the click.
680
+ * If jQuery is present, it will set clientX and clientY
681
+ * for you. Here's how to set it yourself:
682
+ * @codestart
683
+ * Syn.click(
684
+ * {clientX: 20, clientY: 100},
685
+ * 'create',
686
+ * function(){
687
+ * //check something
688
+ * })
689
+ * @codeend
690
+ * You can also provide pageX and pageY and Syn will convert it for you.
691
+ * @param {Object} options
692
+ * @param {HTMLElement} element
693
+ * @param {Function} callback
694
+ */
695
+ "_click" : function(options, element, callback, force){
696
+ Syn.helpers.addOffset(options, element);
697
+ Syn.trigger("mousedown", options, element);
698
+
699
+ //timeout is b/c IE is stupid and won't call focus handlers
700
+ setTimeout(function(){
701
+ Syn.trigger("mouseup", options, element)
702
+ if(!Syn.support.mouseDownUpClicks || force){
703
+ Syn.trigger("click", options, element)
704
+ callback(true)
705
+ }else{
706
+ //we still have to run the default (presumably)
707
+ Syn.create.click.setup('click',options,element)
708
+ Syn.defaults.click.call(element)
709
+ //must give time for callback
710
+ setTimeout(function(){
711
+ callback(true)
712
+ },1)
713
+ }
714
+
715
+ },1)
716
+ },
717
+ /**
718
+ * Right clicks in browsers that support it (everyone but opera).
719
+ * @param {Object} options
720
+ * @param {Object} element
721
+ * @param {Object} callback
722
+ */
723
+ "_rightClick" : function(options, element, callback){
724
+ Syn.helpers.addOffset(options, element);
725
+ var mouseopts = extend( extend({},Syn.mouse.browser.right.mouseup ), options)
726
+
727
+ Syn.trigger("mousedown", mouseopts, element);
728
+
729
+ //timeout is b/c IE is stupid and won't call focus handlers
730
+ setTimeout(function(){
731
+ Syn.trigger("mouseup", mouseopts, element)
732
+ if (Syn.mouse.browser.contextmenu) {
733
+ Syn.trigger("contextmenu",
734
+ extend( extend({},Syn.mouse.browser.right.contextmenu ), options),
735
+ element)
736
+ }
737
+ callback(true)
738
+ },1)
739
+ },
740
+ /**
741
+ * @function dblclick
742
+ * Dblclicks an element. This runs two [Syn.prototype.click click] events followed by
743
+ * a dblclick on the element.
744
+ * <h3>Example</h3>
745
+ * @codestart
746
+ * Syn.dblclick({},'open')
747
+ * @codeend
748
+ * @param {Object} options
749
+ * @param {HTMLElement} element
750
+ * @param {Function} callback
751
+ */
752
+ "_dblclick" : function(options, element, callback){
753
+ Syn.helpers.addOffset(options, element);
754
+ var self = this;
755
+ this._click(options, element, function(){
756
+ setTimeout(function(){
757
+ self._click(options, element, function(){
758
+ Syn.trigger("dblclick", options, element)
759
+ callback(true)
760
+ },true)
761
+ },2)
762
+
763
+ })
764
+ }
765
+ })
766
+
767
+ var actions = ["click","dblclick","move","drag","key","type",'rightClick'],
768
+ makeAction = function(name){
769
+ Syn[name] = function(options, element, callback){
770
+ return Syn("_"+name, options, element, callback)
771
+ }
772
+ Syn.init.prototype[name] = function(options, element, callback){
773
+ return this.then("_"+name, options, element, callback)
774
+ }
775
+ }
776
+ for(var i=0; i < actions.length; i++){
777
+ makeAction(actions[i]);
778
+ }
779
+ /**
780
+ * Used for creating and dispatching synthetic events.
781
+ * @codestart
782
+ * new MVC.Syn('click').send(MVC.$E('id'))
783
+ * @codeend
784
+ * @init Sets up a synthetic event.
785
+ * @param {String} type type of event, ex: 'click'
786
+ * @param {optional:Object} options
787
+ */
788
+
789
+ if (window.jQuery || (window.FuncUnit && window.FuncUnit.jquery)) {
790
+ (window.jQuery || window.FuncUnit.jquery).fn.triggerSyn = function(type, options, callback){
791
+ Syn(type, options, this[0], callback)
792
+ return this;
793
+ };
794
+ }
795
+
796
+ window.Syn = Syn;
797
+
798
+
799
+ })();
800
+
801
+ // funcunit/synthetic/mouse.js
802
+
803
+ (function($){
804
+
805
+
806
+ var h = Syn.helpers;
807
+
808
+ Syn.mouse = {};
809
+ h.extend(Syn.defaults,{
810
+ mousedown : function(options){
811
+ Syn.trigger("focus", {}, this)
812
+ },
813
+ click : function(){
814
+ // prevents the access denied issue in IE if the click causes the element to be destroyed
815
+ var element = this;
816
+ try {
817
+ element.nodeType;
818
+ } catch(e){
819
+ return;
820
+ }
821
+ //get old values
822
+ var href,
823
+ checked = Syn.data(element,"checked"),
824
+ scope = Syn.helpers.getWindow(element),
825
+ nodeName = element.nodeName.toLowerCase();
826
+
827
+ if( (href = Syn.data(element,"href") ) ){
828
+ element.setAttribute('href',href)
829
+ }
830
+
831
+
832
+
833
+ //run href javascript
834
+ if(!Syn.support.linkHrefJS
835
+ && /^\s*javascript:/.test(element.href)){
836
+ //eval js
837
+ var code = element.href.replace(/^\s*javascript:/,"")
838
+
839
+ //try{
840
+ if (code != "//" && code.indexOf("void(0)") == -1) {
841
+ if(window.selenium){
842
+ eval("with(selenium.browserbot.getCurrentWindow()){"+code+"}")
843
+ }else{
844
+ eval("with(scope){"+code+"}")
845
+ }
846
+ }
847
+ }
848
+
849
+ //submit a form
850
+ if(nodeName == "input"
851
+ && element.type == "submit"
852
+ && !(Syn.support.clickSubmits)){
853
+
854
+ var form = Syn.closest(element, "form");
855
+ if(form){
856
+ Syn.trigger("submit",{},form)
857
+ }
858
+
859
+ }
860
+ //follow a link, probably needs to check if in an a.
861
+ if(nodeName == "a"
862
+ && element.href
863
+ && !/^\s*javascript:/.test(element.href)){
864
+
865
+ scope.location.href = element.href;
866
+
867
+ }
868
+
869
+ //change a checkbox
870
+ if(nodeName == "input"
871
+ && element.type == "checkbox"){
872
+
873
+ if(!Syn.support.clickChecks && !Syn.support.changeChecks){
874
+ element.checked = !element.checked;
875
+ }
876
+ if(!Syn.support.clickChanges){
877
+ Syn.trigger("change",{}, element );
878
+ }
879
+ }
880
+
881
+ //change a radio button
882
+ if(nodeName == "input" && element.type == "radio"){ // need to uncheck others if not checked
883
+
884
+ if(!Syn.support.clickChecks && !Syn.support.changeChecks){
885
+ //do the checks manually
886
+ if(!element.checked){ //do nothing, no change
887
+ element.checked = true;
888
+ }
889
+ }
890
+ if(checked != element.checked && !Syn.support.radioClickChanges){
891
+ Syn.trigger("change",{}, element );
892
+ }
893
+ }
894
+ // change options
895
+ if(nodeName == "option" && Syn.data(element,"createChange")){
896
+ Syn.trigger("change",{}, element.parentNode);//does not bubble
897
+ Syn.data(element,"createChange",false)
898
+ }
899
+ }
900
+ })
901
+
902
+
903
+ //add create and setup behavior for mosue events
904
+ h.extend(Syn.create,{
905
+ mouse : {
906
+ options : function(type, options, element){
907
+ var doc = document.documentElement, body = document.body,
908
+ center = [options.pageX || 0, options.pageY || 0],
909
+ //browser might not be loaded yet (doing support code)
910
+ left = Syn.mouse.browser && Syn.mouse.browser.left[type],
911
+ right = Syn.mouse.browser && Syn.mouse.browser.right[type];
912
+ return h.extend({
913
+ bubbles : true,cancelable : true,
914
+ view : window,
915
+ detail : 1,
916
+ screenX : 1, screenY : 1,
917
+ clientX : options.clientX || center[0] -(doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0),
918
+ clientY : options.clientY || center[1] -(doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0),
919
+ ctrlKey : !!Syn.key.ctrlKey,
920
+ altKey : !!Syn.key.altKey,
921
+ shiftKey : !!Syn.key.shiftKey,
922
+ metaKey : !!Syn.key.metaKey,
923
+ button : left && left.button != null ?
924
+ left.button :
925
+ right && right.button || (type == 'contextmenu' ? 2 : 0),
926
+ relatedTarget : document.documentElement
927
+ }, options);
928
+ },
929
+ event : document.createEvent ?
930
+ function(type, defaults, element){ //Everyone Else
931
+ var event;
932
+
933
+ try {
934
+ event = element.ownerDocument.createEvent('MouseEvents');
935
+ event.initMouseEvent(type,
936
+ defaults.bubbles, defaults.cancelable,
937
+ defaults.view,
938
+ defaults.detail,
939
+ defaults.screenX, defaults.screenY,defaults.clientX,defaults.clientY,
940
+ defaults.ctrlKey,defaults.altKey,defaults.shiftKey,defaults.metaKey,
941
+ defaults.button,defaults.relatedTarget);
942
+ } catch(e) {
943
+ event = h.createBasicStandardEvent(type,defaults)
944
+ }
945
+ event.synthetic = true;
946
+ return event;
947
+ } :
948
+ h.createEventObject
949
+ },
950
+ click : {
951
+ setup : function(type, options, element){
952
+ try{
953
+ Syn.data(element,"checked", element.checked);
954
+ }catch(e){}
955
+ if(
956
+ element.nodeName.toLowerCase() == "a"
957
+ && element.href
958
+ && !/^\s*javascript:/.test(element.href)){
959
+
960
+ //save href
961
+ Syn.data(element,"href", element.href)
962
+
963
+ //remove b/c safari/opera will open a new tab instead of changing the page
964
+ element.setAttribute('href','javascript://')
965
+ }
966
+ //if select or option, save old value and mark to change
967
+ if(/option/i.test(element.nodeName)){
968
+ var child = element.parentNode.firstChild,
969
+ i = -1;
970
+ while(child){
971
+ if(child.nodeType ==1){
972
+ i++;
973
+ if(child == element) break;
974
+ }
975
+ child = child.nextSibling;
976
+ }
977
+ if(i !== element.parentNode.selectedIndex){
978
+ //shouldn't this wait on triggering
979
+ //change?
980
+ element.parentNode.selectedIndex = i;
981
+ Syn.data(element,"createChange",true)
982
+ }
983
+ }
984
+ }
985
+ },
986
+ mousedown : {
987
+ setup : function(type,options, element){
988
+ var nn = element.nodeName.toLowerCase();
989
+ //we have to auto prevent default to prevent freezing error in safari
990
+ if(Syn.browser.safari && (nn == "select" || nn == "option" )){
991
+ options._autoPrevent = true;
992
+ }
993
+ }
994
+ }
995
+ });
996
+ //do support code
997
+ (function(){
998
+ if(!document.body){
999
+ setTimeout(arguments.callee,1)
1000
+ return;
1001
+ }
1002
+ var oldSynth = window.__synthTest;
1003
+ window.__synthTest = function(){
1004
+ Syn.support.linkHrefJS = true;
1005
+ }
1006
+ var div = document.createElement("div"),
1007
+ checkbox,
1008
+ submit,
1009
+ form,
1010
+ input,
1011
+ select;
1012
+
1013
+ div.innerHTML = "<form id='outer'>"+
1014
+ "<input name='checkbox' type='checkbox'/>"+
1015
+ "<input name='radio' type='radio' />"+
1016
+ "<input type='submit' name='submitter'/>"+
1017
+ "<input type='input' name='inputter'/>"+
1018
+ "<input name='one'>"+
1019
+ "<input name='two'/>"+
1020
+ "<a href='javascript:__synthTest()' id='synlink'></a>"+
1021
+ "<select><option></option></select>"+
1022
+ "</form>";
1023
+ document.documentElement.appendChild(div);
1024
+ form = div.firstChild
1025
+ checkbox = form.childNodes[0];
1026
+ submit = form.childNodes[2];
1027
+ select = form.getElementsByTagName('select')[0]
1028
+
1029
+ checkbox.checked = false;
1030
+ checkbox.onchange = function(){
1031
+ Syn.support.clickChanges = true;
1032
+ }
1033
+
1034
+ Syn.trigger("click", {}, checkbox)
1035
+ Syn.support.clickChecks = checkbox.checked;
1036
+ checkbox.checked = false;
1037
+
1038
+ Syn.trigger("change", {}, checkbox);
1039
+
1040
+ Syn.support.changeChecks = checkbox.checked;
1041
+
1042
+ form.onsubmit = function(ev){
1043
+ if (ev.preventDefault)
1044
+ ev.preventDefault();
1045
+ Syn.support.clickSubmits = true;
1046
+ return false;
1047
+ }
1048
+ Syn.trigger("click", {}, submit)
1049
+
1050
+
1051
+
1052
+ form.childNodes[1].onchange = function(){
1053
+ Syn.support.radioClickChanges = true;
1054
+ }
1055
+ Syn.trigger("click", {}, form.childNodes[1])
1056
+
1057
+
1058
+ Syn.bind(div, 'click', function(){
1059
+ Syn.support.optionClickBubbles = true;
1060
+ Syn.unbind(div,'click', arguments.callee)
1061
+ })
1062
+ Syn.trigger("click",{},select.firstChild)
1063
+
1064
+
1065
+ Syn.support.changeBubbles = Syn.eventSupported('change');
1066
+
1067
+ //test if mousedown followed by mouseup causes click (opera), make sure there are no clicks after this
1068
+ var clicksCount = 0
1069
+ div.onclick = function(){
1070
+ Syn.support.mouseDownUpClicks = true;
1071
+ //we should use this to check for opera potentially, but would
1072
+ //be difficult to remove element correctly
1073
+ //Syn.support.mouseDownUpRepeatClicks = (2 == (++clicksCount))
1074
+ }
1075
+ Syn.trigger("mousedown",{},div)
1076
+ Syn.trigger("mouseup",{},div)
1077
+
1078
+ //setTimeout(function(){
1079
+ // Syn.trigger("mousedown",{},div)
1080
+ // Syn.trigger("mouseup",{},div)
1081
+ //},1)
1082
+
1083
+
1084
+ document.documentElement.removeChild(div);
1085
+
1086
+ //check stuff
1087
+ window.__synthTest = oldSynth;
1088
+ //support.ready = true;
1089
+ })();
1090
+
1091
+
1092
+
1093
+ })();
1094
+
1095
+ // funcunit/synthetic/browsers.js
1096
+
1097
+ (function($){
1098
+
1099
+ Syn.key.browsers = {
1100
+ webkit : {
1101
+ 'prevent':
1102
+ {"keyup":[],"keydown":["char","keypress"],"keypress":["char"]},
1103
+ 'character':
1104
+ {"keydown":[0,"key"],"keypress":["char","char"],"keyup":[0,"key"]},
1105
+ 'specialChars':
1106
+ {"keydown":[0,"char"],"keyup":[0,"char"]},
1107
+ 'navigation':
1108
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1109
+ 'special':
1110
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1111
+ 'tab':
1112
+ {"keydown":[0,"char"],"keyup":[0,"char"]},
1113
+ 'pause-break':
1114
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1115
+ 'caps':
1116
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1117
+ 'escape':
1118
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1119
+ 'num-lock':
1120
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1121
+ 'scroll-lock':
1122
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1123
+ 'print':
1124
+ {"keyup":[0,"key"]},
1125
+ 'function':
1126
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1127
+ '\r':
1128
+ {"keydown":[0,"key"],"keypress":["char","key"],"keyup":[0,"key"]}
1129
+ },
1130
+ gecko : {
1131
+ 'prevent':
1132
+ {"keyup":[],"keydown":["char"],"keypress":["char"]},
1133
+ 'character':
1134
+ {"keydown":[0,"key"],"keypress":["char",0],"keyup":[0,"key"]},
1135
+ 'specialChars':
1136
+ {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
1137
+ 'navigation':
1138
+ {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
1139
+ 'special':
1140
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1141
+ '\t':
1142
+ {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
1143
+ 'pause-break':
1144
+ {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
1145
+ 'caps':
1146
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1147
+ 'escape':
1148
+ {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
1149
+ 'num-lock':
1150
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1151
+ 'scroll-lock':
1152
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1153
+ 'print':
1154
+ {"keyup":[0,"key"]},
1155
+ 'function':
1156
+ {"keydown":[0,"key"],"keyup":[0,"key"]},
1157
+ '\r':
1158
+ {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]}
1159
+ },
1160
+ msie : {
1161
+ 'prevent':{"keyup":[],"keydown":["char","keypress"],"keypress":["char"]},
1162
+ 'character':{"keydown":[null,"key"],"keypress":[null,"char"],"keyup":[null,"key"]},
1163
+ 'specialChars':{"keydown":[null,"char"],"keyup":[null,"char"]},
1164
+ 'navigation':{"keydown":[null,"key"],"keyup":[null,"key"]},
1165
+ 'special':{"keydown":[null,"key"],"keyup":[null,"key"]},
1166
+ 'tab':{"keydown":[null,"char"],"keyup":[null,"char"]},
1167
+ 'pause-break':{"keydown":[null,"key"],"keyup":[null,"key"]},
1168
+ 'caps':{"keydown":[null,"key"],"keyup":[null,"key"]},
1169
+ 'escape':{"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
1170
+ 'num-lock':{"keydown":[null,"key"],"keyup":[null,"key"]},
1171
+ 'scroll-lock':{"keydown":[null,"key"],"keyup":[null,"key"]},
1172
+ 'print':{"keyup":[null,"key"]},
1173
+ 'function':{"keydown":[null,"key"],"keyup":[null,"key"]},
1174
+ '\r':{"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]}
1175
+ },
1176
+ opera : {
1177
+ 'prevent':
1178
+ {"keyup":[],"keydown":[],"keypress":["char"]},
1179
+ 'character':
1180
+ {"keydown":[null,"key"],"keypress":[null,"char"],"keyup":[null,"key"]},
1181
+ 'specialChars':
1182
+ {"keydown":[null,"char"],"keypress":[null,"char"],"keyup":[null,"char"]},
1183
+ 'navigation':
1184
+ {"keydown":[null,"key"],"keypress":[null,"key"]},
1185
+ 'special':
1186
+ {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
1187
+ 'tab':
1188
+ {"keydown":[null,"char"],"keypress":[null,"char"],"keyup":[null,"char"]},
1189
+ 'pause-break':
1190
+ {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
1191
+ 'caps':
1192
+ {"keydown":[null,"key"],"keyup":[null,"key"]},
1193
+ 'escape':
1194
+ {"keydown":[null,"key"],"keypress":[null,"key"]},
1195
+ 'num-lock':
1196
+ {"keyup":[null,"key"],"keydown":[null,"key"],"keypress":[null,"key"]},
1197
+ 'scroll-lock':
1198
+ {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
1199
+ 'print':
1200
+ {},
1201
+ 'function':
1202
+ {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
1203
+ '\r':
1204
+ {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]}
1205
+ }
1206
+ };
1207
+
1208
+ Syn.mouse.browsers = {
1209
+ webkit : {"right":{"mousedown":{"button":2,"which":3},"mouseup":{"button":2,"which":3},"contextmenu":{"button":2,"which":3}},
1210
+ "left":{"mousedown":{"button":0,"which":1},"mouseup":{"button":0,"which":1},"click":{"button":0,"which":1}}},
1211
+ opera: {"right":{"mousedown":{"button":2,"which":3},"mouseup":{"button":2,"which":3}},
1212
+ "left":{"mousedown":{"button":0,"which":1},"mouseup":{"button":0,"which":1},"click":{"button":0,"which":1}}},
1213
+ msie: { "right":{"mousedown":{"button":2},"mouseup":{"button":2},"contextmenu":{"button":0}},
1214
+ "left":{"mousedown":{"button":1},"mouseup":{"button":1},"click":{"button":0}}},
1215
+ chrome : {"right":{"mousedown":{"button":2,"which":3},"mouseup":{"button":2,"which":3},"contextmenu":{"button":2,"which":3}},
1216
+ "left":{"mousedown":{"button":0,"which":1},"mouseup":{"button":0,"which":1},"click":{"button":0,"which":1}}},
1217
+ gecko: {"left":{"mousedown":{"button":0,"which":1},"mouseup":{"button":0,"which":1},"click":{"button":0,"which":1}},
1218
+ "right":{"mousedown":{"button":2,"which":3},"mouseup":{"button":2,"which":3},"contextmenu":{"button":2,"which":3}}}
1219
+ }
1220
+
1221
+ //set browser
1222
+ Syn.key.browser =
1223
+ (function(){
1224
+ if(Syn.key.browsers[window.navigator.userAgent]){
1225
+ return Syn.key.browsers[window.navigator.userAgent];
1226
+ }
1227
+ for(var browser in Syn.browser){
1228
+ if(Syn.browser[browser] && Syn.key.browsers[browser]){
1229
+ return Syn.key.browsers[browser]
1230
+ }
1231
+ }
1232
+ return Syn.key.browsers.gecko;
1233
+ })();
1234
+
1235
+ Syn.mouse.browser =
1236
+ (function(){
1237
+ if(Syn.mouse.browsers[window.navigator.userAgent]){
1238
+ return Syn.mouse.browsers[window.navigator.userAgent];
1239
+ }
1240
+ for(var browser in Syn.browser){
1241
+ if(Syn.browser[browser] && Syn.mouse.browsers[browser]){
1242
+ return Syn.mouse.browsers[browser]
1243
+ }
1244
+ }
1245
+ return Syn.mouse.browsers.gecko;
1246
+ })();
1247
+
1248
+
1249
+ })();
1250
+
1251
+ // funcunit/synthetic/key.js
1252
+
1253
+ (function($){
1254
+
1255
+
1256
+ var h = Syn.helpers,
1257
+ S = Syn,
1258
+
1259
+ // gets the selection of an input or textarea
1260
+ getSelection = function(el){
1261
+ // use selectionStart if we can
1262
+ if (el.selectionStart !== undefined) {
1263
+ // this is for opera, so we don't have to focus to type how we think we would
1264
+ if(document.activeElement
1265
+ && document.activeElement != el
1266
+ && el.selectionStart == el.selectionEnd
1267
+ && el.selectionStart == 0){
1268
+ return {start: el.value.length, end: el.value.length};
1269
+ }
1270
+ return {start: el.selectionStart, end: el.selectionEnd}
1271
+ }else{
1272
+ //check if we aren't focused
1273
+ //if(document.activeElement && document.activeElement != el){
1274
+
1275
+
1276
+ //}
1277
+ try {
1278
+ //try 2 different methods that work differently (IE breaks depending on type)
1279
+ if (el.nodeName.toLowerCase() == 'input') {
1280
+ var real = h.getWindow(el).document.selection.createRange(), r = el.createTextRange();
1281
+ r.setEndPoint("EndToStart", real);
1282
+
1283
+ var start = r.text.length
1284
+ return {
1285
+ start: start,
1286
+ end: start + real.text.length
1287
+ }
1288
+ }
1289
+ else {
1290
+ var real = h.getWindow(el).document.selection.createRange(), r = real.duplicate(), r2 = real.duplicate(), r3 = real.duplicate();
1291
+ r2.collapse();
1292
+ r3.collapse(false);
1293
+ r2.moveStart('character', -1)
1294
+ r3.moveStart('character', -1)
1295
+ //select all of our element
1296
+ r.moveToElementText(el)
1297
+ //now move our endpoint to the end of our real range
1298
+ r.setEndPoint('EndToEnd', real);
1299
+ var start = r.text.length - real.text.length, end = r.text.length;
1300
+ if (start != 0 && r2.text == "") {
1301
+ start += 2;
1302
+ }
1303
+ if (end != 0 && r3.text == "") {
1304
+ end += 2;
1305
+ }
1306
+ //if we aren't at the start, but previous is empty, we are at start of newline
1307
+ return {
1308
+ start: start,
1309
+ end: end
1310
+ }
1311
+ }
1312
+ }catch(e){
1313
+ return {start: el.value.length, end: el.value.length};
1314
+ }
1315
+ }
1316
+ },
1317
+ // gets all focusable elements
1318
+ getFocusable = function(el){
1319
+ var document = h.getWindow(el).document,
1320
+ res = [];
1321
+
1322
+ var els = document.getElementsByTagName('*'),
1323
+ len = els.length;
1324
+
1325
+ for(var i=0; i< len; i++){
1326
+ Syn.isFocusable(els[i]) && els[i] != document.documentElement && res.push(els[i])
1327
+ }
1328
+ return res;
1329
+
1330
+
1331
+ };
1332
+
1333
+ /**
1334
+ * @add Syn static
1335
+ */
1336
+ h.extend(Syn,{
1337
+ /**
1338
+ * @attribute
1339
+ * A list of the keys and their keycodes codes you can type.
1340
+ * You can add type keys with
1341
+ * @codestart
1342
+ * Syn('key','delete','title');
1343
+ *
1344
+ * //or
1345
+ *
1346
+ * Syn('type','One Two Three[left][left][delete]','title')
1347
+ * @codeend
1348
+ *
1349
+ * The following are a list of keys you can type:
1350
+ * @codestart text
1351
+ * \b - backspace
1352
+ * \t - tab
1353
+ * \r - enter
1354
+ * ' ' - space
1355
+ * a-Z 0-9 - normal characters
1356
+ * /!@#$*,.? - All other typeable characters
1357
+ * page-up - scrolls up
1358
+ * page-down - scrolls down
1359
+ * end - scrolls to bottom
1360
+ * home - scrolls to top
1361
+ * insert - changes how keys are entered
1362
+ * delete - deletes the next character
1363
+ * left - moves cursor left
1364
+ * right - moves cursor right
1365
+ * up - moves the cursor up
1366
+ * down - moves the cursor down
1367
+ * f1-12 - function buttons
1368
+ * shift, ctrl, alt - special keys
1369
+ * pause-break - the pause button
1370
+ * scroll-lock - locks scrolling
1371
+ * caps - makes caps
1372
+ * escape - escape button
1373
+ * num-lock - allows numbers on keypad
1374
+ * print - screen capture
1375
+ * @codeend
1376
+ */
1377
+ keycodes: {
1378
+ //backspace
1379
+ '\b':'8',
1380
+
1381
+ //tab
1382
+ '\t':'9',
1383
+
1384
+ //enter
1385
+ '\r':'13',
1386
+
1387
+ //special
1388
+ 'shift':'16','ctrl':'17','alt':'18',
1389
+
1390
+ //weird
1391
+ 'pause-break':'19',
1392
+ 'caps':'20',
1393
+ 'escape':'27',
1394
+ 'num-lock':'144',
1395
+ 'scroll-lock':'145',
1396
+ 'print' : '44',
1397
+
1398
+ //navigation
1399
+ 'page-up':'33','page-down':'34','end':'35','home':'36',
1400
+ 'left':'37','up':'38','right':'39','down':'40','insert':'45','delete':'46',
1401
+
1402
+ //normal characters
1403
+ ' ':'32',
1404
+ '0':'48','1':'49','2':'50','3':'51','4':'52','5':'53','6':'54','7':'55','8':'56','9':'57',
1405
+ 'a':'65','b':'66','c':'67','d':'68','e':'69','f':'70','g':'71','h':'72','i':'73','j':'74','k':'75','l':'76','m':'77',
1406
+ 'n':'78','o':'79','p':'80','q':'81','r':'82','s':'83','t':'84','u':'85','v':'86','w':'87','x':'88','y':'89','z':'90',
1407
+ //normal-characters, numpad
1408
+ 'num0':'96','num1':'97','num2':'98','num3':'99','num4':'100','num5':'101','num6':'102','num7':'103','num8':'104','num9':'105',
1409
+ '*':'106','+':'107','-':'109','.':'110',
1410
+ //normal-characters, others
1411
+ '/':'111',
1412
+ ';':'186',
1413
+ '=':'187',
1414
+ ',':'188',
1415
+ '-':'189',
1416
+ '.':'190',
1417
+ '/':'191',
1418
+ '`':'192',
1419
+ '[':'219',
1420
+ '\\':'220',
1421
+ ']':'221',
1422
+ "'":'222',
1423
+
1424
+ //ignore these, you shouldn't use them
1425
+ 'left window key':'91','right window key':'92','select key':'93',
1426
+
1427
+
1428
+ 'f1':'112','f2':'113','f3':'114','f4':'115','f5':'116','f6':'117',
1429
+ 'f7':'118','f8':'119','f9':'120','f10':'121','f11':'122','f12':'123'
1430
+ },
1431
+
1432
+ // what we can type in
1433
+ typeable : /input|textarea/i,
1434
+
1435
+ // selects text on an element
1436
+ selectText: function(el, start, end){
1437
+ if(el.setSelectionRange){
1438
+ if(!end){
1439
+ el.focus();
1440
+ el.setSelectionRange(start, start);
1441
+ } else {
1442
+ el.selectionStart = start;
1443
+ el.selectionEnd = end;
1444
+ }
1445
+ }else if (el.createTextRange) {
1446
+ //el.focus();
1447
+ var r = el.createTextRange();
1448
+ r.moveStart('character', start);
1449
+ end = end || start;
1450
+ r.moveEnd('character', end - el.value.length);
1451
+
1452
+ r.select();
1453
+ }
1454
+ },
1455
+ getText: function(el){
1456
+ //first check if the el has anything selected ..
1457
+ if(Syn.typeable.test(el.nodeName)){
1458
+ var sel = getSelection(el);
1459
+ return el.value.substring(sel.start, sel.end)
1460
+ }
1461
+ //otherwise get from page
1462
+ var win = Syn.helpers.getWindow(el);
1463
+ if (win.getSelection) {
1464
+ return win.getSelection().toString();
1465
+ }
1466
+ else if (win.document.getSelection) {
1467
+ return win.document.getSelection().toString()
1468
+ }
1469
+ else {
1470
+ return win.document.selection.createRange().text;
1471
+ }
1472
+ },
1473
+ getSelection : getSelection
1474
+ });
1475
+
1476
+ h.extend(Syn.key,{
1477
+ // retrieves a description of what events for this character should look like
1478
+ data : function(key){
1479
+ //check if it is described directly
1480
+ if(S.key.browser[key]){
1481
+ return S.key.browser[key];
1482
+ }
1483
+ for(var kind in S.key.kinds){
1484
+ if(h.inArray(key, S.key.kinds[kind] ) > -1){
1485
+ return S.key.browser[kind]
1486
+ }
1487
+ }
1488
+ return S.key.browser.character
1489
+ },
1490
+
1491
+ //returns the special key if special
1492
+ isSpecial : function(keyCode){
1493
+ var specials = S.key.kinds.special;
1494
+ for(var i=0; i < specials.length; i++){
1495
+ if(Syn.keycodes[ specials[i] ] == keyCode){
1496
+ return specials[i];
1497
+ }
1498
+ }
1499
+ },
1500
+ /**
1501
+ * @hide
1502
+ * gets the options for a key and event type ...
1503
+ * @param {Object} key
1504
+ * @param {Object} event
1505
+ */
1506
+ options : function(key, event){
1507
+ var keyData = Syn.key.data(key);
1508
+
1509
+ if(!keyData[event]){
1510
+ //we shouldn't be creating this event
1511
+ return null;
1512
+ }
1513
+
1514
+ var charCode = keyData[event][0],
1515
+ keyCode = keyData[event][1],
1516
+ result = {};
1517
+
1518
+ if(keyCode == 'key'){
1519
+ result.keyCode = Syn.keycodes[key]
1520
+ } else if (keyCode == 'char'){
1521
+ result.keyCode = key.charCodeAt(0)
1522
+ }else{
1523
+ result.keyCode = keyCode;
1524
+ }
1525
+
1526
+ if(charCode == 'char'){
1527
+ result.charCode = key.charCodeAt(0)
1528
+ }else if(charCode !== null){
1529
+ result.charCode = charCode;
1530
+ }
1531
+
1532
+
1533
+ return result
1534
+ },
1535
+ //types of event keys
1536
+ kinds : {
1537
+ special : ["shift",'ctrl','alt','caps'],
1538
+ specialChars : ["\b"],
1539
+ navigation: ["page-up",'page-down','end','home','left','up','right','down','insert','delete'],
1540
+ 'function' : ['f1','f2','f3','f4','f5','f6','f7','f8','f9','f10','f11','f12']
1541
+ },
1542
+ //returns the default function
1543
+ getDefault : function(key){
1544
+ //check if it is described directly
1545
+ if(Syn.key.defaults[key]){
1546
+ return Syn.key.defaults[key];
1547
+ }
1548
+ for(var kind in Syn.key.kinds){
1549
+ if(h.inArray(key, Syn.key.kinds[kind])> -1 && Syn.key.defaults[kind] ){
1550
+ return Syn.key.defaults[kind];
1551
+ }
1552
+ }
1553
+ return Syn.key.defaults.character
1554
+ },
1555
+ // default behavior when typing
1556
+ defaults : {
1557
+ 'character' : function(options, scope, key, force, sel){
1558
+ if(/num\d+/.test(key)){
1559
+ key = key.match(/\d+/)[0]
1560
+ }
1561
+
1562
+ if(force || (!S.support.keyCharacters && Syn.typeable.test(this.nodeName))){
1563
+ var current = this.value,
1564
+ before = current.substr(0,sel.start),
1565
+ after = current.substr(sel.end),
1566
+ character = key;
1567
+
1568
+ this.value = before+character+after;
1569
+ //handle IE inserting \r\n
1570
+ var charLength = character == "\n" && S.support.textareaCarriage ? 2 : character.length;
1571
+ Syn.selectText(this, before.length + charLength)
1572
+ }
1573
+ },
1574
+ 'c' : function(options, scope, key, force, sel){
1575
+ if(Syn.key.ctrlKey){
1576
+ Syn.key.clipboard = Syn.getText(this)
1577
+ }else{
1578
+ Syn.key.defaults.character.apply(this, arguments);
1579
+ }
1580
+ },
1581
+ 'v' : function(options, scope, key, force, sel){
1582
+ if(Syn.key.ctrlKey){
1583
+ Syn.key.defaults.character.call(this, options,scope, Syn.key.clipboard, true,sel);
1584
+ }else{
1585
+ Syn.key.defaults.character.apply(this, arguments);
1586
+ }
1587
+ },
1588
+ 'a' : function(options, scope, key, force, sel){
1589
+ if(Syn.key.ctrlKey){
1590
+ Syn.selectText(this, 0, this.value.length)
1591
+ }else{
1592
+ Syn.key.defaults.character.apply(this, arguments);
1593
+ }
1594
+ },
1595
+ 'home' : function(){
1596
+ Syn.onParents(this, function(el){
1597
+ if(el.scrollHeight != el.clientHeight){
1598
+ el.scrollTop = 0;
1599
+ return false;
1600
+ }
1601
+ })
1602
+ },
1603
+ 'end' : function(){
1604
+ Syn.onParents(this, function(el){
1605
+ if(el.scrollHeight != el.clientHeight){
1606
+ el.scrollTop = el.scrollHeight;
1607
+ return false;
1608
+ }
1609
+ })
1610
+ },
1611
+ 'page-down' : function(){
1612
+ //find the first parent we can scroll
1613
+ Syn.onParents(this, function(el){
1614
+ if(el.scrollHeight != el.clientHeight){
1615
+ var ch = el.clientHeight
1616
+ el.scrollTop += ch;
1617
+ return false;
1618
+ }
1619
+ })
1620
+ },
1621
+ 'page-up' : function(){
1622
+ Syn.onParents(this, function(el){
1623
+ if(el.scrollHeight != el.clientHeight){
1624
+ var ch = el.clientHeight
1625
+ el.scrollTop -= ch;
1626
+ return false;
1627
+ }
1628
+ })
1629
+ },
1630
+ '\b' : function(options, scope, key, force, sel){
1631
+ //this assumes we are deleting from the end
1632
+ if(!S.support.backspaceWorks && Syn.typeable.test(this.nodeName)){
1633
+ var current = this.value,
1634
+ before = current.substr(0,sel.start),
1635
+ after = current.substr(sel.end);
1636
+
1637
+ if(sel.start == sel.end && sel.start > 0){
1638
+ //remove a character
1639
+ this.value = before.substring(0, before.length - 1)+after
1640
+ Syn.selectText(this, sel.start-1)
1641
+ }else{
1642
+ this.value = before+after;
1643
+ Syn.selectText(this, sel.start)
1644
+ }
1645
+
1646
+ //set back the selection
1647
+ }
1648
+ },
1649
+ 'delete' : function(options, scope, key, force, sel){
1650
+ if(!S.support.backspaceWorks && Syn.typeable.test(this.nodeName)){
1651
+ var current = this.value,
1652
+ before = current.substr(0,sel.start),
1653
+ after = current.substr(sel.end);
1654
+ if(sel.start == sel.end && sel.start <= this.value.length - 1){
1655
+ this.value = before+after.substring(1)
1656
+ }else{
1657
+ this.value = before+after;
1658
+
1659
+ }
1660
+ Syn.selectText(this, sel.start)
1661
+ }
1662
+ },
1663
+ '\r' : function(options, scope, key, force, sel){
1664
+
1665
+ var nodeName = this.nodeName.toLowerCase()
1666
+ // submit a form
1667
+ if(!S.support.keypressSubmits && nodeName == 'input'){
1668
+ var form = Syn.closest(this, "form");
1669
+ if(form){
1670
+ Syn.trigger("submit", {}, form);
1671
+ }
1672
+
1673
+ }
1674
+ //newline in textarea
1675
+ if(!S.support.keyCharacters && nodeName == 'textarea'){
1676
+ Syn.key.defaults.character.call(this, options, scope, "\n", undefined, sel)
1677
+ }
1678
+ // 'click' hyperlinks
1679
+ if(!S.support.keypressOnAnchorClicks && nodeName == 'a'){
1680
+ Syn.trigger("click", {}, this);
1681
+ }
1682
+ },
1683
+ //
1684
+ // Gets all focusable elements. If the element (this)
1685
+ // doesn't have a tabindex, finds the next element after.
1686
+ // If the element (this) has a tabindex finds the element
1687
+ // with the next higher tabindex OR the element with the same
1688
+ // tabindex after it in the document.
1689
+ // @return the next element
1690
+ //
1691
+ '\t' : function(options, scope){
1692
+ // focusable elements
1693
+ var focusEls = getFocusable(this),
1694
+ // the current element's tabindex
1695
+ tabIndex = Syn.tabIndex(this),
1696
+ // will be set to our guess for the next element
1697
+ current = null,
1698
+ // the next index we care about
1699
+ currentIndex = 1000000000,
1700
+ // set to true once we found 'this' element
1701
+ found = false,
1702
+ i = 0,
1703
+ el,
1704
+ //the tabindex of the tabable element we are looking at
1705
+ elIndex,
1706
+ firstNotIndexed,
1707
+ prev;
1708
+ orders = [];
1709
+ for(; i< focusEls.length; i++){
1710
+ orders.push([focusEls[i], i]);
1711
+ }
1712
+ var sort = function(order1, order2){
1713
+ var el1 = order1[0],
1714
+ el2 = order2[0],
1715
+ tab1 = Syn.tabIndex(el1) || 0,
1716
+ tab2 = Syn.tabIndex(el2) || 0;
1717
+ if(tab1 == tab2){
1718
+ return order1[1] - order2[1]
1719
+ }else{
1720
+ if(tab1 == 0){
1721
+ return 1;
1722
+ }else if(tab2 == 0){
1723
+ return -1;
1724
+ }else{
1725
+ return tab1-tab2;
1726
+ }
1727
+ }
1728
+ }
1729
+ orders.sort(sort);
1730
+ //now find current
1731
+ for(i=0; i< orders.length; i++){
1732
+ el = orders[i][0];
1733
+ if(this== el ){
1734
+ if(!Syn.key.shiftKey){
1735
+ current = orders[i+1][0];
1736
+ if(!current){
1737
+ current = orders[0][0]
1738
+ }
1739
+ }else{
1740
+ current = orders[i-1][0];
1741
+ if(!current){
1742
+ current = orders[focusEls.length-1][0]
1743
+ }
1744
+ }
1745
+
1746
+ }
1747
+ }
1748
+
1749
+ //restart if we didn't find anything
1750
+ if(!current){
1751
+ current = firstNotIndexed;
1752
+ }
1753
+ current && current.focus();
1754
+ return current;
1755
+ },
1756
+ 'left' : function(options, scope, key, force, sel){
1757
+ if( Syn.typeable.test(this.nodeName) ){
1758
+ if(Syn.key.shiftKey){
1759
+ Syn.selectText(this, sel.start == 0 ? 0 : sel.start - 1, sel.end)
1760
+ }else{
1761
+ Syn.selectText(this, sel.start == 0 ? 0 : sel.start - 1)
1762
+ }
1763
+ }
1764
+ },
1765
+ 'right' : function(options, scope, key, force, sel){
1766
+ if( Syn.typeable.test(this.nodeName) ){
1767
+ if(Syn.key.shiftKey){
1768
+ Syn.selectText(this, sel.start, sel.end+1 > this.value.length ? this.value.length : sel.end+1)
1769
+ }else{
1770
+ Syn.selectText(this, sel.end+1 > this.value.length ? this.value.length : sel.end+1)
1771
+ }
1772
+ }
1773
+ },
1774
+ 'up' : function(){
1775
+ if(/select/i.test(this.nodeName)){
1776
+
1777
+ this.selectedIndex = this.selectedIndex ? this.selectedIndex-1 : 0;
1778
+ //set this to change on blur?
1779
+ }
1780
+ },
1781
+ 'down' : function(){
1782
+ if(/select/i.test(this.nodeName)){
1783
+ Syn.changeOnBlur(this, "selectedIndex", this.selectedIndex)
1784
+ this.selectedIndex = this.selectedIndex+1;
1785
+ //set this to change on blur?
1786
+ }
1787
+ },
1788
+ 'shift' : function(){
1789
+ return null;
1790
+ }
1791
+ }
1792
+ });
1793
+
1794
+
1795
+ h.extend(Syn.create,{
1796
+ keydown : {
1797
+ setup : function(type, options, element){
1798
+ if(h.inArray(options,Syn.key.kinds.special ) != -1){
1799
+ Syn.key[options+"Key"] = element;
1800
+ }
1801
+ }
1802
+ },
1803
+ keyup : {
1804
+ setup : function(type, options, element){
1805
+ if(h.inArray(options,Syn.key.kinds.special )!= -1){
1806
+ Syn.key[options+"Key"] = null;
1807
+ }
1808
+ }
1809
+ },
1810
+ key : {
1811
+ // return the options for a key event
1812
+ options : function(type, options, element){
1813
+ //check if options is character or has character
1814
+ options = typeof options != "object" ? {character : options} : options;
1815
+
1816
+ //don't change the orignial
1817
+ options = h.extend({}, options)
1818
+ if(options.character){
1819
+ h.extend(options, S.key.options(options.character, type));
1820
+ delete options.character;
1821
+ }
1822
+
1823
+ options = h.extend({
1824
+ ctrlKey: !!Syn.key.ctrlKey,
1825
+ altKey: !!Syn.key.altKey,
1826
+ shiftKey: !!Syn.key.shiftKey,
1827
+ metaKey: !!Syn.key.metaKey
1828
+ }, options)
1829
+
1830
+ return options;
1831
+ },
1832
+ // creates a key event
1833
+ event : document.createEvent ?
1834
+ function(type, options, element){ //Everyone Else
1835
+ var event;
1836
+
1837
+ try {
1838
+
1839
+ event = element.ownerDocument.createEvent("KeyEvents");
1840
+ event.initKeyEvent(type, true, true, window,
1841
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
1842
+ options.keyCode, options.charCode );
1843
+ } catch(e) {
1844
+ event = h.createBasicStandardEvent(type,options)
1845
+ }
1846
+ event.synthetic = true;
1847
+ return event;
1848
+
1849
+ } :
1850
+ function(type, options, element){
1851
+ var event = h.createEventObject.apply(this,arguments);
1852
+ h.extend(event, options)
1853
+
1854
+ return event;
1855
+ }
1856
+ }
1857
+ });
1858
+
1859
+ var convert = {
1860
+ "enter" : "\r",
1861
+ "backspace" : "\b",
1862
+ "tab" : "\t",
1863
+ "space" : " "
1864
+ }
1865
+
1866
+ /**
1867
+ * @add Syn prototype
1868
+ */
1869
+ h.extend(Syn.init.prototype,
1870
+ {
1871
+ /**
1872
+ * @function key
1873
+ * Types a single key. The key should be
1874
+ * a string that matches a
1875
+ * [Syn.static.keycodes].
1876
+ *
1877
+ * The following sends a carridge return
1878
+ * to the 'name' element.
1879
+ * @codestart
1880
+ * Syn.key('\r','name')
1881
+ * @codeend
1882
+ * For each character, a keydown, keypress, and keyup is triggered if
1883
+ * appropriate.
1884
+ * @param {String} options
1885
+ * @param {HTMLElement} [element]
1886
+ * @param {Function} [callback]
1887
+ * @return {HTMLElement} the element currently focused.
1888
+ */
1889
+ _key : function(options, element, callback){
1890
+ //first check if it is a special up
1891
+ if(/-up$/.test(options)
1892
+ && h.inArray(options.replace("-up",""),Syn.key.kinds.special )!= -1){
1893
+ Syn.trigger('keyup',options.replace("-up",""), element )
1894
+ callback(true, element);
1895
+ return;
1896
+ }
1897
+
1898
+
1899
+ var caret = Syn.typeable.test(element.nodeName) && getSelection(element),
1900
+ key = convert[options] || options,
1901
+ // should we run default events
1902
+ runDefaults = Syn.trigger('keydown',key, element ),
1903
+
1904
+ // a function that gets the default behavior for a key
1905
+ getDefault = Syn.key.getDefault,
1906
+
1907
+ // how this browser handles preventing default events
1908
+ prevent = Syn.key.browser.prevent,
1909
+
1910
+ // the result of the default event
1911
+ defaultResult,
1912
+
1913
+ // options for keypress
1914
+ keypressOptions = Syn.key.options(key, 'keypress')
1915
+
1916
+
1917
+ if(runDefaults){
1918
+ //if the browser doesn't create keypresses for this key, run default
1919
+ if(!keypressOptions){
1920
+ defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret)
1921
+ }else{
1922
+ //do keypress
1923
+ runDefaults = Syn.trigger('keypress',keypressOptions, element )
1924
+ if(runDefaults){
1925
+ defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret)
1926
+ }
1927
+ }
1928
+ }else{
1929
+ //canceled ... possibly don't run keypress
1930
+ if(keypressOptions && h.inArray('keypress',prevent.keydown) == -1 ){
1931
+ Syn.trigger('keypress',keypressOptions, element )
1932
+ }
1933
+ }
1934
+ if(defaultResult && defaultResult.nodeName){
1935
+ element = defaultResult
1936
+ }
1937
+
1938
+ if(defaultResult !== null){
1939
+ setTimeout(function(){
1940
+ Syn.trigger('keyup',Syn.key.options(key, 'keyup'), element )
1941
+ callback(runDefaults, element)
1942
+ },1)
1943
+ }else{
1944
+ callback(runDefaults, element)
1945
+ }
1946
+
1947
+
1948
+ //do mouseup
1949
+
1950
+ return element;
1951
+ // is there a keypress? .. if not , run default
1952
+ // yes -> did we prevent it?, if not run ...
1953
+
1954
+ },
1955
+ /**
1956
+ * @function type
1957
+ * Types sequence of [Syn.prototype.key key actions]. Each
1958
+ * character is typed, one at a type.
1959
+ * Multi-character keys like 'left' should be
1960
+ * enclosed in square brackents.
1961
+ *
1962
+ * The following types 'JavaScript MVC' then deletes the space.
1963
+ * @codestart
1964
+ * Syn.type('JavaScript MVC[left][left][left]\b','name')
1965
+ * @codeend
1966
+ *
1967
+ * Type is able to handle (and move with) tabs (\t).
1968
+ * The following simulates tabing and entering values in a form and
1969
+ * eventually submitting the form.
1970
+ * @codestart
1971
+ * Syn.type("Justin\tMeyer\t27\tjustinbmeyer@gmail.com\r")
1972
+ * @codeend
1973
+ * @param {String} options the text to type
1974
+ * @param {HTMLElement} [element] an element or an id of an element
1975
+ * @param {Function} [callback] a function to callback
1976
+ */
1977
+ _type : function(options, element, callback){
1978
+ //break it up into parts ...
1979
+ //go through each type and run
1980
+ var parts = options.match(/(\[[^\]]+\])|([^\[])/g),
1981
+ self = this,
1982
+ runNextPart = function(runDefaults, el){
1983
+ var part = parts.shift();
1984
+ if(!part){
1985
+ callback(runDefaults, el);
1986
+ return;
1987
+ }
1988
+ el = el || element;
1989
+ if(part.length > 1){
1990
+ part = part.substr(1,part.length - 2)
1991
+ }
1992
+ self._key(part, el, runNextPart)
1993
+ }
1994
+
1995
+ runNextPart();
1996
+
1997
+ }
1998
+ });
1999
+
2000
+
2001
+ //do support code
2002
+ (function(){
2003
+ if(!document.body){
2004
+ setTimeout(arguments.callee,1)
2005
+ return;
2006
+ }
2007
+
2008
+ var div = document.createElement("div"),
2009
+ checkbox,
2010
+ submit,
2011
+ form,
2012
+ input,
2013
+ submitted = false,
2014
+ anchor,
2015
+ textarea;
2016
+
2017
+ div.innerHTML = "<form id='outer'>"+
2018
+ "<input name='checkbox' type='checkbox'/>"+
2019
+ "<input name='radio' type='radio' />"+
2020
+ "<input type='submit' name='submitter'/>"+
2021
+ "<input type='input' name='inputter'/>"+
2022
+ "<input name='one'>"+
2023
+ "<input name='two'/>"+
2024
+ "<a href='#abc'></a>"+
2025
+ "<textarea>1\n2</textarea>"
2026
+ "</form>";
2027
+
2028
+ document.documentElement.appendChild(div);
2029
+ form = div.firstChild;
2030
+ checkbox = form.childNodes[0];
2031
+ submit = form.childNodes[2];
2032
+ anchor = form.getElementsByTagName("a")[0];
2033
+ textarea = form.getElementsByTagName("textarea")[0]
2034
+ form.onsubmit = function(ev){
2035
+ if (ev.preventDefault)
2036
+ ev.preventDefault();
2037
+ S.support.keypressSubmits = true;
2038
+ ev.returnValue = false;
2039
+ return false;
2040
+ }
2041
+ Syn.trigger("keypress", "\r", form.childNodes[3]);
2042
+
2043
+
2044
+ Syn.trigger("keypress", "a", form.childNodes[3]);
2045
+ S.support.keyCharacters = form.childNodes[3].value == "a";
2046
+
2047
+
2048
+ form.childNodes[3].value = "a"
2049
+ Syn.trigger("keypress", "\b", form.childNodes[3]);
2050
+ S.support.backspaceWorks = form.childNodes[3].value == "";
2051
+
2052
+
2053
+
2054
+ form.childNodes[3].onchange = function(){
2055
+ S.support.focusChanges = true;
2056
+ }
2057
+ form.childNodes[3].focus();
2058
+ Syn.trigger("keypress", "a", form.childNodes[3]);
2059
+ form.childNodes[5].focus();
2060
+
2061
+ //test keypress \r on anchor submits
2062
+ S.bind(anchor,"click",function(ev){
2063
+ if (ev.preventDefault)
2064
+ ev.preventDefault();
2065
+ S.support.keypressOnAnchorClicks = true;
2066
+ ev.returnValue = false;
2067
+ return false;
2068
+ })
2069
+ Syn.trigger("keypress", "\r", anchor);
2070
+
2071
+ S.support.textareaCarriage = textarea.value.length == 4
2072
+ document.documentElement.removeChild(div);
2073
+
2074
+ S.support.ready = true;
2075
+ })();
2076
+
2077
+
2078
+
2079
+
2080
+
2081
+ })();
2082
+
2083
+ // funcunit/synthetic/drag/drag.js
2084
+
2085
+ (function($){
2086
+
2087
+ // document body has to exists for this test
2088
+
2089
+ (function(){
2090
+ if (!document.body) {
2091
+ setTimeout(arguments.callee, 1)
2092
+ return;
2093
+ }
2094
+ var div = document.createElement('div')
2095
+ document.body.appendChild(div);
2096
+ Syn.helpers.extend(div.style, {
2097
+ width: "100px",
2098
+ height: "10000px",
2099
+ backgroundColor: "blue",
2100
+ position: "absolute",
2101
+ top: "10px",
2102
+ left: "0px",
2103
+ zIndex: 19999
2104
+ });
2105
+ document.body.scrollTop = 11;
2106
+ if(!document.elementFromPoint){
2107
+ return;
2108
+ }
2109
+ var el = document.elementFromPoint(3, 1)
2110
+ if (el == div) {
2111
+ Syn.support.elementFromClient = true;
2112
+ }
2113
+ else {
2114
+ Syn.support.elementFromPage = true;
2115
+ }
2116
+ document.body.removeChild(div);
2117
+ document.body.scrollTop = 0;
2118
+ })();
2119
+
2120
+
2121
+ //gets an element from a point
2122
+ var elementFromPoint = function(point, element){
2123
+ var clientX = point.clientX, clientY = point.clientY, win = Syn.helpers.getWindow(element)
2124
+
2125
+ if (Syn.support.elementFromPage) {
2126
+ var off = Syn.helpers.scrollOffset(win);
2127
+ clientX = clientX + off.left; //convert to pageX
2128
+ clientY = clientY + off.top; //convert to pageY
2129
+ }
2130
+
2131
+ return win.document.elementFromPoint ? win.document.elementFromPoint(clientX, clientY) : element;
2132
+ }, //creates an event at a certain point
2133
+ createEventAtPoint = function(event, point, element){
2134
+ var el = elementFromPoint(point, element)
2135
+ Syn.trigger(event, point, el || element)
2136
+ return el;
2137
+ }, // creates a mousemove event, but first triggering mouseout / mouseover if appropriate
2138
+ mouseMove = function(point, element, last){
2139
+ var el = elementFromPoint(point, element)
2140
+ if (last != el && el && last) {
2141
+ var options = Syn.helpers.extend({},point);
2142
+ options.relatedTarget = el;
2143
+ Syn.trigger("mouseout", options, last);
2144
+ options.relatedTarget = last;
2145
+ Syn.trigger("mouseover", options, el);
2146
+ }
2147
+
2148
+ Syn.trigger("mousemove", point, el || element)
2149
+ return el;
2150
+ }, // start and end are in clientX, clientY
2151
+ startMove = function(start, end, duration, element, callback){
2152
+ var startTime = new Date(),
2153
+ distX = end.clientX - start.clientX,
2154
+ distY = end.clientY - start.clientY,
2155
+ win = Syn.helpers.getWindow(element),
2156
+ current = elementFromPoint(start, element),
2157
+ cursor = win.document.createElement('div'),
2158
+ calls = 0;
2159
+ move = function(){
2160
+ //get what fraction we are at
2161
+ var now = new Date(),
2162
+ scrollOffset = Syn.helpers.scrollOffset(win),
2163
+ fraction = ( calls == 0 ? 0 : now - startTime) / duration,
2164
+ options = {
2165
+ clientX: distX * fraction + start.clientX,
2166
+ clientY: distY * fraction + start.clientY
2167
+ };
2168
+ calls++;
2169
+ if (fraction < 1) {
2170
+ Syn.helpers.extend(cursor.style, {
2171
+ left: (options.clientX + scrollOffset.left + 2) + "px",
2172
+ top: (options.clientY + scrollOffset.top + 2) + "px"
2173
+ })
2174
+ current = mouseMove(options, element, current)
2175
+ setTimeout(arguments.callee, 15)
2176
+ }
2177
+ else {
2178
+ current = mouseMove(end, element, current);
2179
+ win.document.body.removeChild(cursor)
2180
+ callback();
2181
+ }
2182
+ }
2183
+ Syn.helpers.extend(cursor.style, {
2184
+ height: "5px",
2185
+ width: "5px",
2186
+ backgroundColor: "red",
2187
+ position: "absolute",
2188
+ zIndex: 19999,
2189
+ fontSize: "1px"
2190
+ })
2191
+ win.document.body.appendChild(cursor)
2192
+ move();
2193
+ },
2194
+ startDrag = function(start, end, duration, element, callback){
2195
+ createEventAtPoint("mousedown", start, element);
2196
+ startMove(start, end, duration, element, function(){
2197
+ createEventAtPoint("mouseup", end, element);
2198
+ callback();
2199
+ })
2200
+ },
2201
+ center = function(el){
2202
+ var j = Syn.jquery()(el),
2203
+ o = j.offset();
2204
+ return{
2205
+ pageX: o.left + (j.width() / 2),
2206
+ pageY: o.top + (j.height() / 2)
2207
+ }
2208
+ },
2209
+ convertOption = function(option, win, from){
2210
+ var page = /(\d+)[x ](\d+)/,
2211
+ client = /(\d+)X(\d+)/,
2212
+ relative = /([+-]\d+)[xX ]([+-]\d+)/
2213
+ //check relative "+22x-44"
2214
+ if (typeof option == 'string' && relative.test(option) && from) {
2215
+ var cent = center(from),
2216
+ parts = option.match(relative);
2217
+ option = {
2218
+ pageX: cent.pageX + parseInt(parts[1]),
2219
+ pageY: cent.pageY +parseInt(parts[2])
2220
+ }
2221
+ }
2222
+ if (typeof option == 'string' && page.test(option)) {
2223
+ var parts = option.match(page)
2224
+ option = {
2225
+ pageX: parseInt(parts[1]),
2226
+ pageY: parseInt(parts[2])
2227
+ }
2228
+ }
2229
+ if (typeof option == 'string' && client.test(option)) {
2230
+ var parts = option.match(client)
2231
+ option = {
2232
+ clientX: parseInt(parts[1]),
2233
+ clientY: parseInt(parts[2])
2234
+ }
2235
+ }
2236
+ if (typeof option == 'string') {
2237
+ option = Syn.jquery()(option, win.document)[0];
2238
+ }
2239
+ if (option.nodeName) {
2240
+ option = center(option)
2241
+ }
2242
+ if (option.pageX) {
2243
+ var off = Syn.helpers.scrollOffset(win);
2244
+ option = {
2245
+ clientX: option.pageX - off.left,
2246
+ clientY: option.pageY - off.top
2247
+ }
2248
+ }
2249
+ return option;
2250
+ }
2251
+ /**
2252
+ * @add Syn prototype
2253
+ */
2254
+ Syn.helpers.extend(Syn.init.prototype,{
2255
+ /**
2256
+ * @function move
2257
+ * Moves the cursor from one point to another.
2258
+ * <h3>Quick Example</h3>
2259
+ * The following moves the cursor from (0,0) in
2260
+ * the window to (100,100) in 1 second.
2261
+ * @codestart
2262
+ * Syn.move(
2263
+ * {
2264
+ * from: {clientX: 0, clientY: 0},
2265
+ * to: {clientX: 100, clientY: 100},
2266
+ * duration: 1000
2267
+ * },
2268
+ * document.document)
2269
+ * @codeend
2270
+ * <h2>Options</h2>
2271
+ * There are many ways to configure the endpoints of the move.
2272
+ *
2273
+ * <h3>PageX and PageY</h3>
2274
+ * If you pass pageX or pageY, these will get converted
2275
+ * to client coordinates.
2276
+ * @codestart
2277
+ * Syn.move(
2278
+ * {
2279
+ * from: {pageX: 0, pageY: 0},
2280
+ * to: {pageX: 100, pageY: 100}
2281
+ * },
2282
+ * document.document)
2283
+ * @codeend
2284
+ * <h3>String Coordinates</h3>
2285
+ * You can set the pageX and pageY as strings like:
2286
+ * @codestart
2287
+ * Syn.move(
2288
+ * {
2289
+ * from: "0x0",
2290
+ * to: "100x100"
2291
+ * },
2292
+ * document.document)
2293
+ * @codeend
2294
+ * <h3>Element Coordinates</h3>
2295
+ * If jQuery is present, you can pass an element as the from or to option
2296
+ * and the coordinate will be set as the center of the element.
2297
+ * @codestart
2298
+ * Syn.move(
2299
+ * {
2300
+ * from: $(".recipe")[0],
2301
+ * to: $("#trash")[0]
2302
+ * },
2303
+ * document.document)
2304
+ * @codeend
2305
+ * <h3>Query Strings</h3>
2306
+ * If jQuery is present, you can pass a query string as the from or to option.
2307
+ * @codestart
2308
+ * Syn.move(
2309
+ * {
2310
+ * from: ".recipe",
2311
+ * to: "#trash"
2312
+ * },
2313
+ * document.document)
2314
+ * @codeend
2315
+ * <h3>No From</h3>
2316
+ * If you don't provide a from, the element argument passed to Syn is used.
2317
+ * @codestart
2318
+ * Syn.move(
2319
+ * { to: "#trash" },
2320
+ * 'myrecipe')
2321
+ * @codeend
2322
+ * @param {Object} options
2323
+ * @param {HTMLElement} from
2324
+ * @param {Function} callback
2325
+ */
2326
+ _move : function(options, from, callback){
2327
+ //need to convert if elements
2328
+ var win = Syn.helpers.getWindow(from),
2329
+ fro = convertOption(options.from || from, win),
2330
+ to = convertOption(options.to || options, win);
2331
+
2332
+ startMove(fro, to, options.duration || 500, from, callback);
2333
+ },
2334
+ /**
2335
+ * @function drag
2336
+ * Creates a mousedown and drags from one point to another.
2337
+ * Check out [Syn.prototype.move move] for API details.
2338
+ *
2339
+ * @param {Object} options
2340
+ * @param {Object} from
2341
+ * @param {Object} callback
2342
+ */
2343
+ _drag : function(options, from, callback){
2344
+ //need to convert if elements
2345
+ var win = Syn.helpers.getWindow(from),
2346
+ fro = convertOption(options.from || from, win, from),
2347
+ to = convertOption(options.to || options, win, from);
2348
+
2349
+ startDrag(fro, to, options.duration || 500, from, callback);
2350
+ }
2351
+ })
2352
+
2353
+
2354
+ })();
2355
+