(function() { var h = Syn.helpers, S = Syn, // gets the selection of an input or textarea getSelection = function( el ) { // use selectionStart if we can if ( el.selectionStart !== undefined ) { // this is for opera, so we don't have to focus to type how we think we would if ( document.activeElement && document.activeElement != el && el.selectionStart == el.selectionEnd && el.selectionStart == 0 ) { return { start: el.value.length, end: el.value.length }; } return { start: el.selectionStart, end: el.selectionEnd } } else { //check if we aren't focused try { //try 2 different methods that work differently (IE breaks depending on type) if ( el.nodeName.toLowerCase() == 'input' ) { var real = h.getWindow(el).document.selection.createRange(), r = el.createTextRange(); r.setEndPoint("EndToStart", real); var start = r.text.length return { start: start, end: start + real.text.length } } else { var real = h.getWindow(el).document.selection.createRange(), r = real.duplicate(), r2 = real.duplicate(), r3 = real.duplicate(); r2.collapse(); r3.collapse(false); r2.moveStart('character', -1) r3.moveStart('character', -1) //select all of our element r.moveToElementText(el) //now move our endpoint to the end of our real range r.setEndPoint('EndToEnd', real); var start = r.text.length - real.text.length, end = r.text.length; if ( start != 0 && r2.text == "" ) { start += 2; } if ( end != 0 && r3.text == "" ) { end += 2; } //if we aren't at the start, but previous is empty, we are at start of newline return { start: start, end: end } } } catch (e) { return { start: el.value.length, end: el.value.length }; } } }, // gets all focusable elements getFocusable = function( el ) { var document = h.getWindow(el).document, res = []; var els = document.getElementsByTagName('*'), len = els.length; for ( var i = 0; i < len; i++ ) { Syn.isFocusable(els[i]) && els[i] != document.documentElement && res.push(els[i]) } return res; }; /** * @add Syn static */ h.extend(Syn, { /** * @attribute * A list of the keys and their keycodes codes you can type. * You can add type keys with * @codestart * Syn('key','delete','title'); * * //or * * Syn('type','One Two Three[left][left][delete]','title') * @codeend * * The following are a list of keys you can type: * @codestart text * \b - backspace * \t - tab * \r - enter * ' ' - space * a-Z 0-9 - normal characters * /!@#$*,.? - All other typeable characters * page-up - scrolls up * page-down - scrolls down * end - scrolls to bottom * home - scrolls to top * insert - changes how keys are entered * delete - deletes the next character * left - moves cursor left * right - moves cursor right * up - moves the cursor up * down - moves the cursor down * f1-12 - function buttons * shift, ctrl, alt - special keys * pause-break - the pause button * scroll-lock - locks scrolling * caps - makes caps * escape - escape button * num-lock - allows numbers on keypad * print - screen capture * @codeend */ keycodes: { //backspace '\b': 8, //tab '\t': 9, //enter '\r': 13, //special 'shift': 16, 'ctrl': 17, 'alt': 18, //weird 'pause-break': 19, 'caps': 20, 'escape': 27, 'num-lock': 144, 'scroll-lock': 145, 'print': 44, //navigation 'page-up': 33, 'page-down': 34, 'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40, 'insert': 45, 'delete': 46, //normal characters ' ': 32, '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57, '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, '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, //normal-characters, numpad 'num0': 96, 'num1': 97, 'num2': 98, 'num3': 99, 'num4': 100, 'num5': 101, 'num6': 102, 'num7': 103, 'num8': 104, 'num9': 105, '*': 106, '+': 107, '-': 109, '.': 110, //normal-characters, others '/': 111, ';': 186, '=': 187, ',': 188, '-': 189, '.': 190, '/': 191, '`': 192, '[': 219, '\\': 220, ']': 221, "'": 222, //ignore these, you shouldn't use them 'left window key': 91, 'right window key': 92, 'select key': 93, 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118, 'f8': 119, 'f9': 120, 'f10': 121, 'f11': 122, 'f12': 123 }, // what we can type in typeable: /input|textarea/i, // selects text on an element selectText: function( el, start, end ) { if ( el.setSelectionRange ) { if (!end ) { el.focus(); el.setSelectionRange(start, start); } else { el.selectionStart = start; el.selectionEnd = end; } } else if ( el.createTextRange ) { //el.focus(); var r = el.createTextRange(); r.moveStart('character', start); end = end || start; r.moveEnd('character', end - el.value.length); r.select(); } }, getText: function( el ) { //first check if the el has anything selected .. if ( Syn.typeable.test(el.nodeName) ) { var sel = getSelection(el); return el.value.substring(sel.start, sel.end) } //otherwise get from page var win = Syn.helpers.getWindow(el); if ( win.getSelection ) { return win.getSelection().toString(); } else if ( win.document.getSelection ) { return win.document.getSelection().toString() } else { return win.document.selection.createRange().text; } }, getSelection: getSelection }); h.extend(Syn.key, { // retrieves a description of what events for this character should look like data: function( key ) { //check if it is described directly if ( S.key.browser[key] ) { return S.key.browser[key]; } for ( var kind in S.key.kinds ) { if ( h.inArray(key, S.key.kinds[kind]) > -1 ) { return S.key.browser[kind] } } return S.key.browser.character }, //returns the special key if special isSpecial: function( keyCode ) { var specials = S.key.kinds.special; for ( var i = 0; i < specials.length; i++ ) { if ( Syn.keycodes[specials[i]] == keyCode ) { return specials[i]; } } }, /** * @hide * gets the options for a key and event type ... * @param {Object} key * @param {Object} event */ options: function( key, event ) { var keyData = Syn.key.data(key); if (!keyData[event] ) { //we shouldn't be creating this event return null; } var charCode = keyData[event][0], keyCode = keyData[event][1], result = {}; if ( keyCode == 'key' ) { result.keyCode = Syn.keycodes[key] } else if ( keyCode == 'char' ) { result.keyCode = key.charCodeAt(0) } else { result.keyCode = keyCode; } if ( charCode == 'char' ) { result.charCode = key.charCodeAt(0) } else if ( charCode !== null ) { result.charCode = charCode; } // all current browsers have which property to normalize keyCode/charCode if(result.keyCode){ result.which = result.keyCode; } else { result.which = result.charCode; } return result }, //types of event keys kinds: { special: ["shift", 'ctrl', 'alt', 'caps'], specialChars: ["\b"], navigation: ["page-up", 'page-down', 'end', 'home', 'left', 'up', 'right', 'down', 'insert', 'delete'], 'function': ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12'] }, //returns the default function // some keys have default functions // some 'kinds' of keys have default functions getDefault: function( key ) { //check if it is described directly if ( Syn.key.defaults[key] ) { return Syn.key.defaults[key]; } for ( var kind in Syn.key.kinds ) { if ( h.inArray(key, Syn.key.kinds[kind]) > -1 && Syn.key.defaults[kind] ) { return Syn.key.defaults[kind]; } } return Syn.key.defaults.character }, // default behavior when typing defaults: { 'character': function( options, scope, key, force, sel ) { if (/num\d+/.test(key) ) { key = key.match(/\d+/)[0] } if ( force || (!S.support.keyCharacters && Syn.typeable.test(this.nodeName)) ) { var current = this.value, before = current.substr(0, sel.start), after = current.substr(sel.end), character = key; this.value = before + character + after; //handle IE inserting \r\n var charLength = character == "\n" && S.support.textareaCarriage ? 2 : character.length; Syn.selectText(this, before.length + charLength) } }, 'c': function( options, scope, key, force, sel ) { if ( Syn.key.ctrlKey ) { Syn.key.clipboard = Syn.getText(this) } else { Syn.key.defaults.character.apply(this, arguments); } }, 'v': function( options, scope, key, force, sel ) { if ( Syn.key.ctrlKey ) { Syn.key.defaults.character.call(this, options, scope, Syn.key.clipboard, true, sel); } else { Syn.key.defaults.character.apply(this, arguments); } }, 'a': function( options, scope, key, force, sel ) { if ( Syn.key.ctrlKey ) { Syn.selectText(this, 0, this.value.length) } else { Syn.key.defaults.character.apply(this, arguments); } }, 'home': function() { Syn.onParents(this, function( el ) { if ( el.scrollHeight != el.clientHeight ) { el.scrollTop = 0; return false; } }) }, 'end': function() { Syn.onParents(this, function( el ) { if ( el.scrollHeight != el.clientHeight ) { el.scrollTop = el.scrollHeight; return false; } }) }, 'page-down': function() { //find the first parent we can scroll Syn.onParents(this, function( el ) { if ( el.scrollHeight != el.clientHeight ) { var ch = el.clientHeight el.scrollTop += ch; return false; } }) }, 'page-up': function() { Syn.onParents(this, function( el ) { if ( el.scrollHeight != el.clientHeight ) { var ch = el.clientHeight el.scrollTop -= ch; return false; } }) }, '\b': function( options, scope, key, force, sel ) { //this assumes we are deleting from the end if (!S.support.backspaceWorks && Syn.typeable.test(this.nodeName) ) { var current = this.value, before = current.substr(0, sel.start), after = current.substr(sel.end); if ( sel.start == sel.end && sel.start > 0 ) { //remove a character this.value = before.substring(0, before.length - 1) + after Syn.selectText(this, sel.start - 1) } else { this.value = before + after; Syn.selectText(this, sel.start) } //set back the selection } }, 'delete': function( options, scope, key, force, sel ) { if (!S.support.backspaceWorks && Syn.typeable.test(this.nodeName) ) { var current = this.value, before = current.substr(0, sel.start), after = current.substr(sel.end); if ( sel.start == sel.end && sel.start <= this.value.length - 1 ) { this.value = before + after.substring(1) } else { this.value = before + after; } Syn.selectText(this, sel.start) } }, '\r': function( options, scope, key, force, sel ) { var nodeName = this.nodeName.toLowerCase() // submit a form if (!S.support.keypressSubmits && nodeName == 'input' ) { var form = Syn.closest(this, "form"); if ( form ) { Syn.trigger("submit", {}, form); } } //newline in textarea if (!S.support.keyCharacters && nodeName == 'textarea' ) { Syn.key.defaults.character.call(this, options, scope, "\n", undefined, sel) } // 'click' hyperlinks if (!S.support.keypressOnAnchorClicks && nodeName == 'a' ) { Syn.trigger("click", {}, this); } }, // // Gets all focusable elements. If the element (this) // doesn't have a tabindex, finds the next element after. // If the element (this) has a tabindex finds the element // with the next higher tabindex OR the element with the same // tabindex after it in the document. // @return the next element // '\t': function( options, scope ) { // focusable elements var focusEls = getFocusable(this), // the current element's tabindex tabIndex = Syn.tabIndex(this), // will be set to our guess for the next element current = null, // the next index we care about currentIndex = 1000000000, // set to true once we found 'this' element found = false, i = 0, el, //the tabindex of the tabable element we are looking at elIndex, firstNotIndexed, prev; orders = []; for (; i < focusEls.length; i++ ) { orders.push([focusEls[i], i]); } var sort = function( order1, order2 ) { var el1 = order1[0], el2 = order2[0], tab1 = Syn.tabIndex(el1) || 0, tab2 = Syn.tabIndex(el2) || 0; if ( tab1 == tab2 ) { return order1[1] - order2[1] } else { if ( tab1 == 0 ) { return 1; } else if ( tab2 == 0 ) { return -1; } else { return tab1 - tab2; } } } orders.sort(sort); //now find current for ( i = 0; i < orders.length; i++ ) { el = orders[i][0]; if ( this == el ) { if (!Syn.key.shiftKey ) { current = orders[i + 1][0]; if (!current ) { current = orders[0][0] } } else { current = orders[i - 1][0]; if (!current ) { current = orders[focusEls.length - 1][0] } } } } //restart if we didn't find anything if (!current ) { current = firstNotIndexed; } current && current.focus(); return current; }, 'left': function( options, scope, key, force, sel ) { if ( Syn.typeable.test(this.nodeName) ) { if ( Syn.key.shiftKey ) { Syn.selectText(this, sel.start == 0 ? 0 : sel.start - 1, sel.end) } else { Syn.selectText(this, sel.start == 0 ? 0 : sel.start - 1) } } }, 'right': function( options, scope, key, force, sel ) { if ( Syn.typeable.test(this.nodeName) ) { if ( Syn.key.shiftKey ) { Syn.selectText(this, sel.start, sel.end + 1 > this.value.length ? this.value.length : sel.end + 1) } else { Syn.selectText(this, sel.end + 1 > this.value.length ? this.value.length : sel.end + 1) } } }, 'up': function() { if (/select/i.test(this.nodeName) ) { this.selectedIndex = this.selectedIndex ? this.selectedIndex - 1 : 0; //set this to change on blur? } }, 'down': function() { if (/select/i.test(this.nodeName) ) { Syn.changeOnBlur(this, "selectedIndex", this.selectedIndex) this.selectedIndex = this.selectedIndex + 1; //set this to change on blur? } }, 'shift': function() { return null; } } }); h.extend(Syn.create, { keydown: { setup: function( type, options, element ) { if ( h.inArray(options, Syn.key.kinds.special) != -1 ) { Syn.key[options + "Key"] = element; } } }, keypress: { setup: function( type, options, element ) { // if this browsers supports writing keys on events // but doesn't write them if the element isn't focused // focus on the element (ignored if already focused) if ( S.support.keyCharacters && !S.support.keysOnNotFocused ) { element.focus() } } }, keyup: { setup: function( type, options, element ) { if ( h.inArray(options, Syn.key.kinds.special) != -1 ) { Syn.key[options + "Key"] = null; } } }, key: { // return the options for a key event options: function( type, options, element ) { //check if options is character or has character options = typeof options != "object" ? { character: options } : options; //don't change the orignial options = h.extend({}, options) if ( options.character ) { h.extend(options, S.key.options(options.character, type)); delete options.character; } options = h.extend({ ctrlKey: !! Syn.key.ctrlKey, altKey: !! Syn.key.altKey, shiftKey: !! Syn.key.shiftKey, metaKey: !! Syn.key.metaKey }, options) return options; }, // creates a key event event: function( type, options, element ) { //Everyone Else var doc = h.getWindow(element).document || document; if ( doc.createEvent ) { var event; try { event = doc.createEvent("KeyEvents"); event.initKeyEvent(type, true, true, window, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode); } catch (e) { event = h.createBasicStandardEvent(type, options, doc); } event.synthetic = true; return event; } else { var event; try { event = h.createEventObject.apply(this, arguments); h.extend(event, options) } catch (e) {} return event; } } } }); var convert = { "enter": "\r", "backspace": "\b", "tab": "\t", "space": " " } /** * @add Syn prototype */ h.extend(Syn.init.prototype, { /** * @function key * Types a single key. The key should be * a string that matches a * [Syn.static.keycodes]. * * The following sends a carridge return * to the 'name' element. * @codestart * Syn.key('\r','name') * @codeend * For each character, a keydown, keypress, and keyup is triggered if * appropriate. * @param {String} options * @param {HTMLElement} [element] * @param {Function} [callback] * @return {HTMLElement} the element currently focused. */ _key: function( options, element, callback ) { //first check if it is a special up if (/-up$/.test(options) && h.inArray(options.replace("-up", ""), Syn.key.kinds.special) != -1 ) { Syn.trigger('keyup', options.replace("-up", ""), element) callback(true, element); return; } // keep reference to current activeElement var activeElement = h.getWindow(element).document.activeElement, caret = Syn.typeable.test(element.nodeName) && getSelection(element), key = convert[options] || options, // should we run default events runDefaults = Syn.trigger('keydown', key, element), // a function that gets the default behavior for a key getDefault = Syn.key.getDefault, // how this browser handles preventing default events prevent = Syn.key.browser.prevent, // the result of the default event defaultResult, keypressOptions = Syn.key.options(key, 'keypress'); if ( runDefaults ) { //if the browser doesn't create keypresses for this key, run default if (!keypressOptions ) { defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret) } else { //do keypress // check if activeElement changed b/c someone called focus in keydown if( activeElement !== h.getWindow(element).document.activeElement ) { element = h.getWindow(element).document.activeElement; } runDefaults = Syn.trigger('keypress', keypressOptions, element) if ( runDefaults ) { defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret) } } } else { //canceled ... possibly don't run keypress if ( keypressOptions && h.inArray('keypress', prevent.keydown) == -1 ) { // check if activeElement changed b/c someone called focus in keydown if( activeElement !== h.getWindow(element).document.activeElement ) { element = h.getWindow(element).document.activeElement; } Syn.trigger('keypress', keypressOptions, element) } } if ( defaultResult && defaultResult.nodeName ) { element = defaultResult } if ( defaultResult !== null ) { setTimeout(function() { Syn.trigger('keyup', Syn.key.options(key, 'keyup'), element) callback(runDefaults, element) }, 1) } else { callback(runDefaults, element) } //do mouseup return element; // is there a keypress? .. if not , run default // yes -> did we prevent it?, if not run ... }, /** * @function type * Types sequence of [Syn.key key actions]. Each * character is typed, one at a type. * Multi-character keys like 'left' should be * enclosed in square brackents. * * The following types 'JavaScript MVC' then deletes the space. * @codestart * Syn.type('JavaScript MVC[left][left][left]\b','name') * @codeend * * Type is able to handle (and move with) tabs (\t). * The following simulates tabing and entering values in a form and * eventually submitting the form. * @codestart * Syn.type("Justin\tMeyer\t27\tjustinbmeyer@gmail.com\r") * @codeend * @param {String} options the text to type * @param {HTMLElement} [element] an element or an id of an element * @param {Function} [callback] a function to callback */ _type: function( options, element, callback ) { //break it up into parts ... //go through each type and run var parts = options.match(/(\[[^\]]+\])|([^\[])/g), self = this, runNextPart = function( runDefaults, el ) { var part = parts.shift(); if (!part ) { callback(runDefaults, el); return; } el = el || element; if ( part.length > 1 ) { part = part.substr(1, part.length - 2) } self._key(part, el, runNextPart) } runNextPart(); } }); //do support code (function() { if (!document.body ) { setTimeout(arguments.callee, 1) return; } var div = document.createElement("div"), checkbox, submit, form, input, submitted = false, anchor, textarea, inputter; div.innerHTML = "<form id='outer'>" + "<input name='checkbox' type='checkbox'/>" + "<input name='radio' type='radio' />" + "<input type='submit' name='submitter'/>" + "<input type='input' name='inputter'/>" + "<input name='one'>" + "<input name='two'/>" + "<a href='#abc'></a>" + "<textarea>1\n2</textarea>" + "</form>"; document.documentElement.appendChild(div); form = div.firstChild; checkbox = form.childNodes[0]; submit = form.childNodes[2]; anchor = form.getElementsByTagName("a")[0]; textarea = form.getElementsByTagName("textarea")[0]; inputter = form.childNodes[3]; form.onsubmit = function( ev ) { if ( ev.preventDefault ) ev.preventDefault(); S.support.keypressSubmits = true; ev.returnValue = false; return false; }; // Firefox 4 won't write key events if the element isn't focused inputter.focus(); Syn.trigger("keypress", "\r", inputter); Syn.trigger("keypress", "a", inputter); S.support.keyCharacters = inputter.value == "a"; inputter.value = "a"; Syn.trigger("keypress", "\b", inputter); S.support.backspaceWorks = inputter.value == ""; inputter.onchange = function() { S.support.focusChanges = true; } inputter.focus(); Syn.trigger("keypress", "a", inputter); form.childNodes[5].focus(); // this will throw a change event Syn.trigger("keypress", "b", inputter); S.support.keysOnNotFocused = inputter.value == "ab"; //test keypress \r on anchor submits S.bind(anchor, "click", function( ev ) { if ( ev.preventDefault ) ev.preventDefault(); S.support.keypressOnAnchorClicks = true; ev.returnValue = false; return false; }) Syn.trigger("keypress", "\r", anchor); S.support.textareaCarriage = textarea.value.length == 4; document.documentElement.removeChild(div); S.support.ready++; })(); })()