terminus 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+