terminus 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/README.rdoc +27 -89
  2. data/bin/terminus +7 -23
  3. data/lib/capybara/driver/terminus.rb +30 -11
  4. data/lib/terminus.rb +33 -22
  5. data/lib/terminus/application.rb +13 -7
  6. data/lib/terminus/browser.rb +110 -28
  7. data/lib/terminus/controller.rb +51 -20
  8. data/lib/terminus/host.rb +22 -0
  9. data/lib/terminus/node.rb +23 -4
  10. data/lib/terminus/proxy.rb +62 -0
  11. data/lib/terminus/proxy/driver_body.rb +63 -0
  12. data/lib/terminus/proxy/external.rb +25 -0
  13. data/lib/terminus/proxy/rewrite.rb +20 -0
  14. data/lib/terminus/public/icon.png +0 -0
  15. data/lib/terminus/public/loader.js +1 -0
  16. data/lib/terminus/public/style.css +43 -0
  17. data/lib/terminus/public/syn/browsers.js +150 -0
  18. data/lib/terminus/public/syn/drag/drag.js +322 -0
  19. data/lib/terminus/public/syn/key.js +905 -0
  20. data/lib/terminus/public/syn/mouse.js +284 -0
  21. data/lib/terminus/public/syn/synthetic.js +830 -0
  22. data/lib/{public → terminus/public}/terminus.js +109 -40
  23. data/lib/terminus/server.rb +5 -12
  24. data/lib/terminus/timeouts.rb +2 -2
  25. data/lib/{views/bookmarklet.erb → terminus/views/bootstrap.erb} +17 -12
  26. data/lib/terminus/views/index.erb +21 -0
  27. data/lib/terminus/views/infinite.html +16 -0
  28. data/spec/reports/chrome.txt +748 -0
  29. data/spec/reports/firefox.txt +748 -0
  30. data/spec/reports/opera.txt +748 -0
  31. data/spec/reports/safari.txt +748 -0
  32. data/spec/spec_helper.rb +18 -14
  33. data/spec/terminus_driver_spec.rb +7 -5
  34. data/spec/terminus_session_spec.rb +5 -18
  35. metadata +71 -57
  36. data/lib/public/loader.js +0 -1
  37. data/lib/public/style.css +0 -49
  38. data/lib/public/syn.js +0 -2355
  39. data/lib/views/index.erb +0 -32
@@ -0,0 +1,322 @@
1
+ (function() {
2
+
3
+ // check if elementFromPageExists
4
+ (function() {
5
+
6
+ // document body has to exists for this test
7
+ if (!document.body ) {
8
+ setTimeout(arguments.callee, 1)
9
+ return;
10
+ }
11
+ var div = document.createElement('div')
12
+ document.body.appendChild(div);
13
+ Syn.helpers.extend(div.style, {
14
+ width: "100px",
15
+ height: "10000px",
16
+ backgroundColor: "blue",
17
+ position: "absolute",
18
+ top: "10px",
19
+ left: "0px",
20
+ zIndex: 19999
21
+ });
22
+ document.body.scrollTop = 11;
23
+ if (!document.elementFromPoint ) {
24
+ return;
25
+ }
26
+ var el = document.elementFromPoint(3, 1)
27
+ if ( el == div ) {
28
+ Syn.support.elementFromClient = true;
29
+ }
30
+ else {
31
+ Syn.support.elementFromPage = true;
32
+ }
33
+ document.body.removeChild(div);
34
+ document.body.scrollTop = 0;
35
+ })();
36
+
37
+
38
+ //gets an element from a point
39
+ var elementFromPoint = function( point, element ) {
40
+ var clientX = point.clientX,
41
+ clientY = point.clientY,
42
+ win = Syn.helpers.getWindow(element),
43
+ el;
44
+
45
+
46
+
47
+ if ( Syn.support.elementFromPage ) {
48
+ var off = Syn.helpers.scrollOffset(win);
49
+ clientX = clientX + off.left; //convert to pageX
50
+ clientY = clientY + off.top; //convert to pageY
51
+ }
52
+ el = win.document.elementFromPoint ? win.document.elementFromPoint(clientX, clientY) : element;
53
+ if ( el === win.document.documentElement && (point.clientY < 0 || point.clientX < 0) ) {
54
+ return element;
55
+ } else {
56
+ return el;
57
+ }
58
+ },
59
+ //creates an event at a certain point
60
+ createEventAtPoint = function( event, point, element ) {
61
+ var el = elementFromPoint(point, element)
62
+ Syn.trigger(event, point, el || element)
63
+ return el;
64
+ },
65
+ // creates a mousemove event, but first triggering mouseout / mouseover if appropriate
66
+ mouseMove = function( point, element, last ) {
67
+ var el = elementFromPoint(point, element)
68
+ if ( last != el && el && last ) {
69
+ var options = Syn.helpers.extend({}, point);
70
+ options.relatedTarget = el;
71
+ Syn.trigger("mouseout", options, last);
72
+ options.relatedTarget = last;
73
+ Syn.trigger("mouseover", options, el);
74
+ }
75
+
76
+ Syn.trigger("mousemove", point, el || element)
77
+ return el;
78
+ },
79
+ // start and end are in clientX, clientY
80
+ startMove = function( start, end, duration, element, callback ) {
81
+ var startTime = new Date(),
82
+ distX = end.clientX - start.clientX,
83
+ distY = end.clientY - start.clientY,
84
+ win = Syn.helpers.getWindow(element),
85
+ current = elementFromPoint(start, element),
86
+ cursor = win.document.createElement('div'),
87
+ calls = 0;
88
+ move = function() {
89
+ //get what fraction we are at
90
+ var now = new Date(),
91
+ scrollOffset = Syn.helpers.scrollOffset(win),
92
+ fraction = (calls == 0 ? 0 : now - startTime) / duration,
93
+ options = {
94
+ clientX: distX * fraction + start.clientX,
95
+ clientY: distY * fraction + start.clientY
96
+ };
97
+ calls++;
98
+ if ( fraction < 1 ) {
99
+ Syn.helpers.extend(cursor.style, {
100
+ left: (options.clientX + scrollOffset.left + 2) + "px",
101
+ top: (options.clientY + scrollOffset.top + 2) + "px"
102
+ })
103
+ current = mouseMove(options, element, current)
104
+ setTimeout(arguments.callee, 15)
105
+ }
106
+ else {
107
+ current = mouseMove(end, element, current);
108
+ win.document.body.removeChild(cursor)
109
+ callback();
110
+ }
111
+ }
112
+ Syn.helpers.extend(cursor.style, {
113
+ height: "5px",
114
+ width: "5px",
115
+ backgroundColor: "red",
116
+ position: "absolute",
117
+ zIndex: 19999,
118
+ fontSize: "1px"
119
+ })
120
+ win.document.body.appendChild(cursor)
121
+ move();
122
+ },
123
+ startDrag = function( start, end, duration, element, callback ) {
124
+ createEventAtPoint("mousedown", start, element);
125
+ startMove(start, end, duration, element, function() {
126
+ createEventAtPoint("mouseup", end, element);
127
+ callback();
128
+ })
129
+ },
130
+ center = function( el ) {
131
+ var j = Syn.jquery()(el),
132
+ o = j.offset();
133
+ return {
134
+ pageX: o.left + (j.width() / 2),
135
+ pageY: o.top + (j.height() / 2)
136
+ }
137
+ },
138
+ convertOption = function( option, win, from ) {
139
+ var page = /(\d+)[x ](\d+)/,
140
+ client = /(\d+)X(\d+)/,
141
+ relative = /([+-]\d+)[xX ]([+-]\d+)/
142
+ //check relative "+22x-44"
143
+ if ( typeof option == 'string' && relative.test(option) && from ) {
144
+ var cent = center(from),
145
+ parts = option.match(relative);
146
+ option = {
147
+ pageX: cent.pageX + parseInt(parts[1]),
148
+ pageY: cent.pageY + parseInt(parts[2])
149
+ }
150
+ }
151
+ if ( typeof option == 'string' && page.test(option) ) {
152
+ var parts = option.match(page)
153
+ option = {
154
+ pageX: parseInt(parts[1]),
155
+ pageY: parseInt(parts[2])
156
+ }
157
+ }
158
+ if ( typeof option == 'string' && client.test(option) ) {
159
+ var parts = option.match(client)
160
+ option = {
161
+ clientX: parseInt(parts[1]),
162
+ clientY: parseInt(parts[2])
163
+ }
164
+ }
165
+ if ( typeof option == 'string' ) {
166
+ option = Syn.jquery()(option, win.document)[0];
167
+ }
168
+ if ( option.nodeName ) {
169
+ option = center(option)
170
+ }
171
+ if ( option.pageX ) {
172
+ var off = Syn.helpers.scrollOffset(win);
173
+ option = {
174
+ clientX: option.pageX - off.left,
175
+ clientY: option.pageY - off.top
176
+ }
177
+ }
178
+ return option;
179
+ },
180
+ // if the client chords are not going to be visible ... scroll the page so they will be ...
181
+ adjust = function(from, to, win){
182
+ if(from.clientY < 0){
183
+ var off = Syn.helpers.scrollOffset(win);
184
+ var dimensions = Syn.helpers.scrollDimensions(win),
185
+ top = off.top + (from.clientY) - 100,
186
+ diff = top - off.top
187
+
188
+ // first, lets see if we can scroll 100 px
189
+ if( top > 0){
190
+
191
+ } else {
192
+ top =0;
193
+ diff = -off.top;
194
+ }
195
+ console.log("moving", from.clientY,from.clientY - diff, off.top, top )
196
+ from.clientY = from.clientY - diff;
197
+ to.clientY = to.clientY - diff;
198
+ Syn.helpers.scrollOffset(win,{top: top, left: off.left});
199
+
200
+ //throw "out of bounds!"
201
+ }
202
+ }
203
+ /**
204
+ * @add Syn prototype
205
+ */
206
+ Syn.helpers.extend(Syn.init.prototype, {
207
+ /**
208
+ * @function move
209
+ * Moves the cursor from one point to another.
210
+ *
211
+ * ### Quick Example
212
+ *
213
+ * The following moves the cursor from (0,0) in
214
+ * the window to (100,100) in 1 second.
215
+ *
216
+ * Syn.move(
217
+ * {
218
+ * from: {clientX: 0, clientY: 0},
219
+ * to: {clientX: 100, clientY: 100},
220
+ * duration: 1000
221
+ * },
222
+ * document.document)
223
+ *
224
+ * ## Options
225
+ *
226
+ * There are many ways to configure the endpoints of the move.
227
+ *
228
+ * ### PageX and PageY
229
+ *
230
+ * If you pass pageX or pageY, these will get converted
231
+ * to client coordinates.
232
+ *
233
+ * Syn.move(
234
+ * {
235
+ * from: {pageX: 0, pageY: 0},
236
+ * to: {pageX: 100, pageY: 100}
237
+ * },
238
+ * document.document)
239
+ *
240
+ * ### String Coordinates
241
+ *
242
+ * You can set the pageX and pageY as strings like:
243
+ *
244
+ * Syn.move(
245
+ * {
246
+ * from: "0x0",
247
+ * to: "100x100"
248
+ * },
249
+ * document.document)
250
+ *
251
+ * ### Element Coordinates
252
+ *
253
+ * If jQuery is present, you can pass an element as the from or to option
254
+ * and the coordinate will be set as the center of the element.
255
+
256
+ * Syn.move(
257
+ * {
258
+ * from: $(".recipe")[0],
259
+ * to: $("#trash")[0]
260
+ * },
261
+ * document.document)
262
+ *
263
+ * ### Query Strings
264
+ *
265
+ * If jQuery is present, you can pass a query string as the from or to option.
266
+ *
267
+ * Syn.move(
268
+ * {
269
+ * from: ".recipe",
270
+ * to: "#trash"
271
+ * },
272
+ * document.document)
273
+ *
274
+ * ### No From
275
+ *
276
+ * If you don't provide a from, the element argument passed to Syn is used.
277
+ *
278
+ * Syn.move(
279
+ * { to: "#trash" },
280
+ * 'myrecipe')
281
+ *
282
+ * ### Relative
283
+ *
284
+ * You can move the drag relative to the center of the from element.
285
+ *
286
+ * Syn.move("+20 +30", "myrecipe");
287
+ *
288
+ * @param {Object} options options to configure the drag
289
+ * @param {HTMLElement} from the element to move
290
+ * @param {Function} callback a callback that happens after the drag motion has completed
291
+ */
292
+ _move: function( options, from, callback ) {
293
+ //need to convert if elements
294
+ var win = Syn.helpers.getWindow(from),
295
+ fro = convertOption(options.from || from, win, from),
296
+ to = convertOption(options.to || options, win, from);
297
+
298
+ options.adjust !== false && adjust(fro, to, win);
299
+ startMove(fro, to, options.duration || 500, from, callback);
300
+
301
+ },
302
+ /**
303
+ * @function drag
304
+ * Creates a mousedown and drags from one point to another.
305
+ * Check out [Syn.prototype.move move] for API details.
306
+ *
307
+ * @param {Object} options
308
+ * @param {Object} from
309
+ * @param {Object} callback
310
+ */
311
+ _drag: function( options, from, callback ) {
312
+ //need to convert if elements
313
+ var win = Syn.helpers.getWindow(from),
314
+ fro = convertOption(options.from || from, win, from),
315
+ to = convertOption(options.to || options, win, from);
316
+
317
+ options.adjust !== false && adjust(fro, to, win);
318
+ startDrag(fro, to, options.duration || 500, from, callback);
319
+
320
+ }
321
+ })
322
+ }());
@@ -0,0 +1,905 @@
1
+ steal.then(function(){
2
+ var h = Syn.helpers,
3
+ S = Syn,
4
+
5
+ // gets the selection of an input or textarea
6
+ getSelection = function( el ) {
7
+ // use selectionStart if we can
8
+ if ( el.selectionStart !== undefined ) {
9
+ // this is for opera, so we don't have to focus to type how we think we would
10
+ if ( document.activeElement && document.activeElement != el && el.selectionStart == el.selectionEnd && el.selectionStart == 0 ) {
11
+ return {
12
+ start: el.value.length,
13
+ end: el.value.length
14
+ };
15
+ }
16
+ return {
17
+ start: el.selectionStart,
18
+ end: el.selectionEnd
19
+ }
20
+ } else {
21
+ //check if we aren't focused
22
+ try {
23
+ //try 2 different methods that work differently (IE breaks depending on type)
24
+ if ( el.nodeName.toLowerCase() == 'input' ) {
25
+ var real = h.getWindow(el).document.selection.createRange(),
26
+ r = el.createTextRange();
27
+ r.setEndPoint("EndToStart", real);
28
+
29
+ var start = r.text.length
30
+ return {
31
+ start: start,
32
+ end: start + real.text.length
33
+ }
34
+ }
35
+ else {
36
+ var real = h.getWindow(el).document.selection.createRange(),
37
+ r = real.duplicate(),
38
+ r2 = real.duplicate(),
39
+ r3 = real.duplicate();
40
+ r2.collapse();
41
+ r3.collapse(false);
42
+ r2.moveStart('character', -1)
43
+ r3.moveStart('character', -1)
44
+ //select all of our element
45
+ r.moveToElementText(el)
46
+ //now move our endpoint to the end of our real range
47
+ r.setEndPoint('EndToEnd', real);
48
+ var start = r.text.length - real.text.length,
49
+ end = r.text.length;
50
+ if ( start != 0 && r2.text == "" ) {
51
+ start += 2;
52
+ }
53
+ if ( end != 0 && r3.text == "" ) {
54
+ end += 2;
55
+ }
56
+ //if we aren't at the start, but previous is empty, we are at start of newline
57
+ return {
58
+ start: start,
59
+ end: end
60
+ }
61
+ }
62
+ } catch (e) {
63
+ return {
64
+ start: el.value.length,
65
+ end: el.value.length
66
+ };
67
+ }
68
+ }
69
+ },
70
+ // gets all focusable elements
71
+ getFocusable = function( el ) {
72
+ var document = h.getWindow(el).document,
73
+ res = [];
74
+
75
+ var els = document.getElementsByTagName('*'),
76
+ len = els.length;
77
+
78
+ for ( var i = 0; i < len; i++ ) {
79
+ Syn.isFocusable(els[i]) && els[i] != document.documentElement && res.push(els[i])
80
+ }
81
+ return res;
82
+
83
+
84
+ };
85
+
86
+ /**
87
+ * @add Syn static
88
+ */
89
+ h.extend(Syn, {
90
+ /**
91
+ * @attribute
92
+ * A list of the keys and their keycodes codes you can type.
93
+ * You can add type keys with
94
+ * @codestart
95
+ * Syn('key','delete','title');
96
+ *
97
+ * //or
98
+ *
99
+ * Syn('type','One Two Three[left][left][delete]','title')
100
+ * @codeend
101
+ *
102
+ * The following are a list of keys you can type:
103
+ * @codestart text
104
+ * \b - backspace
105
+ * \t - tab
106
+ * \r - enter
107
+ * ' ' - space
108
+ * a-Z 0-9 - normal characters
109
+ * /!@#$*,.? - All other typeable characters
110
+ * page-up - scrolls up
111
+ * page-down - scrolls down
112
+ * end - scrolls to bottom
113
+ * home - scrolls to top
114
+ * insert - changes how keys are entered
115
+ * delete - deletes the next character
116
+ * left - moves cursor left
117
+ * right - moves cursor right
118
+ * up - moves the cursor up
119
+ * down - moves the cursor down
120
+ * f1-12 - function buttons
121
+ * shift, ctrl, alt - special keys
122
+ * pause-break - the pause button
123
+ * scroll-lock - locks scrolling
124
+ * caps - makes caps
125
+ * escape - escape button
126
+ * num-lock - allows numbers on keypad
127
+ * print - screen capture
128
+ * @codeend
129
+ */
130
+ keycodes: {
131
+ //backspace
132
+ '\b': '8',
133
+
134
+ //tab
135
+ '\t': '9',
136
+
137
+ //enter
138
+ '\r': '13',
139
+
140
+ //special
141
+ 'shift': '16',
142
+ 'ctrl': '17',
143
+ 'alt': '18',
144
+
145
+ //weird
146
+ 'pause-break': '19',
147
+ 'caps': '20',
148
+ 'escape': '27',
149
+ 'num-lock': '144',
150
+ 'scroll-lock': '145',
151
+ 'print': '44',
152
+
153
+ //navigation
154
+ 'page-up': '33',
155
+ 'page-down': '34',
156
+ 'end': '35',
157
+ 'home': '36',
158
+ 'left': '37',
159
+ 'up': '38',
160
+ 'right': '39',
161
+ 'down': '40',
162
+ 'insert': '45',
163
+ 'delete': '46',
164
+
165
+ //normal characters
166
+ ' ': '32',
167
+ '0': '48',
168
+ '1': '49',
169
+ '2': '50',
170
+ '3': '51',
171
+ '4': '52',
172
+ '5': '53',
173
+ '6': '54',
174
+ '7': '55',
175
+ '8': '56',
176
+ '9': '57',
177
+ 'a': '65',
178
+ 'b': '66',
179
+ 'c': '67',
180
+ 'd': '68',
181
+ 'e': '69',
182
+ 'f': '70',
183
+ 'g': '71',
184
+ 'h': '72',
185
+ 'i': '73',
186
+ 'j': '74',
187
+ 'k': '75',
188
+ 'l': '76',
189
+ 'm': '77',
190
+ 'n': '78',
191
+ 'o': '79',
192
+ 'p': '80',
193
+ 'q': '81',
194
+ 'r': '82',
195
+ 's': '83',
196
+ 't': '84',
197
+ 'u': '85',
198
+ 'v': '86',
199
+ 'w': '87',
200
+ 'x': '88',
201
+ 'y': '89',
202
+ 'z': '90',
203
+ //normal-characters, numpad
204
+ 'num0': '96',
205
+ 'num1': '97',
206
+ 'num2': '98',
207
+ 'num3': '99',
208
+ 'num4': '100',
209
+ 'num5': '101',
210
+ 'num6': '102',
211
+ 'num7': '103',
212
+ 'num8': '104',
213
+ 'num9': '105',
214
+ '*': '106',
215
+ '+': '107',
216
+ '-': '109',
217
+ '.': '110',
218
+ //normal-characters, others
219
+ '/': '111',
220
+ ';': '186',
221
+ '=': '187',
222
+ ',': '188',
223
+ '-': '189',
224
+ '.': '190',
225
+ '/': '191',
226
+ '`': '192',
227
+ '[': '219',
228
+ '\\': '220',
229
+ ']': '221',
230
+ "'": '222',
231
+
232
+ //ignore these, you shouldn't use them
233
+ 'left window key': '91',
234
+ 'right window key': '92',
235
+ 'select key': '93',
236
+
237
+
238
+ 'f1': '112',
239
+ 'f2': '113',
240
+ 'f3': '114',
241
+ 'f4': '115',
242
+ 'f5': '116',
243
+ 'f6': '117',
244
+ 'f7': '118',
245
+ 'f8': '119',
246
+ 'f9': '120',
247
+ 'f10': '121',
248
+ 'f11': '122',
249
+ 'f12': '123'
250
+ },
251
+
252
+ // what we can type in
253
+ typeable: /input|textarea/i,
254
+
255
+ // selects text on an element
256
+ selectText: function( el, start, end ) {
257
+ if ( el.setSelectionRange ) {
258
+ if (!end ) {
259
+ el.focus();
260
+ el.setSelectionRange(start, start);
261
+ } else {
262
+ el.selectionStart = start;
263
+ el.selectionEnd = end;
264
+ }
265
+ } else if ( el.createTextRange ) {
266
+ //el.focus();
267
+ var r = el.createTextRange();
268
+ r.moveStart('character', start);
269
+ end = end || start;
270
+ r.moveEnd('character', end - el.value.length);
271
+
272
+ r.select();
273
+ }
274
+ },
275
+ getText: function( el ) {
276
+ //first check if the el has anything selected ..
277
+ if ( Syn.typeable.test(el.nodeName) ) {
278
+ var sel = getSelection(el);
279
+ return el.value.substring(sel.start, sel.end)
280
+ }
281
+ //otherwise get from page
282
+ var win = Syn.helpers.getWindow(el);
283
+ if ( win.getSelection ) {
284
+ return win.getSelection().toString();
285
+ }
286
+ else if ( win.document.getSelection ) {
287
+ return win.document.getSelection().toString()
288
+ }
289
+ else {
290
+ return win.document.selection.createRange().text;
291
+ }
292
+ },
293
+ getSelection: getSelection
294
+ });
295
+
296
+ h.extend(Syn.key, {
297
+ // retrieves a description of what events for this character should look like
298
+ data: function( key ) {
299
+ //check if it is described directly
300
+ if ( S.key.browser[key] ) {
301
+ return S.key.browser[key];
302
+ }
303
+ for ( var kind in S.key.kinds ) {
304
+ if ( h.inArray(key, S.key.kinds[kind]) > -1 ) {
305
+ return S.key.browser[kind]
306
+ }
307
+ }
308
+ return S.key.browser.character
309
+ },
310
+
311
+ //returns the special key if special
312
+ isSpecial: function( keyCode ) {
313
+ var specials = S.key.kinds.special;
314
+ for ( var i = 0; i < specials.length; i++ ) {
315
+ if ( Syn.keycodes[specials[i]] == keyCode ) {
316
+ return specials[i];
317
+ }
318
+ }
319
+ },
320
+ /**
321
+ * @hide
322
+ * gets the options for a key and event type ...
323
+ * @param {Object} key
324
+ * @param {Object} event
325
+ */
326
+ options: function( key, event ) {
327
+ var keyData = Syn.key.data(key);
328
+
329
+ if (!keyData[event] ) {
330
+ //we shouldn't be creating this event
331
+ return null;
332
+ }
333
+
334
+ var charCode = keyData[event][0],
335
+ keyCode = keyData[event][1],
336
+ result = {};
337
+
338
+ if ( keyCode == 'key' ) {
339
+ result.keyCode = Syn.keycodes[key]
340
+ } else if ( keyCode == 'char' ) {
341
+ result.keyCode = key.charCodeAt(0)
342
+ } else {
343
+ result.keyCode = keyCode;
344
+ }
345
+
346
+ if ( charCode == 'char' ) {
347
+ result.charCode = key.charCodeAt(0)
348
+ } else if ( charCode !== null ) {
349
+ result.charCode = charCode;
350
+ }
351
+
352
+
353
+ return result
354
+ },
355
+ //types of event keys
356
+ kinds: {
357
+ special: ["shift", 'ctrl', 'alt', 'caps'],
358
+ specialChars: ["\b"],
359
+ navigation: ["page-up", 'page-down', 'end', 'home', 'left', 'up', 'right', 'down', 'insert', 'delete'],
360
+ 'function': ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12']
361
+ },
362
+ //returns the default function
363
+ getDefault: function( key ) {
364
+ //check if it is described directly
365
+ if ( Syn.key.defaults[key] ) {
366
+ return Syn.key.defaults[key];
367
+ }
368
+ for ( var kind in Syn.key.kinds ) {
369
+ if ( h.inArray(key, Syn.key.kinds[kind]) > -1 && Syn.key.defaults[kind] ) {
370
+ return Syn.key.defaults[kind];
371
+ }
372
+ }
373
+ return Syn.key.defaults.character
374
+ },
375
+ // default behavior when typing
376
+ defaults: {
377
+ 'character': function( options, scope, key, force, sel ) {
378
+ if (/num\d+/.test(key) ) {
379
+ key = key.match(/\d+/)[0]
380
+ }
381
+
382
+ if ( force || (!S.support.keyCharacters && Syn.typeable.test(this.nodeName)) ) {
383
+ var current = this.value,
384
+ before = current.substr(0, sel.start),
385
+ after = current.substr(sel.end),
386
+ character = key;
387
+
388
+ this.value = before + character + after;
389
+ //handle IE inserting \r\n
390
+ var charLength = character == "\n" && S.support.textareaCarriage ? 2 : character.length;
391
+ Syn.selectText(this, before.length + charLength)
392
+ }
393
+ },
394
+ 'c': function( options, scope, key, force, sel ) {
395
+ if ( Syn.key.ctrlKey ) {
396
+ Syn.key.clipboard = Syn.getText(this)
397
+ } else {
398
+ Syn.key.defaults.character.apply(this, arguments);
399
+ }
400
+ },
401
+ 'v': function( options, scope, key, force, sel ) {
402
+ if ( Syn.key.ctrlKey ) {
403
+ Syn.key.defaults.character.call(this, options, scope, Syn.key.clipboard, true, sel);
404
+ } else {
405
+ Syn.key.defaults.character.apply(this, arguments);
406
+ }
407
+ },
408
+ 'a': function( options, scope, key, force, sel ) {
409
+ if ( Syn.key.ctrlKey ) {
410
+ Syn.selectText(this, 0, this.value.length)
411
+ } else {
412
+ Syn.key.defaults.character.apply(this, arguments);
413
+ }
414
+ },
415
+ 'home': function() {
416
+ Syn.onParents(this, function( el ) {
417
+ if ( el.scrollHeight != el.clientHeight ) {
418
+ el.scrollTop = 0;
419
+ return false;
420
+ }
421
+ })
422
+ },
423
+ 'end': function() {
424
+ Syn.onParents(this, function( el ) {
425
+ if ( el.scrollHeight != el.clientHeight ) {
426
+ el.scrollTop = el.scrollHeight;
427
+ return false;
428
+ }
429
+ })
430
+ },
431
+ 'page-down': function() {
432
+ //find the first parent we can scroll
433
+ Syn.onParents(this, function( el ) {
434
+ if ( el.scrollHeight != el.clientHeight ) {
435
+ var ch = el.clientHeight
436
+ el.scrollTop += ch;
437
+ return false;
438
+ }
439
+ })
440
+ },
441
+ 'page-up': function() {
442
+ Syn.onParents(this, function( el ) {
443
+ if ( el.scrollHeight != el.clientHeight ) {
444
+ var ch = el.clientHeight
445
+ el.scrollTop -= ch;
446
+ return false;
447
+ }
448
+ })
449
+ },
450
+ '\b': function( options, scope, key, force, sel ) {
451
+ //this assumes we are deleting from the end
452
+ if (!S.support.backspaceWorks && Syn.typeable.test(this.nodeName) ) {
453
+ var current = this.value,
454
+ before = current.substr(0, sel.start),
455
+ after = current.substr(sel.end);
456
+
457
+ if ( sel.start == sel.end && sel.start > 0 ) {
458
+ //remove a character
459
+ this.value = before.substring(0, before.length - 1) + after
460
+ Syn.selectText(this, sel.start - 1)
461
+ } else {
462
+ this.value = before + after;
463
+ Syn.selectText(this, sel.start)
464
+ }
465
+
466
+ //set back the selection
467
+ }
468
+ },
469
+ 'delete': function( options, scope, key, force, sel ) {
470
+ if (!S.support.backspaceWorks && Syn.typeable.test(this.nodeName) ) {
471
+ var current = this.value,
472
+ before = current.substr(0, sel.start),
473
+ after = current.substr(sel.end);
474
+ if ( sel.start == sel.end && sel.start <= this.value.length - 1 ) {
475
+ this.value = before + after.substring(1)
476
+ } else {
477
+ this.value = before + after;
478
+
479
+ }
480
+ Syn.selectText(this, sel.start)
481
+ }
482
+ },
483
+ '\r': function( options, scope, key, force, sel ) {
484
+
485
+ var nodeName = this.nodeName.toLowerCase()
486
+ // submit a form
487
+ if (!S.support.keypressSubmits && nodeName == 'input' ) {
488
+ var form = Syn.closest(this, "form");
489
+ if ( form ) {
490
+ Syn.trigger("submit", {}, form);
491
+ }
492
+
493
+ }
494
+ //newline in textarea
495
+ if (!S.support.keyCharacters && nodeName == 'textarea' ) {
496
+ Syn.key.defaults.character.call(this, options, scope, "\n", undefined, sel)
497
+ }
498
+ // 'click' hyperlinks
499
+ if (!S.support.keypressOnAnchorClicks && nodeName == 'a' ) {
500
+ Syn.trigger("click", {}, this);
501
+ }
502
+ },
503
+ //
504
+ // Gets all focusable elements. If the element (this)
505
+ // doesn't have a tabindex, finds the next element after.
506
+ // If the element (this) has a tabindex finds the element
507
+ // with the next higher tabindex OR the element with the same
508
+ // tabindex after it in the document.
509
+ // @return the next element
510
+ //
511
+ '\t': function( options, scope ) {
512
+ // focusable elements
513
+ var focusEls = getFocusable(this),
514
+ // the current element's tabindex
515
+ tabIndex = Syn.tabIndex(this),
516
+ // will be set to our guess for the next element
517
+ current = null,
518
+ // the next index we care about
519
+ currentIndex = 1000000000,
520
+ // set to true once we found 'this' element
521
+ found = false,
522
+ i = 0,
523
+ el,
524
+ //the tabindex of the tabable element we are looking at
525
+ elIndex, firstNotIndexed, prev;
526
+ orders = [];
527
+ for (; i < focusEls.length; i++ ) {
528
+ orders.push([focusEls[i], i]);
529
+ }
530
+ var sort = function( order1, order2 ) {
531
+ var el1 = order1[0],
532
+ el2 = order2[0],
533
+ tab1 = Syn.tabIndex(el1) || 0,
534
+ tab2 = Syn.tabIndex(el2) || 0;
535
+ if ( tab1 == tab2 ) {
536
+ return order1[1] - order2[1]
537
+ } else {
538
+ if ( tab1 == 0 ) {
539
+ return 1;
540
+ } else if ( tab2 == 0 ) {
541
+ return -1;
542
+ } else {
543
+ return tab1 - tab2;
544
+ }
545
+ }
546
+ }
547
+ orders.sort(sort);
548
+ //now find current
549
+ for ( i = 0; i < orders.length; i++ ) {
550
+ el = orders[i][0];
551
+ if ( this == el ) {
552
+ if (!Syn.key.shiftKey ) {
553
+ current = orders[i + 1][0];
554
+ if (!current ) {
555
+ current = orders[0][0]
556
+ }
557
+ } else {
558
+ current = orders[i - 1][0];
559
+ if (!current ) {
560
+ current = orders[focusEls.length - 1][0]
561
+ }
562
+ }
563
+
564
+ }
565
+ }
566
+
567
+ //restart if we didn't find anything
568
+ if (!current ) {
569
+ current = firstNotIndexed;
570
+ }
571
+ current && current.focus();
572
+ return current;
573
+ },
574
+ 'left': function( options, scope, key, force, sel ) {
575
+ if ( Syn.typeable.test(this.nodeName) ) {
576
+ if ( Syn.key.shiftKey ) {
577
+ Syn.selectText(this, sel.start == 0 ? 0 : sel.start - 1, sel.end)
578
+ } else {
579
+ Syn.selectText(this, sel.start == 0 ? 0 : sel.start - 1)
580
+ }
581
+ }
582
+ },
583
+ 'right': function( options, scope, key, force, sel ) {
584
+ if ( Syn.typeable.test(this.nodeName) ) {
585
+ if ( Syn.key.shiftKey ) {
586
+ Syn.selectText(this, sel.start, sel.end + 1 > this.value.length ? this.value.length : sel.end + 1)
587
+ } else {
588
+ Syn.selectText(this, sel.end + 1 > this.value.length ? this.value.length : sel.end + 1)
589
+ }
590
+ }
591
+ },
592
+ 'up': function() {
593
+ if (/select/i.test(this.nodeName) ) {
594
+
595
+ this.selectedIndex = this.selectedIndex ? this.selectedIndex - 1 : 0;
596
+ //set this to change on blur?
597
+ }
598
+ },
599
+ 'down': function() {
600
+ if (/select/i.test(this.nodeName) ) {
601
+ Syn.changeOnBlur(this, "selectedIndex", this.selectedIndex)
602
+ this.selectedIndex = this.selectedIndex + 1;
603
+ //set this to change on blur?
604
+ }
605
+ },
606
+ 'shift': function() {
607
+ return null;
608
+ }
609
+ }
610
+ });
611
+
612
+
613
+ h.extend(Syn.create, {
614
+ keydown: {
615
+ setup: function( type, options, element ) {
616
+ if ( h.inArray(options, Syn.key.kinds.special) != -1 ) {
617
+ Syn.key[options + "Key"] = element;
618
+ }
619
+ }
620
+ },
621
+ keypress: {
622
+ setup: function( type, options, element ) {
623
+ // if this browsers supports writing keys on events
624
+ // but doesn't write them if the element isn't focused
625
+ // focus on the element (ignored if already focused)
626
+ if ( S.support.keyCharacters && !S.support.keysOnNotFocused ) {
627
+ element.focus()
628
+ }
629
+ }
630
+ },
631
+ keyup: {
632
+ setup: function( type, options, element ) {
633
+ if ( h.inArray(options, Syn.key.kinds.special) != -1 ) {
634
+ Syn.key[options + "Key"] = null;
635
+ }
636
+ }
637
+ },
638
+ key: {
639
+ // return the options for a key event
640
+ options: function( type, options, element ) {
641
+ //check if options is character or has character
642
+ options = typeof options != "object" ? {
643
+ character: options
644
+ } : options;
645
+
646
+ //don't change the orignial
647
+ options = h.extend({}, options)
648
+ if ( options.character ) {
649
+ h.extend(options, S.key.options(options.character, type));
650
+ delete options.character;
651
+ }
652
+
653
+ options = h.extend({
654
+ ctrlKey: !! Syn.key.ctrlKey,
655
+ altKey: !! Syn.key.altKey,
656
+ shiftKey: !! Syn.key.shiftKey,
657
+ metaKey: !! Syn.key.metaKey
658
+ }, options)
659
+
660
+ return options;
661
+ },
662
+ // creates a key event
663
+ event: function( type, options, element ) { //Everyone Else
664
+ var doc = h.getWindow(element).document || document;
665
+ if ( doc.createEvent ) {
666
+ var event;
667
+
668
+ try {
669
+
670
+ event = doc.createEvent("KeyEvents");
671
+ event.initKeyEvent(type, true, true, window, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode);
672
+ }
673
+ catch (e) {
674
+ event = h.createBasicStandardEvent(type, options, doc);
675
+ }
676
+ event.synthetic = true;
677
+ return event;
678
+ }
679
+ else {
680
+ var event;
681
+ try {
682
+ event = h.createEventObject.apply(this, arguments);
683
+ h.extend(event, options)
684
+ }
685
+ catch (e) {}
686
+
687
+ return event;
688
+ }
689
+ }
690
+ }
691
+ });
692
+
693
+ var convert = {
694
+ "enter": "\r",
695
+ "backspace": "\b",
696
+ "tab": "\t",
697
+ "space": " "
698
+ }
699
+
700
+ /**
701
+ * @add Syn prototype
702
+ */
703
+ h.extend(Syn.init.prototype, {
704
+ /**
705
+ * @function key
706
+ * Types a single key. The key should be
707
+ * a string that matches a
708
+ * [Syn.static.keycodes].
709
+ *
710
+ * The following sends a carridge return
711
+ * to the 'name' element.
712
+ * @codestart
713
+ * Syn.key('\r','name')
714
+ * @codeend
715
+ * For each character, a keydown, keypress, and keyup is triggered if
716
+ * appropriate.
717
+ * @param {String} options
718
+ * @param {HTMLElement} [element]
719
+ * @param {Function} [callback]
720
+ * @return {HTMLElement} the element currently focused.
721
+ */
722
+ _key: function( options, element, callback ) {
723
+ //first check if it is a special up
724
+ if (/-up$/.test(options) && h.inArray(options.replace("-up", ""), Syn.key.kinds.special) != -1 ) {
725
+ Syn.trigger('keyup', options.replace("-up", ""), element)
726
+ callback(true, element);
727
+ return;
728
+ }
729
+
730
+
731
+ var caret = Syn.typeable.test(element.nodeName) && getSelection(element),
732
+ key = convert[options] || options,
733
+ // should we run default events
734
+ runDefaults = Syn.trigger('keydown', key, element),
735
+
736
+ // a function that gets the default behavior for a key
737
+ getDefault = Syn.key.getDefault,
738
+
739
+ // how this browser handles preventing default events
740
+ prevent = Syn.key.browser.prevent,
741
+
742
+ // the result of the default event
743
+ defaultResult,
744
+
745
+ // options for keypress
746
+ keypressOptions = Syn.key.options(key, 'keypress')
747
+
748
+
749
+ if ( runDefaults ) {
750
+ //if the browser doesn't create keypresses for this key, run default
751
+ if (!keypressOptions ) {
752
+ defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret)
753
+ } else {
754
+ //do keypress
755
+ runDefaults = Syn.trigger('keypress', keypressOptions, element)
756
+ if ( runDefaults ) {
757
+ defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret)
758
+ }
759
+ }
760
+ } else {
761
+ //canceled ... possibly don't run keypress
762
+ if ( keypressOptions && h.inArray('keypress', prevent.keydown) == -1 ) {
763
+ Syn.trigger('keypress', keypressOptions, element)
764
+ }
765
+ }
766
+ if ( defaultResult && defaultResult.nodeName ) {
767
+ element = defaultResult
768
+ }
769
+
770
+ if ( defaultResult !== null ) {
771
+ setTimeout(function() {
772
+ Syn.trigger('keyup', Syn.key.options(key, 'keyup'), element)
773
+ callback(runDefaults, element)
774
+ }, 1)
775
+ } else {
776
+ callback(runDefaults, element)
777
+ }
778
+
779
+
780
+ //do mouseup
781
+ return element;
782
+ // is there a keypress? .. if not , run default
783
+ // yes -> did we prevent it?, if not run ...
784
+ },
785
+ /**
786
+ * @function type
787
+ * Types sequence of [Syn.key key actions]. Each
788
+ * character is typed, one at a type.
789
+ * Multi-character keys like 'left' should be
790
+ * enclosed in square brackents.
791
+ *
792
+ * The following types 'JavaScript MVC' then deletes the space.
793
+ * @codestart
794
+ * Syn.type('JavaScript MVC[left][left][left]\b','name')
795
+ * @codeend
796
+ *
797
+ * Type is able to handle (and move with) tabs (\t).
798
+ * The following simulates tabing and entering values in a form and
799
+ * eventually submitting the form.
800
+ * @codestart
801
+ * Syn.type("Justin\tMeyer\t27\tjustinbmeyer@gmail.com\r")
802
+ * @codeend
803
+ * @param {String} options the text to type
804
+ * @param {HTMLElement} [element] an element or an id of an element
805
+ * @param {Function} [callback] a function to callback
806
+ */
807
+ _type: function( options, element, callback ) {
808
+ //break it up into parts ...
809
+ //go through each type and run
810
+ var parts = options.match(/(\[[^\]]+\])|([^\[])/g),
811
+ self = this,
812
+ runNextPart = function( runDefaults, el ) {
813
+ var part = parts.shift();
814
+ if (!part ) {
815
+ callback(runDefaults, el);
816
+ return;
817
+ }
818
+ el = el || element;
819
+ if ( part.length > 1 ) {
820
+ part = part.substr(1, part.length - 2)
821
+ }
822
+ self._key(part, el, runNextPart)
823
+ }
824
+
825
+ runNextPart();
826
+
827
+ }
828
+ });
829
+
830
+
831
+ //do support code
832
+ (function() {
833
+ if (!document.body ) {
834
+ setTimeout(arguments.callee, 1)
835
+ return;
836
+ }
837
+
838
+ var div = document.createElement("div"),
839
+ checkbox, submit, form, input, submitted = false,
840
+ anchor, textarea, inputter;
841
+
842
+ div.innerHTML = "<form id='outer'>" +
843
+ "<input name='checkbox' type='checkbox'/>" +
844
+ "<input name='radio' type='radio' />" +
845
+ "<input type='submit' name='submitter'/>" +
846
+ "<input type='input' name='inputter'/>" +
847
+ "<input name='one'>" +
848
+ "<input name='two'/>" +
849
+ "<a href='#abc'></a>" +
850
+ "<textarea>1\n2</textarea>" +
851
+ "</form>";
852
+
853
+ document.documentElement.appendChild(div);
854
+ form = div.firstChild;
855
+ checkbox = form.childNodes[0];
856
+ submit = form.childNodes[2];
857
+ anchor = form.getElementsByTagName("a")[0];
858
+ textarea = form.getElementsByTagName("textarea")[0];
859
+ inputter = form.childNodes[3];
860
+
861
+ form.onsubmit = function( ev ) {
862
+ if ( ev.preventDefault ) ev.preventDefault();
863
+ S.support.keypressSubmits = true;
864
+ ev.returnValue = false;
865
+ return false;
866
+ };
867
+ // Firefox 4 won't write key events if the element isn't focused
868
+ inputter.focus();
869
+ Syn.trigger("keypress", "\r", inputter);
870
+
871
+
872
+ Syn.trigger("keypress", "a", inputter);
873
+ S.support.keyCharacters = inputter.value == "a";
874
+
875
+
876
+ inputter.value = "a";
877
+ Syn.trigger("keypress", "\b", inputter);
878
+ S.support.backspaceWorks = inputter.value == "";
879
+
880
+
881
+
882
+ inputter.onchange = function() {
883
+ S.support.focusChanges = true;
884
+ }
885
+ inputter.focus();
886
+ Syn.trigger("keypress", "a", inputter);
887
+ form.childNodes[5].focus(); // this will throw a change event
888
+ Syn.trigger("keypress", "b", inputter);
889
+ S.support.keysOnNotFocused = inputter.value == "ab";
890
+
891
+ //test keypress \r on anchor submits
892
+ S.bind(anchor, "click", function( ev ) {
893
+ if ( ev.preventDefault ) ev.preventDefault();
894
+ S.support.keypressOnAnchorClicks = true;
895
+ ev.returnValue = false;
896
+ return false;
897
+ })
898
+ Syn.trigger("keypress", "\r", anchor);
899
+
900
+ S.support.textareaCarriage = textarea.value.length == 4
901
+ document.documentElement.removeChild(div);
902
+
903
+ S.support.ready++;
904
+ })();
905
+ })