/* ################################################################################ ox.ui.js requires jquery-1.4.js ox.js ################################################################################ */ // also see test.js, in demos ... // fixme: render might be a better function name than construct (function() { // fixme: move into Ox.UI var oxui = { defaultTheme: 'classic', elements: {}, getDimensions: function(orientation) { return orientation == 'horizontal' ? ['width', 'height'] : ['height', 'width']; }, getEdges: function(orientation) { return orientation == 'horizontal' ? ['left', 'right', 'top', 'bottom'] : ['top', 'bottom', 'left', 'right']; }, getBarSize: function(size) { var sizes = { small: 20, medium: 24, large: 28, }; return sizes[size]; }, path: $('script[src*="ox.ui.js"]').attr('src').replace('js/ox.ui.js', ''), scrollbarSize: $.browser.mozilla ? 16 : 12, symbols: { alt: '\u2325', apple: '\uF8FF', arrow_down: '\u2193', arrow_left: '\u2190', arrow_right: '\u2192', arrow_up: '\u2191', backspace: '\u232B', backup: '\u2707', ballot: '\u2717', black_star: '\u2605', burn: '\u2622', caps_lock: '\u21EA', check: '\u2713', //clear: '\u2327', clear: '\u00D7', click: '\uF803', close: '\u2715', command: '\u2318', control: '\u2303', cut: '\u2702', 'delete': '\u2326', diamond: '\u25C6', edit: '\uF802', eject: '\u23CF', escape: '\u238B', end: '\u2198', enter: '\u2324', fly: '\u2708', gear: '\u2699', home: '\u2196', info: '\u24D8', navigate: '\u2388', option: '\u2387', page_up: '\u21DE', page_down: '\u21DF', redo: '\u21BA', 'return': '\u21A9', //select: '\u21D5', select: '\u25BE', shift: '\u21E7', sound: '\u266B', space: '\u2423', tab: '\u21E5', trash: '\u267A', triangle_down: '\u25BC', triangle_left: '\u25C0', triangle_right: '\u25BA', triangle_up: '\u25B2', undo: '\u21BB', voltage: '\u26A1', warning: '\u26A0', white_star: '\u2606' } }, $elements = {}, $window, $document, $body; _$elements = $elements; $(function() { $window = $(window); $document = $(document); $body = $('body'); Ox.theme(oxui.defaultTheme); }); /* ============================================================================ Application ============================================================================ */ Ox.App = (function() { /*** Ox.App Basic application instance that communicates with a JSON API. The JSON API should support at least the following actions: api returns all api methods init returns {config: {...}, user: {...}} Options timeout API timeout in msec type 'GET' or 'POST' url URL of the API Methods api[action] make a request api.cancel cancel a request launch launch the App options get or set options ***/ return function(options) { options = options || {}; var self = {}, that = this; self.time = +new Date(); self.options = $.extend({ timeout: 60000, type: 'POST', url: '/api/', }, options); that.$element = new Ox.Element('body'); function getUserAgent() { var userAgent = ''; $.each(['Chrome', 'Firefox', 'Internet Explorer', 'Opera', 'Safari'], function(i, v) { if (navigator.userAgent.indexOf(v) > -1) { userAgent = v; return false; } }); if (!userAgent && $.browser.mozilla) { userAgent = 'Firefox'; } if (!userAgent && $.browser.webkit) { userAgent = 'Chrome'; } return userAgent; } function getUserData() { return { navigator: { cookieEnabled: navigator.cookieEnabled, plugins: $.map(navigator.plugins, function(plugin, i) { return plugin.name; }), userAgent: navigator.userAgent }, screen: screen, time: (+new Date() - self.time) / 1000, window: { innerHeight: window.innerHeight, innerWidth: window.innerWidth, outerHeight: window.outerHeight, outerWidth: window.outerWidth, screenLeft: window.screenLeft, screenTop: window.screenTop } }; } function loadImages(callback) { window.OxImageCache = []; $.getJSON(oxui.path + 'json/ox.ui.images.json', function(data) { var counter = 0, length = data.length; data.forEach(function(src, i) { var image = new Image(); image.src = oxui.path + src; image.onload = function() { (++counter == length) && callback(); } window.OxImageCache.push(image); }); }); } self.change = function(key, value) { }; that.api = { api: function(callback) { Ox.Request.send({ url: self.options.url, data: { action: 'api' }, callback: callback }); }, cancel: function(id) { Ox.Request.cancel(id); } }; that.bindEvent = function() { }; that.launch = function(callback) { var time = +new Date(), userAgent = getUserAgent(), userAgents = ['Chrome', 'Firefox', 'Opera', 'Safari']; $.ajaxSetup({ timeout: self.options.timeour, type: self.options.type, url: self.options.url }); userAgents.indexOf(userAgent) > -1 ? start() : stop(); function start() { // fixme: rename config to site? var counter = 0, config, user; that.api.api(function(result) { $.each(result.data.actions, function(key, value) { that.api[key] = function(data, callback) { if (arguments.length == 1 && Ox.isFunction(data)) { callback = data; data = {}; } return Ox.Request.send($.extend({ url: self.options.url, data: { action: key, data: JSON.stringify(data) }, callback: callback }, !value.cache ? {age: 0}: {})); }; }); that.api.init(getUserData(), function(result) { config = result.data.config; user = result.data.user; document.title = config.site.name; launchCallback(); }); }); loadImages(launchCallback); function launchCallback() { ++counter == 2 && $(function() { var $div = $body.find('div'); $body.find('img').remove(); $div.animate({ opacity: 0 }, 1000, function() { $div.remove(); }); callback({config: config, user: user}); }); } } function stop() { that.request.send(self.options.init, getUserData(), function() {}); } return that; }; that.options = function() { return Ox.getset(self.options, Array.prototype.slice.call(arguments), self.change, that); }; return that; }; }()); Ox.Clipboard = function() { /*** Ox.Clipboard Basic clipboard handler Methods copy(data) copy data to clipboard paste paste data from clipboard ***/ var clipboard = {}; return { _print: function() { Ox.print(JSON.stringify(clipboard)); }, copy: function(data) { clipboard = data; Ox.print('copy', JSON.stringify(clipboard)); }, paste: function(type) { return clipboard; } }; }(); Ox.Focus = function() { /*** Ox.Focus Basic focus handler Methods blur(id) blur element focus(id) focus element focused() return id of focused element, or null ***/ var stack = []; return { _print: function() { Ox.print(stack); }, blur: function(id) { var index = stack.indexOf(id); if (index > -1 && index == stack.length - 1) { stack.length == 1 ? stack.pop() : stack.splice(stack.length - 2, 0, stack.pop()); //$elements[id].removeClass('OxFocus'); $('.OxFocus').removeClass('OxFocus'); // fixme: the above is better, and should work stack.length && $elements[stack[stack.length - 1]].addClass('OxFocus'); Ox.print('blur', id, stack); } }, focus: function(id) { var index = stack.indexOf(id); if (index == -1 || index < stack.length - 1) { index > -1 && stack.splice(index, 1); stack.push(id); $('.OxFocus').removeClass('OxFocus'); // fixme: see above $elements[id].addClass('OxFocus'); Ox.print('focus', id, stack); } }, focused: function() { return stack.length ? stack[stack.length - 1] : null; } }; }(); /*** Ox.History ***/ /*** Ox.Keyboard ***/ (function() { var buffer = '', bufferTime = 0, bufferTimeout = 1000, // wrapped in function so it can be collapsed in text editor keyNames = (function() { return { 0: 'section', 8: 'backspace', 9: 'tab', 12: 'clear', 13: 'enter', 16: 'shift', 17: 'control', 18: 'alt', 20: 'capslock', 27: 'escape', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down', 45: 'insert', 46: 'delete', 47: 'help', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7', 56: '8', 57: '9', 65: 'a', 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v', 87: 'w', 88: 'x', 89: 'y', 90: 'z', 91: 'meta.left', 92: 'meta.right', 93: 'select', 96: '0.numpad', 97: '1.numpad', 98: '2.numpad', 99: '3.numpad', 100: '4.numpad', 101: '5.numpad', 102: '6.numpad', 103: '7.numpad', 104: '8.numpad', 105: '9.numpad', 106: 'asterisk.numpad', 107: 'plus.numpad', 109: 'minus.numpad', 108: 'enter.numpad', 110: 'dot.numpad', 111: 'slash.numpad', 112: 'f1', 113: 'f2', 114: 'f3', 115: 'f4', 116: 'f5', 117: 'f6', 118: 'f7', 119: 'f8', 120: 'f9', 121: 'f10', 122: 'f11', 123: 'f12', 124: 'f13', 125: 'f14', 126: 'f15', 127: 'f16', 144: 'numlock', 145: 'scrolllock', 186: 'semicolon', 187: 'equal', 188: 'comma', 189: 'minus', 190: 'dot', 191: 'slash', 192: 'backtick', 219: 'openbracket', 220: 'backslash', 221: 'closebracket', 222: 'quote' // see dojo, for ex. }; })(), modifierNames = { altKey: 'alt', // mac: option ctrlKey: 'control', // metaKey: 'meta', // mac: command shiftKey: 'shift' }; $(function() { // fixme: how to do this better? // in firefox on mac, keypress doesn't fire for up/down // if the cursor is at the start/end of an input element // on linux, it doesn't seem to fire if the input element has focus if ($.browser.mozilla) { $document.keypress(keypress); $document.keydown(function(event) { var $element = $('input:focus'); if ($element.length) { if ( ( keyNames[event.keyCode] == 'up' && $element[0].selectionStart + $element[0].selectionEnd == 0 ) || ( keyNames[event.keyCode] == 'down' && $element[0].selectionStart == $element.val().length && $element[0].selectionEnd == $element.val().length ) ) { keypress(event); } } }); } else { $document.keydown(keypress); } }); function keypress(event) { var focused = Ox.Focus.focused(), key, keys = [], //ret = true, time; $.each(modifierNames, function(k, v) { event[k] && keys.push(v); }); // avoid pushing modifier twice //Ox.print('keys', keys) if (keyNames[event.keyCode] && keys.indexOf(keyNames[event.keyCode]) == -1) { keys.push(keyNames[event.keyCode]); } key = keys.join('_'); if (key.match(/^[\w\d\-]$|SPACE/)) { time = Ox.getTime(); if (time - bufferTime > bufferTimeout) { buffer = ''; } buffer += key == 'SPACE' ? ' ' : key; bufferTime = time; } focused && $elements[focused].triggerEvent('key_' + key); if (['down', 'space', 'up'].indexOf(key) > -1 && !$elements[focused].hasClass('OxInput')) { // prevent chrome from scrolling return false; } /* $.each(stack, function(i, v) { // fixme: we dont get the return value! ret = Ox.event.trigger(keyboard + Ox.toCamelCase(key) + '.' + v); return ret; }); */ } })(); Ox.Request = function(options) { /*** Ox.Request Basic request handler Options timeout Methods cancel() cancel request clearCache() clear cache options() get or set options requests() return number of active requests send() send request ***/ var cache = {}, pending = {}, requests = {}, self = { options: $.extend({ timeout: 60000, type: 'POST', url: '/api/' }, options) }; return { cancel: function() { if (arguments.length == 0) { // cancel all requests requests = {}; } else if (Ox.isFunction(arguments[0])) { // cancel with function $.each(requests, function(id, req) { if (arguments[0](req)) { delete requests[id]; } }); } else { // cancel by id delete requests[arguments[0]]; } }, clearCache: function() { cache = {}; }, options: function(options) { return Ox.getset(self.options, options, $.noop(), this); }, requests: function() { return Ox.length(requests); }, send: function(options) { var options = $.extend({ age: -1, callback: null, id: Ox.uid(), timeout: self.options.timeout, type: self.options.type, url: self.options.url }, options), req = JSON.stringify({ url: options.url, data: options.data }); if (pending[options.id]) { setTimeout(function() { Ox.Request.send(options); }, 0); } else { requests[options.id] = { url: options.url, data: options.data }; if (cache[req] && (options.age == -1 || options.age > Ox.getTime() - cache[req].time)) { setTimeout(function() { callback && callback(cache[req].data); }, 0); } else { pending[options.id] = true; $.ajax({ data: options.data, dataType: 'json', error: error, success: success, timeout: options.timeout, type: options.type, url: options.url }); } } function callback(data) { delete requests[options.id]; //Ox.length(requests) == 0 && $body.trigger('requestStop'); options.callback && options.callback(data); } function debug(request) { var $iframe = $('