diff --git a/tests/e2e/libs/syn.js b/tests/e2e/libs/syn.js new file mode 100644 index 00000000..640572fb --- /dev/null +++ b/tests/e2e/libs/syn.js @@ -0,0 +1,2858 @@ +/** + * Syn - 0.0.2 + * + * @copyright 2014 Bitovi + * Mon, 30 Jun 2014 22:44:59 GMT + * @license MIT + */ + +!function(window) { + +// ## src/synthetic.js +var __m2 = (function () { + //allow for configuration of Syn + var opts = window.Syn ? window.Syn : {}; + + var extend = function (d, s) { + var p; + for (p in s) { + d[p] = s[p]; + } + return d; + }, + // only uses browser detection for key events + browser = { + msie: !! (window.attachEvent && !window.opera), + opera: !! window.opera, + webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + safari: navigator.userAgent.indexOf('AppleWebKit/') > -1 && navigator.userAgent.indexOf('Chrome/') === -1, + gecko: navigator.userAgent.indexOf('Gecko') > -1, + mobilesafari: !! navigator.userAgent.match(/Apple.*Mobile.*Safari/), + rhino: navigator.userAgent.match(/Rhino/) && true + }, + createEventObject = function (type, options, element) { + var event = element.ownerDocument.createEventObject(); + return extend(event, options); + }, + data = {}, + id = 1, + expando = "_synthetic" + new Date() + .getTime(), + bind, unbind, schedule, key = /keypress|keyup|keydown/, + page = /load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll/, + //this is maintained so we can click on html and blur the active element + activeElement, + + /** + * @class Syn + * @download funcunit/dist/syn.js + * @test funcunit/synthetic/qunit.html + * Syn is used to simulate user actions. It creates synthetic events and + * performs their default behaviors. + * + *

Basic Use

+ * The following clicks an input element with id='description' + * and then types 'Hello World'. + * + @codestart + Syn.click({},'description') + .type("Hello World") + @codeend + *

User Actions and Events

+ *

Syn is typically used to simulate user actions as opposed to triggering events. Typing characters + * is an example of a user action. The keypress that represents an 'a' + * character being typed is an example of an event. + *

+ *

+ * While triggering events is supported, it's much more useful to simulate actual user behavior. The + * following actions are supported by Syn: + *

+ * + * All actions run asynchronously. + * Click on the links above for more + * information on how to use the specific action. + *

Asynchronous Callbacks

+ * Actions don't complete immediately. This is almost + * entirely because focus() + * doesn't run immediately in IE. + * If you provide a callback function to Syn, it will + * be called after the action is completed. + *
The following checks that "Hello World" was entered correctly: + @codestart + Syn.click({},'description') + .type("Hello World", function(){ + + ok("Hello World" == document.getElementById('description').value) + }) + @codeend +

Asynchronous Chaining

+

You might have noticed the [Syn.prototype.then then] method. It provides chaining + so you can do a sequence of events with a single (final) callback. +

+ If an element isn't provided to then, it uses the previous Syn's element. +

+ The following does a lot of stuff before checking the result: + @codestart + Syn.type('ice water','title') + .type('ice and water','description') + .click({},'create') + .drag({to: 'favorites'},'newRecipe', + function(){ + ok($('#newRecipe').parents('#favorites').length); + }) + @codeend + +

jQuery Helper

+ If jQuery is present, Syn adds a triggerSyn helper you can use like: + @codestart + $("#description").triggerSyn("type","Hello World"); + @codeend + *

Key Event Recording

+ *

Every browser has very different rules for dispatching key events. + * As there is no way to feature detect how a browser handles key events, + * synthetic uses a description of how the browser behaves generated + * by a recording application.

+ *

+ * If you want to support a browser not currently supported, you can + * record that browser's key event description and add it to + * Syn.key.browsers by it's navigator agent. + *

+ @codestart + Syn.key.browsers["Envjs\ Resig/20070309 PilotFish/1.2.0.10\1.6"] = { + 'prevent': + {"keyup":[],"keydown":["char","keypress"],"keypress":["char"]}, + 'character': + { ... } + } + @codeend + *

Limitations

+ * Syn fully supports IE 6+, FF 3+, Chrome, Safari, Opera 10+. + * With FF 1+, drag / move events are only partially supported. They will + * not trigger mouseover / mouseout events.
+ * Safari crashes when a mousedown is triggered on a select. Syn will not + * create this event. + *

Contributing to Syn

+ * Have we missed something? We happily accept patches. The following are + * important objects and properties of Syn: + * + *

Roll Your Own Functional Test Framework

+ *

Syn is really the foundation of JavaScriptMVC's functional testing framework - [FuncUnit]. + * But, we've purposely made Syn work without any dependencies in the hopes that other frameworks or + * testing solutions can use it as well. + *

+ * @constructor + * @signature `Syn(type, options, element, callback)` + * Creates a synthetic event on the element. + * @param {Object} type + * @param {Object} options + * @param {Object} element + * @param {Object} callback + * @return {Syn} returns the Syn object. + */ + Syn = function (type, options, element, callback) { + return (new Syn.init(type, options, element, callback)); + }; + + Syn.config = opts; + + // helper for supporting IE8 and below: + // focus will throw in some circumnstances, like element being invisible + Syn.__tryFocus = function tryFocus(element) { + try { + element.focus(); + } catch (e) {} + }; + + bind = function (el, ev, f) { + return el.addEventListener ? el.addEventListener(ev, f, false) : el.attachEvent("on" + ev, f); + }; + unbind = function (el, ev, f) { + return el.addEventListener ? el.removeEventListener(ev, f, false) : el.detachEvent("on" + ev, f); + }; + + schedule = Syn.config.schedule || function (fn, ms) { + setTimeout(fn, ms); + }; + /** + * @Static + */ + extend(Syn, { + /** + * Creates a new synthetic event instance + * @hide + * @param {Object} type + * @param {Object} options + * @param {Object} element + * @param {Object} callback + */ + init: function (type, options, element, callback) { + var args = Syn.args(options, element, callback), + self = this; + this.queue = []; + this.element = args.element; + + //run event + if (typeof this[type] === "function") { + this[type](args.options, args.element, function (defaults, el) { + if (args.callback) { + args.callback.apply(self, arguments); + } + self.done.apply(self, arguments); + }); + } else { + this.result = Syn.trigger(type, args.options, args.element); + if (args.callback) { + args.callback.call(this, args.element, this.result); + } + } + }, + jquery: function (el, fast) { + if (window.FuncUnit && window.FuncUnit.jQuery) { + return window.FuncUnit.jQuery; + } + if (el) { + return Syn.helpers.getWindow(el) + .jQuery || window.jQuery; + } else { + return window.jQuery; + } + }, + /** + * Returns an object with the args for a Syn. + * @hide + * @return {Object} + */ + args: function () { + var res = {}, + i = 0; + for (; i < arguments.length; i++) { + if (typeof arguments[i] === 'function') { + res.callback = arguments[i]; + } else if (arguments[i] && arguments[i].jquery) { + res.element = arguments[i][0]; + } else if (arguments[i] && arguments[i].nodeName) { + res.element = arguments[i]; + } else if (res.options && typeof arguments[i] === 'string') { //we can get by id + res.element = document.getElementById(arguments[i]); + } else if (arguments[i]) { + res.options = arguments[i]; + } + } + return res; + }, + click: function (options, element, callback) { + Syn('click!', options, element, callback); + }, + /** + * @hide + * @attribute defaults + * Default actions for events. Each default function is called with this as its + * element. It should return true if a timeout + * should happen after it. If it returns an element, a timeout will happen + * and the next event will happen on that element. + */ + defaults: { + focus: function focus() { + if (!Syn.support.focusChanges) { + var element = this, + nodeName = element.nodeName.toLowerCase(); + Syn.data(element, "syntheticvalue", element.value); + + //TODO, this should be textarea too + //and this might be for only text style inputs ... hmmmmm .... + if (nodeName === "input" || nodeName === "textarea") { + bind(element, "blur", function () { + if (Syn.data(element, "syntheticvalue") !== element.value) { + + Syn.trigger("change", {}, element); + } + unbind(element, "blur", focus); + }); + + } + } + }, + submit: function () { + Syn.onParents(this, function (el) { + if (el.nodeName.toLowerCase() === 'form') { + el.submit(); + return false; + } + }); + } + }, + changeOnBlur: function (element, prop, value) { + + bind(element, "blur", function onblur() { + if (value !== element[prop]) { + Syn.trigger("change", {}, element); + } + unbind(element, "blur", onblur); + }); + + }, + /** + * Returns the closest element of a particular type. + * @hide + * @param {Object} el + * @param {Object} type + */ + closest: function (el, type) { + while (el && el.nodeName.toLowerCase() !== type.toLowerCase()) { + el = el.parentNode; + } + return el; + }, + /** + * adds jQuery like data (adds an expando) and data exists FOREVER :) + * @hide + * @param {Object} el + * @param {Object} key + * @param {Object} value + */ + data: function (el, key, value) { + var d; + if (!el[expando]) { + el[expando] = id++; + } + if (!data[el[expando]]) { + data[el[expando]] = {}; + } + d = data[el[expando]]; + if (value) { + data[el[expando]][key] = value; + } else { + return data[el[expando]][key]; + } + }, + /** + * Calls a function on the element and all parents of the element until the function returns + * false. + * @hide + * @param {Object} el + * @param {Object} func + */ + onParents: function (el, func) { + var res; + while (el && res !== false) { + res = func(el); + el = el.parentNode; + } + return el; + }, + //regex to match focusable elements + focusable: /^(a|area|frame|iframe|label|input|select|textarea|button|html|object)$/i, + /** + * Returns if an element is focusable + * @hide + * @param {Object} elem + */ + isFocusable: function (elem) { + var attributeNode; + + // IE8 Standards doesn't like this on some elements + if (elem.getAttributeNode) { + attributeNode = elem.getAttributeNode("tabIndex"); + } + + return this.focusable.test(elem.nodeName) || + (attributeNode && attributeNode.specified) && + Syn.isVisible(elem); + }, + /** + * Returns if an element is visible or not + * @hide + * @param {Object} elem + */ + isVisible: function (elem) { + return (elem.offsetWidth && elem.offsetHeight) || (elem.clientWidth && elem.clientHeight); + }, + /** + * Gets the tabIndex as a number or null + * @hide + * @param {Object} elem + */ + tabIndex: function (elem) { + var attributeNode = elem.getAttributeNode("tabIndex"); + return attributeNode && attributeNode.specified && (parseInt(elem.getAttribute('tabIndex')) || 0); + }, + bind: bind, + unbind: unbind, + /** + * @function Syn.schedule schedule() + * @param {Function} fn Function to be ran + * @param {Number} ms Milliseconds to way before calling fn + * @signature `Syn.schedule(fn, ms)` + * @parent config + * + * Schedules a function to be ran later. + * Must be registered prior to Syn loading, otherwise `setTimeout` will be + * used as the scheduler. + * @codestart + * Syn = { + * schedule: function(fn, ms) { + * Platform.run.later(fn, ms); + * } + * }; + * @codeend + */ + schedule: schedule, + browser: browser, + //some generic helpers + helpers: { + createEventObject: createEventObject, + createBasicStandardEvent: function (type, defaults, doc) { + var event; + try { + event = doc.createEvent("Events"); + } catch (e2) { + event = doc.createEvent("UIEvents"); + } finally { + event.initEvent(type, true, true); + extend(event, defaults); + } + return event; + }, + inArray: function (item, array) { + var i = 0; + for (; i < array.length; i++) { + if (array[i] === item) { + return i; + } + } + return -1; + }, + getWindow: function (element) { + if (element.ownerDocument) { + return element.ownerDocument.defaultView || element.ownerDocument.parentWindow; + } + }, + extend: extend, + scrollOffset: function (win, set) { + var doc = win.document.documentElement, + body = win.document.body; + if (set) { + window.scrollTo(set.left, set.top); + + } else { + return { + left: (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0), + top: (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0) + }; + } + + }, + scrollDimensions: function (win) { + var doc = win.document.documentElement, + body = win.document.body, + docWidth = doc.clientWidth, + docHeight = doc.clientHeight, + compat = win.document.compatMode === "CSS1Compat"; + + return { + height: compat && docHeight || + body.clientHeight || docHeight, + width: compat && docWidth || + body.clientWidth || docWidth + }; + }, + addOffset: function (options, el) { + var jq = Syn.jquery(el), + off; + if (typeof options === 'object' && options.clientX === undefined && options.clientY === undefined && options.pageX === undefined && options.pageY === undefined && jq) { + el = jq(el); + off = el.offset(); + options.pageX = off.left + el.width() / 2; + options.pageY = off.top + el.height() / 2; + } + } + }, + // place for key data + key: { + ctrlKey: null, + altKey: null, + shiftKey: null, + metaKey: null + }, + //triggers an event on an element, returns true if default events should be run + /** + * Dispatches an event and returns true if default events should be run. + * @hide + * @param {Object} event + * @param {Object} element + * @param {Object} type + * @param {Object} autoPrevent + */ + dispatch: function (event, element, type, autoPrevent) { + + // dispatchEvent doesn't always work in IE (mostly in a popup) + if (element.dispatchEvent && event) { + var preventDefault = event.preventDefault, + prevents = autoPrevent ? -1 : 0; + + //automatically prevents the default behavior for this event + //this is to protect agianst nasty browser freezing bug in safari + if (autoPrevent) { + bind(element, type, function ontype(ev) { + ev.preventDefault(); + unbind(this, type, ontype); + }); + } + + event.preventDefault = function () { + prevents++; + if (++prevents > 0) { + preventDefault.apply(this, []); + } + }; + element.dispatchEvent(event); + return prevents <= 0; + } else { + try { + window.event = event; + } catch (e) {} + //source element makes sure element is still in the document + return element.sourceIndex <= 0 || (element.fireEvent && element.fireEvent('on' + type, event)); + } + }, + /** + * @attribute + * @hide + * An object of eventType -> function that create that event. + */ + create: { + //-------- PAGE EVENTS --------------------- + page: { + event: function (type, options, element) { + var doc = Syn.helpers.getWindow(element) + .document || document, + event; + if (doc.createEvent) { + event = doc.createEvent("Events"); + + event.initEvent(type, true, true); + return event; + } else { + try { + event = createEventObject(type, options, element); + } catch (e) {} + return event; + } + } + }, + // unique events + focus: { + event: function (type, options, element) { + Syn.onParents(element, function (el) { + if (Syn.isFocusable(el)) { + if (el.nodeName.toLowerCase() !== 'html') { + Syn.__tryFocus(el); + activeElement = el; + } else if (activeElement) { + // TODO: The HTML element isn't focasable in IE, but it is + // in FF. We should detect this and do a true focus instead + // of just a blur + var doc = Syn.helpers.getWindow(element) + .document; + if (doc !== window.document) { + return false; + } else if (doc.activeElement) { + doc.activeElement.blur(); + activeElement = null; + } else { + activeElement.blur(); + activeElement = null; + } + + } + return false; + } + }); + return true; + } + } + }, + /** + * @attribute support + * @hide + * + * Feature detected properties of a browser's event system. + * Support has the following properties: + * + * - `backspaceWorks` - typing a backspace removes a character + * - `clickChanges` - clicking on an option element creates a change event. + * - `clickSubmits` - clicking on a form button submits the form. + * - `focusChanges` - focus/blur creates a change event. + * - `keypressOnAnchorClicks` - Keying enter on an anchor triggers a click. + * - `keypressSubmits` - enter key submits + * - `keyCharacters` - typing a character shows up + * - `keysOnNotFocused` - enters keys when not focused. + * - `linkHrefJS` - An achor's href JavaScript is run. + * - `mouseDownUpClicks` - A mousedown followed by mouseup creates a click event. + * - `mouseupSubmits` - a mouseup on a form button submits the form. + * - `radioClickChanges` - clicking a radio button changes the radio. + * - `tabKeyTabs` - A tab key changes tabs. + * - `textareaCarriage` - a new line in a textarea creates a carriage return. + * + * + */ + support: { + clickChanges: false, + clickSubmits: false, + keypressSubmits: false, + mouseupSubmits: false, + radioClickChanges: false, + focusChanges: false, + linkHrefJS: false, + keyCharacters: false, + backspaceWorks: false, + mouseDownUpClicks: false, + tabKeyTabs: false, + keypressOnAnchorClicks: false, + optionClickBubbles: false, + ready: 0 + }, + /** + * @function Syn.trigger trigger() + * @parent actions + * @signature `Syn.trigger(type, options, element)` + * Creates a synthetic event and dispatches it on the element. + * This will run any default actions for the element. + * Typically you want to use Syn, but if you want the return value, use this. + * @param {String} type + * @param {Object} options + * @param {HTMLElement} element + * @return {Boolean} true if default events were run, false if otherwise. + */ + trigger: function (type, options, element) { + if (!options) { + options = {}; + } + + var create = Syn.create, + setup = create[type] && create[type].setup, + kind = key.test(type) ? 'key' : (page.test(type) ? "page" : "mouse"), + createType = create[type] || {}, + createKind = create[kind], + event, ret, autoPrevent, dispatchEl = element; + + //any setup code? + if (Syn.support.ready === 2 && setup) { + setup(type, options, element); + } + + autoPrevent = options._autoPrevent; + //get kind + delete options._autoPrevent; + + if (createType.event) { + ret = createType.event(type, options, element); + } else { + //convert options + options = createKind.options ? createKind.options(type, options, element) : options; + + if (!Syn.support.changeBubbles && /option/i.test(element.nodeName)) { + dispatchEl = element.parentNode; //jQuery expects clicks on select + } + + //create the event + event = createKind.event(type, options, dispatchEl); + + //send the event + ret = Syn.dispatch(event, dispatchEl, type, autoPrevent); + } + + if (ret && Syn.support.ready === 2 && Syn.defaults[type]) { + Syn.defaults[type].call(element, options, autoPrevent); + } + return ret; + }, + eventSupported: function (eventName) { + var el = document.createElement("div"); + eventName = "on" + eventName; + + var isSupported = (eventName in el); + if (!isSupported) { + el.setAttribute(eventName, "return;"); + isSupported = typeof el[eventName] === "function"; + } + el = null; + + return isSupported; + } + + }); + /** + * @Prototype + */ + extend(Syn.init.prototype, { + /** + * @function Syn.then then() + * @parent chained + *

+ * Then is used to chain a sequence of actions to be run one after the other. + * This is useful when many asynchronous actions need to be performed before some + * final check needs to be made. + *

+ *

The following clicks and types into the id='age' element and then checks that only numeric characters can be entered.

+ *

Example

+ * @codestart + * Syn('click',{},'age') + * .then('type','I am 12',function(){ + * equals($('#age').val(),"12") + * }) + * @codeend + * If the element argument is undefined, then the last element is used. + * + * @param {String} type The type of event or action to create: "_click", "_dblclick", "_drag", "_type". + * @param {Object} options Optiosn to pass to the event. + * @param {String|HTMLElement} [element] A element's id or an element. If undefined, defaults to the previous element. + * @param {Function} [callback] A function to callback after the action has run, but before any future chained actions are run. + */ + then: function (type, options, element, callback) { + if (Syn.autoDelay) { + this.delay(); + } + var args = Syn.args(options, element, callback), + self = this; + + //if stack is empty run right away + //otherwise ... unshift it + this.queue.unshift(function (el, prevented) { + + if (typeof this[type] === "function") { + this.element = args.element || el; + this[type](args.options, this.element, function (defaults, el) { + if (args.callback) { + args.callback.apply(self, arguments); + } + self.done.apply(self, arguments); + }); + } else { + this.result = Syn.trigger(type, args.options, args.element); + if (args.callback) { + args.callback.call(this, args.element, this.result); + } + return this; + } + }); + return this; + }, + /** + * @function Syn.delay delay() + * @parent chained + * Delays the next command a set timeout. + * @param {Number} [timeout] + * @param {Function} [callback] + */ + delay: function (timeout, callback) { + if (typeof timeout === 'function') { + callback = timeout; + timeout = null; + } + timeout = timeout || 600; + var self = this; + this.queue.unshift(function () { + schedule(function () { + if (callback) { + callback.apply(self, []); + } + self.done.apply(self, arguments); + }, timeout); + }); + return this; + }, + done: function (defaults, el) { + if (el) { + this.element = el; + } + if (this.queue.length) { + this.queue.pop() + .call(this, this.element, defaults); + } + + }, + /** + * @function Syn.click click() + * @parent mouse + * @signature `Syn.click(options, element, callback, force)` + * Clicks an element by triggering a mousedown, + * mouseup, + * and a click event. + *

Example

+ * @codestart + * Syn.click({},'create',function(){ + * //check something + * }) + * @codeend + * You can also provide the coordinates of the click. + * If jQuery is present, it will set clientX and clientY + * for you. Here's how to set it yourself: + * @codestart + * Syn.click( + * {clientX: 20, clientY: 100}, + * 'create', + * function(){ + * //check something + * }) + * @codeend + * You can also provide pageX and pageY and Syn will convert it for you. + * @param {Object} options + * @param {HTMLElement} element + * @param {Function} callback + */ + "_click": function (options, element, callback, force) { + Syn.helpers.addOffset(options, element); + Syn.trigger("mousedown", options, element); + + //timeout is b/c IE is stupid and won't call focus handlers + schedule(function () { + Syn.trigger("mouseup", options, element); + if (!Syn.support.mouseDownUpClicks || force) { + Syn.trigger("click", options, element); + callback(true); + } else { + //we still have to run the default (presumably) + Syn.create.click.setup('click', options, element); + Syn.defaults.click.call(element); + //must give time for callback + schedule(function () { + callback(true); + }, 1); + } + + }, 1); + }, + /** + * @function Syn.rightClick rightClick() + * @parent mouse + * @signature `Syn.rightClick(options, element, callback)` + * Right clicks in browsers that support it (everyone but opera). + * @param {Object} options + * @param {Object} element + * @param {Object} callback + */ + "_rightClick": function (options, element, callback) { + Syn.helpers.addOffset(options, element); + var mouseopts = extend(extend({}, Syn.mouse.browser.right.mouseup), options); + + Syn.trigger("mousedown", mouseopts, element); + + //timeout is b/c IE is stupid and won't call focus handlers + schedule(function () { + Syn.trigger("mouseup", mouseopts, element); + if (Syn.mouse.browser.right.contextmenu) { + Syn.trigger("contextmenu", extend(extend({}, Syn.mouse.browser.right.contextmenu), options), element); + } + callback(true); + }, 1); + }, + /** + * @function Syn.dblclick dblclick() + * @parent mouse + * @signature `Syn.dblclick(options, element, callback)` + * Dblclicks an element. This runs two [Syn.click click] events followed by + * a dblclick on the element. + *

Example

+ * @codestart + * Syn.dblclick({},'open') + * @codeend + * @param {Object} options + * @param {HTMLElement} element + * @param {Function} callback + */ + "_dblclick": function (options, element, callback) { + Syn.helpers.addOffset(options, element); + var self = this; + this._click(options, element, function () { + schedule(function () { + self._click(options, element, function () { + Syn.trigger("dblclick", options, element); + callback(true); + }, true); + }, 2); + + }); + } + }); + + var actions = ["click", "dblclick", "move", "drag", "key", "type", 'rightClick'], + makeAction = function (name) { + Syn[name] = function (options, element, callback) { + return Syn("_" + name, options, element, callback); + }; + Syn.init.prototype[name] = function (options, element, callback) { + return this.then("_" + name, options, element, callback); + }; + }, + i = 0; + + for (; i < actions.length; i++) { + makeAction(actions[i]); + } + + return Syn; +})(); + +// ## src/mouse.js +var __m3 = (function (Syn) { + //handles mosue events + + var h = Syn.helpers, + getWin = h.getWindow; + + Syn.mouse = {}; + h.extend(Syn.defaults, { + mousedown: function (options) { + Syn.trigger("focus", {}, this); + }, + click: function () { + // prevents the access denied issue in IE if the click causes the element to be destroyed + var element = this, + href, type, createChange, radioChanged, nodeName, scope; + try { + href = element.href; + type = element.type; + createChange = Syn.data(element, "createChange"); + radioChanged = Syn.data(element, "radioChanged"); + scope = getWin(element); + nodeName = element.nodeName.toLowerCase(); + } catch (e) { + return; + } + //get old values + + //this code was for restoring the href attribute to prevent popup opening + //if ((href = Syn.data(element, "href"))) { + // element.setAttribute('href', href) + //} + + //run href javascript + if (!Syn.support.linkHrefJS && /^\s*javascript:/.test(href)) { + //eval js + var code = href.replace(/^\s*javascript:/, ""); + + //try{ + if (code !== "//" && code.indexOf("void(0)") === -1) { + if (window.selenium) { + eval("with(selenium.browserbot.getCurrentWindow()){" + code + "}"); + } else { + eval("with(scope){" + code + "}"); + } + } + } + + //submit a form + if (!(Syn.support.clickSubmits) && (nodeName === "input" && + type === "submit") || + nodeName === 'button') { + + var form = Syn.closest(element, "form"); + if (form) { + Syn.trigger("submit", {}, form); + } + + } + //follow a link, probably needs to check if in an a. + if (nodeName === "a" && element.href && !/^\s*javascript:/.test(href)) { + scope.location.href = href; + + } + + //change a checkbox + if (nodeName === "input" && type === "checkbox") { + + //if(!Syn.support.clickChecks && !Syn.support.changeChecks){ + // element.checked = !element.checked; + //} + if (!Syn.support.clickChanges) { + Syn.trigger("change", {}, element); + } + } + + //change a radio button + if (nodeName === "input" && type === "radio") { // need to uncheck others if not checked + if (radioChanged && !Syn.support.radioClickChanges) { + Syn.trigger("change", {}, element); + } + } + // change options + if (nodeName === "option" && createChange) { + Syn.trigger("change", {}, element.parentNode); //does not bubble + Syn.data(element, "createChange", false); + } + } + }); + + //add create and setup behavior for mosue events + h.extend(Syn.create, { + mouse: { + options: function (type, options, element) { + var doc = document.documentElement, + body = document.body, + center = [options.pageX || 0, options.pageY || 0], + //browser might not be loaded yet (doing support code) + left = Syn.mouse.browser && Syn.mouse.browser.left[type], + right = Syn.mouse.browser && Syn.mouse.browser.right[type]; + return h.extend({ + bubbles: true, + cancelable: true, + view: window, + detail: 1, + screenX: 1, + screenY: 1, + clientX: options.clientX || center[0] - (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0), + clientY: options.clientY || center[1] - (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0), + ctrlKey: !! Syn.key.ctrlKey, + altKey: !! Syn.key.altKey, + shiftKey: !! Syn.key.shiftKey, + metaKey: !! Syn.key.metaKey, + button: left && left.button !== null ? left.button : right && right.button || (type === 'contextmenu' ? 2 : 0), + relatedTarget: document.documentElement + }, options); + }, + event: function (type, defaults, element) { //Everyone Else + var doc = getWin(element) + .document || document, + event; + if (doc.createEvent) { + try { + event = doc.createEvent('MouseEvents'); + event.initMouseEvent(type, defaults.bubbles, defaults.cancelable, + defaults.view, defaults.detail, + defaults.screenX, defaults.screenY, + defaults.clientX, defaults.clientY, + defaults.ctrlKey, defaults.altKey, + defaults.shiftKey, defaults.metaKey, + defaults.button, defaults.relatedTarget); + } catch (e) { + event = h.createBasicStandardEvent(type, defaults, doc); + } + event.synthetic = true; + return event; + } else { + try { + event = h.createEventObject(type, defaults, element); + } catch (e) {} + + return event; + } + + } + }, + click: { + setup: function (type, options, element) { + var nodeName = element.nodeName.toLowerCase(); + + //we need to manually 'check' in browser that can't check + //so checked has the right value + if (!Syn.support.clickChecks && !Syn.support.changeChecks && nodeName === "input") { + type = element.type.toLowerCase(); //pretty sure lowercase isn't needed + if (type === 'checkbox') { + element.checked = !element.checked; + } + if (type === "radio") { + //do the checks manually + if (!element.checked) { //do nothing, no change + try { + Syn.data(element, "radioChanged", true); + } catch (e) {} + element.checked = true; + } + } + } + + if (nodeName === "a" && element.href && !/^\s*javascript:/.test(element.href)) { + + //save href + Syn.data(element, "href", element.href); + + //remove b/c safari/opera will open a new tab instead of changing the page + // this has been removed because newer versions don't have this problem + //element.setAttribute('href', 'javascript://') + //however this breaks scripts using the href + //we need to listen to this and prevent the default behavior + //and run the default behavior ourselves. Boo! + } + //if select or option, save old value and mark to change + if (/option/i.test(element.nodeName)) { + var child = element.parentNode.firstChild, + i = -1; + while (child) { + if (child.nodeType === 1) { + i++; + if (child === element) { + break; + } + } + child = child.nextSibling; + } + if (i !== element.parentNode.selectedIndex) { + //shouldn't this wait on triggering + //change? + element.parentNode.selectedIndex = i; + Syn.data(element, "createChange", true); + } + } + + } + }, + mousedown: { + setup: function (type, options, element) { + var nn = element.nodeName.toLowerCase(); + //we have to auto prevent default to prevent freezing error in safari + if (Syn.browser.safari && (nn === "select" || nn === "option")) { + options._autoPrevent = true; + } + } + } + }); + + return Syn; +})(__m2); + +// ## src/browsers.js +var __m4 = (function (Syn) { + Syn.key.browsers = { + webkit: { + 'prevent': { + "keyup": [], + "keydown": ["char", "keypress"], + "keypress": ["char"] + }, + 'character': { + "keydown": [0, "key"], + "keypress": ["char", "char"], + "keyup": [0, "key"] + }, + 'specialChars': { + "keydown": [0, "char"], + "keyup": [0, "char"] + }, + 'navigation': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'special': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'tab': { + "keydown": [0, "char"], + "keyup": [0, "char"] + }, + 'pause-break': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'caps': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'escape': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'num-lock': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'scroll-lock': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'print': { + "keyup": [0, "key"] + }, + 'function': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + '\r': { + "keydown": [0, "key"], + "keypress": ["char", "key"], + "keyup": [0, "key"] + } + }, + gecko: { + 'prevent': { + "keyup": [], + "keydown": ["char"], + "keypress": ["char"] + }, + 'character': { + "keydown": [0, "key"], + "keypress": ["char", 0], + "keyup": [0, "key"] + }, + 'specialChars': { + "keydown": [0, "key"], + "keypress": [0, "key"], + "keyup": [0, "key"] + }, + 'navigation': { + "keydown": [0, "key"], + "keypress": [0, "key"], + "keyup": [0, "key"] + }, + 'special': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + '\t': { + "keydown": [0, "key"], + "keypress": [0, "key"], + "keyup": [0, "key"] + }, + 'pause-break': { + "keydown": [0, "key"], + "keypress": [0, "key"], + "keyup": [0, "key"] + }, + 'caps': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'escape': { + "keydown": [0, "key"], + "keypress": [0, "key"], + "keyup": [0, "key"] + }, + 'num-lock': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'scroll-lock': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + 'print': { + "keyup": [0, "key"] + }, + 'function': { + "keydown": [0, "key"], + "keyup": [0, "key"] + }, + '\r': { + "keydown": [0, "key"], + "keypress": [0, "key"], + "keyup": [0, "key"] + } + }, + msie: { + 'prevent': { + "keyup": [], + "keydown": ["char", "keypress"], + "keypress": ["char"] + }, + 'character': { + "keydown": [null, "key"], + "keypress": [null, "char"], + "keyup": [null, "key"] + }, + 'specialChars': { + "keydown": [null, "char"], + "keyup": [null, "char"] + }, + 'navigation': { + "keydown": [null, "key"], + "keyup": [null, "key"] + }, + 'special': { + "keydown": [null, "key"], + "keyup": [null, "key"] + }, + 'tab': { + "keydown": [null, "char"], + "keyup": [null, "char"] + }, + 'pause-break': { + "keydown": [null, "key"], + "keyup": [null, "key"] + }, + 'caps': { + "keydown": [null, "key"], + "keyup": [null, "key"] + }, + 'escape': { + "keydown": [null, "key"], + "keypress": [null, "key"], + "keyup": [null, "key"] + }, + 'num-lock': { + "keydown": [null, "key"], + "keyup": [null, "key"] + }, + 'scroll-lock': { + "keydown": [null, "key"], + "keyup": [null, "key"] + }, + 'print': { + "keyup": [null, "key"] + }, + 'function': { + "keydown": [null, "key"], + "keyup": [null, "key"] + }, + '\r': { + "keydown": [null, "key"], + "keypress": [null, "key"], + "keyup": [null, "key"] + } + }, + opera: { + 'prevent': { + "keyup": [], + "keydown": [], + "keypress": ["char"] + }, + 'character': { + "keydown": [null, "key"], + "keypress": [null, "char"], + "keyup": [null, "key"] + }, + 'specialChars': { + "keydown": [null, "char"], + "keypress": [null, "char"], + "keyup": [null, "char"] + }, + 'navigation': { + "keydown": [null, "key"], + "keypress": [null, "key"] + }, + 'special': { + "keydown": [null, "key"], + "keypress": [null, "key"], + "keyup": [null, "key"] + }, + 'tab': { + "keydown": [null, "char"], + "keypress": [null, "char"], + "keyup": [null, "char"] + }, + 'pause-break': { + "keydown": [null, "key"], + "keypress": [null, "key"], + "keyup": [null, "key"] + }, + 'caps': { + "keydown": [null, "key"], + "keyup": [null, "key"] + }, + 'escape': { + "keydown": [null, "key"], + "keypress": [null, "key"] + }, + 'num-lock': { + "keyup": [null, "key"], + "keydown": [null, "key"], + "keypress": [null, "key"] + }, + 'scroll-lock': { + "keydown": [null, "key"], + "keypress": [null, "key"], + "keyup": [null, "key"] + }, + 'print': {}, + 'function': { + "keydown": [null, "key"], + "keypress": [null, "key"], + "keyup": [null, "key"] + }, + '\r': { + "keydown": [null, "key"], + "keypress": [null, "key"], + "keyup": [null, "key"] + } + } + }; + + Syn.mouse.browsers = { + webkit: { + "right": { + "mousedown": { + "button": 2, + "which": 3 + }, + "mouseup": { + "button": 2, + "which": 3 + }, + "contextmenu": { + "button": 2, + "which": 3 + } + }, + "left": { + "mousedown": { + "button": 0, + "which": 1 + }, + "mouseup": { + "button": 0, + "which": 1 + }, + "click": { + "button": 0, + "which": 1 + } + } + }, + opera: { + "right": { + "mousedown": { + "button": 2, + "which": 3 + }, + "mouseup": { + "button": 2, + "which": 3 + } + }, + "left": { + "mousedown": { + "button": 0, + "which": 1 + }, + "mouseup": { + "button": 0, + "which": 1 + }, + "click": { + "button": 0, + "which": 1 + } + } + }, + msie: { + "right": { + "mousedown": { + "button": 2 + }, + "mouseup": { + "button": 2 + }, + "contextmenu": { + "button": 0 + } + }, + "left": { + "mousedown": { + "button": 1 + }, + "mouseup": { + "button": 1 + }, + "click": { + "button": 0 + } + } + }, + chrome: { + "right": { + "mousedown": { + "button": 2, + "which": 3 + }, + "mouseup": { + "button": 2, + "which": 3 + }, + "contextmenu": { + "button": 2, + "which": 3 + } + }, + "left": { + "mousedown": { + "button": 0, + "which": 1 + }, + "mouseup": { + "button": 0, + "which": 1 + }, + "click": { + "button": 0, + "which": 1 + } + } + }, + gecko: { + "left": { + "mousedown": { + "button": 0, + "which": 1 + }, + "mouseup": { + "button": 0, + "which": 1 + }, + "click": { + "button": 0, + "which": 1 + } + }, + "right": { + "mousedown": { + "button": 2, + "which": 3 + }, + "mouseup": { + "button": 2, + "which": 3 + }, + "contextmenu": { + "button": 2, + "which": 3 + } + } + } + }; + + //set browser + Syn.key.browser = + (function () { + if (Syn.key.browsers[window.navigator.userAgent]) { + return Syn.key.browsers[window.navigator.userAgent]; + } + for (var browser in Syn.browser) { + if (Syn.browser[browser] && Syn.key.browsers[browser]) { + return Syn.key.browsers[browser]; + } + } + return Syn.key.browsers.gecko; + })(); + + Syn.mouse.browser = + (function () { + if (Syn.mouse.browsers[window.navigator.userAgent]) { + return Syn.mouse.browsers[window.navigator.userAgent]; + } + for (var browser in Syn.browser) { + if (Syn.browser[browser] && Syn.mouse.browsers[browser]) { + return Syn.mouse.browsers[browser]; + } + } + return Syn.mouse.browsers.gecko; + })(); + return Syn; +})(__m2, __m3); + +// ## src/typeable.js +var __m6 = (function (Syn) { + // Holds functions that test for typeability + var typeables = []; + + // IE <= 8 doesn't implement [].indexOf. + // This shim was extracted from CoffeeScript: + var __indexOf = [].indexOf || function (item) { + for (var i = 0, l = this.length; i < l; i++) { + if (i in this && this[i] === item) { + return i; + } + } + return -1; + }; + + /* + * @function typeable + * Registers a function that is used to determine if an + * element can be typed into. The user can define as many + * test functions as needed. By default there are 2 typeable + * functions, one for inputs and textareas, and another + * for contenteditable elements. + * + * @param {Function} fn Function to register. + */ + Syn.typeable = function (fn) { + if (__indexOf.call(typeables, fn) === -1) { + typeables.push(fn); + } + }; + + /* + * @function test + * Tests whether an element can be typed into using the test + * functions registered by [Syn.typeable typeable]. If any of the + * test functions returns true, `test` will return true and allow + * the element to be typed into. + * + * @param {HTMLElement} el the element to test. + * @return {Boolean} true if the element can be typed into. + */ + Syn.typeable.test = function (el) { + for (var i = 0, len = typeables.length; i < len; i++) { + if (typeables[i](el)) { + return true; + } + } + return false; + }; + + var type = Syn.typeable; + + // Inputs and textareas + var typeableExp = /input|textarea/i; + type(function (el) { + return typeableExp.test(el.nodeName); + }); + + // Content editable + type(function (el) { + return __indexOf.call(["", "true"], el.getAttribute("contenteditable")) !== -1; + }); + + return Syn; +})(__m2); + +// ## src/key.js +var __m5 = (function (Syn) { + var h = Syn.helpers, + + // gets the selection of an input or textarea + getSelection = function (el) { + var real, r, start; + + // 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') { + real = h.getWindow(el) + .document.selection.createRange(); + r = el.createTextRange(); + r.setEndPoint("EndToStart", real); + + start = r.text.length; + return { + start: start, + end: start + real.text.length + }; + } else { + real = h.getWindow(el) + .document.selection.createRange(); + r = real.duplicate(); + var 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); + start = r.text.length - real.text.length; + var 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) { + var prop = formElExp.test(el.nodeName) ? "value" : "textContent"; + + return { + start: el[prop].length, + end: el[prop].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++) { + if (Syn.isFocusable(els[i]) && els[i] !== document.documentElement) { + res.push(els[i]); + } + } + return res; + }, + formElExp = /input|textarea/i, + textProperty = (function(){ + var el = document.createElement("span"); + return el.textContent != null ? 'textContent' : 'innerText'; + })(), + + // Get the text from an element. + getText = function (el) { + if (formElExp.test(el.nodeName)) { + return el.value; + } + return el[textProperty]; + }, + // Set the text of an element. + setText = function (el, value) { + if (formElExp.test(el.nodeName)) { + el.value = value; + } else { + el[textProperty] = value; + } + }; + + /** + * + */ + h.extend(Syn, { + /** + * @attribute + * @parent keys + * 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 + * subtract - subtract (keypad) - + * dash - dash - + * divide - divide (keypad) / + * forward-slash - forward slash / + * decimal - decimal (keypad) . + * period - period . + * @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, + 'subtract': 109, + 'decimal': 110, + //normal-characters, others + 'divide': 111, + ';': 186, + '=': 187, + ',': 188, + 'dash': 189, + '-': 189, + 'period': 190, + '.': 190, + 'forward-slash': 191, + '/': 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 + }, + + // selects text on an element + selectText: function (el, start, end) { + if (el.setSelectionRange) { + if (!end) { + Syn.__tryFocus(el); + el.setSelectionRange(start, start); + } else { + el.selectionStart = start; + el.selectionEnd = end; + } + } else if (el.createTextRange) { + //Syn.__tryFocus(el); + 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)) { + 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 (Syn.key.browser[key]) { + return Syn.key.browser[key]; + } + for (var kind in Syn.key.kinds) { + if (h.inArray(key, Syn.key.kinds[kind]) > -1) { + return Syn.key.browser[kind]; + } + } + return Syn.key.browser.character; + }, + + //returns the special key if special + isSpecial: function (keyCode) { + var specials = Syn.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 || (!Syn.support.keyCharacters && Syn.typeable.test(this))) { + var current = getText(this), + before = current.substr(0, sel.start), + after = current.substr(sel.end), + character = key; + + setText(this, before + character + after); + //handle IE inserting \r\n + var charLength = character === "\n" && Syn.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, getText(this) + .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 (!Syn.support.backspaceWorks && Syn.typeable.test(this)) { + var current = getText(this), + before = current.substr(0, sel.start), + after = current.substr(sel.end); + + if (sel.start === sel.end && sel.start > 0) { + //remove a character + setText(this, before.substring(0, before.length - 1) + after); + Syn.selectText(this, sel.start - 1); + } else { + setText(this, before + after); + Syn.selectText(this, sel.start); + } + + //set back the selection + } + }, + 'delete': function (options, scope, key, force, sel) { + if (!Syn.support.backspaceWorks && Syn.typeable.test(this)) { + var current = getText(this), + before = current.substr(0, sel.start), + after = current.substr(sel.end); + if (sel.start === sel.end && sel.start <= getText(this) + .length - 1) { + setText(this, before + after.substring(1)); + } else { + setText(this, before + after); + } + Syn.selectText(this, sel.start); + } + }, + '\r': function (options, scope, key, force, sel) { + + var nodeName = this.nodeName.toLowerCase(); + // submit a form + if (nodeName === 'input') { + Syn.trigger("change", {}, this); + } + + if (!Syn.support.keypressSubmits && nodeName === 'input') { + var form = Syn.closest(this, "form"); + if (form) { + Syn.trigger("submit", {}, form); + } + + } + //newline in textarea + if (!Syn.support.keyCharacters && nodeName === 'textarea') { + Syn.key.defaults.character.call(this, options, scope, "\n", + undefined, sel); + } + // 'click' hyperlinks + if (!Syn.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), + // will be set to our guess for the next element + current = null, + i = 0, + el, + //the tabindex of the tabable element we are looking at + firstNotIndexed, + 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; + } else { + Syn.__tryFocus(current); + } + return current; + }, + 'left': function (options, scope, key, force, sel) { + if (Syn.typeable.test(this)) { + 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)) { + if (Syn.key.shiftKey) { + Syn.selectText(this, sel.start, sel.end + 1 > getText(this) + .length ? getText(this) + .length : sel.end + 1); + } else { + Syn.selectText(this, sel.end + 1 > getText(this) + .length ? getText(this) + .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; + }, + 'ctrl': 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 (Syn.support.keyCharacters && !Syn.support.keysOnNotFocused) { + Syn.__tryFocus(element); + } + } + }, + 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, Syn.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, + event; + if (doc.createEvent) { + 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 { + try { + event = h.createEventObject.apply(this, arguments); + h.extend(event, options); + } catch (e) {} + + return event; + } + } + } + }); + + var convert = { + "enter": "\r", + "backspace": "\b", + "tab": "\t", + "space": " " + }; + + /** + * + */ + h.extend(Syn.init.prototype, { + /** + * @function Syn.key key() + * @parent keys + * @signature `Syn.key(options, element, callback)` + * 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|Number} 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); + return callback(true, element); + } + + // keep reference to current activeElement + var activeElement = h.getWindow(element) + .document.activeElement, + caret = Syn.typeable.test(element) && 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) { + Syn.schedule(function () { + if (Syn.support.oninput) { + Syn.trigger('input', Syn.key.options(key, 'input'), element); + } + 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 Syn.type type() + * @parent keys + * @signature `Syn.type(options, element, callback)` + * 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(); + + } + }); + + return Syn; +})(__m2, __m6, __m4); + +// ## src/drag/drag.js +var __m7 = (function (Syn) { + + // check if elementFromPageExists + (function dragSupport() { + + // document body has to exists for this test + if (!document.body) { + Syn.schedule(dragSupport, 1); + return; + } + var div = document.createElement('div'); + document.body.appendChild(div); + Syn.helpers.extend(div.style, { + width: "100px", + height: "10000px", + backgroundColor: "blue", + position: "absolute", + top: "10px", + left: "0px", + zIndex: 19999 + }); + document.body.scrollTop = 11; + if (!document.elementFromPoint) { + return; + } + var el = document.elementFromPoint(3, 1); + if (el === div) { + Syn.support.elementFromClient = true; + } else { + Syn.support.elementFromPage = true; + } + document.body.removeChild(div); + document.body.scrollTop = 0; + })(); + + //gets an element from a point + var elementFromPoint = function (point, element) { + var clientX = point.clientX, + clientY = point.clientY, + win = Syn.helpers.getWindow(element), + el; + + if (Syn.support.elementFromPage) { + var off = Syn.helpers.scrollOffset(win); + clientX = clientX + off.left; //convert to pageX + clientY = clientY + off.top; //convert to pageY + } + el = win.document.elementFromPoint ? win.document.elementFromPoint(clientX, clientY) : element; + if (el === win.document.documentElement && (point.clientY < 0 || point.clientX < 0)) { + return element; + } else { + return el; + } + }, + //creates an event at a certain point + createEventAtPoint = function (event, point, element) { + var el = elementFromPoint(point, element); + Syn.trigger(event, point, el || element); + return el; + }, + // creates a mousemove event, but first triggering mouseout / mouseover if appropriate + mouseMove = function (point, element, last) { + var el = elementFromPoint(point, element); + if (last !== el && el && last) { + var options = Syn.helpers.extend({}, point); + options.relatedTarget = el; + Syn.trigger("mouseout", options, last); + options.relatedTarget = last; + Syn.trigger("mouseover", options, el); + } + + Syn.trigger("mousemove", point, el || element); + return el; + }, + // start and end are in clientX, clientY + startMove = function (start, end, duration, element, callback) { + var startTime = new Date(), + distX = end.clientX - start.clientX, + distY = end.clientY - start.clientY, + win = Syn.helpers.getWindow(element), + current = elementFromPoint(start, element), + cursor = win.document.createElement('div'), + calls = 0, + move; + move = function onmove() { + //get what fraction we are at + var now = new Date(), + scrollOffset = Syn.helpers.scrollOffset(win), + fraction = (calls === 0 ? 0 : now - startTime) / duration, + options = { + clientX: distX * fraction + start.clientX, + clientY: distY * fraction + start.clientY + }; + calls++; + if (fraction < 1) { + Syn.helpers.extend(cursor.style, { + left: (options.clientX + scrollOffset.left + 2) + "px", + top: (options.clientY + scrollOffset.top + 2) + "px" + }); + current = mouseMove(options, element, current); + Syn.schedule(onmove, 15); + } else { + current = mouseMove(end, element, current); + win.document.body.removeChild(cursor); + callback(); + } + }; + Syn.helpers.extend(cursor.style, { + height: "5px", + width: "5px", + backgroundColor: "red", + position: "absolute", + zIndex: 19999, + fontSize: "1px" + }); + win.document.body.appendChild(cursor); + move(); + }, + startDrag = function (start, end, duration, element, callback) { + createEventAtPoint("mousedown", start, element); + startMove(start, end, duration, element, function () { + createEventAtPoint("mouseup", end, element); + callback(); + }); + }, + center = function (el) { + var j = Syn.jquery()(el), + o = j.offset(); + return { + pageX: o.left + (j.outerWidth() / 2), + pageY: o.top + (j.outerHeight() / 2) + }; + }, + convertOption = function (option, win, from) { + var page = /(\d+)[x ](\d+)/, + client = /(\d+)X(\d+)/, + relative = /([+-]\d+)[xX ]([+-]\d+)/, + parts; + //check relative "+22x-44" + if (typeof option === 'string' && relative.test(option) && from) { + var cent = center(from); + parts = option.match(relative); + option = { + pageX: cent.pageX + parseInt(parts[1]), + pageY: cent.pageY + parseInt(parts[2]) + }; + } + if (typeof option === "string" && page.test(option)) { + parts = option.match(page); + option = { + pageX: parseInt(parts[1]), + pageY: parseInt(parts[2]) + }; + } + if (typeof option === 'string' && client.test(option)) { + parts = option.match(client); + option = { + clientX: parseInt(parts[1]), + clientY: parseInt(parts[2]) + }; + } + if (typeof option === 'string') { + option = Syn.jquery()(option, win.document)[0]; + } + if (option.nodeName) { + option = center(option); + } + if (option.pageX) { + var off = Syn.helpers.scrollOffset(win); + option = { + clientX: option.pageX - off.left, + clientY: option.pageY - off.top + }; + } + return option; + }, + // if the client chords are not going to be visible ... scroll the page so they will be ... + adjust = function (from, to, win) { + if (from.clientY < 0) { + var off = Syn.helpers.scrollOffset(win); + var top = off.top + (from.clientY) - 100, + diff = top - off.top; + + // first, lets see if we can scroll 100 px + if (top > 0) { + + } else { + top = 0; + diff = -off.top; + } + from.clientY = from.clientY - diff; + to.clientY = to.clientY - diff; + Syn.helpers.scrollOffset(win, { + top: top, + left: off.left + }); + } + }; + /** + * @add Syn prototype + */ + Syn.helpers.extend(Syn.init.prototype, { + /** + * @function Syn.move move() + * @parent mouse + * @signature `Syn.move(options, from, callback)` + * Moves the cursor from one point to another. + * + * ### Quick Example + * + * The following moves the cursor from (0,0) in + * the window to (100,100) in 1 second. + * + * Syn.move( + * { + * from: {clientX: 0, clientY: 0}, + * to: {clientX: 100, clientY: 100}, + * duration: 1000 + * }, + * document.document) + * + * ## Options + * + * There are many ways to configure the endpoints of the move. + * + * ### PageX and PageY + * + * If you pass pageX or pageY, these will get converted + * to client coordinates. + * + * Syn.move( + * { + * from: {pageX: 0, pageY: 0}, + * to: {pageX: 100, pageY: 100} + * }, + * document.document) + * + * ### String Coordinates + * + * You can set the pageX and pageY as strings like: + * + * Syn.move( + * { + * from: "0x0", + * to: "100x100" + * }, + * document.document) + * + * ### Element Coordinates + * + * If jQuery is present, you can pass an element as the from or to option + * and the coordinate will be set as the center of the element. + + * Syn.move( + * { + * from: $(".recipe")[0], + * to: $("#trash")[0] + * }, + * document.document) + * + * ### Query Strings + * + * If jQuery is present, you can pass a query string as the from or to option. + * + * Syn.move( + * { + * from: ".recipe", + * to: "#trash" + * }, + * document.document) + * + * ### No From + * + * If you don't provide a from, the element argument passed to Syn is used. + * + * Syn.move( + * { to: "#trash" }, + * 'myrecipe') + * + * ### Relative + * + * You can move the drag relative to the center of the from element. + * + * Syn.move("+20 +30", "myrecipe"); + * + * @param {Object} options options to configure the drag + * @param {HTMLElement} from the element to move + * @param {Function} callback a callback that happens after the drag motion has completed + */ + _move: function (options, from, callback) { + //need to convert if elements + var win = Syn.helpers.getWindow(from), + fro = convertOption(options.from || from, win, from), + to = convertOption(options.to || options, win, from); + + if (options.adjust !== false) { + adjust(fro, to, win); + } + startMove(fro, to, options.duration || 500, from, callback); + + }, + /** + * @function Syn.drag drag() + * @parent mouse + * @signature `Syn.drag(options, from, callback)` + * Creates a mousedown and drags from one point to another. + * Check out [Syn.prototype.move move] for API details. + * + * @param {Object} options + * @param {Object} from + * @param {Object} callback + */ + _drag: function (options, from, callback) { + //need to convert if elements + var win = Syn.helpers.getWindow(from), + fro = convertOption(options.from || from, win, from), + to = convertOption(options.to || options, win, from); + + if (options.adjust !== false) { + adjust(fro, to, win); + } + startDrag(fro, to, options.duration || 500, from, callback); + + } + }); + return Syn; +})(__m2); + +// ## src/syn.js +var __m1 = (function (Syn) { + window.Syn = Syn; + + return Syn; + })(__m2, __m3, __m4, __m5, __m7); + +}(window); \ No newline at end of file diff --git a/tests/e2e/test.html b/tests/e2e/test.html index 3fda08ac..3c869eb8 100644 --- a/tests/e2e/test.html +++ b/tests/e2e/test.html @@ -7,6 +7,7 @@ + diff --git a/tests/e2e/tests.js b/tests/e2e/tests.js index a5c29e34..6d3cae96 100644 --- a/tests/e2e/tests.js +++ b/tests/e2e/tests.js @@ -235,3 +235,30 @@ test('node identity shuffle and remove', function() { equal(dummyEl.firstChild.firstChild.nextSibling.nextSibling, e1, 'e1 is third element') equal(dummyEl.firstChild.firstChild.nextSibling.nextSibling.nextSibling, e2, 'e2 is fourth element') }) + +asyncTest('issue214 regression', function() { + // see https://github.com/lhorie/mithril.js/issues/214 + // this test will pass using phantomjs, because phantomjs + // doesn't provide window.requestAnimationFrame + expect(2) + + function controller() { + this.inputValue = m.prop('') + } + + function view(ctrl) { + return m('input#testinput', { + value: ctrl.inputValue(), + onkeyup: m.withAttr('value', ctrl.inputValue) + }) + } + + var ctrl = m.module(dummyEl, { controller: controller, view: view }) + + Syn.click({}, 'testinput') + .type('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', function() { + equal(ctrl.inputValue(), 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee') + equal(document.getElementById('testinput').value, 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee') + start() + }) +})