From 33c550f1c94088ac935964e39c94cbf8466886fd Mon Sep 17 00:00:00 2001 From: gpa Date: Tue, 11 Jan 2011 12:14:20 +0530 Subject: [PATCH 01/86] [IMP]partner:added suppliers menu domain bzr revid: gpa@tinyerp.com-20110111064420-9p05fwjsizqbtubz --- bin/addons/base/res/partner/partner_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/addons/base/res/partner/partner_view.xml b/bin/addons/base/res/partner/partner_view.xml index 8c949a7f410..3f225f29e49 100644 --- a/bin/addons/base/res/partner/partner_view.xml +++ b/bin/addons/base/res/partner/partner_view.xml @@ -394,7 +394,7 @@ ir.actions.act_window res.partner form - [] + [('supplier','=',1)] {'default_customer':0, 'search_default_supplier': 1, 'default_supplier':1} You can access all information regarding your suppliers from the supplier form: accounting data, history of emails, meetings, purchases, etc. You can uncheck the 'Suppliers' filter button in order to search in all your partners, including customers and prospects. From 6a11b89e1729c6ac06efba1e75405e1ecd24dc8a Mon Sep 17 00:00:00 2001 From: Julien Thewys Date: Wed, 9 Feb 2011 11:07:35 +0100 Subject: [PATCH 02/86] [FIX] ensures no illegitimate cli argument is silently discarded This avoids an insidious behavior: when copy-pasting a command from a Microsoft document to the cmd terminal, one might not see that a hyphen ('-') preceding an option has been converted to a dash (u'\u2012') which is then interpreted as a normal cli argument (i.e. not an option) and thus silently discarded. bzr revid: jth@openerp.com-20110209100735-jaschhlv3cfqjry5 --- openerp/tools/config.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 7d0b04739e4..626720a43c2 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -251,12 +251,14 @@ class configmanager(object): self.parse_config() def parse_config(self, args=[]): - opt = self.parser.parse_args(args)[0] + opt, args = self.parser.parse_args() def die(cond, msg): if cond: - print msg - sys.exit(1) + self.parser.error(msg) + + # Ensures no illegitimate argument is silently discarded (avoids insidious "hyphen to dash" problem) + die(args, "unrecognized parameters were passed: '%s'" % " ".join(args)) die(bool(opt.syslog) and bool(opt.logfile), "the syslog and logfile options are exclusive") From 2a86b85cadbd3895e04f79b97b51c9b77fd0e149 Mon Sep 17 00:00:00 2001 From: "vda (OpenERP)" Date: Thu, 7 Apr 2011 16:48:18 +0530 Subject: [PATCH 03/86] [ADD] Diagragm using Raphel javascript Library. bzr revid: vda@tinyerp.com-20110407111818-3j2l1ciknar2r2qd --- addons/base/static/src/base.html | 6 + addons/base/static/src/js/base.js | 1 + addons/base/static/src/xml/base.xml | 4 + addons/base_diagram/__init__.py | 1 + addons/base_diagram/__openerp__.py | 6 + addons/base_diagram/controllers/__init__.py | 1 + addons/base_diagram/controllers/main.py | 125 ++++ .../base_diagram/static/lib/js/Curry-1.0.1.js | 29 + .../static/lib/js/dracula_algorithms.js | 599 ++++++++++++++++++ .../static/lib/js/dracula_graffle.js | 106 ++++ .../static/lib/js/dracula_graph.coffee | 524 +++++++++++++++ .../static/lib/js/dracula_graph.js | 525 +++++++++++++++ .../base_diagram/static/lib/js/raphael-min.js | 7 + .../base_diagram/static/lib/js/seedrandom.js | 266 ++++++++ addons/base_diagram/static/src/js/diagram.js | 175 +++++ addons/base_diagram/static/src/js/views.js | 12 - 16 files changed, 2375 insertions(+), 12 deletions(-) create mode 100644 addons/base_diagram/__init__.py create mode 100644 addons/base_diagram/__openerp__.py create mode 100644 addons/base_diagram/controllers/__init__.py create mode 100644 addons/base_diagram/controllers/main.py create mode 100644 addons/base_diagram/static/lib/js/Curry-1.0.1.js create mode 100644 addons/base_diagram/static/lib/js/dracula_algorithms.js create mode 100644 addons/base_diagram/static/lib/js/dracula_graffle.js create mode 100644 addons/base_diagram/static/lib/js/dracula_graph.coffee create mode 100644 addons/base_diagram/static/lib/js/dracula_graph.js create mode 100644 addons/base_diagram/static/lib/js/raphael-min.js create mode 100644 addons/base_diagram/static/lib/js/seedrandom.js create mode 100644 addons/base_diagram/static/src/js/diagram.js delete mode 100644 addons/base_diagram/static/src/js/views.js diff --git a/addons/base/static/src/base.html b/addons/base/static/src/base.html index 8bb4793d42c..f6f7742343c 100644 --- a/addons/base/static/src/base.html +++ b/addons/base/static/src/base.html @@ -23,6 +23,12 @@ + + + + + + diff --git a/addons/base/static/src/js/base.js b/addons/base/static/src/js/base.js index 4833551c24e..3adf4129cbb 100644 --- a/addons/base/static/src/js/base.js +++ b/addons/base/static/src/js/base.js @@ -122,6 +122,7 @@ openerp.base = function(instance) { openerp.base.search(instance); openerp.base.list(instance); openerp.base.form(instance); + openerp.base.diagram(instance); }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 9a41ba630f3..8d8040fd48e 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -167,6 +167,10 @@
+ +

+
+

diff --git a/addons/base_diagram/__init__.py b/addons/base_diagram/__init__.py new file mode 100644 index 00000000000..355939a4922 --- /dev/null +++ b/addons/base_diagram/__init__.py @@ -0,0 +1 @@ +import controllers \ No newline at end of file diff --git a/addons/base_diagram/__openerp__.py b/addons/base_diagram/__openerp__.py new file mode 100644 index 00000000000..fb2a849f630 --- /dev/null +++ b/addons/base_diagram/__openerp__.py @@ -0,0 +1,6 @@ +{ + "name" : "OpenERP Web base Diagram", + "version" : "2.0", + "depends" : [], + 'active': True, +} diff --git a/addons/base_diagram/controllers/__init__.py b/addons/base_diagram/controllers/__init__.py new file mode 100644 index 00000000000..039d9715fab --- /dev/null +++ b/addons/base_diagram/controllers/__init__.py @@ -0,0 +1 @@ +import main \ No newline at end of file diff --git a/addons/base_diagram/controllers/main.py b/addons/base_diagram/controllers/main.py new file mode 100644 index 00000000000..b742021b5d7 --- /dev/null +++ b/addons/base_diagram/controllers/main.py @@ -0,0 +1,125 @@ +from base.controllers.main import View +import openerpweb + +class DiagramView(View): + _cp_path = "/base_diagram/diagram" + + @openerpweb.jsonrequest + def load(self, req, model, view_id): + fields_view = self.fields_view_get(req.session, model, view_id, 'diagram') + return {'fields_view': fields_view} + + @openerpweb.jsonrequest + def get_diagram_info(self, req, **kw): + id = kw['id'] + model = kw['model'] + node = kw['node'] + connector = kw['connector'] + src_node = kw['src_node'] + des_node = kw['des_node'] + + visible_node_fields = kw.get('visible_node_fields',[]) + invisible_node_fields = kw.get('invisible_node_fields',[]) + node_fields_string = kw.get('node_fields_string',[]) + connector_fields = kw.get('connector_fields',[]) + connector_fields_string = kw.get('connector_fields_string',[]) + + bgcolors = {} + shapes = {} + bgcolor = kw.get('bgcolor','') + shape = kw.get('shape','') + + if bgcolor: + for color_spec in bgcolor.split(';'): + if color_spec: + colour, color_state = color_spec.split(':') + bgcolors[colour] = color_state + + if shape: + for shape_spec in shape.split(';'): + if shape_spec: + shape_colour, shape_color_state = shape_spec.split(':') + shapes[shape_colour] = shape_color_state + + ir_view = req.session.model('ir.ui.view') + graphs = ir_view.graph_get(id, model, node, connector, src_node, des_node, False, + (140, 180), req.session.context) + + nodes = graphs['nodes'] + transitions = graphs['transitions'] + isolate_nodes = {} + + for node in graphs['blank_nodes']: + isolate_nodes[node['id']] = node + else: + y = map(lambda t: t['y'],filter(lambda x: x['y'] if x['x']==20 else None, nodes.values())) + y_max = (y and max(y)) or 120 + + connectors = {} + list_tr = [] + + for tr in transitions: + list_tr.append(tr) + connectors.setdefault(tr, { + 'id': tr, + 's_id': transitions[tr][0], + 'd_id': transitions[tr][1] + }) + connector_tr = req.session.model(connector) + connector_ids = connector_tr.search([('id', 'in', list_tr)], 0, 0, 0, req.session.context) + + data_connectors =connector_tr.read(connector_ids, connector_fields, req.session.context) + + + for tr in data_connectors: + t = connectors.get(str(tr['id'])) + t.update({ + 'source': tr[src_node][1], + 'destination': tr[des_node][1], + 'options': {} + }) + + for i, fld in enumerate(connector_fields): + t['options'][connector_fields_string[i]] = tr[fld] + + fields = req.session.model('ir.model.fields') + field_ids = fields.search([('model', '=', model), ('relation', '=', node)], 0, 0, 0, req.session.context) + field_data = fields.read(field_ids, ['relation_field'], req.session.context) + + node_act = req.session.model(node) + search_acts = node_act.search([(field_data[0]['relation_field'], '=', id)], 0, 0, 0, req.session.context) + data_acts = node_act.read(search_acts, invisible_node_fields + visible_node_fields, req.session.context) + + for act in data_acts: + n = nodes.get(str(act['id'])) + if not n: + n = isolate_nodes.get(act['id'], {}) + y_max += 140 + n.update({'x': 20, 'y': y_max}) + nodes[act['id']] = n + + n.update( + id=act['id'], + color='white', + shape='ellipse', + options={} + ) + for color, expr in bgcolors.items(): + if eval(expr, act): + n['color'] = color + + for shape, expr in shapes.items(): + if eval(expr, act): + n['shape'] = shape + + for i, fld in enumerate(visible_node_fields): + n['options'][node_fields_string[i]] = act[fld] + + #to relate m2o field of transition to corresponding o2m in activity + in_transition_field_id = fields.search([('relation', '=', connector), ('relation_field', '=', des_node), ('model', '=', node)], 0, 0, 0, req.session.context) + in_transition_field = fields.read(in_transition_field_id[0], ['name'], req.session.context)['name'] + + out_transition_field_id = fields.search([('relation', '=', connector), ('relation_field', '=', src_node), ('model', '=', node)], 0, 0, 0, req.session.context) + out_transition_field = fields.read(out_transition_field_id[0], ['name'], req.session.context)['name'] + + return dict(nodes=nodes, conn=connectors, in_transition_field=in_transition_field, out_transition_field=out_transition_field) \ No newline at end of file diff --git a/addons/base_diagram/static/lib/js/Curry-1.0.1.js b/addons/base_diagram/static/lib/js/Curry-1.0.1.js new file mode 100644 index 00000000000..f5aa0f993a6 --- /dev/null +++ b/addons/base_diagram/static/lib/js/Curry-1.0.1.js @@ -0,0 +1,29 @@ +/** + * Curry - Function currying + * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) + * Date: 10/4/2008 + * + * @author Ariel Flesler + * @version 1.0.1 + */ + +function curry( fn ){ + return function(){ + var args = curry.args(arguments), + master = arguments.callee, + self = this; + + return args.length >= fn.length ? fn.apply(self,args) : function(){ + return master.apply( self, args.concat(curry.args(arguments)) ); + }; + }; +}; + +curry.args = function( args ){ + return Array.prototype.slice.call(args); +}; + +Function.prototype.curry = function(){ + return curry(this); +}; \ No newline at end of file diff --git a/addons/base_diagram/static/lib/js/dracula_algorithms.js b/addons/base_diagram/static/lib/js/dracula_algorithms.js new file mode 100644 index 00000000000..0fbb0857fc8 --- /dev/null +++ b/addons/base_diagram/static/lib/js/dracula_algorithms.js @@ -0,0 +1,599 @@ +/* + * Various algorithms and data structures, licensed under the MIT-license. + * (c) 2010 by Johann Philipp Strathausen + * http://strathausen.eu + * + */ + + + +/* + Bellman-Ford + + Path-finding algorithm, finds the shortest paths from one node to all nodes. + + + Complexity + + O( |E| · |V| ), where E = edges and V = vertices (nodes) + + + Constraints + + Can run on graphs with negative edge weights as long as they do not have + any negative weight cycles. + + */ +function bellman_ford(g, source) { + + /* STEP 1: initialisation */ + for(var n in g.nodes) + g.nodes[n].distance = Infinity; + /* predecessors are implicitly null */ + source.distance = 0; + + step("Initially, all distances are infinite and all predecessors are null."); + + /* STEP 2: relax each edge (this is at the heart of Bellman-Ford) */ + /* repeat this for the number of nodes minus one */ + for(var i = 1; i < g.nodes.length; i++) + /* for each edge */ + for(var e in g.edges) { + var edge = g.edges[e]; + if(edge.source.distance + edge.weight < edge.target.distance) { + step("Relax edge between " + edge.source.id + " and " + edge.target.id + "."); + edge.target.distance = edge.source.distance + edge.weight; + edge.target.predecessor = edge.source; + } + //Added by Jake Stothard (Needs to be tested) + if(!edge.style.directed) { + if(edge.target.distance + edge.weight < edge.source.distance) { + g.snapShot("Relax edge between "+edge.target.id+" and "+edge.source.id+"."); + edge.source.distance = edge.target.distance + edge.weight; + edge.source.predecessor = edge.target; + } + } + } + step("Ready."); + + /* STEP 3: TODO Check for negative cycles */ + /* For now we assume here that the graph does not contain any negative + weights cycles. (this is left as an excercise to the reader[tm]) */ +} + + + +/* + Path-finding algorithm Dijkstra + + - worst-case running time is O((|E| + |V|) · log |V| ) thus better than + Bellman-Ford for sparse graphs (with less edges), but cannot handle + negative edge weights + */ +function dijkstra(g, source) { + + /* initially, all distances are infinite and all predecessors are null */ + for(var n in g.nodes) + g.nodes[n].distance = Infinity; + /* predecessors are implicitly null */ + + g.snapShot("Initially, all distances are infinite and all predecessors are null."); + + source.distance = 0; + /* set of unoptimized nodes, sorted by their distance (but a Fibonacci heap + would be better) */ + var q = new BinaryMinHeap(g.nodes, "distance"); + + /* pointer to the node in focus */ + var node; + + /* get the node with the smallest distance + as long as we have unoptimized nodes. q.min() can have O(log n). */ + while(q.min() != undefined) { + /* remove the latest */ + node = q.extractMin(); + node.optimized = true; + + /* no nodes accessible from this one, should not happen */ + if(node.distance == Infinity) + throw "Orphaned node!"; + + /* for each neighbour of node */ + for(e in node.edges) { + var other = (node == node.edges[e].target) ? node.edges[e].source : node.edges[e].target; + + if(other.optimized) + continue; + + /* look for an alternative route */ + var alt = node.distance + node.edges[e].weight; + + /* update distance and route if a better one has been found */ + if (alt < other.distance) { + + /* update distance of neighbour */ + other.distance = alt; + + /* update priority queue */ + q.heapify(); + + /* update path */ + other.predecessor = node; + g.snapShot("Enhancing node.") + } + } + } +} + + +/* All-Pairs-Shortest-Paths */ +/* Runs at worst in O(|V|³) and at best in Omega(|V|³) :-) + complexity Sigma(|V|²) */ +/* This implementation is not yet ready for general use, but works with the + Dracula graph library. */ +function floyd_warshall(g, source) { + + /* Step 1: initialising empty path matrix (second dimension is implicit) */ + var path = []; + var next = []; + var n = g.nodes.length; + + /* construct path matrix, initialize with Infinity */ + for(j in g.nodes) { + path[j] = []; + next[j] = []; + for(i in g.nodes) + path[j][i] = j == i ? 0 : Infinity; + } + + /* initialize path with edge weights */ + for(e in g.edges) + path[g.edges[e].source.id][g.edges[e].target.id] = g.edges[e].weight; + + /* Note: Usually, the initialisation is done by getting the edge weights + from a node matrix representation of the graph, not by iterating through + a list of edges as done here. */ + + /* Step 2: find best distances (the heart of Floyd-Warshall) */ + for(k in g.nodes){ + for(i in g.nodes) { + for(j in g.nodes) + if(path[i][j] > path[i][k] + path[k][j]) { + path[i][j] = path[i][k] + path[k][j]; + /* Step 2.b: remember the path */ + next[i][j] = k; + } + } + } + + /* Step 3: Path reconstruction, get shortest path */ + function getPath(i, j) { + if(path[i][j] == Infinity) + throw "There is no path."; + var intermediate = next[i][j]; + if(intermediate == undefined) + return null; + else + return getPath(i, intermediate) + .concat([intermediate]) + .concat(getPath(intermediate, j)); + } + + /* TODO use the knowledge, e.g. mark path in graph */ +} + +/* + Ford-Fulkerson + + Max-Flow-Min-Cut Algorithm finding the maximum flow through a directed + graph from source to sink. + + + Complexity + + O(E * max(f)), max(f) being the maximum flow + + + Description + + As long as there is an open path through the residual graph, send the + minimum of the residual capacities on the path. + + + Constraints + + The algorithm works only if all weights are integers. Otherwise it is + possible that the Ford–Fulkerson algorithm will not converge to the maximum + value. + + + Input + + g - Graph object + s - Source ID + t - Target (sink) ID + + + Output + + Maximum flow from Source s to Target t + + */ +/* + Edmonds-Karp + + Max-Flow-Min-Cut Algorithm finding the maximum flow through a directed + graph from source to sink. An implementation of the Ford-Fulkerson + algorithm. + + + Complexity + + O(|V|*|E|²) + + + Input + + g - Graph object (with node and edge lists, capacity is a property of edge) + s - source ID + t - sink ID + + */ +function edmonds_karp(g, s, t) { + +} + +/* + A simple binary min-heap serving as a priority queue + - takes an array as the input, with elements having a key property + - elements will look like this: + { + key: "... key property ...", + value: "... element content ..." + } + - provides insert(), min(), extractMin() and heapify() + - example usage (e.g. via the Firebug or Chromium console): + var x = {foo: 20, hui: "bla"}; + var a = new BinaryMinHeap([x,{foo:3},{foo:10},{foo:20},{foo:30},{foo:6},{foo:1},{foo:3}],"foo"); + console.log(a.extractMin()); + console.log(a.extractMin()); + x.foo = 0; // update key + a.heapify(); // call this always after having a key updated + console.log(a.extractMin()); + console.log(a.extractMin()); + - can also be used on a simple array, like [9,7,8,5] + */ +function BinaryMinHeap(array, key) { + + /* Binary tree stored in an array, no need for a complicated data structure */ + var tree = []; + + var key = key || 'key'; + + /* Calculate the index of the parent or a child */ + var parent = function(index) { return Math.floor((index - 1)/2); }; + var right = function(index) { return 2 * index + 2; }; + var left = function(index) { return 2 * index + 1; }; + + /* Helper function to swap elements with their parent + as long as the parent is bigger */ + function bubble_up(i) { + var p = parent(i); + while((p >= 0) && (tree[i][key] < tree[p][key])) { + /* swap with parent */ + tree[i] = tree.splice(p, 1, tree[i])[0]; + /* go up one level */ + i = p; + p = parent(i); + } + } + + /* Helper function to swap elements with the smaller of their children + as long as there is one */ + function bubble_down(i) { + var l = left(i); + var r = right(i); + + /* as long as there are smaller children */ + while(tree[l] && (tree[i][key] > tree[l][key]) || tree[r] && (tree[i][key] > tree[r][key])) { + + /* find smaller child */ + var child = tree[l] ? tree[r] ? tree[l][key] > tree[r][key] ? r : l : l : l; + + /* swap with smaller child with current element */ + tree[i] = tree.splice(child, 1, tree[i])[0]; + + /* go up one level */ + i = child; + l = left(i); + r = right(i); + } + } + + /* Insert a new element with respect to the heap property + 1. Insert the element at the end + 2. Bubble it up until it is smaller than its parent */ + this.insert = function(element) { + + /* make sure there's a key property */ + (element[key] == undefined) && (element = {key:element}); + + /* insert element at the end */ + tree.push(element); + + /* bubble up the element */ + bubble_up(tree.length - 1); + } + + /* Only show us the minimum */ + this.min = function() { + return tree.length == 1 ? undefined : tree[0]; + } + + /* Return and remove the minimum + 1. Take the root as the minimum that we are looking for + 2. Move the last element to the root (thereby deleting the root) + 3. Compare the new root with both of its children, swap it with the + smaller child and then check again from there (bubble down) + */ + this.extractMin = function() { + var result = this.min(); + + /* move the last element to the root or empty the tree completely */ + /* bubble down the new root if necessary */ + (tree.length == 1) && (tree = []) || (tree[0] = tree.pop()) && bubble_down(0); + + return result; + } + + /* currently unused, TODO implement */ + this.changeKey = function(index, key) { + throw "function not implemented"; + } + + this.heapify = function() { + for(var start = Math.floor((tree.length - 2) / 2); start >= 0; start--) { + bubble_down(start); + } + } + + /* insert the input elements one by one only when we don't have a key property (TODO can be done more elegant) */ + for(i in (array || [])) + this.insert(array[i]); +} + + + +/* + Quick Sort: + 1. Select some random value from the array, the median. + 2. Divide the array in three smaller arrays according to the elements + being less, equal or greater than the median. + 3. Recursively sort the array containg the elements less than the + median and the one containing elements greater than the median. + 4. Concatenate the three arrays (less, equal and greater). + 5. One or no element is always sorted. + TODO: This could be implemented more efficiently by using only one array object and several pointers. +*/ +function quickSort(arr) { + /* recursion anchor: one element is always sorted */ + if(arr.length <= 1) return arr; + /* randomly selecting some value */ + var median = arr[Math.floor(Math.random() * arr.length)]; + var arr1 = [], arr2 = [], arr3 = []; + for(var i in arr) { + arr[i] < median && arr1.push(arr[i]); + arr[i] == median && arr2.push(arr[i]); + arr[i] > median && arr3.push(arr[i]); + } + /* recursive sorting and assembling final result */ + return quickSort(arr1).concat(arr2).concat(quickSort(arr3)); +} + +/* + Selection Sort: + 1. Select the minimum and remove it from the array + 2. Sort the rest recursively + 3. Return the minimum plus the sorted rest + 4. An array with only one element is already sorted +*/ +function selectionSort(arr) { + /* recursion anchor: one element is always sorted */ + if(arr.length == 1) return arr; + var minimum = Infinity; + var index; + for(var i in arr) { + if(arr[i] < minimum) { + minimum = arr[i]; + index = i; /* remember the minimum index for later removal */ + } + } + /* remove the minimum */ + arr.splice(index, 1); + /* assemble result and sort recursively (could be easily done iteratively as well)*/ + return [minimum].concat(selectionSort(arr)); +} + +/* + Merge Sort: + 1. Cut the array in half + 2. Sort each of them recursively + 3. Merge the two sorted arrays + 4. An array with only one element is considered sorted + +*/ +function mergeSort(arr) { + /* merges two sorted arrays into one sorted array */ + function merge(a, b) { + /* result set */ + var c = []; + /* as long as there are elements in the arrays to be merged */ + while(a.length > 0 || b.length > 0){ + /* are there elements to be merged, if yes, compare them and merge */ + var n = a.length > 0 && b.length > 0 ? a[0] < b[0] ? a.shift() : b.shift() : b.length > 0 ? b.shift() : a.length > 0 ? a.shift() : null; + /* always push the smaller one onto the result set */ + n != null && c.push(n); + } + return c; + } + /* this mergeSort implementation cuts the array in half, wich should be fine with randomized arrays, but introduces the risk of a worst-case scenario */ + median = Math.floor(arr.length / 2); + var part1 = arr.slice(0, median); /* for some reason it doesn't work if inserted directly in the return statement (tried so with firefox) */ + var part2 = arr.slice(median - arr.length); + return arr.length <= 1 ? arr : merge( + mergeSort(part1), /* first half */ + mergeSort(part2) /* second half */ + ); +} + +/* Balanced Red-Black-Tree */ +function RedBlackTree(arr) { + +} + +function BTree(arr) { + +} + +function NaryTree(n, arr) { + +} + +/** + * Knuth-Morris-Pratt string matching algorithm - finds a pattern in a text. + * FIXME: Doesn't work correctly yet. + */ +function kmp(p, t) { + + /** + * PREFIX, OVERLAP or FALIURE function for KMP. Computes how many iterations + * the algorithm can skip after a mismatch. + * + * @input p - pattern (string) + * @result array of skippable iterations + */ + function prefix(p) { + /* pi contains the computed skip marks */ + var pi = [0], k = 0; + for(q = 1; q < p.length; q++) { + while(k > 0 && (p.charAt(k) != p.charAt(q))) + k = pi[k-1]; + + (p.charAt(k) == p.charAt(q)) && k++; + + pi[q] = k; + } + return pi; + } + + /* The actual KMP algorithm starts here. */ + + var pi = prefix(p), q = 0, result = []; + + for(var i = 0; i < t.length; i++) { + /* jump forward as long as the character doesn't match */ + while((q > 0) && (p.charAt(q) != t.charAt(i))) + q = pi[q]; + + (p.charAt(q) == t.charAt(i)) && q++; + + (q == p.length) && result.push(i - p.length) && (q = pi[q]); + } + + return result; +} + +/* step for algorithm visualisation */ +function step(comment, funct) { + //wait for input + //display comment (before or after waiting) +// next.wait(); + /* execute callback function */ + funct(); +} + +/** + * Curry - Function currying + * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) + * Date: 10/4/2008 + * + * @author Ariel Flesler + * @version 1.0.1 + */ +function curry( fn ){ + return function(){ + var args = curry.args(arguments), + master = arguments.callee, + self = this; + + return args.length >= fn.length ? fn.apply(self,args) : function(){ + return master.apply( self, args.concat(curry.args(arguments)) ); + }; + }; +}; + +curry.args = function( args ){ + return Array.prototype.slice.call(args); +}; + +Function.prototype.curry = function(){ + return curry(this); +}; + +/** + * Topological Sort + * + * Sort a directed graph based on incoming edges + * + * Coded by Jake Stothard + */ +function topological_sort(g) { + //Mark nodes as "deleted" instead of actually deleting them + //That way we don't have to copy g + + for(i in g.nodes) + g.nodes[i].deleted = false; + + var ret = topological_sort_helper(g); + + //Cleanup: Remove the deleted property + for(i in g.nodes) + delete g.nodes[i].deleted + + return ret; +} +function topological_sort_helper(g) { + //Find node with no incoming edges + var node; + for(i in g.nodes) { + if(g.nodes[i].deleted) + continue; //Bad style, meh + + var incoming = false; + for(j in g.nodes[i].edges) { + if(g.nodes[i].edges[j].target == g.nodes[i] + && g.nodes[i].edges[j].source.deleted == false) { + incoming = true; + break; + } + } + if(!incoming) { + node = g.nodes[i]; + break; + } + } + + // Either unsortable or done. Either way, GTFO + if(node == undefined) + return []; + + //"Delete" node from g + node.deleted = true; + + var tail = topological_sort_helper(g); + + tail.unshift(node); + + return tail; +} diff --git a/addons/base_diagram/static/lib/js/dracula_graffle.js b/addons/base_diagram/static/lib/js/dracula_graffle.js new file mode 100644 index 00000000000..9d4fd2099e8 --- /dev/null +++ b/addons/base_diagram/static/lib/js/dracula_graffle.js @@ -0,0 +1,106 @@ +/** + * Originally grabbed from the official RaphaelJS Documentation + * http://raphaeljs.com/graffle.html + * Adopted (arrows) and commented by Philipp Strathausen http://blog.ameisenbar.de + * Licenced under the MIT licence. + */ + +/** + * Usage: + * connect two shapes + * parameters: + * source shape [or connection for redrawing], + * target shape, + * style with { fg : linecolor, bg : background color, directed: boolean } + * returns: + * connection { draw = function() } + */ +Raphael.fn.connection = function (obj1, obj2, style) { + var selfRef = this; + /* create and return new connection */ + var edge = {/* + from : obj1, + to : obj2, + style : style,*/ + draw : function() { + /* get bounding boxes of target and source */ + var bb1 = obj1.getBBox(); + var bb2 = obj2.getBBox(); + var off1 = 0; + var off2 = 0; + /* coordinates for potential connection coordinates from/to the objects */ + var p = [ + {x: bb1.x + bb1.width / 2, y: bb1.y - off1}, /* NORTH 1 */ + {x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + off1}, /* SOUTH 1 */ + {x: bb1.x - off1, y: bb1.y + bb1.height / 2}, /* WEST 1 */ + {x: bb1.x + bb1.width + off1, y: bb1.y + bb1.height / 2}, /* EAST 1 */ + {x: bb2.x + bb2.width / 2, y: bb2.y - off2}, /* NORTH 2 */ + {x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + off2}, /* SOUTH 2 */ + {x: bb2.x - off2, y: bb2.y + bb2.height / 2}, /* WEST 2 */ + {x: bb2.x + bb2.width + off2, y: bb2.y + bb2.height / 2} /* EAST 2 */ + ]; + + /* distances between objects and according coordinates connection */ + var d = {}, dis = []; + + /* + * find out the best connection coordinates by trying all possible ways + */ + /* loop the first object's connection coordinates */ + for (var i = 0; i < 4; i++) { + /* loop the seond object's connection coordinates */ + for (var j = 4; j < 8; j++) { + var dx = Math.abs(p[i].x - p[j].x), + dy = Math.abs(p[i].y - p[j].y); + if ((i == j - 4) || (((i != 3 && j != 6) || p[i].x < p[j].x) && ((i != 2 && j != 7) || p[i].x > p[j].x) && ((i != 0 && j != 5) || p[i].y > p[j].y) && ((i != 1 && j != 4) || p[i].y < p[j].y))) { + dis.push(dx + dy); + d[dis[dis.length - 1].toFixed(3)] = [i, j]; + } + } + } + var res = dis.length == 0 ? [0, 4] : d[Math.min.apply(Math, dis).toFixed(3)]; + /* bezier path */ + var x1 = p[res[0]].x, + y1 = p[res[0]].y, + x4 = p[res[1]].x, + y4 = p[res[1]].y, + dx = Math.max(Math.abs(x1 - x4) / 2, 10), + dy = Math.max(Math.abs(y1 - y4) / 2, 10), + x2 = [x1, x1, x1 - dx, x1 + dx][res[0]].toFixed(3), + y2 = [y1 - dy, y1 + dy, y1, y1][res[0]].toFixed(3), + x3 = [0, 0, 0, 0, x4, x4, x4 - dx, x4 + dx][res[1]].toFixed(3), + y3 = [0, 0, 0, 0, y1 + dy, y1 - dy, y4, y4][res[1]].toFixed(3); + /* assemble path and arrow */ + var path = ["M", x1.toFixed(3), y1.toFixed(3), "C", x2, y2, x3, y3, x4.toFixed(3), y4.toFixed(3)].join(","); + /* arrow */ + if(style && style.directed) { + /* magnitude, length of the last path vector */ + var mag = Math.sqrt((y4 - y3) * (y4 - y3) + (x4 - x3) * (x4 - x3)); + /* vector normalisation to specified length */ + var norm = function(x,l){return (-x*(l||5)/mag);}; + /* calculate array coordinates (two lines orthogonal to the path vector) */ + var arr = [ + {x:(norm(x4-x3)+norm(y4-y3)+x4).toFixed(3), y:(norm(y4-y3)+norm(x4-x3)+y4).toFixed(3)}, + {x:(norm(x4-x3)-norm(y4-y3)+x4).toFixed(3), y:(norm(y4-y3)-norm(x4-x3)+y4).toFixed(3)} + ]; + path = path + ",M"+arr[0].x+","+arr[0].y+",L"+x4+","+y4+",L"+arr[1].x+","+arr[1].y; + } + /* function to be used for moving existent path(s), e.g. animate() or attr() */ + var move = "attr"; + /* applying path(s) */ + edge.fg && edge.fg[move]({path:path}) + || (edge.fg = selfRef.path(path).attr({stroke: style && style.stroke || "#000", fill: "none"}).toBack()); + edge.bg && edge.bg[move]({path:path}) + || style && style.fill && (edge.bg = style.fill.split && selfRef.path(path).attr({stroke: style.fill.split("|")[0], fill: "none", "stroke-width": style.fill.split("|")[1] || 3}).toBack()); + /* setting label */ + style && style.label + && (edge.label && edge.label.attr({x:(x1+x4)/2, y:(y1+y4)/2}) + || (edge.label = selfRef.text((x1+x4)/2, (y1+y4)/2, style.label).attr({fill: "#000", "font-size": style["font-size"] || "12px"}))); +// && selfRef.text(x4, y4, style.label).attr({stroke: style && style.stroke || "#fff", "font-weight":"bold", "font-size":"20px"}) +// style && style.callback && style.callback(edge); + } + } + edge.draw(); + return edge; +}; +//Raphael.prototype.set.prototype.dodo=function(){console.log("works");}; diff --git a/addons/base_diagram/static/lib/js/dracula_graph.coffee b/addons/base_diagram/static/lib/js/dracula_graph.coffee new file mode 100644 index 00000000000..bb91945098d --- /dev/null +++ b/addons/base_diagram/static/lib/js/dracula_graph.coffee @@ -0,0 +1,524 @@ +### + * Dracula Graph Layout and Drawing Framework 0.0.3alpha + * (c) 2010 Philipp Strathausen , http://strathausen.eu + * + * Contributions by: + * Branched by Jake Stothard . + * + * based on the Graph JavaScript framework, version 0.0.1 + * (c) 2006 Aslak Hellesoy + * (c) 2006 Dave Hoover + * + * Ported from Graph::Layouter::Spring in + * http://search.cpan.org/~pasky/Graph-Layderer-0.02/ + * The algorithm is based on a spring-style layouter of a Java-based social + * network tracker PieSpy written by Paul Mutton Epaul@jibble.orgE. + * + * This code is freely distributable under the terms of an MIT-style license. + * For details, see the Graph web site: http://dev.buildpatternd.com/trac + * + * Links: + * + * Graph Dracula JavaScript Framework: + * http://graphdracula.net + * + * Demo of the original applet: + * http://redsquirrel.com/dave/work/webdep/ + * + * Mirrored original source code at snipplr: + * http://snipplr.com/view/1950/graph-javascript-framework-version-001/ + * + * Original usage example: + * http://ajaxian.com/archives/new-javascriptcanvas-graph-library + * +### + + +### + Edge Factory +### +AbstractEdge = -> + +AbstractEdge.prototype = + hide: -> + @connection.fg.hide() + @connection.bg && @bg.connection.hide() + +EdgeFactory = -> + @template = new AbstractEdge() + @template.style = new Object() + @template.style.directed = false + @template.weight = 1 + +EdgeFactory.prototype = + build: (source, target) -> + e = jQuery.extend true, {}, @template + e.source = source + e.target = target + e + +### + Graph +### +Graph = -> + @nodes = {} + @edges = [] + @snapshots = [] # previous graph states TODO to be implemented + @edgeFactory = new EdgeFactory() + +Graph.prototype = +### + add a node + @id the node's ID (string or number) + @content (optional, dictionary) can contain any information that is + being interpreted by the layout algorithm or the graph + representation +### + addNode: (id, content) -> + # testing if node is already existing in the graph + if @nodes[id] == undefined + @nodes[id] = new Graph.Node id, content + @nodes[id] + + addEdge: (source, target, style) -> + s = @addNode source + t = @addNode target + var edge = @edgeFactory.build s, t + jQuery.extend edge.style, style + s.edges.push edge + @edges.push edge + # NOTE: Even directed edges are added to both nodes. + t.edges.push edge + + # TODO to be implemented + # Preserve a copy of the graph state (nodes, positions, ...) + # @comment a comment describing the state + snapShot: (comment) -> + ###/* FIXME + var graph = new Graph() + graph.nodes = jQuery.extend(true, {}, @nodes) + graph.edges = jQuery.extend(true, {}, @edges) + @snapshots.push({comment: comment, graph: graph}) + */ + ### + + removeNode: (id) -> + delete @nodes[id] + for i = 0; i < @edges.length; i++ + if @edges[i].source.id == id || @edges[i].target.id == id + @edges.splice(i, 1) + i-- + +/* + * Node + */ +Graph.Node = (id, node) -> + node = node || {} + node.id = id + node.edges = [] + node.hide = -> + @hidden = true + @shape && @shape.hide() # FIXME this is representation specific code and should be elsewhere */ + for(i in @edges) + (@edges[i].source.id == id || @edges[i].target == id) && @edges[i].hide && @edges[i].hide() + + node.show = -> + @hidden = false + @shape && @shape.show() + for(i in @edges) + (@edges[i].source.id == id || @edges[i].target == id) && @edges[i].show && @edges[i].show() + + node + +Graph.Node.prototype = { } + +### + Renderer base class +### +Graph.Renderer = { } + +### + Renderer implementation using RaphaelJS +### +Graph.Renderer.Raphael = (element, graph, width, height) -> + @width = width || 400 + @height = height || 400 + var selfRef = this + @r = Raphael element, @width, @height + @radius = 40 # max dimension of a node + @graph = graph + @mouse_in = false + + # TODO default node rendering + if(!@graph.render) { + @graph.render = -> + return + } + } + + /* + * Dragging + */ + @isDrag = false + @dragger = (e) -> + @dx = e.clientX + @dy = e.clientY + selfRef.isDrag = this + @set && @set.animate "fill-opacity": .1, 200 && @set.toFront() + e.preventDefault && e.preventDefault() + + document.onmousemove = (e) { + e = e || window.event + if (selfRef.isDrag) { + var bBox = selfRef.isDrag.set.getBBox() + // TODO round the coordinates here (eg. for proper image representation) + var newX = e.clientX - selfRef.isDrag.dx + (bBox.x + bBox.width / 2) + var newY = e.clientY - selfRef.isDrag.dy + (bBox.y + bBox.height / 2) + /* prevent shapes from being dragged out of the canvas */ + var clientX = e.clientX - (newX < 20 ? newX - 20 : newX > selfRef.width - 20 ? newX - selfRef.width + 20 : 0) + var clientY = e.clientY - (newY < 20 ? newY - 20 : newY > selfRef.height - 20 ? newY - selfRef.height + 20 : 0) + selfRef.isDrag.set.translate(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)) + // console.log(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)) + for (var i in selfRef.graph.edges) { + selfRef.graph.edges[i].connection && selfRef.graph.edges[i].connection.draw() + } + //selfRef.r.safari() + selfRef.isDrag.dx = clientX + selfRef.isDrag.dy = clientY + } + } + document.onmouseup = -> + selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500) + selfRef.isDrag = false + } + @draw() +} +Graph.Renderer.Raphael.prototype = { + translate: (point) { + return [ + (point[0] - @graph.layoutMinX) * @factorX + @radius, + (point[1] - @graph.layoutMinY) * @factorY + @radius + ] + }, + + rotate: (point, length, angle) { + var dx = length * Math.cos(angle) + var dy = length * Math.sin(angle) + return [point[0]+dx, point[1]+dy] + }, + + draw: -> + @factorX = (@width - 2 * @radius) / (@graph.layoutMaxX - @graph.layoutMinX) + @factorY = (@height - 2 * @radius) / (@graph.layoutMaxY - @graph.layoutMinY) + for (i in @graph.nodes) { + @drawNode(@graph.nodes[i]) + } + for (var i = 0; i < @graph.edges.length; i++) { + @drawEdge(@graph.edges[i]) + } + }, + + drawNode: (node) { + var point = @translate([node.layoutPosX, node.layoutPosY]) + node.point = point + + /* if node has already been drawn, move the nodes */ + if(node.shape) { + var oBBox = node.shape.getBBox() + var opoint = { x: oBBox.x + oBBox.width / 2, y: oBBox.y + oBBox.height / 2} + node.shape.translate(Math.round(point[0] - opoint.x), Math.round(point[1] - opoint.y)) + @r.safari() + return node + }/* else, draw new nodes */ + + var shape + + /* if a node renderer is provided by the user, then use it + or the default render instead */ + if(!node.render) { + node.render = (r, node) { + /* the default node drawing */ + var color = Raphael.getColor() + var ellipse = r.ellipse(0, 0, 30, 20).attr({fill: color, stroke: color, "stroke-width": 2}) + /* set DOM node ID */ + ellipse.node.id = node.label || node.id + shape = r.set(). + push(ellipse). + push(r.text(0, 30, node.label || node.id)) + return shape + } + } + /* or check for an ajax representation of the nodes */ + if(node.shapes) { + // TODO ajax representation evaluation + } + + shape = node.render(@r, node).hide() + + shape.attr({"fill-opacity": .6}) + /* re-reference to the node an element belongs to, needed for dragging all elements of a node */ + shape.items.forEach((item){ item.set = shape; item.node.style.cursor = "move"; }) + shape.mousedown(@dragger) + + var box = shape.getBBox() + shape.translate(Math.round(point[0]-(box.x+box.width/2)),Math.round(point[1]-(box.y+box.height/2))) + //console.log(box,point) + node.hidden || shape.show() + node.shape = shape + }, + drawEdge: (edge) { + /* if this edge already exists the other way around and is undirected */ + if(edge.backedge) + return + if(edge.source.hidden || edge.target.hidden) { + edge.connection && edge.connection.fg.hide() | edge.connection.bg && edge.connection.bg.hide() + return + } + /* if edge already has been drawn, only refresh the edge */ + if(!edge.connection) { + edge.style && edge.style.callback && edge.style.callback(edge); // TODO move this somewhere else + edge.connection = @r.connection(edge.source.shape, edge.target.shape, edge.style) + return + } + //FIXME showing doesn't work well + edge.connection.fg.show() + edge.connection.bg && edge.connection.bg.show() + edge.connection.draw() + } +} +Graph.Layout = {} +Graph.Layout.Spring = (graph) { + @graph = graph + @iterations = 500 + @maxRepulsiveForceDistance = 6 + @k = 2 + @c = 0.01 + @maxVertexMovement = 0.5 + @layout() +} +Graph.Layout.Spring.prototype = { + layout: -> + @layoutPrepare() + for (var i = 0; i < @iterations; i++) { + @layoutIteration() + } + @layoutCalcBounds() + }, + + layoutPrepare: -> + for (i in @graph.nodes) { + var node = @graph.nodes[i] + node.layoutPosX = 0 + node.layoutPosY = 0 + node.layoutForceX = 0 + node.layoutForceY = 0 + } + + }, + + layoutCalcBounds: -> + var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity + + for (i in @graph.nodes) { + var x = @graph.nodes[i].layoutPosX + var y = @graph.nodes[i].layoutPosY + + if(x > maxx) maxx = x + if(x < minx) minx = x + if(y > maxy) maxy = y + if(y < miny) miny = y + } + + @graph.layoutMinX = minx + @graph.layoutMaxX = maxx + @graph.layoutMinY = miny + @graph.layoutMaxY = maxy + }, + + layoutIteration: -> + // Forces on nodes due to node-node repulsions + + var prev = new Array() + for(var c in @graph.nodes) { + var node1 = @graph.nodes[c] + for (var d in prev) { + var node2 = @graph.nodes[prev[d]] + @layoutRepulsive(node1, node2) + + } + prev.push(c) + } + + // Forces on nodes due to edge attractions + for (var i = 0; i < @graph.edges.length; i++) { + var edge = @graph.edges[i] + @layoutAttractive(edge); + } + + // Move by the given force + for (i in @graph.nodes) { + var node = @graph.nodes[i] + var xmove = @c * node.layoutForceX + var ymove = @c * node.layoutForceY + + var max = @maxVertexMovement + if(xmove > max) xmove = max + if(xmove < -max) xmove = -max + if(ymove > max) ymove = max + if(ymove < -max) ymove = -max + + node.layoutPosX += xmove + node.layoutPosY += ymove + node.layoutForceX = 0 + node.layoutForceY = 0 + } + }, + + layoutRepulsive: (node1, node2) { + var dx = node2.layoutPosX - node1.layoutPosX + var dy = node2.layoutPosY - node1.layoutPosY + var d2 = dx * dx + dy * dy + if(d2 < 0.01) { + dx = 0.1 * Math.random() + 0.1 + dy = 0.1 * Math.random() + 0.1 + var d2 = dx * dx + dy * dy + } + var d = Math.sqrt(d2) + if(d < @maxRepulsiveForceDistance) { + var repulsiveForce = @k * @k / d + node2.layoutForceX += repulsiveForce * dx / d + node2.layoutForceY += repulsiveForce * dy / d + node1.layoutForceX -= repulsiveForce * dx / d + node1.layoutForceY -= repulsiveForce * dy / d + } + }, + + layoutAttractive: (edge) { + var node1 = edge.source + var node2 = edge.target + + var dx = node2.layoutPosX - node1.layoutPosX + var dy = node2.layoutPosY - node1.layoutPosY + var d2 = dx * dx + dy * dy + if(d2 < 0.01) { + dx = 0.1 * Math.random() + 0.1 + dy = 0.1 * Math.random() + 0.1 + var d2 = dx * dx + dy * dy + } + var d = Math.sqrt(d2) + if(d > @maxRepulsiveForceDistance) { + d = @maxRepulsiveForceDistance + d2 = d * d + } + var attractiveForce = (d2 - @k * @k) / @k + if(edge.attraction == undefined) edge.attraction = 1 + attractiveForce *= Math.log(edge.attraction) * 0.5 + 1 + + node2.layoutForceX -= attractiveForce * dx / d + node2.layoutForceY -= attractiveForce * dy / d + node1.layoutForceX += attractiveForce * dx / d + node1.layoutForceY += attractiveForce * dy / d + } +} + +Graph.Layout.Ordered = (graph, order) { + @graph = graph + @order = order + @layout() +} +Graph.Layout.Ordered.prototype = { + layout: -> + @layoutPrepare() + @layoutCalcBounds() + }, + + layoutPrepare: (order) { + for (i in @graph.nodes) { + var node = @graph.nodes[i] + node.layoutPosX = 0 + node.layoutPosY = 0 + } + var counter = 0 + for (i in @order) { + var node = @order[i] + node.layoutPosX = counter + node.layoutPosY = Math.random() + counter++ + } + }, + + layoutCalcBounds: -> + var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity + + for (i in @graph.nodes) { + var x = @graph.nodes[i].layoutPosX + var y = @graph.nodes[i].layoutPosY + + if(x > maxx) maxx = x + if(x < minx) minx = x + if(y > maxy) maxy = y + if(y < miny) miny = y + } + + @graph.layoutMinX = minx + @graph.layoutMaxX = maxx + + @graph.layoutMinY = miny + @graph.layoutMaxY = maxy + } +} + +/* + * usefull JavaScript extensions, + */ + + log(a) {console.log&&console.log(a);} + +/* + * Raphael Tooltip Plugin + * - attaches an element as a tooltip to another element + * + * Usage example, adding a rectangle as a tooltip to a circle: + * + * paper.circle(100,100,10).tooltip(paper.rect(0,0,20,30)) + * + * If you want to use more shapes, you'll have to put them into a set. + * + */ +Raphael.el.tooltip = (tp) { + @tp = tp + @tp.o = {x: 0, y: 0} + @tp.hide() + @hover( + (event){ + @mousemove((event){ + @tp.translate(event.clientX - + @tp.o.x,event.clientY - @tp.o.y) + @tp.o = {x: event.clientX, y: event.clientY} + }) + @tp.show().toFront() + }, + (event){ + @tp.hide() + @unmousemove() + }) + return this +} + +/* For IE */ +if (!Array.prototype.forEach) +{ + Array.prototype.forEach = (fun /*, thisp*/) + { + var len = @length + if (typeof fun != "") + throw new TypeError() + + var thisp = arguments[1] + for (var i = 0; i < len; i++) + { + if (i in this) + fun.call(thisp, this[i], i, this) + } + } +} diff --git a/addons/base_diagram/static/lib/js/dracula_graph.js b/addons/base_diagram/static/lib/js/dracula_graph.js new file mode 100644 index 00000000000..f7069c5d2e5 --- /dev/null +++ b/addons/base_diagram/static/lib/js/dracula_graph.js @@ -0,0 +1,525 @@ +/* + * Dracula Graph Layout and Drawing Framework 0.0.3alpha + * (c) 2010 Philipp Strathausen , http://strathausen.eu + * Contributions by Jake Stothard . + * + * based on the Graph JavaScript framework, version 0.0.1 + * (c) 2006 Aslak Hellesoy + * (c) 2006 Dave Hoover + * + * Ported from Graph::Layouter::Spring in + * http://search.cpan.org/~pasky/Graph-Layderer-0.02/ + * The algorithm is based on a spring-style layouter of a Java-based social + * network tracker PieSpy written by Paul Mutton . + * + * This code is freely distributable under the MIT license. Commercial use is + * hereby granted without any cost or restriction. + * + * Links: + * + * Graph Dracula JavaScript Framework: + * http://graphdracula.net + * + /*--------------------------------------------------------------------------*/ + +/* + * Edge Factory + */ +var AbstractEdge = function() { +} +AbstractEdge.prototype = { + hide: function() { + this.connection.fg.hide(); + this.connection.bg && this.bg.connection.hide(); + } +}; +var EdgeFactory = function() { + this.template = new AbstractEdge(); + this.template.style = new Object(); + this.template.style.directed = false; + this.template.weight = 1; +}; +EdgeFactory.prototype = { + build: function(source, target) { + var e = jQuery.extend(true, {}, this.template); + e.source = source; + e.target = target; + return e; + } +}; + +/* + * Graph + */ +var Graph = function() { + this.nodes = {}; + this.edges = []; + this.snapshots = []; // previous graph states TODO to be implemented + this.edgeFactory = new EdgeFactory(); +}; +Graph.prototype = { + /* + * add a node + * @id the node's ID (string or number) + * @content (optional, dictionary) can contain any information that is + * being interpreted by the layout algorithm or the graph + * representation + */ + addNode: function(id, content) { + /* testing if node is already existing in the graph */ + if(this.nodes[id] == undefined) { + this.nodes[id] = new Graph.Node(id, content); + } + return this.nodes[id]; + }, + + addEdge: function(source, target, style) { + var s = this.addNode(source); + var t = this.addNode(target); + var edge = this.edgeFactory.build(s, t); + jQuery.extend(edge.style,style); + s.edges.push(edge); + this.edges.push(edge); + // NOTE: Even directed edges are added to both nodes. + t.edges.push(edge); + }, + + /* TODO to be implemented + * Preserve a copy of the graph state (nodes, positions, ...) + * @comment a comment describing the state + */ + snapShot: function(comment) { + /* FIXME + var graph = new Graph(); + graph.nodes = jQuery.extend(true, {}, this.nodes); + graph.edges = jQuery.extend(true, {}, this.edges); + this.snapshots.push({comment: comment, graph: graph}); + */ + }, + removeNode: function(id) { + delete this.nodes[id]; + for(var i = 0; i < this.edges.length; i++) { + if (this.edges[i].source.id == id || this.edges[i].target.id == id) { + this.edges.splice(i, 1); + i--; + } + } + } +}; + +/* + * Node + */ +Graph.Node = function(id, node){ + node = node || {}; + node.id = id; + node.edges = []; + node.hide = function() { + this.hidden = true; + this.shape && this.shape.hide(); /* FIXME this is representation specific code and should be elsewhere */ + for(i in this.edges) + (this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].hide && this.edges[i].hide(); + }; + node.show = function() { + this.hidden = false; + this.shape && this.shape.show(); + for(i in this.edges) + (this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].show && this.edges[i].show(); + }; + return node; +}; +Graph.Node.prototype = { +}; + +/* + * Renderer base class + */ +Graph.Renderer = {}; + +/* + * Renderer implementation using RaphaelJS + */ +Graph.Renderer.Raphael = function(element, graph, width, height) { + this.width = width || 800; + this.height = height || 800; + var selfRef = this; + this.r = Raphael(element, this.width, this.height); + this.radius = 40; /* max dimension of a node */ + this.graph = graph; + this.mouse_in = false; + + /* TODO default node rendering function */ + if(!this.graph.render) { + this.graph.render = function() { + return; + } + } + + /* + * Dragging + */ + this.isDrag = false; + this.dragger = function (e) { + this.dx = e.clientX; + this.dy = e.clientY; + selfRef.isDrag = this; + this.set && this.set.animate({"fill-opacity": .1}, 200) && this.set.toFront(); + e.preventDefault && e.preventDefault(); + }; + + var d = document.getElementById(element); + d.onmousemove = function (e) { + e = e || window.event; + if (selfRef.isDrag) { + var bBox = selfRef.isDrag.set.getBBox(); + // TODO round the coordinates here (eg. for proper image representation) + var newX = e.clientX - selfRef.isDrag.dx + (bBox.x + bBox.width / 2); + var newY = e.clientY - selfRef.isDrag.dy + (bBox.y + bBox.height / 2); + /* prevent shapes from being dragged out of the canvas */ + var clientX = e.clientX - (newX < 20 ? newX - 20 : newX > selfRef.width - 20 ? newX - selfRef.width + 20 : 0); + var clientY = e.clientY - (newY < 20 ? newY - 20 : newY > selfRef.height - 20 ? newY - selfRef.height + 20 : 0); + selfRef.isDrag.set.translate(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)); + // console.log(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)); + for (var i in selfRef.graph.edges) { + selfRef.graph.edges[i].connection && selfRef.graph.edges[i].connection.draw(); + } + //selfRef.r.safari(); + selfRef.isDrag.dx = clientX; + selfRef.isDrag.dy = clientY; + } + }; + d.onmouseup = function () { + selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500); + selfRef.isDrag = false; + }; + this.draw(); +}; +Graph.Renderer.Raphael.prototype = { + translate: function(point) { + return [ + (point[0] - this.graph.layoutMinX) * this.factorX + this.radius, + (point[1] - this.graph.layoutMinY) * this.factorY + this.radius + ]; + }, + + rotate: function(point, length, angle) { + var dx = length * Math.cos(angle); + var dy = length * Math.sin(angle); + return [point[0]+dx, point[1]+dy]; + }, + + draw: function() { + this.factorX = (this.width - 2 * this.radius) / (this.graph.layoutMaxX - this.graph.layoutMinX); + this.factorY = (this.height - 2 * this.radius) / (this.graph.layoutMaxY - this.graph.layoutMinY); + for (i in this.graph.nodes) { + this.drawNode(this.graph.nodes[i]); + } + for (var i = 0; i < this.graph.edges.length; i++) { + this.drawEdge(this.graph.edges[i]); + } + }, + + drawNode: function(node) { + var point = this.translate([node.layoutPosX, node.layoutPosY]); + node.point = point; + + /* if node has already been drawn, move the nodes */ + if(node.shape) { + var oBBox = node.shape.getBBox(); + var opoint = { x: oBBox.x + oBBox.width / 2, y: oBBox.y + oBBox.height / 2}; + node.shape.translate(Math.round(point[0] - opoint.x), Math.round(point[1] - opoint.y)); + this.r.safari(); + return node; + }/* else, draw new nodes */ + + var shape; + + /* if a node renderer function is provided by the user, then use it + or the default render function instead */ + if(!node.render) { + node.render = function(r, node) { + /* the default node drawing */ + var color = Raphael.getColor(); + var ellipse = r.ellipse(0, 0, 30, 20).attr({fill: color, stroke: color, "stroke-width": 2}); + /* set DOM node ID */ + ellipse.node.id = node.label || node.id; + shape = r.set(). + push(ellipse). + push(r.text(0, 30, node.label || node.id)); + return shape; + } + } + /* or check for an ajax representation of the nodes */ + if(node.shapes) { + // TODO ajax representation evaluation + } + + shape = node.render(this.r, node).hide(); + + shape.attr({"fill-opacity": .6}); + /* re-reference to the node an element belongs to, needed for dragging all elements of a node */ + shape.items.forEach(function(item){ item.set = shape; item.node.style.cursor = "move"; }); + shape.mousedown(this.dragger); + + var box = shape.getBBox(); + shape.translate(Math.round(point[0]-(box.x+box.width/2)),Math.round(point[1]-(box.y+box.height/2))) + //console.log(box,point); + node.hidden || shape.show(); + node.shape = shape; + }, + drawEdge: function(edge) { + /* if this edge already exists the other way around and is undirected */ + if(edge.backedge) + return; + if(edge.source.hidden || edge.target.hidden) { + edge.connection && edge.connection.fg.hide() | edge.connection.bg && edge.connection.bg.hide(); + return; + } + /* if edge already has been drawn, only refresh the edge */ + if(!edge.connection) { + edge.style && edge.style.callback && edge.style.callback(edge); // TODO move this somewhere else + edge.connection = this.r.connection(edge.source.shape, edge.target.shape, edge.style); + return; + } + //FIXME showing doesn't work well + edge.connection.fg.show(); + edge.connection.bg && edge.connection.bg.show(); + edge.connection.draw(); + } +}; +Graph.Layout = {}; +Graph.Layout.Spring = function(graph) { + this.graph = graph; + this.iterations = 500; + this.maxRepulsiveForceDistance = 6; + this.k = 2; + this.c = 0.01; + this.maxVertexMovement = 0.5; + this.layout(); +}; +Graph.Layout.Spring.prototype = { + layout: function() { + this.layoutPrepare(); + for (var i = 0; i < this.iterations; i++) { + this.layoutIteration(); + } + this.layoutCalcBounds(); + }, + + layoutPrepare: function() { + for (i in this.graph.nodes) { + var node = this.graph.nodes[i]; + node.layoutPosX = 0; + node.layoutPosY = 0; + node.layoutForceX = 0; + node.layoutForceY = 0; + } + + }, + + layoutCalcBounds: function() { + var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity; + + for (i in this.graph.nodes) { + var x = this.graph.nodes[i].layoutPosX; + var y = this.graph.nodes[i].layoutPosY; + + if(x > maxx) maxx = x; + if(x < minx) minx = x; + if(y > maxy) maxy = y; + if(y < miny) miny = y; + } + + this.graph.layoutMinX = minx; + this.graph.layoutMaxX = maxx; + this.graph.layoutMinY = miny; + this.graph.layoutMaxY = maxy; + }, + + layoutIteration: function() { + // Forces on nodes due to node-node repulsions + + var prev = new Array(); + for(var c in this.graph.nodes) { + var node1 = this.graph.nodes[c]; + for (var d in prev) { + var node2 = this.graph.nodes[prev[d]]; + this.layoutRepulsive(node1, node2); + + } + prev.push(c); + } + + // Forces on nodes due to edge attractions + for (var i = 0; i < this.graph.edges.length; i++) { + var edge = this.graph.edges[i]; + this.layoutAttractive(edge); + } + + // Move by the given force + for (i in this.graph.nodes) { + var node = this.graph.nodes[i]; + var xmove = this.c * node.layoutForceX; + var ymove = this.c * node.layoutForceY; + + var max = this.maxVertexMovement; + if(xmove > max) xmove = max; + if(xmove < -max) xmove = -max; + if(ymove > max) ymove = max; + if(ymove < -max) ymove = -max; + + node.layoutPosX += xmove; + node.layoutPosY += ymove; + node.layoutForceX = 0; + node.layoutForceY = 0; + } + }, + + layoutRepulsive: function(node1, node2) { + var dx = node2.layoutPosX - node1.layoutPosX; + var dy = node2.layoutPosY - node1.layoutPosY; + var d2 = dx * dx + dy * dy; + if(d2 < 0.01) { + dx = 0.1 * Math.random() + 0.1; + dy = 0.1 * Math.random() + 0.1; + var d2 = dx * dx + dy * dy; + } + var d = Math.sqrt(d2); + if(d < this.maxRepulsiveForceDistance) { + var repulsiveForce = this.k * this.k / d; + node2.layoutForceX += repulsiveForce * dx / d; + node2.layoutForceY += repulsiveForce * dy / d; + node1.layoutForceX -= repulsiveForce * dx / d; + node1.layoutForceY -= repulsiveForce * dy / d; + } + }, + + layoutAttractive: function(edge) { + var node1 = edge.source; + var node2 = edge.target; + + var dx = node2.layoutPosX - node1.layoutPosX; + var dy = node2.layoutPosY - node1.layoutPosY; + var d2 = dx * dx + dy * dy; + if(d2 < 0.01) { + dx = 0.1 * Math.random() + 0.1; + dy = 0.1 * Math.random() + 0.1; + var d2 = dx * dx + dy * dy; + } + var d = Math.sqrt(d2); + if(d > this.maxRepulsiveForceDistance) { + d = this.maxRepulsiveForceDistance; + d2 = d * d; + } + var attractiveForce = (d2 - this.k * this.k) / this.k; + if(edge.attraction == undefined) edge.attraction = 1; + attractiveForce *= Math.log(edge.attraction) * 0.5 + 1; + + node2.layoutForceX -= attractiveForce * dx / d; + node2.layoutForceY -= attractiveForce * dy / d; + node1.layoutForceX += attractiveForce * dx / d; + node1.layoutForceY += attractiveForce * dy / d; + } +}; + +Graph.Layout.Ordered = function(graph, order) { + this.graph = graph; + this.order = order; + this.layout(); +}; +Graph.Layout.Ordered.prototype = { + layout: function() { + this.layoutPrepare(); + this.layoutCalcBounds(); + }, + + layoutPrepare: function(order) { + for (i in this.graph.nodes) { + var node = this.graph.nodes[i]; + node.layoutPosX = 0; + node.layoutPosY = 0; + } + var counter = 0; + for (i in this.order) { + var node = this.order[i]; + node.layoutPosX = counter; + node.layoutPosY = Math.random(); + counter++; + } + }, + + layoutCalcBounds: function() { + var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity; + + for (i in this.graph.nodes) { + var x = this.graph.nodes[i].layoutPosX; + var y = this.graph.nodes[i].layoutPosY; + + if(x > maxx) maxx = x; + if(x < minx) minx = x; + if(y > maxy) maxy = y; + if(y < miny) miny = y; + } + + this.graph.layoutMinX = minx; + this.graph.layoutMaxX = maxx; + + this.graph.layoutMinY = miny; + this.graph.layoutMaxY = maxy; + } +}; + +/* + * usefull JavaScript extensions, + */ + +function log(a) {console.log&&console.log(a);} + +/* + * Raphael Tooltip Plugin + * - attaches an element as a tooltip to another element + * + * Usage example, adding a rectangle as a tooltip to a circle: + * + * paper.circle(100,100,10).tooltip(paper.rect(0,0,20,30)); + * + * If you want to use more shapes, you'll have to put them into a set. + * + */ +Raphael.el.tooltip = function (tp) { + this.tp = tp; + this.tp.o = {x: 0, y: 0}; + this.tp.hide(); + this.hover( + function(event){ + this.mousemove(function(event){ + this.tp.translate(event.clientX - + this.tp.o.x,event.clientY - this.tp.o.y); + this.tp.o = {x: event.clientX, y: event.clientY}; + }); + this.tp.show().toFront(); + }, + function(event){ + this.tp.hide(); + this.unmousemove(); + }); + return this; +}; + +/* For IE */ +if (!Array.prototype.forEach) +{ + Array.prototype.forEach = function(fun /*, thisp*/) + { + var len = this.length; + if (typeof fun != "function") + throw new TypeError(); + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + { + if (i in this) + fun.call(thisp, this[i], i, this); + } + }; +} diff --git a/addons/base_diagram/static/lib/js/raphael-min.js b/addons/base_diagram/static/lib/js/raphael-min.js new file mode 100644 index 00000000000..8718b5bc749 --- /dev/null +++ b/addons/base_diagram/static/lib/js/raphael-min.js @@ -0,0 +1,7 @@ +/* + * Raphael 1.3.1 - JavaScript Vector Library + * + * Copyright (c) 2008 - 2009 Dmitry Baranovskiy (http://raphaeljs.com) + * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. + */ +Raphael=(function(){var a=/[, ]+/,aO=/^(circle|rect|path|ellipse|text|image)$/,L=document,au=window,l={was:"Raphael" in au,is:au.Raphael},an=function(){if(an.is(arguments[0],"array")){var d=arguments[0],e=w[aW](an,d.splice(0,3+an.is(d[0],al))),S=e.set();for(var R=0,a0=d[m];R

";if(ag.childNodes[m]!=2){return null;}}an.svg=!(an.vml=an.type=="VML");aT[aY]=an[aY];an._id=0;an._oid=0;an.fn={};an.is=function(e,d){d=aZ.call(d);return((d=="object"||d=="undefined")&&typeof e==d)||(e==null&&d=="null")||aZ.call(aw.call(e).slice(8,-1))==d;};an.setWindow=function(d){au=d;L=au.document;};var aD=function(e){if(an.vml){var d=/^\s+|\s+$/g;aD=aj(function(R){var S;R=(R+at)[aP](d,at);try{var a0=new ActiveXObject("htmlfile");a0.write("");a0.close();S=a0.body;}catch(a2){S=createPopup().document.body;}var i=S.createTextRange();try{S.style.color=R;var a1=i.queryCommandValue("ForeColor");a1=((a1&255)<<16)|(a1&65280)|((a1&16711680)>>>16);return"#"+("000000"+a1[aA](16)).slice(-6);}catch(a2){return"none";}});}else{var E=L.createElement("i");E.title="Rapha\xebl Colour Picker";E.style.display="none";L.body[aL](E);aD=aj(function(i){E.style.color=i;return L.defaultView.getComputedStyle(E,at).getPropertyValue("color");});}return aD(e);};an.hsb2rgb=aj(function(a3,a1,a7){if(an.is(a3,"object")&&"h" in a3&&"s" in a3&&"b" in a3){a7=a3.b;a1=a3.s;a3=a3.h;}var R,S,a8;if(a7==0){return{r:0,g:0,b:0,hex:"#000"};}if(a3>1||a1>1||a7>1){a3/=255;a1/=255;a7/=255;}var a0=~~(a3*6),a4=(a3*6)-a0,E=a7*(1-a1),e=a7*(1-(a1*a4)),a9=a7*(1-(a1*(1-a4)));R=[a7,e,E,E,a9,a7,a7][a0];S=[a9,a7,a7,e,E,E,a9][a0];a8=[E,E,a9,a7,a7,e,E][a0];R*=255;S*=255;a8*=255;var a5={r:R,g:S,b:a8},d=(~~R)[aA](16),a2=(~~S)[aA](16),a6=(~~a8)[aA](16);d=d[aP](aU,"0");a2=a2[aP](aU,"0");a6=a6[aP](aU,"0");a5.hex="#"+d+a2+a6;return a5;},an);an.rgb2hsb=aj(function(d,e,a1){if(an.is(d,"object")&&"r" in d&&"g" in d&&"b" in d){a1=d.b;e=d.g;d=d.r;}if(an.is(d,"string")){var a3=an.getRGB(d);d=a3.r;e=a3.g;a1=a3.b;}if(d>1||e>1||a1>1){d/=255;e/=255;a1/=255;}var a0=g(d,e,a1),i=aI(d,e,a1),R,E,S=a0;if(i==a0){return{h:0,s:0,b:a0};}else{var a2=(a0-i);E=a2/a0;if(d==a0){R=(e-a1)/a2;}else{if(e==a0){R=2+((a1-d)/a2);}else{R=4+((d-e)/a2);}}R/=6;R<0&&R++;R>1&&R--;}return{h:R,s:E,b:S};},an);var aE=/,?([achlmqrstvxz]),?/gi;an._path2string=function(){return this.join(",")[aP](aE,"$1");};function aj(E,e,d){function i(){var R=Array[aY].slice.call(arguments,0),a0=R[az]("\u25ba"),S=i.cache=i.cache||{},a1=i.count=i.count||[];if(S[Q](a0)){return d?d(S[a0]):S[a0];}a1[m]>=1000&&delete S[a1.shift()];a1[f](a0);S[a0]=E[aW](e,R);return d?d(S[a0]):S[a0];}return i;}an.getRGB=aj(function(d){if(!d||!!((d=d+at).indexOf("-")+1)){return{r:-1,g:-1,b:-1,hex:"none",error:1};}if(d=="none"){return{r:-1,g:-1,b:-1,hex:"none"};}!(({hs:1,rg:1})[Q](d.substring(0,2))||d.charAt()=="#")&&(d=aD(d));var S,i,E,a2,a3,a0=d.match(x);if(a0){if(a0[2]){a2=G(a0[2].substring(5),16);E=G(a0[2].substring(3,5),16);i=G(a0[2].substring(1,3),16);}if(a0[3]){a2=G((a3=a0[3].charAt(3))+a3,16);E=G((a3=a0[3].charAt(2))+a3,16);i=G((a3=a0[3].charAt(1))+a3,16);}if(a0[4]){a0=a0[4][z](/\s*,\s*/);i=W(a0[0]);E=W(a0[1]);a2=W(a0[2]);}if(a0[5]){a0=a0[5][z](/\s*,\s*/);i=W(a0[0])*2.55;E=W(a0[1])*2.55;a2=W(a0[2])*2.55;}if(a0[6]){a0=a0[6][z](/\s*,\s*/);i=W(a0[0]);E=W(a0[1]);a2=W(a0[2]);return an.hsb2rgb(i,E,a2);}if(a0[7]){a0=a0[7][z](/\s*,\s*/);i=W(a0[0])*2.55;E=W(a0[1])*2.55;a2=W(a0[2])*2.55;return an.hsb2rgb(i,E,a2);}a0={r:i,g:E,b:a2};var e=(~~i)[aA](16),R=(~~E)[aA](16),a1=(~~a2)[aA](16);e=e[aP](aU,"0");R=R[aP](aU,"0");a1=a1[aP](aU,"0");a0.hex="#"+e+R+a1;return a0;}return{r:-1,g:-1,b:-1,hex:"none",error:1};},an);an.getColor=function(e){var i=this.getColor.start=this.getColor.start||{h:0,s:1,b:e||0.75},d=this.hsb2rgb(i.h,i.s,i.b);i.h+=0.075;if(i.h>1){i.h=0;i.s-=0.2;i.s<=0&&(this.getColor.start={h:0,s:1,b:i.b});}return d.hex;};an.getColor.reset=function(){delete this.start;};an.parsePathString=aj(function(d){if(!d){return null;}var i={a:7,c:6,h:1,l:2,m:2,q:4,s:4,t:2,v:1,z:0},e=[];if(an.is(d,"array")&&an.is(d[0],"array")){e=av(d);}if(!e[m]){(d+at)[aP](/([achlmqstvz])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?\s*,?\s*)+)/ig,function(R,E,a1){var a0=[],S=aZ.call(E);a1[aP](/(-?\d*\.?\d*(?:e[-+]?\d+)?)\s*,?\s*/ig,function(a3,a2){a2&&a0[f](+a2);});while(a0[m]>=i[S]){e[f]([E][aS](a0.splice(0,i[S])));if(!i[S]){break;}}});}e[aA]=an._path2string;return e;});an.findDotsAtSegment=function(e,d,be,bc,a0,R,a2,a1,a8){var a6=1-a8,a5=aM(a6,3)*e+aM(a6,2)*3*a8*be+a6*3*a8*a8*a0+aM(a8,3)*a2,a3=aM(a6,3)*d+aM(a6,2)*3*a8*bc+a6*3*a8*a8*R+aM(a8,3)*a1,ba=e+2*a8*(be-e)+a8*a8*(a0-2*be+e),a9=d+2*a8*(bc-d)+a8*a8*(R-2*bc+d),bd=be+2*a8*(a0-be)+a8*a8*(a2-2*a0+be),bb=bc+2*a8*(R-bc)+a8*a8*(a1-2*R+bc),a7=(1-a8)*e+a8*be,a4=(1-a8)*d+a8*bc,E=(1-a8)*a0+a8*a2,i=(1-a8)*R+a8*a1,S=(90-ab.atan((ba-bd)/(a9-bb))*180/ab.PI);(ba>bd||a91){bi=ab.sqrt(by)*bi;bg=ab.sqrt(by)*bg;}var E=bi*bi,br=bg*bg,bt=(a4==S?-1:1)*ab.sqrt(ab.abs((E*br-E*bn*bn-br*bo*bo)/(E*bn*bn+br*bo*bo))),bd=bt*bi*bn/bg+(a9+a8)/2,bc=bt*-bg*bo/bi+(bE+bD)/2,a3=ab.asin(((bE-bc)/bg).toFixed(7)),a2=ab.asin(((bD-bc)/bg).toFixed(7));a3=a9a2){a3=a3-R*2;}if(!S&&a2>a3){a2=a2-R*2;}}else{a3=bb[0];a2=bb[1];bd=bb[2];bc=bb[3];}var a7=a2-a3;if(ab.abs(a7)>bf){var be=a2,bh=a8,a5=bD;a2=a3+bf*(S&&a2>a3?1:-1);a8=bd+bi*ab.cos(a2);bD=bc+bg*ab.sin(a2);bm=K(a8,bD,bi,bg,ba,0,S,bh,a5,[a2,be,bd,bc]);}a7=a2-a3;var a1=ab.cos(a3),bC=ab.sin(a3),a0=ab.cos(a2),bB=ab.sin(a2),bp=ab.tan(a7/4),bs=4/3*bi*bp,bq=4/3*bg*bp,bz=[a9,bE],bx=[a9+bs*bC,bE-bq*a1],bw=[a8+bs*bB,bD-bq*a0],bu=[a8,bD];bx[0]=2*bz[0]-bx[0];bx[1]=2*bz[1]-bx[1];if(bb){return[bx,bw,bu][aS](bm);}else{bm=[bx,bw,bu][aS](bm)[az]()[z](",");var bk=[];for(var bv=0,bl=bm[m];bv1000000000000&&(a0=0.5);ab.abs(S)>1000000000000&&(S=0.5);if(a0>0&&a0<1){e=M(i,d,R,E,a9,a8,a5,a2,a0);a6[f](e.x);a3[f](e.y);}if(S>0&&S<1){e=M(i,d,R,E,a9,a8,a5,a2,S);a6[f](e.x);a3[f](e.y);}a7=(a8-2*E+d)-(a2-2*a8+E);a4=2*(E-d)-2*(a8-E);a1=d-E;a0=(-a4+ab.sqrt(a4*a4-4*a7*a1))/2/a7;S=(-a4-ab.sqrt(a4*a4-4*a7*a1))/2/a7;ab.abs(a0)>1000000000000&&(a0=0.5);ab.abs(S)>1000000000000&&(S=0.5);if(a0>0&&a0<1){e=M(i,d,R,E,a9,a8,a5,a2,a0);a6[f](e.x);a3[f](e.y);}if(S>0&&S<1){e=M(i,d,R,E,a9,a8,a5,a2,S);a6[f](e.x);a3[f](e.y);}return{min:{x:aI[aW](0,a6),y:aI[aW](0,a3)},max:{x:g[aW](0,a6),y:g[aW](0,a3)}};}),H=aj(function(a9,a4){var R=r(a9),a5=a4&&r(a4),a6={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},d={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},a0=function(ba,bb){var i,bc;if(!ba){return["C",bb.x,bb.y,bb.x,bb.y,bb.x,bb.y];}!(ba[0] in {T:1,Q:1})&&(bb.qx=bb.qy=null);switch(ba[0]){case"M":bb.X=ba[1];bb.Y=ba[2];break;case"A":ba=["C"][aS](K[aW](0,[bb.x,bb.y][aS](ba.slice(1))));break;case"S":i=bb.x+(bb.x-(bb.bx||bb.x));bc=bb.y+(bb.y-(bb.by||bb.y));ba=["C",i,bc][aS](ba.slice(1));break;case"T":bb.qx=bb.x+(bb.x-(bb.qx||bb.x));bb.qy=bb.y+(bb.y-(bb.qy||bb.y));ba=["C"][aS](aK(bb.x,bb.y,bb.qx,bb.qy,ba[1],ba[2]));break;case"Q":bb.qx=ba[1];bb.qy=ba[2];ba=["C"][aS](aK(bb.x,bb.y,ba[1],ba[2],ba[3],ba[4]));break;case"L":ba=["C"][aS](aX(bb.x,bb.y,ba[1],ba[2]));break;case"H":ba=["C"][aS](aX(bb.x,bb.y,ba[1],bb.y));break;case"V":ba=["C"][aS](aX(bb.x,bb.y,bb.x,ba[1]));break;case"Z":ba=["C"][aS](aX(bb.x,bb.y,bb.X,bb.Y));break;}return ba;},e=function(ba,bb){if(ba[bb][m]>7){ba[bb].shift();var bc=ba[bb];while(bc[m]){ba.splice(bb++,0,["C"][aS](bc.splice(0,6)));}ba.splice(bb,1);a7=g(R[m],a5&&a5[m]||0);}},E=function(be,bd,bb,ba,bc){if(be&&bd&&be[bc][0]=="M"&&bd[bc][0]!="M"){bd.splice(bc,0,["M",ba.x,ba.y]);bb.bx=0;bb.by=0;bb.x=be[bc][1];bb.y=be[bc][2];a7=g(R[m],a5&&a5[m]||0);}};for(var a2=0,a7=g(R[m],a5&&a5[m]||0);a23){return{container:1,x:arguments[0],y:arguments[1],width:arguments[2],height:arguments[3]};}}},aG=function(d,i){var e=this;for(var E in i){if(i[Q](E)&&!(E in d)){switch(typeof i[E]){case"function":(function(R){d[E]=d===e?R:function(){return R[aW](e,arguments);};})(i[E]);break;case"object":d[E]=d[E]||{};aG.call(this,d[E],i[E]);break;default:d[E]=i[E];break;}}}},ak=function(d,e){d==e.top&&(e.top=d.prev);d==e.bottom&&(e.bottom=d.next);d.next&&(d.next.prev=d.prev);d.prev&&(d.prev.next=d.next);},Y=function(d,e){if(e.top===d){return;}ak(d,e);d.next=null;d.prev=e.top;e.top.next=d;e.top=d;},k=function(d,e){if(e.bottom===d){return;}ak(d,e);d.next=e.bottom;d.prev=null;e.bottom.prev=d;e.bottom=d;},A=function(e,d,i){ak(e,i);d==i.top&&(i.top=e);d.next&&(d.next.prev=e);e.next=d.next;e.prev=d;d.next=e;},aq=function(e,d,i){ak(e,i);d==i.bottom&&(i.bottom=e);d.prev&&(d.prev.next=e);e.prev=d.prev;d.prev=e;e.next=d;},s=function(d){return function(){throw new Error("Rapha\xebl: you are calling to method \u201c"+d+"\u201d of removed object");};},ar=/^r(?:\(([^,]+?)\s*,\s*([^\)]+?)\))?/;if(an.svg){aT[aY].svgns="http://www.w3.org/2000/svg";aT[aY].xlink="http://www.w3.org/1999/xlink";var O=function(d){return +d+(~~d===d)*0.5;},V=function(S){for(var e=0,E=S[m];e0.5)*2-1);aM(a1-0.5,2)+aM(S-0.5,2)>0.25&&(S=ab.sqrt(0.25-aM(a1-0.5,2))*ba+0.5)&&S!=0.5&&(S=S.toFixed(5)-0.00001*ba);}return at;});a7=a7[z](/\s*\-\s*/);if(a4=="linear"){var a0=a7.shift();a0=-W(a0);if(isNaN(a0)){return null;}var R=[0,0,ab.cos(a0*ab.PI/180),ab.sin(a0*ab.PI/180)],a6=1/(g(ab.abs(R[2]),ab.abs(R[3]))||1);R[2]*=a6;R[3]*=a6;if(R[2]<0){R[0]=-R[2];R[2]=0;}if(R[3]<0){R[1]=-R[3];R[3]=0;}}var a3=p(a7);if(!a3){return null;}var e=aJ(a4+"Gradient");e.id="r"+(an._id++)[aA](36);aJ(e,a4=="radial"?{fx:a1,fy:S}:{x1:R[0],y1:R[1],x2:R[2],y2:R[3]});d.defs[aL](e);for(var a2=0,a8=a3[m];a2a1.height)&&(a1.height=a0.y+a0.height-a1.y);(a0.x+a0.width-a1.x>a1.width)&&(a1.width=a0.x+a0.width-a1.x);}}E&&this.hide();return a1;};ax[aY].attr=function(){if(this.removed){return this;}if(arguments[m]==0){var R={};for(var E in this.attrs){if(this.attrs[Q](E)){R[E]=this.attrs[E];}}this._.rt.deg&&(R.rotation=this.rotate());(this._.sx!=1||this._.sy!=1)&&(R.scale=this.scale());R.gradient&&R.fill=="none"&&(R.fill=R.gradient)&&delete R.gradient;return R;}if(arguments[m]==1&&an.is(arguments[0],"string")){if(arguments[0]=="translation"){return t.call(this);}if(arguments[0]=="rotation"){return this.rotate();}if(arguments[0]=="scale"){return this.scale();}if(arguments[0]=="fill"&&this.attrs.fill=="none"&&this.attrs.gradient){return this.attrs.gradient;}return this.attrs[arguments[0]];}if(arguments[m]==1&&an.is(arguments[0],"array")){var d={};for(var e in arguments[0]){if(arguments[0][Q](e)){d[arguments[0][e]]=this.attrs[arguments[0][e]];}}return d;}if(arguments[m]==2){var S={};S[arguments[0]]=arguments[1];aa(this,S);}else{if(arguments[m]==1&&an.is(arguments[0],"object")){aa(this,arguments[0]);}}return this;};ax[aY].toFront=function(){if(this.removed){return this;}this.node.parentNode[aL](this.node);var d=this.paper;d.top!=this&&Y(this,d);return this;};ax[aY].toBack=function(){if(this.removed){return this;}if(this.node.parentNode.firstChild!=this.node){this.node.parentNode.insertBefore(this.node,this.node.parentNode.firstChild);k(this,this.paper);var d=this.paper;}return this;};ax[aY].insertAfter=function(d){if(this.removed){return this;}var e=d.node;if(e.nextSibling){e.parentNode.insertBefore(this.node,e.nextSibling);}else{e.parentNode[aL](this.node);}A(this,d,this.paper);return this;};ax[aY].insertBefore=function(d){if(this.removed){return this;}var e=d.node;e.parentNode.insertBefore(this.node,e);aq(this,d,this.paper);return this;};var P=function(e,d,S,R){d=O(d);S=O(S);var E=aJ("circle");e.canvas&&e.canvas[aL](E);var i=new ax(E,e);i.attrs={cx:d,cy:S,r:R,fill:"none",stroke:"#000"};i.type="circle";aJ(E,i.attrs);return i;};var aF=function(i,d,a1,e,S,a0){d=O(d);a1=O(a1);var R=aJ("rect");i.canvas&&i.canvas[aL](R);var E=new ax(R,i);E.attrs={x:d,y:a1,width:e,height:S,r:a0||0,rx:a0||0,ry:a0||0,fill:"none",stroke:"#000"};E.type="rect";aJ(R,E.attrs);return E;};var ai=function(e,d,a0,S,R){d=O(d);a0=O(a0);var E=aJ("ellipse");e.canvas&&e.canvas[aL](E);var i=new ax(E,e);i.attrs={cx:d,cy:a0,rx:S,ry:R,fill:"none",stroke:"#000"};i.type="ellipse";aJ(E,i.attrs);return i;};var o=function(i,a0,d,a1,e,S){var R=aJ("image");aJ(R,{x:d,y:a1,width:e,height:S,preserveAspectRatio:"none"});R.setAttributeNS(i.xlink,"href",a0);i.canvas&&i.canvas[aL](R);var E=new ax(R,i);E.attrs={x:d,y:a1,width:e,height:S,src:a0};E.type="image";return E;};var X=function(e,d,S,R){var E=aJ("text");aJ(E,{x:d,y:S,"text-anchor":"middle"});e.canvas&&e.canvas[aL](E);var i=new ax(E,e);i.attrs={x:d,y:S,"text-anchor":"middle",text:R,font:j.font,stroke:"none",fill:"#000"};i.type="text";aa(i,i.attrs);return i;};var aV=function(e,d){this.width=e||this.width;this.height=d||this.height;this.canvas[v]("width",this.width);this.canvas[v]("height",this.height);return this;};var w=function(){var E=ao[aW](null,arguments),i=E&&E.container,e=E.x,a0=E.y,R=E.width,d=E.height;if(!i){throw new Error("SVG container not found.");}var S=aJ("svg");R=R||512;d=d||342;aJ(S,{xmlns:"http://www.w3.org/2000/svg",version:1.1,width:R,height:d});if(i==1){S.style.cssText="position:absolute;left:"+e+"px;top:"+a0+"px";L.body[aL](S);}else{if(i.firstChild){i.insertBefore(S,i.firstChild);}else{i[aL](S);}}i=new aT;i.width=R;i.height=d;i.canvas=S;aG.call(i,i,an.fn);i.clear();return i;};aT[aY].clear=function(){var d=this.canvas;while(d.firstChild){d.removeChild(d.firstChild);}this.bottom=this.top=null;(this.desc=aJ("desc"))[aL](L.createTextNode("Created with Rapha\xebl"));d[aL](this.desc);d[aL](this.defs=aJ("defs"));};aT[aY].remove=function(){this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas);for(var d in this){this[d]=s(d);}};}if(an.vml){var aH=function(a8){var a5=/[ahqstv]/ig,a0=r;(a8+at).match(a5)&&(a0=H);a5=/[clmz]/g;if(a0==r&&!(a8+at).match(a5)){var e={M:"m",L:"l",C:"c",Z:"x",m:"t",l:"r",c:"v",z:"x"},R=/([clmz]),?([^clmz]*)/gi,S=/-?[^,\s-]+/g;var a4=(a8+at)[aP](R,function(a9,bb,i){var ba=[];i[aP](S,function(bc){ba[f](O(bc));});return e[bb]+ba;});return a4;}var a6=a0(a8),E,a4=[],d;for(var a2=0,a7=a6[m];a21&&(e=1);a7.opacity=e;}a8.fill&&(a7.on=true);if(a7.on==null||a8.fill=="none"){a7.on=false;}if(a7.on&&a8.fill){var i=a8.fill.match(c);if(i){a7.src=i[1];a7.type="tile";}else{a7.color=an.getRGB(a8.fill).hex;a7.src=at;a7.type="solid";if(an.getRGB(a8.fill).error&&(bd.type in {circle:1,ellipse:1}||(a8.fill+at).charAt()!="r")&&b(bd,a8.fill)){a9.fill="none";a9.gradient=a8.fill;}}}ba&&a6[aL](a7);var R=(a6.getElementsByTagName("stroke")&&a6.getElementsByTagName("stroke")[0]),bb=false;!R&&(bb=R=ah("stroke"));if((a8.stroke&&a8.stroke!="none")||a8["stroke-width"]||a8["stroke-opacity"]!=null||a8["stroke-dasharray"]||a8["stroke-miterlimit"]||a8["stroke-linejoin"]||a8["stroke-linecap"]){R.on=true;}(a8.stroke=="none"||R.on==null||a8.stroke==0||a8["stroke-width"]==0)&&(R.on=false);R.on&&a8.stroke&&(R.color=an.getRGB(a8.stroke).hex);var e=((+a9["stroke-opacity"]+1||2)-1)*((+a9.opacity+1||2)-1),a4=(W(a8["stroke-width"])||1)*0.75;e<0&&(e=0);e>1&&(e=1);a8["stroke-width"]==null&&(a4=a9["stroke-width"]);a8["stroke-width"]&&(R.weight=a4);a4&&a4<1&&(e*=a4)&&(R.weight=1);R.opacity=e;a8["stroke-linejoin"]&&(R.joinstyle=a8["stroke-linejoin"]||"miter");R.miterlimit=a8["stroke-miterlimit"]||8;a8["stroke-linecap"]&&(R.endcap=a8["stroke-linecap"]=="butt"?"flat":a8["stroke-linecap"]=="square"?"square":"round");if(a8["stroke-dasharray"]){var a5={"-":"shortdash",".":"shortdot","-.":"shortdashdot","-..":"shortdashdotdot",". ":"dot","- ":"dash","--":"longdash","- .":"dashdot","--.":"longdashdot","--..":"longdashdotdot"};R.dashstyle=a5[Q](a8["stroke-dasharray"])?a5[a8["stroke-dasharray"]]:at;}bb&&a6[aL](R);}if(bd.type=="text"){var a0=bd.paper.span.style;a9.font&&(a0.font=a9.font);a9["font-family"]&&(a0.fontFamily=a9["font-family"]);a9["font-size"]&&(a0.fontSize=a9["font-size"]);a9["font-weight"]&&(a0.fontWeight=a9["font-weight"]);a9["font-style"]&&(a0.fontStyle=a9["font-style"]);bd.node.string&&(bd.paper.span.innerHTML=(bd.node.string+at)[aP](/"));bd.W=a9.w=bd.paper.span.offsetWidth;bd.H=a9.h=bd.paper.span.offsetHeight;bd.X=a9.x;bd.Y=a9.y+O(bd.H/2);switch(a9["text-anchor"]){case"start":bd.node.style["v-text-align"]="left";bd.bbx=O(bd.W/2);break;case"end":bd.node.style["v-text-align"]="right";bd.bbx=-O(bd.W/2);break;default:bd.node.style["v-text-align"]="center";break;}}};var b=function(d,a1){d.attrs=d.attrs||{};var a2=d.attrs,a4=d.node.getElementsByTagName("fill"),S="linear",a0=".5 .5";d.attrs.gradient=a1;a1=(a1+at)[aP](ar,function(a6,a7,i){S="radial";if(a7&&i){a7=W(a7);i=W(i);aM(a7-0.5,2)+aM(i-0.5,2)>0.25&&(i=ab.sqrt(0.25-aM(a7-0.5,2))*((i>0.5)*2-1)+0.5);a0=a7+am+i;}return at;});a1=a1[z](/\s*\-\s*/);if(S=="linear"){var e=a1.shift();e=-W(e);if(isNaN(e)){return null;}}var R=p(a1);if(!R){return null;}d=d.shape||d.node;a4=a4[0]||ah("fill");if(R[m]){a4.on=true;a4.method="none";a4.type=(S=="radial")?"gradientradial":"gradient";a4.color=R[0].color;a4.color2=R[R[m]-1].color;var a5=[];for(var E=0,a3=R[m];E');};}catch(af){ah=function(d){return L.createElement("<"+d+' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');};}var w=function(){var i=ao[aW](null,arguments),d=i.container,a2=i.height,a3,e=i.width,a1=i.x,a0=i.y;if(!d){throw new Error("VML container not found.");}var R=new aT,S=R.canvas=L.createElement("div"),E=S.style;e=e||512;a2=a2||342;e==+e&&(e+="px");a2==+a2&&(a2+="px");R.width=1000;R.height=1000;R.coordsize="1000 1000";R.coordorigin="0 0";R.span=L.createElement("span");R.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";S[aL](R.span);E.cssText=an.format("width:{0};height:{1};position:absolute;clip:rect(0 {0} {1} 0);overflow:hidden",e,a2);if(d==1){L.body[aL](S);E.left=a1+"px";E.top=a0+"px";}else{d.style.width=e;d.style.height=a2;if(d.firstChild){d.insertBefore(S,d.firstChild);}else{d[aL](S);}}aG.call(R,R,an.fn);return R;};aT[aY].clear=function(){this.canvas.innerHTML=at;this.span=L.createElement("span");this.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";this.canvas[aL](this.span);this.bottom=this.top=null;};aT[aY].remove=function(){this.canvas.parentNode.removeChild(this.canvas);for(var d in this){this[d]=s(d);}};}if((/^Apple|^Google/).test(navigator.vendor)&&!(navigator.userAgent.indexOf("Version/4.0")+1)){aT[aY].safari=function(){var d=this.rect(-99,-99,this.width+99,this.height+99);setTimeout(function(){d.remove();});};}else{aT[aY].safari=function(){};}var ae=(function(){if(L.addEventListener){return function(R,i,e,d){var E=function(S){return e.call(d,S);};R.addEventListener(i,E,false);return function(){R.removeEventListener(i,E,false);return true;};};}else{if(L.attachEvent){return function(S,E,i,e){var R=function(a0){return i.call(e,a0||au.event);};S.attachEvent("on"+E,R);var d=function(){S.detachEvent("on"+E,R);return true;};return d;};}}})();for(var ac=F[m];ac--;){(function(d){ax[aY][d]=function(e){if(an.is(e,"function")){this.events=this.events||[];this.events.push({name:d,f:e,unbind:ae(this.shape||this.node,d,e,this)});}return this;};ax[aY]["un"+d]=function(E){var i=this.events,e=i[m];while(e--){if(i[e].name==d&&i[e].f==E){i[e].unbind();i.splice(e,1);!i.length&&delete this.events;return this;}}return this;};})(F[ac]);}ax[aY].hover=function(e,d){return this.mouseover(e).mouseout(d);};ax[aY].unhover=function(e,d){return this.unmouseover(e).unmouseout(d);};aT[aY].circle=function(d,i,e){return P(this,d||0,i||0,e||0);};aT[aY].rect=function(d,R,e,i,E){return aF(this,d||0,R||0,e||0,i||0,E||0);};aT[aY].ellipse=function(d,E,i,e){return ai(this,d||0,E||0,i||0,e||0);};aT[aY].path=function(d){d&&!an.is(d,"string")&&!an.is(d[0],"array")&&(d+=at);return q(an.format[aW](an,arguments),this);};aT[aY].image=function(E,d,R,e,i){return o(this,E||"about:blank",d||0,R||0,e||0,i||0);};aT[aY].text=function(d,i,e){return X(this,d||0,i||0,e||at);};aT[aY].set=function(d){arguments[m]>1&&(d=Array[aY].splice.call(arguments,0,arguments[m]));return new T(d);};aT[aY].setSize=aV;aT[aY].top=aT[aY].bottom=null;aT[aY].raphael=an;function u(){return this.x+am+this.y;}ax[aY].scale=function(a6,a5,E,e){if(a6==null&&a5==null){return{x:this._.sx,y:this._.sy,toString:u};}a5=a5||a6;!+a5&&(a5=a6);var ba,a8,a9,a7,bm=this.attrs;if(a6!=0){var a4=this.getBBox(),a1=a4.x+a4.width/2,R=a4.y+a4.height/2,bl=a6/this._.sx,bk=a5/this._.sy;E=(+E||E==0)?E:a1;e=(+e||e==0)?e:R;var a3=~~(a6/ab.abs(a6)),a0=~~(a5/ab.abs(a5)),be=this.node.style,bo=E+(a1-E)*bl,bn=e+(R-e)*bk;switch(this.type){case"rect":case"image":var a2=bm.width*a3*bl,bd=bm.height*a0*bk;this.attr({height:bd,r:bm.r*aI(a3*bl,a0*bk),width:a2,x:bo-a2/2,y:bn-bd/2});break;case"circle":case"ellipse":this.attr({rx:bm.rx*a3*bl,ry:bm.ry*a0*bk,r:bm.r*aI(a3*bl,a0*bk),cx:bo,cy:bn});break;case"path":var bg=ad(bm.path),bh=true;for(var bj=0,bc=bg[m];bjS){if(e&&!a8.start){a6=an.findDotsAtSegment(a5,a4,E[1],E[2],E[3],E[4],E[5],E[6],(S-a3)/a1);R+=["C",a6.start.x,a6.start.y,a6.m.x,a6.m.y,a6.x,a6.y];if(a0){return R;}a8.start=R;R=["M",a6.x,a6.y+"C",a6.n.x,a6.n.y,a6.end.x,a6.end.y,E[5],E[6]][az]();a3+=a1;a5=+E[5];a4=+E[6];continue;}if(!d&&!e){a6=an.findDotsAtSegment(a5,a4,E[1],E[2],E[3],E[4],E[5],E[6],(S-a3)/a1);return{x:a6.x,y:a6.y,alpha:a6.alpha};}}a3+=a1;a5=+E[5];a4=+E[6];}R+=E;}a8.end=R;a6=d?a3:e?a8:an.findDotsAtSegment(a5,a4,E[1],E[2],E[3],E[4],E[5],E[6],1);a6.alpha&&(a6={x:a6.x,y:a6.y,alpha:a6.alpha});return a6;};},n=aj(function(E,d,a0,S,a6,a5,a4,a3){var R={x:0,y:0},a2=0;for(var a1=0;a1<1.01;a1+=0.01){var e=M(E,d,a0,S,a6,a5,a4,a3,a1);a1&&(a2+=ab.sqrt(aM(R.x-e.x,2)+aM(R.y-e.y,2)));R=e;}return a2;});var ap=aB(1),C=aB(),J=aB(0,1);ax[aY].getTotalLength=function(){if(this.type!="path"){return;}return ap(this.attrs.path);};ax[aY].getPointAtLength=function(d){if(this.type!="path"){return;}return C(this.attrs.path,d);};ax[aY].getSubpath=function(i,e){if(this.type!="path"){return;}if(ab.abs(this.getTotalLength()-e)<0.000001){return J(this.attrs.path,i).end;}var d=J(this.attrs.path,e,1);return i?J(d,i).end:d;};an.easing_formulas={linear:function(d){return d;},"<":function(d){return aM(d,3);},">":function(d){return aM(d-1,3)+1;},"<>":function(d){d=d*2;if(d<1){return aM(d,3)/2;}d-=2;return(aM(d,3)+2)/2;},backIn:function(e){var d=1.70158;return e*e*((d+1)*e-d);},backOut:function(e){e=e-1;var d=1.70158;return e*e*((d+1)*e+d)+1;},elastic:function(i){if(i==0||i==1){return i;}var e=0.3,d=e/4;return aM(2,-10*i)*ab.sin((i-d)*(2*ab.PI)/e)+1;},bounce:function(E){var e=7.5625,i=2.75,d;if(E<(1/i)){d=e*E*E;}else{if(E<(2/i)){E-=(1.5/i);d=e*E*E+0.75;}else{if(E<(2.5/i)){E-=(2.25/i);d=e*E*E+0.9375;}else{E-=(2.625/i);d=e*E*E+0.984375;}}}return d;}};var I={length:0},aR=function(){var a2=+new Date;for(var be in I){if(be!="length"&&I[Q](be)){var bj=I[be];if(bj.stop){delete I[be];I[m]--;continue;}var a0=a2-bj.start,bb=bj.ms,ba=bj.easing,bf=bj.from,a7=bj.diff,E=bj.to,a6=bj.t,a9=bj.prev||0,a1=bj.el,R=bj.callback,a8={},d;if(a0255?255:(d<0?0:d);},t=function(d,i){if(d==null){return{x:this._.tx,y:this._.ty,toString:u};}this._.tx+=+d;this._.ty+=+i;switch(this.type){case"circle":case"ellipse":this.attr({cx:+d+this.attrs.cx,cy:+i+this.attrs.cy});break;case"rect":case"image":case"text":this.attr({x:+d+this.attrs.x,y:+i+this.attrs.y});break;case"path":var e=ad(this.attrs.path);e[0][1]+=+d;e[0][2]+=+i;this.attr({path:e});break;}return this;};ax[aY].animateWith=function(e,i,d,R,E){I[e.id]&&(i.start=I[e.id].start);return this.animate(i,d,R,E);};ax[aY].animateAlong=ay();ax[aY].animateAlongBack=ay(1);function ay(d){return function(E,i,e,S){var R={back:d};an.is(e,"function")?(S=e):(R.rot=e);E&&E.constructor==ax&&(E=E.attrs.path);E&&(R.along=E);return this.animate(R,i,S);};}ax[aY].onAnimation=function(d){this._run=d||0;return this;};ax[aY].animate=function(be,a5,a4,E){if(an.is(a4,"function")||!a4){E=a4||null;}var a9={},e={},a2={};for(var a6 in be){if(be[Q](a6)){if(Z[Q](a6)){a9[a6]=this.attr(a6);(a9[a6]==null)&&(a9[a6]=j[a6]);e[a6]=be[a6];switch(Z[a6]){case"along":var bc=ap(be[a6]),a7=C(be[a6],bc*!!be.back),R=this.getBBox();a2[a6]=bc/a5;a2.tx=R.x;a2.ty=R.y;a2.sx=a7.x;a2.sy=a7.y;e.rot=be.rot;e.back=be.back;e.len=bc;be.rot&&(a2.r=W(this.rotate())||0);break;case"number":a2[a6]=(e[a6]-a9[a6])/a5;break;case"colour":a9[a6]=an.getRGB(a9[a6]);var a8=an.getRGB(e[a6]);a2[a6]={r:(a8.r-a9[a6].r)/a5,g:(a8.g-a9[a6].g)/a5,b:(a8.b-a9[a6].b)/a5};break;case"path":var S=H(a9[a6],e[a6]);a9[a6]=S[0];var a3=S[1];a2[a6]=[];for(var bb=0,a1=a9[a6][m];bb +// +// Math.seedrandom('yipee'); Sets Math.random to a function that is +// initialized using the given explicit seed. +// +// Math.seedrandom(); Sets Math.random to a function that is +// seeded using the current time, dom state, +// and other accumulated local entropy. +// The generated seed string is returned. +// +// Math.seedrandom('yowza', true); +// Seeds using the given explicit seed mixed +// together with accumulated entropy. +// +// +// Seeds using physical random bits downloaded +// from random.org. +// +// Examples: +// +// Math.seedrandom("hello"); // Use "hello" as the seed. +// document.write(Math.random()); // Always 0.5463663768140734 +// document.write(Math.random()); // Always 0.43973793770592234 +// var rng1 = Math.random; // Remember the current prng. +// +// var autoseed = Math.seedrandom(); // New prng with an automatic seed. +// document.write(Math.random()); // Pretty much unpredictable. +// +// Math.random = rng1; // Continue "hello" prng sequence. +// document.write(Math.random()); // Always 0.554769432473455 +// +// Math.seedrandom(autoseed); // Restart at the previous seed. +// document.write(Math.random()); // Repeat the 'unpredictable' value. +// +// Notes: +// +// Each time seedrandom('arg') is called, entropy from the passed seed +// is accumulated in a pool to help generate future seeds for the +// zero-argument form of Math.seedrandom, so entropy can be injected over +// time by calling seedrandom with explicit data repeatedly. +// +// On speed - This javascript implementation of Math.random() is about +// 3-10x slower than the built-in Math.random() because it is not native +// code, but this is typically fast enough anyway. Seeding is more expensive, +// especially if you use auto-seeding. Some details (timings on Chrome 4): +// +// Our Math.random() - avg less than 0.002 milliseconds per call +// seedrandom('explicit') - avg less than 0.5 milliseconds per call +// seedrandom('explicit', true) - avg less than 2 milliseconds per call +// seedrandom() - avg about 38 milliseconds per call +// +// LICENSE (BSD): +// +// Copyright 2010 David Bau, all rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of this module nor the names of its contributors may +// be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +/** + * All code is in an anonymous closure to keep the global namespace clean. + * + * @param {number=} overflow + * @param {number=} startdenom + */ +(function (pool, math, width, chunks, significance, overflow, startdenom) { + + +// +// seedrandom() +// This is the seedrandom function described above. +// +math['seedrandom'] = function seedrandom(seed, use_entropy) { + var key = []; + var arc4; + + // Flatten the seed string or build one from local entropy if needed. + seed = mixkey(flatten( + use_entropy ? [seed, pool] : + arguments.length ? seed : + [new Date().getTime(), pool, window], 3), key); + + // Use the seed to initialize an ARC4 generator. + arc4 = new ARC4(key); + + // Mix the randomness into accumulated entropy. + mixkey(arc4.S, pool); + + // Override Math.random + + // This function returns a random double in [0, 1) that contains + // randomness in every bit of the mantissa of the IEEE 754 value. + + math['random'] = function random() { // Closure to return a random double: + var n = arc4.g(chunks); // Start with a numerator n < 2 ^ 48 + var d = startdenom; // and denominator d = 2 ^ 48. + var x = 0; // and no 'extra last byte'. + while (n < significance) { // Fill up all significant digits by + n = (n + x) * width; // shifting numerator and + d *= width; // denominator and generating a + x = arc4.g(1); // new least-significant-byte. + } + while (n >= overflow) { // To avoid rounding up, before adding + n /= 2; // last byte, shift everything + d /= 2; // right using integer math until + x >>>= 1; // we have exactly the desired bits. + } + return (n + x) / d; // Form the number within [0, 1). + }; + + // Return the seed that was used + return seed; +}; + +// +// ARC4 +// +// An ARC4 implementation. The constructor takes a key in the form of +// an array of at most (width) integers that should be 0 <= x < (width). +// +// The g(count) method returns a pseudorandom integer that concatenates +// the next (count) outputs from ARC4. Its return value is a number x +// that is in the range 0 <= x < (width ^ count). +// +/** @constructor */ +function ARC4(key) { + var t, u, me = this, keylen = key.length; + var i = 0, j = me.i = me.j = me.m = 0; + me.S = []; + me.c = []; + + // The empty key [] is treated as [0]. + if (!keylen) { key = [keylen++]; } + + // Set up S using the standard key scheduling algorithm. + while (i < width) { me.S[i] = i++; } + for (i = 0; i < width; i++) { + t = me.S[i]; + j = lowbits(j + t + key[i % keylen]); + u = me.S[j]; + me.S[i] = u; + me.S[j] = t; + } + + // The "g" method returns the next (count) outputs as one number. + me.g = function getnext(count) { + var s = me.S; + var i = lowbits(me.i + 1); var t = s[i]; + var j = lowbits(me.j + t); var u = s[j]; + s[i] = u; + s[j] = t; + var r = s[lowbits(t + u)]; + while (--count) { + i = lowbits(i + 1); t = s[i]; + j = lowbits(j + t); u = s[j]; + s[i] = u; + s[j] = t; + r = r * width + s[lowbits(t + u)]; + } + me.i = i; + me.j = j; + return r; + }; + // For robust unpredictability discard an initial batch of values. + // See http://www.rsa.com/rsalabs/node.asp?id=2009 + me.g(width); +} + +// +// flatten() +// Converts an object tree to nested arrays of strings. +// +/** @param {Object=} result + * @param {string=} prop */ +function flatten(obj, depth, result, prop) { + result = []; + if (depth && typeof(obj) == 'object') { + for (prop in obj) { + if (prop.indexOf('S') < 5) { // Avoid FF3 bug (local/sessionStorage) + try { result.push(flatten(obj[prop], depth - 1)); } catch (e) {} + } + } + } + return result.length ? result : '' + obj; +} + +// +// mixkey() +// Mixes a string seed into a key that is an array of integers, and +// returns a shortened string seed that is equivalent to the result key. +// +/** @param {number=} smear + * @param {number=} j */ +function mixkey(seed, key, smear, j) { + seed += ''; // Ensure the seed is a string + smear = 0; + for (j = 0; j < seed.length; j++) { + key[lowbits(j)] = + lowbits((smear ^= key[lowbits(j)] * 19) + seed.charCodeAt(j)); + } + seed = ''; + for (j in key) { seed += String.fromCharCode(key[j]); } + return seed; +} + +// +// lowbits() +// A quick "n mod width" for width a power of 2. +// +function lowbits(n) { return n & (width - 1); } + +// +// The following constants are related to IEEE 754 limits. +// +startdenom = math.pow(width, chunks); +significance = math.pow(2, significance); +overflow = significance * 2; + +// +// When seedrandom.js is loaded, we immediately mix a few bits +// from the built-in RNG into the entropy pool. Because we do +// not want to intefere with determinstic PRNG state later, +// seedrandom will not call math.random on its own again after +// initialization. +// +mixkey(math.random(), pool); + +// End anonymous scope, and pass initial values. +})( + [], // pool: entropy pool starts empty + Math, // math: package containing random, pow, and seedrandom + 256, // width: each RC4 output is 0 <= x < 256 + 6, // chunks: at least six RC4 outputs for each double + 52 // significance: there are 52 significant digits in a double +); diff --git a/addons/base_diagram/static/src/js/diagram.js b/addons/base_diagram/static/src/js/diagram.js new file mode 100644 index 00000000000..cc0f69b9eb5 --- /dev/null +++ b/addons/base_diagram/static/src/js/diagram.js @@ -0,0 +1,175 @@ +/*--------------------------------------------------------- + * OpenERP base library + *---------------------------------------------------------*/ + +openerp.base.diagram = function (openerp) { + +openerp.base.views.add('diagram', 'openerp.base.DiagramView'); +openerp.base.DiagramView = openerp.base.Controller.extend({ + init: function(view_manager, session, element_id, dataset, view_id){ + this._super(session, element_id); + this.view_manager = view_manager; + this.dataset = dataset; + this.model = dataset.model; + this.view_id = view_id; + this.name = ""; + this.domain = this.dataset._domain ? this.dataset._domain: []; + this.context = {}; + this.ids = this.dataset.ids; + + console.log('data set>>',this.dataset) + }, + start: function() { + this.rpc("/base_diagram/diagram/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded); + }, + + toTitleCase: function(str) { + return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); + }, + + on_loaded: function(result) { + + var self = this; + if(this.ids && this.ids.length) { + this.id = this.ids[0]; + } + + this.fields_view = result.fields_view; + this.view_id = this.fields_view.view_id; + this.name = this.fields_view.name; + + this.fields = this.fields_view.fields; + + var children = this.fields_view.arch.children; + /* + * For Nodes (Fields) + */ + this.node = ''; + this.bgcolor = ''; + this.shape = ''; + this.visible_fields_nodes = []; + this.invisible_fields_nodes = []; + this.fields_nodes_string = []; + + /* + * For Arraows(Connector) + */ + this.connector = ''; + this.src_node = ''; + this.des_node = ''; + this.connector_fields = []; + this.fields_connector_string = []; + + for(ch in children) { + if(children[ch]['tag'] == 'node') { + this.node = children[ch]['attrs']['object']; + this.bgcolor = children[ch]['attrs']['bgcolor'] || ''; + this.shape = children[ch]['attrs']['shape'] || ''; + for(node_chld in children[ch]['children']) { + if (children[ch]['children'][node_chld]['tag'] = 'field') { + var ch_name = children[ch]['children'][node_chld]['attrs']['name']; + + if (children[ch]['children'][node_chld]['attrs']['invisible']) { + if (children[ch]['children'][node_chld]['attrs']['invisible'] == 1 && children[ch]['children'][node_chld]['attrs']['invisible'] == '1') { + this.invisible_fields_nodes.push(ch_name) + } + } + else { + this.visible_fields_nodes.push(ch_name); + var ch_node_string = this.fields[ch_name]['string'] || this.toTitleCase(ch_name); + this.fields_nodes_string.push(ch_node_string) + } + } + } + } else if(children[ch]['tag'] == 'arrow') { + this.connector = children[ch]['attrs']['object']; + this.src_node = children[ch]['attrs']['source']; + this.des_node = children[ch]['attrs']['destination']; + for (arrow_chld in children[ch]['children']) { + if (children[ch]['children'][arrow_chld]['tag'] = 'field') { + var arr_ch_name = children[ch]['children'][arrow_chld]['attrs']['name']; + var ch_node_string = this.fields[arr_ch_name]['string'] || this.toTitleCase(arr_ch_name); + this.fields_connector_string.push(ch_node_string); + this.connector_fields.push(arr_ch_name); + } + } + } + } + this.$element.html(QWeb.render("DiagramView", {"fields_view": this.fields_view})); + + +// g.addEdge("strawberry", "cherry"); +// g.addEdge("strawberry", "apple"); +// g.addEdge("strawberry", "tomato"); +// +// g.addEdge("tomato", "apple"); +// g.addEdge("tomato", "kiwi"); +// +// g.addEdge("cherry", "apple"); +// g.addEdge("cherry", "kiwi"); + + + + if(this.id) { + this.rpc( + '/base_diagram/diagram/get_diagram_info', + { + 'id': this.id, + 'model': this.model, + 'bgcolor': this.bgcolor, + 'shape': this.shape, + 'node': this.node, + 'connector': this.connector, + 'src_node': this.src_node, + 'des_node': this.des_node, + 'visible_node_fields': this.visible_fields_nodes, + 'invisible_node_fields': this.invisible_fields_nodes, + 'node_fields_string': this.fields_nodes_string, + 'connector_fields': this.connector_fields, + 'connector_fields_string': this.fields_connector_string + }, + function(result) { + self.draw_diagram(result); + } + ) + } + }, + + draw_diagram: function(result) { + console.log('this>>>',this) + var g = new Graph(); + + this.in_transition_field = result['in_transition_field']; + this.out_transition_field = result['out_transition_field']; + var res_nodes = result['nodes']; + var res_connectors = result['conn']; +// for(nd in res_nodes) { +// var res_node = res_nodes[nd]; +// console.log('nodes',res_node, res_node['shape']) +// var state; +// } + + for(cr in res_connectors) { + var res_connector = res_connectors[cr]; + g.addEdge(res_connector['source'], res_connector['destination']); + + } + + var layouter = new Graph.Layout.Spring(g); + layouter.layout(); + + var renderer = new Graph.Renderer.Raphael('dia-canvas', g, 800, 500); + renderer.draw(); + }, + + do_show: function () { + this.$element.show(); + }, + + do_hide: function () { + this.$element.hide(); + } +}); +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base_diagram/static/src/js/views.js b/addons/base_diagram/static/src/js/views.js deleted file mode 100644 index 606e1e5c78c..00000000000 --- a/addons/base_diagram/static/src/js/views.js +++ /dev/null @@ -1,12 +0,0 @@ -/*--------------------------------------------------------- - * OpenERP base library - *---------------------------------------------------------*/ - -openerp.base_diagram = function(openerp) { - -openerp.base_diagram.DiagramView = openerp.base.Controller.extend({ -}); - -}; - -// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: From 70d622c151562d97ffaa3f4c061e9c79aace725a Mon Sep 17 00:00:00 2001 From: "vda (OpenERP)" Date: Thu, 7 Apr 2011 18:36:23 +0530 Subject: [PATCH 04/86] [FIX] Specific Shape accordingly node will display. bzr revid: vda@tinyerp.com-20110407130623-iw7nf7vv0dc4jiqk --- addons/base/static/src/xml/base.xml | 2 +- addons/base_diagram/static/src/js/diagram.js | 50 ++++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 8d8040fd48e..1ad8f56511e 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -169,7 +169,7 @@

-
+

diff --git a/addons/base_diagram/static/src/js/diagram.js b/addons/base_diagram/static/src/js/diagram.js index cc0f69b9eb5..af56c256ee7 100644 --- a/addons/base_diagram/static/src/js/diagram.js +++ b/addons/base_diagram/static/src/js/diagram.js @@ -97,19 +97,6 @@ openerp.base.DiagramView = openerp.base.Controller.extend({ } this.$element.html(QWeb.render("DiagramView", {"fields_view": this.fields_view})); - -// g.addEdge("strawberry", "cherry"); -// g.addEdge("strawberry", "apple"); -// g.addEdge("strawberry", "tomato"); -// -// g.addEdge("tomato", "apple"); -// g.addEdge("tomato", "kiwi"); -// -// g.addEdge("cherry", "apple"); -// g.addEdge("cherry", "kiwi"); - - - if(this.id) { this.rpc( '/base_diagram/diagram/get_diagram_info', @@ -138,27 +125,48 @@ openerp.base.DiagramView = openerp.base.Controller.extend({ draw_diagram: function(result) { console.log('this>>>',this) var g = new Graph(); - +// var raphel = new this.in_transition_field = result['in_transition_field']; this.out_transition_field = result['out_transition_field']; var res_nodes = result['nodes']; var res_connectors = result['conn']; -// for(nd in res_nodes) { -// var res_node = res_nodes[nd]; -// console.log('nodes',res_node, res_node['shape']) -// var state; -// } + + var render = function(r, n) { + var set; + if (n.node.shape == 'ellipse') { + set = r.set().push( + r.ellipse(n.node.x - 30, n.node.y - 13, 40, 40).attr({ + "fill": n.node.color, + r: "12px", + "stroke-width": n.distance == 0 ? "3px" : "1px" + })).push(r.text(n.node.x - 30, n.node.y - 10, (n.label || n.id))); + } else { + set = r.set().push( + r.rect(n.node.x-30, n.node.y-13, 60, 44).attr({"fill": n.node.color, r : "12px", "stroke-width" : n.distance == 0 ? "3px" : "1px" })).push( + r.text(n.point[0], n.point[1] + 10, (n.label || n.id) + "\n(" + (n.distance == undefined ? "Infinity" : n.distance) + ")")); + + } + return set; + }; + + for(nd in res_nodes) { + var res_node = res_nodes[nd]; + g.addNode(res_node['name'], + { + node: res_node, + render: render + }); + } for(cr in res_connectors) { var res_connector = res_connectors[cr]; g.addEdge(res_connector['source'], res_connector['destination']); - } var layouter = new Graph.Layout.Spring(g); layouter.layout(); - var renderer = new Graph.Renderer.Raphael('dia-canvas', g, 800, 500); + var renderer = new Graph.Renderer.Raphael('dia-canvas', g, 800, 800); renderer.draw(); }, From da9ef6e6ed7d710efb389cf35bbb3dc472c5c234 Mon Sep 17 00:00:00 2001 From: "noz (OpenERP)" Date: Fri, 8 Apr 2011 16:28:06 +0530 Subject: [PATCH 05/86] [FIX] Minor starting for web chat. bzr revid: noz@tinyerp.com-20110408105806-cofc88534c7fab19 --- addons/web_chat/controllers/main.py | 42 ++++++++++++++++++----------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/addons/web_chat/controllers/main.py b/addons/web_chat/controllers/main.py index d8d33bac445..24409ebce65 100644 --- a/addons/web_chat/controllers/main.py +++ b/addons/web_chat/controllers/main.py @@ -2,7 +2,7 @@ import sys, time import simplejson - +import random import openerpweb #---------------------------------------------------------- @@ -17,13 +17,17 @@ class PollServerMessageQueue(object): # should contains: { # 'user1234' : { s:1, m:"status message", timestamp: last_contact_timestamp } # } - def userlist(): - # return user list - pass + def userlist(self, req): + userlist = [ + {"u": "Guest130205108745.47", "s": {"s": 1, "m": ""}, "g": "Users"}, + {"u": "Guest130209838956.76", "s": {"s": 1, "m": ""}, "g": "Users"}, + ] + + return userlist def write(self, m_type, m_sender, m_recipent, m_message, m_group): # appends messages to l - # when status message uupdate users + # when status message update users pass def read(self, recpient, timestamp): # return matching message @@ -38,6 +42,7 @@ class PollServer(openerpweb.Controller): @openerpweb.httprequest def login(self, req, **kw): + """ --> POST http://ajaxim.com/wp-content/plugins/im/ajaxim.php/login Form Data @@ -56,21 +61,26 @@ class PollServer(openerpweb.Controller): ] } """ - mq = req.applicationsession.setdefault("web_chat",PollServerMessageQueue()) - print "chat login",kw - # r = 'loggued in' + mq = req.applicationsession.setdefault("web_chat", PollServerMessageQueue()) + + #r = 'loggued in' #u = generate random.randint(0,2**32) #s = cherrypy cookie id #f = mq.userlist() - return """ + +# username = 'Guest'+ str(random.randint(0, 2**32)) +# +# if not req.applicationsession.get('users'): +# req.applicationsession['users'] = [{'u': username, 's':{'s':1, 'm':''}, 'g':'Users'}] +# else: +# req.applicationsession['users'].append({'u': username, 's':{'s':1, 'm':''}, 'g':'Users'}) + + return """ { "r":"logged in", "u":"Guest130213866190.85", "s":"f9e1811536f19ad5b9e00376f9ff1532", - "f":[ - {"u":"Guest130205108745.47","s":{"s":1,"m":""},"g":"Users"}, - {"u":"Guest130209838956.76","s":{"s":1,"m":""},"g":"Users"}, - ] + "f":""" + str(mq.userlist(req)) + """ } """ @@ -98,7 +108,7 @@ class PollServer(openerpweb.Controller): jsonp1302140441577([{"t":"s","s":"Guest130214038974.31","r":"","m":"0:"}]); receive message: - jsonp1302140191599([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"fuck"}]); + jsonp1302140191599([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"xxxxxx"}]); ('t' => $msg->type, 's' => $msg->from, 'r' => $msg->to, 'm' => $msg->message ) mag type s or m @@ -114,10 +124,10 @@ class PollServer(openerpweb.Controller): # sleep 2 # else # return emptylist - print "chat poll",kw + time.sleep(2) # it's http://localhost:8002/web_chat/pollserver/poll?method=long?callback=jsonp1302147330483&_1302147330483= - return '%s([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"fuck"}]);'%kw.get('callback','') + return '%s([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"xxxxxx"}]);'%kw.get('callback','') return None @openerpweb.jsonrequest From 9262c3c00dfdad634a8f2829abe2d14b6540e63a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 8 Apr 2011 14:40:20 +0200 Subject: [PATCH 06/86] [FIX] fuckup with deferred for rendering of list (when beyond one section), also only remove old body after new list has rendered otherwise we get an empty list flash even for lists with a bunch of elements bzr revid: xmo@openerp.com-20110408124020-25dprn6gu19y02m6 --- addons/base/static/src/js/list.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 4b1eeaaf993..4fc3ef07720 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -89,7 +89,7 @@ openerp.base.ListView = openerp.base.Controller.extend( var $table = this.$element.find('table'); // remove all data lines - $table.find('tbody').remove(); + var $old_body = $table.find('tbody'); // add new content var columns = this.columns, @@ -102,8 +102,8 @@ openerp.base.ListView = openerp.base.Controller.extend( body = 0, $body = $('').appendTo($table); + var rendered = $.Deferred(); var render_body = function () { - var rendered = $.Deferred(); setTimeout(function () { $body.append( QWeb.render("ListView.rows", { @@ -118,9 +118,12 @@ openerp.base.ListView = openerp.base.Controller.extend( rendered.resolve(); } }, 0); - return rendered.promise(); }; - return render_body(); + render_body(); + + return rendered.promise().then(function () { + $old_body.remove(); + }); }, on_select_row: function (event) { var $target = $(event.currentTarget); From b19baeb6a57811839be7afd56cf9621d9d73f560 Mon Sep 17 00:00:00 2001 From: "noz (OpenERP)" Date: Sat, 9 Apr 2011 23:20:50 +0530 Subject: [PATCH 07/86] [IMP] More implementation on poll, send etc... (Dont test anything, still need to modify more) bzr revid: noz@tinyerp.com-20110409175050-qf4hf3krdrxhzjdh --- addons/web_chat/controllers/main.py | 96 +++++++++++++++++----- addons/web_chat/static/lib/AjaxIM/js/im.js | 2 +- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/addons/web_chat/controllers/main.py b/addons/web_chat/controllers/main.py index 24409ebce65..456fd0f5340 100644 --- a/addons/web_chat/controllers/main.py +++ b/addons/web_chat/controllers/main.py @@ -11,27 +11,32 @@ import openerpweb class PollServerMessageQueue(object): def __init__(self): # message queue - self.l = [] + self.messages = [] # online users self.users = {} # should contains: { # 'user1234' : { s:1, m:"status message", timestamp: last_contact_timestamp } # } def userlist(self, req): - userlist = [ - {"u": "Guest130205108745.47", "s": {"s": 1, "m": ""}, "g": "Users"}, - {"u": "Guest130209838956.76", "s": {"s": 1, "m": ""}, "g": "Users"}, - ] + userlist = [users for users in req.applicationsession['users']] + +# userlist = [ +# {"u": "Guest130205108745.47", "s": {"s": 1, "m": ""}, "g": "Users"}, +# {"u": "Guest130209838956.76", "s": {"s": 1, "m": ""}, "g": "Users"}, +# ] return userlist - def write(self, m_type, m_sender, m_recipent, m_message, m_group): - # appends messages to l + def write(self, m_type, m_from, m_to, m_message, m_group): + self.messages.append({'type': m_type, 'from': m_from, 'to': m_to, 'message': m_message, 'group': m_group}) # when status message update users pass - def read(self, recpient, timestamp): - # return matching message - pass + + def read(self, recipient, timestamp): + for msg in self.messages: + if msg['to'] == recipient: + return self.messages + def gc(): # remove message older than 300s from self.l # remove dead users from self.users @@ -63,7 +68,7 @@ class PollServer(openerpweb.Controller): """ mq = req.applicationsession.setdefault("web_chat", PollServerMessageQueue()) - #r = 'loggued in' + #r = 'logged in' #u = generate random.randint(0,2**32) #s = cherrypy cookie id #f = mq.userlist() @@ -74,11 +79,17 @@ class PollServer(openerpweb.Controller): # req.applicationsession['users'] = [{'u': username, 's':{'s':1, 'm':''}, 'g':'Users'}] # else: # req.applicationsession['users'].append({'u': username, 's':{'s':1, 'm':''}, 'g':'Users'}) + req.applicationsession['users'] = [{'u': 'Guest1', 's':{'s':1, 'm':'111'}, 'g':'Users'}, + {'u': 'Guest2', 's':{'s':1, 'm':'222'}, 'g':'Users'}, + {'u': 'Guest3', 's':{'s':1, 'm':'333'}, 'g':'Users'}] + + # Temporary Guest1 is my current user + req.applicationsession['current_user'] = 'Guest1' return """ { "r":"logged in", - "u":"Guest130213866190.85", + "u":'Guest1', "s":"f9e1811536f19ad5b9e00376f9ff1532", "f":""" + str(mq.userlist(req)) + """ } @@ -93,6 +104,11 @@ class PollServer(openerpweb.Controller): @openerpweb.httprequest def poll(self, req, **kw): + + mq = req.applicationsession.setdefault("web_chat", PollServerMessageQueue()) + print "================ poll ======= ", kw + # Long Polling + method = kw.get('method') """ --> GET http://im.ajaxim.com/poll?callback=jsonp1302138663582&_1302138663582= <-- 200 OK @@ -114,8 +130,16 @@ class PollServer(openerpweb.Controller): mag type s or m echo '' - """ + + for i in range(60): + received_msg = mq.read('Guest2', i); + if received_msg: + msg = self._pollParseMessages(received_msg) + print "============ msg...", msg + else: + time.sleep(2) + # for i in range(60): #r = mq.read(username,timestamp) # if messages @@ -125,13 +149,20 @@ class PollServer(openerpweb.Controller): # else # return emptylist - time.sleep(2) + # it's http://localhost:8002/web_chat/pollserver/poll?method=long?callback=jsonp1302147330483&_1302147330483= return '%s([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"xxxxxx"}]);'%kw.get('callback','') return None - - @openerpweb.jsonrequest + + @openerpweb.httprequest def send(self, req, **kw): + print "========= send ========", kw + + to = kw.get('to') + message = kw.get('message') + + mq = req.applicationsession.setdefault("web_chat", PollServerMessageQueue()) + """ --> GET http://im.ajaxim.com/send?callback=jsonp1302139980022&to=Guest130205108745.47&message=test&_1302139980022= callback: jsonp1302139980022 @@ -148,12 +179,22 @@ class PollServer(openerpweb.Controller): return array('r' => 'error', 'e' => 'send error'); """ - print "chat send",kw - # mq.write() - return {"action": actions} + + if not req.applicationsession['current_user']: + return dict(r='error', e='no session found') + + if not to: + return dict(r='error', e='no_recipient') + + if message: + mq.write(m_type="text", m_from=req.applicationsession['current_user'], m_to=to, m_message=message, m_group="Users") + + return {'r': 'sent'} - @openerpweb.jsonrequest + @openerpweb.httprequest def status(self, req, **kw): + mq = req.applicationsession.setdefault("web_chat", PollServerMessageQueue()) + """ --> GET status call const Offline = 0; @@ -168,8 +209,19 @@ class PollServer(openerpweb.Controller): return array('r' => 'error', 'e' => 'no session found'); return array('r' => 'error', 'e' => 'status error'); """ - print "chat status",kw + print "======== chat status ========",kw # mq.write() - return {"action": actions} + return {"action": ""} + + def _pollParseMessages(self, messages): + msg_arr = [] + print "=========== messages...", messages + for msg in messages: + print "=========== msg..", msg + msg_arr.append({'t': msg['type'], 's': msg['from'], 'r': msg['to'], 'm': msg['message']}) + return msg_arr + + def _sanitize(self, message): + return message.replace('>', '>').replace('<', '<').replace('&', '&'); diff --git a/addons/web_chat/static/lib/AjaxIM/js/im.js b/addons/web_chat/static/lib/AjaxIM/js/im.js index fbf6a1552f3..ef80481a246 100644 --- a/addons/web_chat/static/lib/AjaxIM/js/im.js +++ b/addons/web_chat/static/lib/AjaxIM/js/im.js @@ -1 +1 @@ -// = im.js = // // **Copyright © 2005 – 2010 Joshua Gross**\\ // //MIT Licensed// // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // This is the main library for Ajax IM. It encompasses the UI controls, // and connecting with the server. It does //not// handle registration or // account management. (function($, _instance) { AjaxIM = function(options, actions) { if(this instanceof AjaxIM) { var self = this; // === {{{ defaults }}} === // // These are the available settings for Ajax IM, and the associated // defaults: // // * {{{pollType}}} determines the way in which the client will talk to // the server. // ** {{{comet}}} will use http streaming, wherein a connection to the server will be held open indefinitely, and the server will push down new events (messages, status updates) as they occur. // ** {{{long}}} will hold open a connection with the server for as long as possible, or until a new event is received. Upon the server sending an event or closing the connection, the client will automatically—and immediately—reconnect. // ** {{{short}}} will open a connection, and the server (if this method is supported) will //immediately// provide a response as to whether or not there are any new events. Once a response is received, the client will wait 5 seconds, and then reconnect. // * {{{pollServer}}} is the default URL to which all actions refer. It is // possible to specify certain action URLs separately (as is used with the // NodeJS server). // * {{{cookieName}}} is the name of the session cookie used by the server. // If this is not set properly, the IM engine will not be able to automatically // reinitialize sessions. // * {{{theme}}} is the name of the theme folder that defines the HTML and // CSS of the IM bar and chat boxes. Usually, themes are deposited in the // provided "themes" folder and specified by that path, e.g. {{{themes/default}}}. // Theme files within the theme folder must be named {{{theme.html}}} and // {{{theme.css}}}. // * {{{storageMethod}}} defines the way in which data (chat sessions) are // temporarily stored client-side. By default, {{{"flash"}}} is used because // it is the most widely supported method. However, // [[http://eric.garside.name/docs.html?p=jstore#js-engines|other storage engines]] // are available, with their respective up- and down-sides // outlined, on the jStore website. // * {{{storeSession}}} (**not implemented**) sets the number of days to // retain stored chat session data before it should be deleted. // * {{{checkResume}}} is a flag that sets whether or not the client should // make a call to the server before resuming the session (such as on a page // reload). This will ensure that the session has not expired. If set to {{{false}}}, // a call to the server will not be made, and the session will be assumed to // be active. var defaults = { pollType: 'long', pollServer: './ajaxim.php', cookieName: 'ajaxim_session', theme: 'themes/default', storageMethod: 'auto', flashStorage: 'js/jStore.Flash.html', storeSession: 5, // number of days to store chat data (0 for current session only) checkResume: true }; // === {{{AjaxIM.}}}**{{{settings}}}** === // // These are the settings for the IM. If particular options are not specified, // the defaults (see above) will be used. //These options will be defined // upon calling the initialization function, and not set directly.// this.settings = $.extend(defaults, options); // === {{{AjaxIM.}}}**{{{actions}}}** === // // Each individual action that the IM engine can execute is predefined here. // By default, it merely appends the action onto the end of the {{{pollServer}}} url, // however, it is possible to define actions individually. //The alternative actions // will be defined upon calling the initialization function, and not set directly.// // // Should you define an action at a different URL, Ajax IM will determine whether // or not this URL is within the current domain. If it is within a subdomain of // the current domain, it will set the document.domain variable for you, // to match a broader hostname scope; the action will continue to use {{{$.post}}} // (the default AJAX method for Ajax IM). // // On the other hand, should you choose a URL outside the current domain // Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid // cross-domain scripting issues. This means that a server on a different // port or at a different address will need to be able to handle GET // requests rather than POST requests (such as how the Node.JS Ajax IM // server works). this.actions = $.extend({ login: this.settings.pollServer + '/login', logout: this.settings.pollServer + '/logout', register: this.settings.pollServer + '/register', poll: this.settings.pollServer + '/poll?method=' + this.settings.pollType, send: this.settings.pollServer + '/send', status: this.settings.pollServer + '/status', resume: this.settings.pollServer + '/resume' }, actions); var httpRx = new RegExp('^http://', 'i'), slashslashRx = new RegExp('^//', 'i'), queryStrRx = new RegExp('[?](.+)$'), subdomainRx = new RegExp('((http[s]?:)?//)?(.+?)[.]' + window.location.host, 'i'); $.each(this.actions, function(name, action) { if(name == 'poll') { if(self.settings.pollType != 'comet') action += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; action = ['jsonp', action]; } else { action = ['ajax', action]; if(subdomainRx.test(action[1])) { document.domain = '.' + window.location.host; } else if((http = httpRx.test(action[1])) || slashslashRx.test(action[1])) { if(!(new RegExp('//' + window.location.host + '/', 'i')).test(action[1])) { action[1] += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; action = ['jsonp', action[1]]; } } } self.actions[name] = action; }); // We load the theme dynamically based on the passed // settings. If the theme is set to false, we assume // that the user is going to load it himself. this.themeLoaded = false; if(this.settings.theme) { $('
').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip', function() { self.themeLoaded = true; setup.apply(self); } ); if(typeof document.createStyleSheet == 'function') document.createStyleSheet(this.settings.theme + '/theme.css'); else $('body').append(''); } else { this.themeLoaded = true; setup.apply(this); } // Client-side storage for keeping track of // conversation states, active tabs, etc. // The default is flash storage, however, other // options are available via the jStore library. if(this.settings.storageMethod) { this.storageBrowserKey = 'unknown'; this.storeKey = ''; $.each($.browser, function(k, v) { if(k == 'version') return true; else if(v == true) { self.storageBrowserKey = k; return false; } }); if(this.settings.storageMethod == 'auto') { $.each(['ie', 'html5', 'local', 'flash'], function() { if($.jStore.Availability[this]()) { self.settings.storageMethod = this; return false; } }); } $.extend($.jStore.defaults, { project: 'im.js', engine: this.settings.storageMethod, flash: this.settings.flashStorage }); this.storageReady = false; if(this.settings.storageMethod == 'flash') { $.jStore.ready(function(engine) { $.jStore.flashReady(function() { self.storageReady = true; setup.apply(self); }); }) } else { $.jStore.ready(function(engine) { self.storageReady = true; setup.apply(self); }); } $.jStore.load(); } else { this.storageReady = true; setup.apply(this); } // Allow a chatbox to me minimized $('.imjs-chatbox').live('click', function(e) { e.preventDefault(); return false; }); $('.imjs-chatbox .imjs-minimize').live('click', function() { $(this).parents('.imjs-chatbox').data('tab').click(); }); // Allow a chatbox to be closed $('.imjs-chatbox .imjs-close').live('click', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.data('tab') .data('state', 'closed').css('display', 'none'); if(self.settings.storageMethod && self.storageReady) { delete self.chatstore[chatbox.data('username')]; if(self.storageReady) { $.jStore.store( self.storageBrowserKey + '-' + self.username + '-chats', self.chatstore); } } }); // Setup message sending for all chatboxes $('.imjs-chatbox .imjs-input').live('keydown', function(event) { var obj = $(this); if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) { self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } }).live('keyup', function(event) { if(event.keyCode == 13) { if($.browser.msie && $.browser.version < 8) { var obj = $(this); self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } var obj = $(this); obj.val(''); obj.height(obj.data('height')); } }).live('keypress', function(e) { var obj = $(this); if(!($.browser.msie && $.browser.opera)) obj.height(0); if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) { obj.height(this.scrollHeight); } }); $('.imjs-msglog').live('click', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.find('.imjs-input').focus(); }); // Create a chatbox when a buddylist item is clicked $('.imjs-friend').live('click', function() { var chatbox = self._createChatbox($(this).data('friend')); if(chatbox.data('tab').data('state') != 'active') chatbox.data('tab').click(); chatbox.find('.imjs-input').focus(); }); // Setup and hide the scrollers $('.imjs-scroll').css('display', 'none'); $('#imjs-scroll-left').live('click', function() { var hiddenTab = $('#imjs-bar li.imjs-tab:visible').slice(-1) .next('#imjs-bar li.imjs-tab:hidden') .filter(function() { return $(this).data('state') != 'closed' }) .not('.imjs-default').slice(-1).css('display', ''); if(hiddenTab.length) { $('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none'); $(this).html(parseInt($(this).html()) - 1); $('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1); } return false; }); $('#imjs-scroll-right').live('click', function() { var hiddenTab = $('#imjs-bar li.imjs-tab:visible').eq(0) .prev('#imjs-bar li.imjs-tab:hidden') .filter(function() { return $(this).data('state') != 'closed' }) .not('.imjs-default').slice(-1).css('display', ''); if(hiddenTab.length) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); $(this).html(parseInt($(this).html()) - 1); $('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1); } return false; }); // Initialize the chatbox hash this.chats = {}; // Try to resume any existing session this.resume(); $(window).resize(function() { self.bar._scrollers(); }); } else return AjaxIM.init(options); }; // We predefine all public functions here... // If they are called before everything (theme, storage engine) has loaded, // then they get put into a "prequeue" and run when everything *does* // finally load. // // This ensures that nothing loads without all of the principal components // being pre-loaded. If that were to occur (without this prequeue), things // would surely break. var prequeue = []; var empty = function() { var func = this; return function() { prequeue.push([func, arguments]) }; }; $.extend(AjaxIM.prototype, { settings: {}, friends: {}, chats: {}, storage: empty.apply('storage'), login: empty.apply('login'), logout: empty.apply('logout'), resume: empty.apply('resume'), form: empty.apply('form'), poll: empty.apply('poll'), incoming: empty.apply('incoming'), send: empty.apply('send'), status: empty.apply('status'), statuses: {}, bar: { initialize: empty.apply('bar.initialize'), activateTab: empty.apply('bar.activateTab'), closeTab: empty.apply('bar.closeTab'), addTab: empty.apply('bar.addTab'), notification: empty.apply('bar.notification'), _scrollers: empty.apply('bar._scrollers') } }); setup = (function() { var self = this; if(!this.storageReady || !this.themeLoaded) return; $(self).trigger('loadComplete'); $(window).resize(); $.extend(AjaxIM.prototype, { // == Cookies == // // The "cookies" functions can be used to set, get, and erase JSON-based cookies. // These functions are primarily used to manage and read the server-set cookie // that handles the user's chat session ID. cookies: { // === {{{AjaxIM.}}}**{{{cookies.set(name, value, days)}}}** === // // Sets a cookie, stringifying the JSON value upon storing it. // // ==== Parameters ==== // * {{{name}}} is the cookie name.\\ // * {{{value}}} is the cookie data that you would like to store.\\ // * {{{days}}} is the number of days that the cookie will be stored for. set: function(name, value, days) { if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "; expires=" + date.toGMTString(); } else var expires = ""; document.cookie = name + "=" + $.compactJSON(value) + expires + "; path=/"; }, // === {{{AjaxIM.}}}**{{{cookies.get(name)}}}** === // // Gets a cookie, decoding the JSON value before returning the data. // // ==== Parameters ==== // * {{{name}}} is the cookie name that you would like to retrieve. get: function(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) { var cval = decodeURIComponent(c.substring(nameEQ.length, c.length)); return $.secureEvalJSON(cval); } } return null; }, // === {{{AjaxIM.}}}**{{{cookies.erase(name)}}}** === // // Deletes a cookie. // // {{{name}}} is the existing cookie that you would like to delete. erase: function(name) { self.cookies.set(name, '', -1); } }, // == Main == // === {{{AjaxIM.}}}**{{{storage()}}}** === // // Retrieves chat session data from whatever storage engine is enabled // (provided that one is enabled at all). If a page reloads, this function // is called to restore the user's chat state (existing conversations, active tab). // This function is called //automatically//, upon initialization of the IM engine. storage: function() { if(!self.storeKey.length) return; try { var chatstore = $.jStore.store(self.storeKey + 'chats') || {}; } catch(e) { $.jStore.remove(self.storeKey + 'chats'); var chatstore = {}; } if(this.chatstore) { $.each(this.chatstore, function(username, convo) { if(username in chatstore) chatstore[username] = $.merge(chatstore[username], self.chatstore[username]); else chatstore[username] = self.chatstore[username]; }); this.chatstore = chatstore; $.jStore.store(self.storeKey + 'chats', chatstore); } else { this.chatstore = chatstore; } $.each(this.chatstore, function(username, convo) { if(!convo.length) return; var chatbox = self._createChatbox(username, true); chatbox.data('lastDateStamp', null).css('display', 'none'); // Remove the automatic date stamp chatbox.find('.imjs-msglog').empty(); // Restore all messages, date stamps, and errors $.each(convo, function() { switch(this[0]) { case 'error': self._addError(chatbox, decodeURIComponent(this[2]), this[3]); break; case 'datestamp': self._addDateStamp(chatbox, this[3]); break; case 'a': case 'b': self._addMessage(this[0], chatbox, this[1], decodeURIComponent(this[2]), this[3]); break; } }); $(self).trigger('chatRestored', [username, chatbox]); }); var activeTab = $.jStore.store(self.storeKey + 'activeTab') || []; if(activeTab.length && (activeTab = activeTab[0]) && activeTab in this.chats) { this.chats[activeTab].data('tab').click(); var msglog = this.chats[activeTab].find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } }, // === {{{AjaxIM.}}}**{{{login(username, password)}}}** === // // Authenticates a user and initializes the IM engine. If the user is // already logged in, [s]he is logged out, the session is cleared, and // the new user is logged in. // // Returns the user's properly formatted username, session ID, and online // friends in JSON, if successful; e.g.:\\ // {{{ {u: 'username', s: 'longsessionid', f: [{u: 'friend', s: 1, g: 'group'}]} }}} // // If unsuccessful, {{{false}}} is returned. // // ==== Parameters ==== // * {{{username}}} is the user's username.\\ // * {{{password}}} is the user's password. This password will be MD5 hashed // before it is sent to the server. login: function(username, password) { if(!username) username = ''; if(!password) password = ''; if(this.username) return true; // Already logged in! // hash password before sending it to the server password = $.md5(password); // authenticate AjaxIM.request( this.actions.login, {'username': username, 'password': password}, function(auth) { if(auth.r == 'logged in') { self.username = ('u' in auth ? auth.u : username); if(self.settings.storageMethod) self.storeKey = [self.storageBrowserKey, self.username, ''].join('-'); var existing = self.cookies.get(self.settings.cookieName); self.storage(); // Begin the session $.each(auth.f, function() { self.friends[this.u] = {status: [this.s, ''], group: this.g}; }); self._session(self.friends); self._storeFriends(); $(self).trigger('loginSuccessful', [auth]); return auth; } else { $(self).trigger('loginError', [auth]); } return false; } ); }, // === {{{AjaxIM.}}}**{{{logout()}}}** === // // Logs the user out and removes his/her session cookie. As well, it // will close all existing chat windows, clear the storage engine, and // remove the IM bar. logout: function() { AjaxIM.request( this.actions.logout, {}, function(done) { if(done) { self.cookies.erase(self.settings.cookieName); self._clearSession(); $(self).trigger('logoutSuccessful'); return true; } else { $(self).trigger('logoutError'); return false; } } ); }, // === {{{AjaxIM.}}}**{{{resume()}}}** === // // Resumes an existing session based on a session ID stored in the // server-set cookie. This function is called //automatically// upon IM // engine (re-)initialization, so the user does not need to re-login // should a session already exist. resume: function() { var session = this.cookies.get(this.settings.cookieName); if(session && session.sid) { this.username = session.user; this.storeKey = [this.storageBrowserKey, this.username, ''].join('-'); var friends = $.jStore.store(this.storeKey + 'friends') || []; if(self.settings.checkResume) { AjaxIM.request( this.actions.resume, {}, function(response) { if(response.r == 'connected') { self._session(friends); self.storage(); } else { var username = this.username; self._clearSession(); $(self).trigger('sessionNotResumed', [username]); } } ); } else { self._session(friends); self.storage(); } } else { $(self).trigger('noSession'); } }, // === //private// {{{AjaxIM.}}}**{{{_session(friends)}}}** === // // Restores session data (username, friends) and begins polling the server. // Called only by {{{AjaxIM.resume()}}}. // // ==== Parameters ==== // * {{{friends}}} is a list of "friend" objects, e.g.:\\ // {{{[{u: 'friend', s: 1, g: 'group'}, ...]}}} // ** {{{u}}} being the friend's username. // ** {{{s}}} being one of the available status codes (see {{{AjaxIM.statuses}}}), depending on the friend's current status. // ** {{{g}}} being the group that the friend is in. _session: function(friends) { $('#imjs-friends-panel .imjs-header span').html(this.username); $('#imjs-friends').removeClass('imjs-not-connected'); $.each(friends, function(friend, info) { self.addFriend.apply(self, [friend, info.status, info.group]); }); self._storeFriends(); $(self).trigger('sessionResumed', [this.username]); setTimeout(function() { self.poll(); }, 0); }, // === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** === // // Clears all session data from the last known user. _clearSession: function() { if(self.settings.storageMethod && self.storageReady) { $.jStore.remove(self.storeKey + 'chats'); $.jStore.remove(self.storeKey + 'friends'); $.jStore.remove(self.storeKey + 'activeTab'); } self.chats = {}; $('.imjs-tab').not('.imjs-tab.imjs-default').remove(); self.cookies.erase('ajaxim_session'); delete self.username; }, // === {{{AjaxIM.}}}**{{{form(element)}}}** === // // Loads a login and registration form into the specified element // or, if no element is supplied, to the location on the page from // which this function was called. form: function(element) { $(element).load(this.settings.theme + '/theme.html #imjs-lr', function() { $('#imjs-lr .error').hide(); if(self.username) { $('#imjs-register, #imjs-login fieldset').hide(); $('#imjs-logged-in') .show() .html($('#imjs-logged-in').html().replace('{username}', self.username)); } else { $('#imjs-logged-in').hide(); } // Handle logout success $(self).bind('logoutSuccessful', function() { $('#imjs-login fieldset').slideDown(); $('#imjs-register').slideDown(); $('#imjs-logged-in') .html($('#imjs-logged-in strong').html('{username}')) .slideUp(); }); $('#imjs-logged-in a').click(function() { self.logout(); return false; }); // Handle login error or success $(self).bind('loginError', function() { $('#imjs-login .error').html(AjaxIM.i18n.authInvalid).slideDown('fast'); $('#imjs-login input') .addClass('imjs-lr-error') .blur(function() { $(this).removeClass('imjs-lr-error'); }); }).bind('loginSuccessful', function() { $('#imjs-login fieldset').slideUp(); $('#imjs-register').slideUp(); $('#imjs-logged-in') .html($('#imjs-logged-in').html().replace('{username}', self.username)) .slideDown(); }); var login = function() { self.login($('#imjs-login-username').val(), $('#imjs-login-password').val()); return false; }; $('#imjs-login').submit(login); $('#imjs-login-submit').click(login); var regIssues = false; var regError = function(error, fields) { $('#imjs-register .error') .append(AjaxIM.i18n['register' + error] + ' ') .slideDown(); $(fields) .addClass('imjs-lr-error') .blur(function() { $(this).removeClass('imjs-lr-error'); }); regIssues = true; }; var register = function() { $('#imjs-register .error').empty(); regIssues = false; var username = $('#imjs-register-username').val(); var password = $('#imjs-register-password').val(); if(password.length < 4) regError('PasswordLength', '#imjs-register-password'); if(password != $('#imjs-register-cpassword').val()) regError('PasswordMatch', '#imjs-register-password, #imjs-register-cpassword'); if(username.length <= 2 || !$('#imjs-register-username').val().match(/^[A-Za-z0-9_.]+$/)) regError('UsernameLength', '#imjs-register-username'); if(!regIssues) { AjaxIM.request( self.actions.register, {username: username, password: password}, function(response) { if(response.r == 'registered') { self.login(username, password); } else if(response.r == 'error') { switch(response.e) { case 'unknown': regError('Unknown', ''); break; case 'invalid password': regError('PasswordLength', '#imjs-register-password'); break; case 'invalid username': regError('UsernameLength', '#imjs-register-username'); break; case 'username taken': regError('UsernameTaken', '#imjs-register-username'); break; } } }, function(error) { regError('Unknown', ''); } ); } return false; }; $('#imjs-register').submit(register); $('#imjs-register-submit').click(register); }); }, // === {{{AjaxIM.}}}**{{{poll()}}}** === // // Queries the server for new messages. If a 'long' or 'short' poll // type is used, jQuery's {{{$.post}}} or {{{$.getJSON}}} will be // used. If 'comet' is used, the server connection will be deferred // to the comet set of functions. poll: function() { if(/^(short|long)$/.test(this.settings.pollType)) { AjaxIM.request( this.actions.poll, {}, function(response) { if(!response['e']) { if(response.length) self._parseMessages(response); setTimeout(function() { self.poll(); }, 0); } else { switch(response.e) { case 'no session found': self._notConnected(); break; } $(self).trigger('pollFailed', [response.e]); } }, function(error) { self._notConnected(); $(self).trigger('pollFailed', ['not connected']); // try reconnecting? } ); } else if(this.settings.pollType == 'comet') { this.comet.connect(); } }, // === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** === // // Handles an incoming message array:\\ // {{{[{t: 'type', s: 'sender', r: 'recipient', m: 'message'}, ...]}}} // // * {{{t}}} (message type) is one of: // ** {{{m}}} — a standard message // ** {{{s}}} — a user's status update // ** {{{b}}} — a broadcasted message (sent to many users simultaneously) // * {{{s}}} is the sender of the message. // * {{{r}}} is the intended recipient of the message. Most of the time, this will // simply be the logged in user, however, a broadcasted message may not specify // a recipient or may specify a different recipient. Also provides future // compatability for chatrooms. // * {{{m}}} is the actual message. For something such as a status update, this can // be a JSON object or parsable string; e.g. {{{"2:I'm away."}}} // // ==== Parameters ==== // * {{{messages}}} is the message array _parseMessages: function(messages) { if($.isArray(messages)) { $.each(messages, function() { $(self).trigger('parseMessage', [this]); switch(this.t) { case 'm': self.incoming(this.s, this.m); break; case 's': var status = this.m.split(':'); if(this['g']) self.addFriend(this.s, status, this.g); self._friendUpdate(this.s, status[0], status.slice(1).join(':')); self._storeFriends(); break; case 'b': break; default: break; } }); } }, // === {{{AjaxIM.}}}**{{{incoming(from, message)}}}** === // // Handles a new message from another user. If a chatbox for that // user does not yet exist, one is created. If it does exist, but // is minimized, the user is notified but the chatbox is not brought // to the front. This function also stores the message, if a storage // method is set. // // ==== Parameters ==== // * {{{from}}} is the username of the sender. // * {{{message}}} is the body. incoming: function(from, message) { // check if IM exists, otherwise create new window // TODO: If friend is not on the buddylist, // should add them to a temp list? var chatbox = this._createChatbox(from); if(!$('#imjs-bar .imjs-selected').length) { chatbox.data('tab').click(); } else if(chatbox.data('tab').data('state') != 'active') { this.bar.notification(chatbox.data('tab')); } var time = this._addMessage('b', chatbox, from, message); this._storeMessage('b', chatbox, from, message, time); }, // === {{{AjaxIM.}}}**{{{addFriend(username, group)}}}** === // // Inserts a new friend into the friends list. If the group specified // doesn't exist, it is created. If the friend is already in this group, // they aren't added again, however, the friend item is returned. // // ==== Parameters ==== // * {{{username}}} is the username of the new friend. // * {{{status}}} is the current status of the friend. // * {{{group}}} is the user group to which the friend should be added. addFriend: function(username, status, group) { var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status[0] == val) { status_name = key; return false; } }); var group_id = 'imjs-group-' + $.md5(group); if(!(group_item = $('#' + group_id)).length) { var group_item = $('.imjs-friend-group.imjs-default').clone() .removeClass('imjs-default') .attr('id', group_id) .data('group', group) .appendTo('#imjs-friends-list'); var group_header = group_item.find('.imjs-friend-group-header'); group_header.html(group_header.html().replace('{group}', group)); } var user_id = 'imjs-friend-' + $.md5(username + group); if(!$('#' + user_id).length) { var user_item = group_item.find('ul li.imjs-default').clone() .removeClass('imjs-default') .addClass('imjs-' + status_name) .attr('id', user_id) .data('friend', username) .appendTo(group_item.find('ul')); if(status[0] == 0) user_item.hide(); user_item.html(user_item.html().replace('{username}', username)); } this.friends[username] = {'status': status, group: group}; this._updateFriendCount(); return this.friends[username]; }, // === //private// {{{AjaxIM.}}}**{{{_updateFriendCount()}}}** === // // Counts the number of online friends and updates the friends count // in the friend tab. _updateFriendCount: function() { var friendsLength = 0; for(var f in this.friends) { if(this.friends[f].status[0] != 0) friendsLength++; } $('#imjs-friends .imjs-tab-text span span').html(friendsLength); }, // === //private// {{{AjaxIM.}}}**{{{_storeFriends()}}}** === // // If a storage method is enabled, the current state of the // user's friends list is stored. _storeFriends: function() { if(this.settings.storageMethod && this.storageReady) $.jStore.store(this.storeKey + 'friends', this.friends); }, // === //private// {{{AjaxIM.}}}**{{{_createChatbox(username)}}}** === // // Builds a chatbox based on the default chatbox HTML and CSS defined // in the current theme. Should a chatbox for this user already exist, // a new one is not created. Instead, it is either given focus (should // no other windows already have focus), or a notification is issued. // // As well, if the chatbox does not exist, an associated tab will be // created. // // ==== Parameters ==== // * {{{username}}} is the name of the user for whom the chatbox is intended // for. // * {{{no_stamp}}} sets whther or not to add a date stamp to the chatbox // upon creation. // // //Note:// New chatboxes are given an automatically generated ID in the // format of {{{#imjs-[md5 of username]}}}. _createChatbox: function(username, no_stamp) { var chatbox_id = 'imjs-' + $.md5(username); if(!(chatbox = $('#' + chatbox_id)).size()) { // add a tab var tab = this.bar.addTab(username, '#' + chatbox_id); var chatbox = tab.find('.imjs-chatbox'); chatbox.attr('id', chatbox_id); chatbox.data('tab', tab); // remove default items from the message log var message_log = chatbox.find('.imjs-msglog').empty(); // setup the chatbox header var cb_header = chatbox.find('.imjs-header'); cb_header.html(cb_header.html().replace('{username}', username)); if(!no_stamp) { // add a date stamp var time = this._addDateStamp(chatbox); this._storeNonMessage('datestamp', username, null, time); } // associate the username with the object and vice-versa this.chats[username] = chatbox; chatbox.data('username', username); // did this chatbox fall down? this.bar._scrollers(); if(username in this.friends) { status = this.friends[username].status; var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status[0] == val) { status_name = key; return false; } }); tab.addClass('imjs-' + status_name); } // store inputbox height //var input = chatbox.find('.imjs-input'); //input.data('height', input.height()); } else if(chatbox.data('tab').data('state') == 'closed') { chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old'); var tab = chatbox.data('tab'); if(tab.css('display') == 'none') tab.css('display', '').removeClass('imjs-selected') .appendTo('#imjs-bar'); if(!no_stamp) { // possibly add a date stamp var time = this._addDateStamp(chatbox); this._storeNonMessage('datestamp', username, null, time); } if(!$('#imjs-bar .imjs-selected').length) { tab.click(); } else { this.bar.notification(tab); } } return chatbox; }, // === //private// {{{AjaxIM.}}}**{{{_addDateStamp(chatbox)}}}** // // // Adds a date/time notifier to a chatbox. These are generally // inserted upon creation of a chatbox, or upon the date changing // since the last time a date stamp was added. If a date stamp for // the current date already exists, a new one will not be added. // // ==== Parameters ==== // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{time}}} is the date/time the date stamp will show. It is specified // in milliseconds since the Unix Epoch. This is //only// defined when // date stamps are being restored from storage; if not specified, the // current computer time will be used. _addDateStamp: function(chatbox, time) { var message_log = $(chatbox).find('.imjs-msglog'); if(!time) time = (new Date()).getTime(); var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); var date_stamp_time = date_stamp.find('.imjs-msg-time'); if(date_stamp_time.length) date_stamp_time.html(AjaxIM.dateFormat(time, date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date'); var formatted_date = AjaxIM.dateFormat(time, date_stamp_date.html()); if(chatbox.data('lastDateStamp') != formatted_date) { if(date_stamp_date.length) date_stamp_date.html(AjaxIM.dateFormat(time, date_stamp_date.html())); chatbox.data('lastDateStamp', formatted_date); date_stamp.appendTo(message_log); } else { //$('
').appendTo(message_log); } return time; }, // === //private// {{{AjaxIM.}}}**{{{_addError(chatbox, error)}}}** // // // Adds an error to a chatbox. These are generally inserted after // a user sends a message unsuccessfully. If an error message // was already added, another one will be added anyway. // // ==== Parameters ==== // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{error}}} is the error message string. // * {{{time}}} is the date/time the error occurred. It is specified in // milliseconds since the Unix Epoch. This is //only// defined when // errors are being restored from storage; if not specified, the current // computer time will be used. _addError: function(chatbox, error, time) { var message_log = $(chatbox).find('.imjs-msglog'); var error_item = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-error').clone(); var error_item_time = error_item.find('.imjs-msg-time'); if(error_item_time.length) { if(!time) time = (new Date()).getTime(); error_item_time.html(AjaxIM.dateFormat(time, error_item_time.html())); } error_item.find('.imjs-error-error').html(error); error_item.appendTo(message_log); message_log[0].scrollTop = message_log[0].scrollHeight; }, // === //private// {{{AjaxIM.}}}**{{{_addMessage(ab, chatbox, username, message, time)}}}** // // // Adds a message to a chatbox. Depending on the {{{ab}}} value, // the color of the username may change as a way of visually // identifying users (however, this depends on the theme's CSS). // A timestamp is added to the message, and the chatbox is scrolled // to the bottom, such that the new message is visible. // // Messages will be automatically tag-escaped, so as to prevent // any potential cross-site scripting problems. Additionally, // URLs will be automatically linked. // // ==== Parameters ==== // * {{{ab}}} refers to whether the user is "a" or "b" in a conversation. // For the general case, "you" are "a" and "they" are "b". // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{username}}} is the username of the user who sent the message. // * {{{time}}} is the time the message was sent in milliseconds since // the Unix Epoch. This is //only// defined when messages are being // restored from storage. For new messages, the current computer // time is automatically used. _addMessage: function(ab, chatbox, username, message, time) { var last_message = chatbox.find('.imjs-msglog > *:last-child'); if(last_message.hasClass('imjs-msg-' + ab)) { // Last message was from the same person, so let's just add another imjs-msg-*-msg var message_container = (last_message.hasClass('imjs-msg-' + ab + '-container') ? last_message : last_message.find('.imjs-msg-' + ab + '-container')); var single_message = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-msg-' + ab + '-msg') .clone().appendTo(message_container); single_message.html(single_message.html().replace('{username}', username)); } else if(!last_message.length || !last_message.hasClass('imjs-msg-' + ab)) { var message_group = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msg-' + ab) .clone().appendTo(chatbox.find('.imjs-msglog')); message_group.html(message_group.html().replace('{username}', username)); var single_message = message_group.find('.imjs-msg-' + ab + '-msg'); } // clean up the message message = message.replace(//g, '>') .replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1$2$3'); // autolink URLs message = message.replace( new RegExp('([A-Za-z][A-Za-z0-9+.-]{1,120}:[A-Za-z0-9/]' + '(([A-Za-z0-9$_.+!*,;/?:@&~=-])|%[A-Fa-f0-9]{2}){1,333}' + '(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*,;/?:@&~=%-]{0,1000}))?)', 'g'), '$1'); // insert the message single_message.html(single_message.html().replace('{message}', message)); // set the message time var msgtime = single_message.find('.imjs-msg-time'); if(!time) time = new Date(); if(typeof time != 'string') time = AjaxIM.dateFormat(time, msgtime.html()); msgtime.html(time); var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; return time; }, // === //private// {{{AjaxIM.}}}**{{{_storeNonMessage(type, username, data, time)}}}** === // // **Redundant?**\\ // Similar to {{{AjaxIM._storeMessage}}}, but stores items that aren't messages, // such as datestamps and errors. // // ==== Parameters ==== // * {{{type}}} is the type of non-message (error, datestamp). // * {{{username}}} is the username of the user being chatted with. // * {{{data}}} is the (optional) data of the non-message to be stored. // * {{{time}}} is the time of the message. _storeNonMessage: function(type, username, data, time) { // If storage is on & ready, store the non-message if(this.settings.storageMethod) { if(!this.chatstore) this.chatstore = {}; if(!(username in this.chatstore)) this.chatstore[username] = []; // If the chat store gets too long, it becomes slow to load. if(this.chatstore[username].length > 300) this.chatstore[username].shift(); // For some reason, if we don't encode and decode the message, it *will* break // (at least) the Flash storage engine's retrieval. Gah! this.chatstore[username].push( [type, username, encodeURIComponent(data), time]); if(this.storageReady) $.jStore.store(self.storeKey + 'chats', this.chatstore); } }, // === //private// {{{AjaxIM.}}}**{{{_storeMessage(ab, chatbox, username, message, time)}}}** === // // Taking the same arguments as {{{AjaxIM._addMessage}}}, {{{_storeMessage}}} pushes a message // into the storage hash, if storage is enabled. // // Messages are added to a message array, by username. The entire chat hash is stored as // a {{{'chats'}}} object in whatever storage engine is enabled. _storeMessage: function(ab, chatbox, username, message, time) { // If storage is on & ready, store the message if(this.settings.storageMethod) { if(!this.chatstore) this.chatstore = {}; message = message.replace(//g, '>'); if(!(username in this.chatstore)) { this.chatstore[username] = []; } else if(this.chatstore[username].length > 300) { // If the chat store gets too long, it becomes slow to load. this.chatstore[username].shift(); } // For some reason, if we don't encode and decode the message, it *will* break // (at least) the Flash storage engine's retrieval. Gah! this.chatstore[chatbox.data('username')].push( [ab, username, encodeURIComponent(message), time]); if(this.storageReady) $.jStore.store(this.storeKey + 'chats', this.chatstore); } }, // === //private// {{{AjaxIM.}}}**{{{_friendUpdate(friend, status, statusMessage)}}}** === // // Called when a friend's status is updated. This function will update all locations // where a status icon is displayed (chat tab, friends list), as well as insert // a notification, should a chatbox be open. // // ==== Parameters ==== // * {{{friend}}} is the username of the friend. // * {{{status}}} is the new status code. See {{{AjaxIM.statuses}}} for a list of available // codes. //Note: If an invalid status is specified, no action will be taken.// // * {{{statusMessage}}} is a message that was, optionally, specified by the user. It will be // used should "you" send the user an IM while they are away, or if their status is viewed // in another way (such as via the friends list [**not yet implemented**]). _friendUpdate: function(friend, status, statusMessage) { // add friend to buddylist, update their status, etc. var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status == val) { status_name = key; return false; } }); if(this.chats[friend]) { var tab = this.chats[friend].data('tab'); var tab_class = 'imjs-tab'; if(tab.data('state') == 'active') tab_class += ' imjs-selected'; tab_class += ' imjs-' + status_name; tab.attr('class', tab_class); // display the status in the chatbox var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); var date_stamp_time = date_stamp.find('.imjs-msg-time'); if(date_stamp_time.length) date_stamp_time.html(AjaxIM.dateFormat(date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date').html( AjaxIM.i18n[ 'chat' + status_name[0].toUpperCase() + status_name.slice(1) ].replace(/%s/g, friend)); var msglog = this.chats[friend].find('.imjs-msglog'); date_stamp.appendTo(msglog); msglog[0].scrollTop = msglog[0].scrollHeight; } if(this.friends[friend]) { var friend_id = 'imjs-friend-' + $.md5(friend + this.friends[friend].group); $('#' + friend_id).attr('class', 'imjs-friend imjs-' + status_name); if(status == 0) { $('#' + friend_id + ':visible').slideUp(); $('#' + friend_id + ':hidden').hide(); } else if(!$('#' + friend_id + ':visible').length) { $('#' + friend_id).slideDown(); } this.friends[friend].status = [status, statusMessage]; this._updateFriendCount(); } }, // === //private// {{{AjaxIM.}}}**{{{_notConnected()}}}** === // // Puts the user into a visible state of disconnection. Sets the // friends list to "not connected" and empties it; disallows new messages // to be sent. _notConnected: function() { $('#imjs-friends').addClass('imjs-not-connected').unbind('click', this.activateTab); }, // === {{{AjaxIM.}}}**{{{send(to, message)}}}** === // // Sends a message to another user. The message will be added // to the chatbox before it is actually sent, however, if an // error occurs during sending, that will be indicated immediately // afterward. // // After sending the message, one of three status codes should be // returned as a JSON object, e.g. {{{{r: 'code'}}}}: // * {{{ok}}} — Message was sent successfully. // * {{{offline}}} — The user is offline or unavailable to // receive messages. // * {{{error}}} — a problem occurred, unrelated to the user // being unavailable. // // ==== Parameters ==== // * {{{to}}} is the username of the recipient. // * {{{message}}} is the content to be sent. send: function(to, message) { if(!message) return; if(this.chats[to]) { // REMOVE ME? // possibly add a datestamp var time = self._addDateStamp(this.chats[to]); this._storeNonMessage('datestamp', to, null, time); time = this._addMessage('a', this.chats[to], this.username, message); this._storeMessage('a', this.chats[to], this.username, message, time); } $(self).trigger('sendingMessage', [to, message]); AjaxIM.request( this.actions.send, {'to': to, 'message': message}, function(result) { switch(result.r) { case 'ok': $(self).trigger('sendMessageSuccessful', [to, message]); break; case 'offline': $(self).trigger('sendMessageFailed', ['offline', to, message]); break; case 'error': default: if(result.e == 'no session found') { self._notConnected(); self._addError(self.chats[to], AjaxIM.i18n.notConnected); self._storeNonMessage('error', to, AjaxIM.i18n.notConnected, (new Date()).getTime()); } $(self).trigger('sendMessageFailed', [result.e, to, message]); break; } }, function(error) { self._notConnected(); self._addError(self.chats[to], AjaxIM.i18n.notConnected); self._storeNonMessage('error', to, AjaxIM.i18n.notConnected, (new Date()).getTime()); $(self).trigger('sendMessageFailed', ['not connected', to, message]); } ); }, // === {{{AjaxIM.}}}**{{{status(s, message)}}}** === // // Sets the user's status and status message. It is possible to not // set a status message by setting it to an empty string. The status // will be sent to the server, where upon the server will broadcast // the update to all individuals with "you" on their friends list. // // ==== Parameters ==== // * {{{s}}} is the status code, as defined by {{{AjaxIM.statuses}}}. // * {{{message}}} is the custom status message. status: function(s, message) { // update status icon(s) if(!this.statuses[s]) return; $('#imjs-friends').attr('class', 'imjs-' + s); $(self).trigger('changingStatus', [s, message]); AjaxIM.request( this.actions.status, {'status': this.statuses[s], 'message': message}, function(result) { switch(result.r) { case 'ok': $(self).trigger('changeStatusSuccessful', [s, message]); break; case 'error': default: $(self).trigger('changeStatusFailed', [result.e, s, message]); break; } }, function(error) { $(self).trigger('changeStatusFailed', ['not connected', s, message]); } ); }, // === {{{AjaxIM.}}}**{{{statuses}}}** === // // These are the available status codes and their associated identities: // * {{{offline}}} (0) — Only used when signing out/when another // user has signed out, as once this status is set, the user is removed // from the server and friends will be unable to contact the user. // * {{{available}}} (1) — The user is online and ready to be messaged. // * {{{away}}} (2) — The user is online but is not available. Others // may still contact this user, however, the user may not respond. Anyone // contacting an away user will receive a notice stating that the user is away, // and (if one is set) their custom status message. // * {{{invisible}}} (3; **not yet implemented**) — The user is online, // but other users are made unaware, and the user will be represented // as being offline. It is still possible to contact this user, and for this // user to contact others; no status message or notice will be sent to others // messaging this user. statuses: {offline: 0, available: 1, away: 2, invisible: 3}, // == Footer bar == // // The footer bar is the bar that sits at the bottom of the page, in a fixed // position. It contains a tab for the friends list, and tabs for any open // chat boxes. It is also possible to add custom tabs for other functionality. bar: { // === {{{AjaxIM.}}}**{{{bar.initialize()}}}** === // // Setup the footer bar and enable tab actions. This function // uses {{{jQuery.live}}} to set hooks on any bar tabs created // in the future. initialize: function() { // Set up your standard tab actions $('.imjs-tab') .live('click', this.activateTab); $('.imjs-tab .imjs-close') .live('click', this.closeTab); // Set up the friends list actions var self = this; $(document).click(function(e) { if(e.target.id == 'imjs-friends' || $(e.target).parents('#imjs-friends').length) { return; } if($('#imjs-friends').data('state') == 'active') self.activateTab.call($('#imjs-friends')); }); $('#imjs-friends') .data('state', 'minimized') .click(function(e) { if(!$(this).hasClass('imjs-not-connected') && e.target.id != 'imjs-friends-panel' && !$(e.target).parents('#imjs-friends-panel').length) self.activateTab.call(this); }) .mouseenter(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip').css('display', 'block'); $('.imjs-tooltip p').html(AjaxIM.i18n.notConnectedTip); var tip_left = $(this).offset().left - $('.imjs-tooltip').outerWidth() + ($(this).outerWidth() / 2); var tip_top = $(this).offset().top - $('.imjs-tooltip').outerHeight(true); $('.imjs-tooltip').css({ left: tip_left, top: tip_top }); } }) .mouseleave(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip').css('display', ''); } }); $('#imjs-friends-panel') .data('tab', $('#imjs-friends')) .css('display', 'none'); }, // === {{{AjaxIM.}}}**{{{bar.activateTab()}}}** === // // Activate a tab by setting it to the 'active' state and // showing any related chatbox. If a chatbox is available // for this tab, also focus the input box. // // //Note:// {{{this}}}, here, refers to the tab DOM element. activateTab: function() { var chatbox = $(this).find('.imjs-chatbox') || false; if($(this).data('state') != 'active') { if($(this).attr('id') != 'imjs-friends') { $('#imjs-bar > li') .not($(this)) .not('#imjs-friends') .removeClass('imjs-selected') .each(function() { if($(this).data('state') != 'closed') { $(this).data('state', 'minimized'); var chatbox = $(this).find('.imjs-chatbox'); if(chatbox.length) chatbox.css('display', 'none'); } }); } if(chatbox && chatbox.css('display') == 'none') chatbox.css('display', ''); // set the tab to active... var tab = $(this).addClass('imjs-selected').data('state', 'active'); // ...and hide and reset the notification icon tab.find('.imjs-notification').css('display', 'none') .data('count', 0); if(self.settings.storageMethod && self.storageReady && chatbox && (username = chatbox.data('username'))) { $.jStore.store(self.storeKey + 'activeTab', [username]); } $(self).trigger('tabToggled', ['activated', tab]); } else { var tab = $(this).removeClass('imjs-selected').data('state', 'minimized'); if(chatbox && chatbox.css('display') != 'none') chatbox.css('display', 'none'); if(self.settings.storageMethod && self.storageReady) { $.jStore.store(self.storeKey + 'activeTab', ['*']); } $(self).trigger('tabToggled', ['minimized', tab]); } if(chatbox) { if(!(input = chatbox.find('.imjs-input')).data('height')) { // store the height for resizing later input.data('height', input.height()); } try { var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } catch(e) {} try { chatbox.find('.imjs-input').focus(); } catch(e) {} } }, // === {{{AjaxIM.}}}**{{{bar.closeTab()}}}** === // // Close a tab and hide any related chatbox, such that // the chatbox can not be reopened without reinitializing // the tab. // // //Note:// {{{this}}}, here, refers to the tab DOM element. closeTab: function() { var tab = $(this).parents('.imjs-tab'); tab.css('display', 'none').data('state', 'closed'); if(self.settings.storageMethod && self.storageReady) { delete self.chatstore[tab.find('.imjs-chatbox').data('username')]; if(self.storageReady) $.jStore.store(self.storeKey + 'chats', self.chatstore); } $(self).trigger('tabToggled', ['closed', tab]); self.bar._scrollers(); return false; }, // === {{{AjaxIM.}}}**{{{bar.addTab(label, action, closable)}}}** === // // Adds a tab to the tab bar, with the label {{{label}}}. When // clicked, it will call a callback function, {{{action}}}. If // {{{action}}} is a string, it is assumed that the string is // referring to a chatbox ID. // // ==== Parameters ==== // * {{{label}}} is the text that will be displayed on the tab.\\ // * {{{action}}} is the callback function, if it is a non-chatbox // tab, or a string if it //is// a chatbox tab.\\ // * {{{closable}}} is a boolean value that determines whether or not // it is possible for a user to close this tab. // // //Note:// New tabs are given an automatically generated ID // in the format of {{{#imjs-tab-[md5 of label]}}}. addTab: function(label, action, closable) { var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-right'); tab.removeClass('imjs-default') .attr('id', 'imjs-tab-' + $.md5(label)) .html(tab.html().replace('{label}', label)) .data('state', 'minimized'); var notification = tab.find('.imjs-notification'); notification.css('display', 'none') .data('count', 0) .data('default-text', notification.html()) .html(notification.html().replace('{count}', '0')); if(closable === false) tab.find('.imjs-close').eq(0).remove(); if(typeof action == 'string') { //tab.data('chatbox', action); } else { tab.find('.imjs-chatbox').remove(); tab.click(action); } return tab; }, // === {{{AjaxIM.}}}**{{{bar.notification(tab)}}}** === // // Displays a notification on a tab. Generally, this is called when // a tab is minimized to let the user know that there is an update // for them. The way the notification is displayed depends on the // theme CSS. // // ==== Parameters ==== // * {{{tab}}} is the jQuery-selected tab DOM element. notification: function(tab) { var notify = tab.find('.imjs-notification'); var notify_count = notify.data('count') + 1; notify.data('count', notify_count) .html(notify.data('default-text').replace('{count}', notify_count)) .css('display', ''); }, // === //private// {{{AjaxIM.}}}**{{{bar._scrollers()}}}** === // // Document me! _scrollers: function() { var needScrollers = false; $('.imjs-tab').filter(function() { return $(this).data('state') != 'closed' }).css('display', ''); $.each(self.chats, function(username, chatbox) { var tab = chatbox.data('tab'); if(tab.data('state') == 'closed') return true; if(tab.position().top > $('#imjs-bar').height()) { $('.imjs-scroll').css('display', ''); tab.css('display', 'none'); needScrollers = true; } else { tab.css('display', ''); } }); if(!needScrollers) { $('.imjs-scroll').css('display', 'none'); } if($('#imjs-scroll-left').css('display') != 'none' && $('#imjs-scroll-left').position().top > $('#imjs-bar').height()) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); } var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').slice(-1) .nextAll('#imjs-bar li.imjs-tab:hidden') .not('.imjs-default') .filter(function() { return $(this).data('state') != 'closed' }).length; var hiddenRight = $('#imjs-bar li.imjs-tab:visible').eq(0) .prevAll('#imjs-bar li.imjs-tab:hidden') .not('.imjs-default') .filter(function() { return $(this).data('state') != 'closed' }).length; $('#imjs-scroll-left').html(hiddenLeft); $('#imjs-scroll-right').html(hiddenRight); } }, // == Comet == // // Comet, or HTTP streaming, holds open a connection between the client and // the server indefinitely. As the server receives new messages or events, // they are passed down to the client in a {{{<script>}}} // tag which calls the {{{AjaxIM.incoming()}}} function. The connection is // opened using either an {{{iframe}}} (in Opera or Internet Explorer) or // an {{{XMLHTTPRequest}}} object. Due to the extra flexibility necessary // with the {{{XMLHTTPRequest}}} object, jQuery's {{{$.ajax}}} is not used. comet: { type: '', obj: null, onbeforeunload: null, onreadystatechange: null, iframe: function() { if(/loaded|complete/i.test(this.obj.readyState)) throw new Error("IM server not available!"); }, // === {{{AjaxIM.}}}**{{{comet.connect()}}}** === // // Creates and initializes the object and connection between the server // and the client. For Internet Explorer and Opera, we use an // {{{<iframe>}}} element; for all other browsers, we create an // {{{XMLHTTPRequest}}} object. The server connects to the URI defined // as the "poll" action. This function is called automatically, when // the IM engine is initialized and the {{{AjaxIM.poll()}}} function // is called. connect: function() { var self = _instance; if($.browser.opera || $.browser.msie) { var iframe = $(''); with(iframe) { css({ position: 'absolute', visibility: 'visible', display: 'block', left: '-10000px', top: '-10000px', width: '1px', height: '1px' }); attr('src', self.actions.poll[1]); appendTo('body'); bind('readystatechange', self.comet.onreadystatechange = function() { self.comet.iframe() }); bind('beforeunload', self.comet.onbeforeunload = function() { self.comet.disconnect() }); } self.comet.type = 'iframe'; self.comet.obj = iframe; } else { var xhr = new XMLHttpRequest; var length = 1029; var code = /^\s*]*>parent\.(.+);<\/script>$/; xhr.open('get', self.actions.poll[1], true); xhr.onreadystatechange = function(){ if(xhr.readyState > 2) { if(xhr.status == 200) { responseText = xhr.responseText.substring(length); length = xhr.responseText.length; if(responseText != ' ') eval(responseText.replace(code, "$1")); } // We need an "else" here. If the state changes to // "loaded", the user needs to know they're // disconnected. } }; self.comet.type = 'xhr'; self.comet.obj = xhr; addEventListener('beforeunload', self.comet.beforeunload = function() { self.comet.disconnect(); }, false); setTimeout(function() { xhr.send(null) }, 10); } }, // === {{{AjaxIM.}}}**{{{comet.disconnect()}}}** === // // Disconnect from the server and destroy the connection object. This // function is called before the page unloads, so that we plug up and // potential leaks and free memory. disconnect: function() { var self = _instance.comet; if(!this.type || !this.obj) return; if(this.type == 'iframe') { detachEvent('onreadystatechange', this.onreadystatechange); detachEvent('onbeforeunload', this.onbeforeunload); this.obj.src = '.'; $(this.obj).remove(); } else { removeEventListener('beforeunload', this.onbeforeunload, false); this.obj.onreadystatechange = function(){}; this.obj.abort(); } delete this.obj; } } }) self.bar.initialize(); if(prequeue.length) { $.each(prequeue, function() { var func = this[0].split('.'); if(func.length > 1) self[func[0]][func[1]].apply(self, this[1]); else self[func[0]].apply(self, this[1]); }); } }); // == Static functions and variables == // // The following functions and variables are available outside of an initialized // {{{AjaxIM}}} object. // === {{{AjaxIM.}}}**{{{client}}}** === // // Once {{{AjaxIM.init()}}} is called, this will be set to the active AjaxIM // object. Only one AjaxIM object instance can exist at a time. This variable // can and should be accessed directly. AjaxIM.client = null; // === {{{AjaxIM.}}}**{{{init(options, actions)}}}** === // // Initialize the AjaxIM client object and engine. Here, you can define your // options and actions as outlined at the top of this documentation. // // ==== Parameters ==== // * {{{options}}} is the hash of custom settings to initialize Ajax IM with. // * {{{actions}}} is the hash of any custom action URLs. AjaxIM.init = function(options, actions) { if(!_instance) { _instance = new AjaxIM(options, actions); AjaxIM.client = _instance; } return _instance; } // === {{{AjaxIM.}}}**{{{request(url, data, successFunc, failureFunc)}}}** === // // Wrapper around {{{$.jsonp}}}, the JSON-P library for jQuery, and {{{$.ajax}}}, // jQuery's ajax library. Allows either function to be called, automatically, // depending on the request's URL array (see {{{AjaxIM.actions}}}). // // ==== Parameters ==== // {{{url}}} is the URL of the request. // {{{data}}} are any arguments that go along with the request. // {{{success}}} is a callback function called when a request has completed // without issue. // {{{_ignore_}}} is simply to provide compatability with {{{$.post}}}. // {{{failure}}} is a callback function called when a request hasn't not // completed successfully. AjaxIM.request = function(url, data, successFunc, failureFunc) { if(typeof failureFunc != 'function'); failureFunc = function(){}; $[url[0]]({ 'url': url[1], 'data': data, dataType: (url[0] == 'ajax' ? 'json' : 'jsonp'), type: 'POST', cache: false, timeout: 60000, callback: 'jsonp' + (new Date()).getTime(), success: function(json, textStatus) { successFunc(json); }, error: function(xOptions, error) { failureFunc(error); } }); // This prevents Firefox from spinning indefinitely // while it waits for a response. Why? Fuck if I know. if(url[0] == 'jsonp' && $.browser.mozilla) { $.jsonp({ 'url': 'about:', timeout: 0 }); } }; // === {{{AjaxIM.}}}**{{{incoming(data)}}}** === // // Never call this directly. It is used as a connecting function between // client and server for Comet. // // //Note:// There are two {{{AjaxIM.incoming()}}} functions. This one is a // static function called outside of the initialized AjaxIM object; the other // is only called within the initalized AjaxIM object. AjaxIM.incoming = function(data) { if(!_instance) return false; if(data.length) _instance._parseMessages(data); } // === {{{AjaxIM.}}}**{{{loaded}}}** === // // If Ajax IM has been loaded with the im.load.js file, this function will be // called when the library is finally loaded and ready for use. Similar to // jQuery's $(document).ready(), but for Ajax IM. AjaxIM.loaded = function() { if(typeof AjaxIMLoadedFunction == 'function') { AjaxIMLoadedFunction(); delete AjaxIMLoadedFunction; // clean up the global namespace } }; // === {{{AjaxIM.}}}**{{{dateFormat([date,] [mask,] utc)}}}** === // // Date Format 1.2.3\\ // © 2007-2009 Steven Levithan ([[http://blog.stevenlevithan.com/archives/date-time-format|stevenlevithan.com]])\\ // MIT license // // Includes enhancements by Scott Trenda // and Kris Kowal ([[http://cixar.com/~kris.kowal/|cixar.com/~kris.kowal/]]) // // Accepts a date, a mask, or a date and a mask and returns a formatted version // of the given date. // // ==== Parameters ==== // * {{{date}}} is a {{{Date()}}} object. If not specified, the date defaults to the // current date/time. // * {{{mask}}} is a string that defines the formatting of the date. Formatting // options can be found in the // [[http://blog.stevenlevithan.com/archives/date-time-format|Date Format]] // documentation. If not specified, the mask defaults to {{{dateFormat.masks.default}}}. AjaxIM.dateFormat = function () { var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, timezone = new RegExp('\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) ' + '(?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b', 'g'), timezoneClip = /[^-+\dA-Z]/g, pad = function (val, len) { val = String(val); len = len || 2; while (val.length < len) val = "0" + val; return val; }; // Regexes and supporting functions are cached through closure return function (date, mask, utc) { var dF = AjaxIM.dateFormat; // You can't provide utc if you skip other args (use the "UTC:" mask prefix) if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { mask = date; date = undefined; } // Passing date through Date applies Date.parse, if necessary date = date ? new Date(date) : new Date; if (isNaN(date)) throw SyntaxError("invalid date"); mask = String(dF.masks[mask] || mask || dF.masks["default"]); // Allow setting the utc argument via the mask if (mask.slice(0, 4) == "UTC:") { mask = mask.slice(4); utc = true; } var _ = utc ? "getUTC" : "get", d = date[_ + "Date"](), D = date[_ + "Day"](), m = date[_ + "Month"](), y = date[_ + "FullYear"](), H = date[_ + "Hours"](), M = date[_ + "Minutes"](), s = date[_ + "Seconds"](), L = date[_ + "Milliseconds"](), o = utc ? 0 : date.getTimezoneOffset(), flags = { d: d, dd: pad(d), ddd: AjaxIM.i18n.dayNames[D], dddd: AjaxIM.i18n.dayNames[D + 7], m: m + 1, mm: pad(m + 1), mmm: AjaxIM.i18n.monthNames[m], mmmm: AjaxIM.i18n.monthNames[m + 12], yy: String(y).slice(2), yyyy: y, h: H % 12 || 12, hh: pad(H % 12 || 12), H: H, HH: pad(H), M: M, MM: pad(M), s: s, ss: pad(s), l: pad(L, 3), L: pad(L > 99 ? Math.round(L / 10) : L), t: H < 12 ? "a" : "p", tt: H < 12 ? "am" : "pm", T: H < 12 ? "A" : "P", TT: H < 12 ? "AM" : "PM", Z: utc ? "UTC" : (String(date).match(timezone) || [""]) .pop().replace(timezoneClip, ""), o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] }; return mask.replace(token, function ($0) { return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); }); }; }(); // Some common format strings AjaxIM.dateFormat.masks = { "default": "ddd mmm dd yyyy HH:MM:ss", shortDate: "m/d/yy", mediumDate: "mmm d, yyyy", longDate: "mmmm d, yyyy", fullDate: "dddd, mmmm d, yyyy", shortTime: "h:MM TT", mediumTime: "h:MM:ss TT", longTime: "h:MM:ss TT Z", isoDate: "yyyy-mm-dd", isoTime: "HH:MM:ss", isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" }; // === {{{AjaxIM.}}}**{{{i18n}}}** === // // Text strings used by Ajax IM. Should you want to translate Ajax IM into // another language, merely change these strings. // // {{{%s}}} denotes text that will be automatically replaced when the string is // used. AjaxIM.i18n = { dayNames: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], monthNames: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], chatOffline: '%s signed off.', chatAvailable: '%s became available.', chatAway: '%s went away.', notConnected: 'You are currently not connected or the server is not available. ' + 'Please ensure that you are signed in and try again.', notConnectedTip: 'You are currently not connected.', authInvalid: 'Invalid username or password.', registerPasswordLength: 'Passwords must be more than 4 characters in length.', registerUsernameLength: 'Usernames must be more than 2 characters in length and ' + ' contain only A-Z, a-z, 0-9, underscores (_) and periods (.).', registerPasswordMatch: 'Entered passwords do not match.', registerUsernameTaken: 'The chosen username is already in use; please choose another.', registerUnknown: 'An unknown error occurred. Please try again.' } })(jQuery, false); \ No newline at end of file +// = im.js = // // **Copyright © 2005 – 2010 Joshua Gross**\\ // //MIT Licensed// // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // This is the main library for Ajax IM. It encompasses the UI controls, // and connecting with the server. It does //not// handle registration or // account management. (function($, _instance) { AjaxIM = function(options, actions) { if(this instanceof AjaxIM) { var self = this; // === {{{ defaults }}} === // // These are the available settings for Ajax IM, and the associated // defaults: // // * {{{pollType}}} determines the way in which the client will talk to // the server. // ** {{{comet}}} will use http streaming, wherein a connection to the server will be held open indefinitely, and the server will push down new events (messages, status updates) as they occur. // ** {{{long}}} will hold open a connection with the server for as long as possible, or until a new event is received. Upon the server sending an event or closing the connection, the client will automatically—and immediately—reconnect. // ** {{{short}}} will open a connection, and the server (if this method is supported) will //immediately// provide a response as to whether or not there are any new events. Once a response is received, the client will wait 5 seconds, and then reconnect. // * {{{pollServer}}} is the default URL to which all actions refer. It is // possible to specify certain action URLs separately (as is used with the // NodeJS server). // * {{{cookieName}}} is the name of the session cookie used by the server. // If this is not set properly, the IM engine will not be able to automatically // reinitialize sessions. // * {{{theme}}} is the name of the theme folder that defines the HTML and // CSS of the IM bar and chat boxes. Usually, themes are deposited in the // provided "themes" folder and specified by that path, e.g. {{{themes/default}}}. // Theme files within the theme folder must be named {{{theme.html}}} and // {{{theme.css}}}. // * {{{storageMethod}}} defines the way in which data (chat sessions) are // temporarily stored client-side. By default, {{{"flash"}}} is used because // it is the most widely supported method. However, // [[http://eric.garside.name/docs.html?p=jstore#js-engines|other storage engines]] // are available, with their respective up- and down-sides // outlined, on the jStore website. // * {{{storeSession}}} (**not implemented**) sets the number of days to // retain stored chat session data before it should be deleted. // * {{{checkResume}}} is a flag that sets whether or not the client should // make a call to the server before resuming the session (such as on a page // reload). This will ensure that the session has not expired. If set to {{{false}}}, // a call to the server will not be made, and the session will be assumed to // be active. var defaults = { pollType: 'long', pollServer: './ajaxim.php', cookieName: 'ajaxim_session', theme: 'themes/default', storageMethod: 'auto', flashStorage: 'js/jStore.Flash.html', storeSession: 5, // number of days to store chat data (0 for current session only) checkResume: true }; // === {{{AjaxIM.}}}**{{{settings}}}** === // // These are the settings for the IM. If particular options are not specified, // the defaults (see above) will be used. //These options will be defined // upon calling the initialization function, and not set directly.// this.settings = $.extend(defaults, options); // === {{{AjaxIM.}}}**{{{actions}}}** === // // Each individual action that the IM engine can execute is predefined here. // By default, it merely appends the action onto the end of the {{{pollServer}}} url, // however, it is possible to define actions individually. //The alternative actions // will be defined upon calling the initialization function, and not set directly.// // // Should you define an action at a different URL, Ajax IM will determine whether // or not this URL is within the current domain. If it is within a subdomain of // the current domain, it will set the document.domain variable for you, // to match a broader hostname scope; the action will continue to use {{{$.post}}} // (the default AJAX method for Ajax IM). // // On the other hand, should you choose a URL outside the current domain // Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid // cross-domain scripting issues. This means that a server on a different // port or at a different address will need to be able to handle GET // requests rather than POST requests (such as how the Node.JS Ajax IM // server works). this.actions = $.extend({ login: this.settings.pollServer + '/login', logout: this.settings.pollServer + '/logout', register: this.settings.pollServer + '/register', poll: this.settings.pollServer + '/poll?method=' + this.settings.pollType, send: this.settings.pollServer + '/send', status: this.settings.pollServer + '/status', resume: this.settings.pollServer + '/resume' }, actions); var httpRx = new RegExp('^http://', 'i'), slashslashRx = new RegExp('^//', 'i'), queryStrRx = new RegExp('[?](.+)$'), subdomainRx = new RegExp('((http[s]?:)?//)?(.+?)[.]' + window.location.host, 'i'); $.each(this.actions, function(name, action) { if(name == 'poll' || name == 'send') { if(self.settings.pollType != 'comet') action += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; action = ['jsonp', action]; } else { action = ['ajax', action]; if(subdomainRx.test(action[1])) { document.domain = '.' + window.location.host; } else if((http = httpRx.test(action[1])) || slashslashRx.test(action[1])) { if(!(new RegExp('//' + window.location.host + '/', 'i')).test(action[1])) { action[1] += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; action = ['jsonp', action[1]]; } } } self.actions[name] = action; }); // We load the theme dynamically based on the passed // settings. If the theme is set to false, we assume // that the user is going to load it himself. this.themeLoaded = false; if(this.settings.theme) { $('
').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip', function() { self.themeLoaded = true; setup.apply(self); } ); if(typeof document.createStyleSheet == 'function') document.createStyleSheet(this.settings.theme + '/theme.css'); else $('body').append(''); } else { this.themeLoaded = true; setup.apply(this); } // Client-side storage for keeping track of // conversation states, active tabs, etc. // The default is flash storage, however, other // options are available via the jStore library. if(this.settings.storageMethod) { this.storageBrowserKey = 'unknown'; this.storeKey = ''; $.each($.browser, function(k, v) { if(k == 'version') return true; else if(v == true) { self.storageBrowserKey = k; return false; } }); if(this.settings.storageMethod == 'auto') { $.each(['ie', 'html5', 'local', 'flash'], function() { if($.jStore.Availability[this]()) { self.settings.storageMethod = this; return false; } }); } $.extend($.jStore.defaults, { project: 'im.js', engine: this.settings.storageMethod, flash: this.settings.flashStorage }); this.storageReady = false; if(this.settings.storageMethod == 'flash') { $.jStore.ready(function(engine) { $.jStore.flashReady(function() { self.storageReady = true; setup.apply(self); }); }) } else { $.jStore.ready(function(engine) { self.storageReady = true; setup.apply(self); }); } $.jStore.load(); } else { this.storageReady = true; setup.apply(this); } // Allow a chatbox to me minimized $('.imjs-chatbox').live('click', function(e) { e.preventDefault(); return false; }); $('.imjs-chatbox .imjs-minimize').live('click', function() { $(this).parents('.imjs-chatbox').data('tab').click(); }); // Allow a chatbox to be closed $('.imjs-chatbox .imjs-close').live('click', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.data('tab') .data('state', 'closed').css('display', 'none'); if(self.settings.storageMethod && self.storageReady) { delete self.chatstore[chatbox.data('username')]; if(self.storageReady) { $.jStore.store( self.storageBrowserKey + '-' + self.username + '-chats', self.chatstore); } } }); // Setup message sending for all chatboxes $('.imjs-chatbox .imjs-input').live('keydown', function(event) { var obj = $(this); if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) { self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } }).live('keyup', function(event) { if(event.keyCode == 13) { if($.browser.msie && $.browser.version < 8) { var obj = $(this); self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } var obj = $(this); obj.val(''); obj.height(obj.data('height')); } }).live('keypress', function(e) { var obj = $(this); if(!($.browser.msie && $.browser.opera)) obj.height(0); if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) { obj.height(this.scrollHeight); } }); $('.imjs-msglog').live('click', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.find('.imjs-input').focus(); }); // Create a chatbox when a buddylist item is clicked $('.imjs-friend').live('click', function() { var chatbox = self._createChatbox($(this).data('friend')); if(chatbox.data('tab').data('state') != 'active') chatbox.data('tab').click(); chatbox.find('.imjs-input').focus(); }); // Setup and hide the scrollers $('.imjs-scroll').css('display', 'none'); $('#imjs-scroll-left').live('click', function() { var hiddenTab = $('#imjs-bar li.imjs-tab:visible').slice(-1) .next('#imjs-bar li.imjs-tab:hidden') .filter(function() { return $(this).data('state') != 'closed' }) .not('.imjs-default').slice(-1).css('display', ''); if(hiddenTab.length) { $('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none'); $(this).html(parseInt($(this).html()) - 1); $('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1); } return false; }); $('#imjs-scroll-right').live('click', function() { var hiddenTab = $('#imjs-bar li.imjs-tab:visible').eq(0) .prev('#imjs-bar li.imjs-tab:hidden') .filter(function() { return $(this).data('state') != 'closed' }) .not('.imjs-default').slice(-1).css('display', ''); if(hiddenTab.length) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); $(this).html(parseInt($(this).html()) - 1); $('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1); } return false; }); // Initialize the chatbox hash this.chats = {}; // Try to resume any existing session this.resume(); $(window).resize(function() { self.bar._scrollers(); }); } else return AjaxIM.init(options); }; // We predefine all public functions here... // If they are called before everything (theme, storage engine) has loaded, // then they get put into a "prequeue" and run when everything *does* // finally load. // // This ensures that nothing loads without all of the principal components // being pre-loaded. If that were to occur (without this prequeue), things // would surely break. var prequeue = []; var empty = function() { var func = this; return function() { prequeue.push([func, arguments]) }; }; $.extend(AjaxIM.prototype, { settings: {}, friends: {}, chats: {}, storage: empty.apply('storage'), login: empty.apply('login'), logout: empty.apply('logout'), resume: empty.apply('resume'), form: empty.apply('form'), poll: empty.apply('poll'), incoming: empty.apply('incoming'), send: empty.apply('send'), status: empty.apply('status'), statuses: {}, bar: { initialize: empty.apply('bar.initialize'), activateTab: empty.apply('bar.activateTab'), closeTab: empty.apply('bar.closeTab'), addTab: empty.apply('bar.addTab'), notification: empty.apply('bar.notification'), _scrollers: empty.apply('bar._scrollers') } }); setup = (function() { var self = this; if(!this.storageReady || !this.themeLoaded) return; $(self).trigger('loadComplete'); $(window).resize(); $.extend(AjaxIM.prototype, { // == Cookies == // // The "cookies" functions can be used to set, get, and erase JSON-based cookies. // These functions are primarily used to manage and read the server-set cookie // that handles the user's chat session ID. cookies: { // === {{{AjaxIM.}}}**{{{cookies.set(name, value, days)}}}** === // // Sets a cookie, stringifying the JSON value upon storing it. // // ==== Parameters ==== // * {{{name}}} is the cookie name.\\ // * {{{value}}} is the cookie data that you would like to store.\\ // * {{{days}}} is the number of days that the cookie will be stored for. set: function(name, value, days) { if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "; expires=" + date.toGMTString(); } else var expires = ""; document.cookie = name + "=" + $.compactJSON(value) + expires + "; path=/"; }, // === {{{AjaxIM.}}}**{{{cookies.get(name)}}}** === // // Gets a cookie, decoding the JSON value before returning the data. // // ==== Parameters ==== // * {{{name}}} is the cookie name that you would like to retrieve. get: function(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) { var cval = decodeURIComponent(c.substring(nameEQ.length, c.length)); return $.secureEvalJSON(cval); } } return null; }, // === {{{AjaxIM.}}}**{{{cookies.erase(name)}}}** === // // Deletes a cookie. // // {{{name}}} is the existing cookie that you would like to delete. erase: function(name) { self.cookies.set(name, '', -1); } }, // == Main == // === {{{AjaxIM.}}}**{{{storage()}}}** === // // Retrieves chat session data from whatever storage engine is enabled // (provided that one is enabled at all). If a page reloads, this function // is called to restore the user's chat state (existing conversations, active tab). // This function is called //automatically//, upon initialization of the IM engine. storage: function() { if(!self.storeKey.length) return; try { var chatstore = $.jStore.store(self.storeKey + 'chats') || {}; } catch(e) { $.jStore.remove(self.storeKey + 'chats'); var chatstore = {}; } if(this.chatstore) { $.each(this.chatstore, function(username, convo) { if(username in chatstore) chatstore[username] = $.merge(chatstore[username], self.chatstore[username]); else chatstore[username] = self.chatstore[username]; }); this.chatstore = chatstore; $.jStore.store(self.storeKey + 'chats', chatstore); } else { this.chatstore = chatstore; } $.each(this.chatstore, function(username, convo) { if(!convo.length) return; var chatbox = self._createChatbox(username, true); chatbox.data('lastDateStamp', null).css('display', 'none'); // Remove the automatic date stamp chatbox.find('.imjs-msglog').empty(); // Restore all messages, date stamps, and errors $.each(convo, function() { switch(this[0]) { case 'error': self._addError(chatbox, decodeURIComponent(this[2]), this[3]); break; case 'datestamp': self._addDateStamp(chatbox, this[3]); break; case 'a': case 'b': self._addMessage(this[0], chatbox, this[1], decodeURIComponent(this[2]), this[3]); break; } }); $(self).trigger('chatRestored', [username, chatbox]); }); var activeTab = $.jStore.store(self.storeKey + 'activeTab') || []; if(activeTab.length && (activeTab = activeTab[0]) && activeTab in this.chats) { this.chats[activeTab].data('tab').click(); var msglog = this.chats[activeTab].find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } }, // === {{{AjaxIM.}}}**{{{login(username, password)}}}** === // // Authenticates a user and initializes the IM engine. If the user is // already logged in, [s]he is logged out, the session is cleared, and // the new user is logged in. // // Returns the user's properly formatted username, session ID, and online // friends in JSON, if successful; e.g.:\\ // {{{ {u: 'username', s: 'longsessionid', f: [{u: 'friend', s: 1, g: 'group'}]} }}} // // If unsuccessful, {{{false}}} is returned. // // ==== Parameters ==== // * {{{username}}} is the user's username.\\ // * {{{password}}} is the user's password. This password will be MD5 hashed // before it is sent to the server. login: function(username, password) { if(!username) username = ''; if(!password) password = ''; if(this.username) return true; // Already logged in! // hash password before sending it to the server password = $.md5(password); // authenticate AjaxIM.request( this.actions.login, {'username': username, 'password': password}, function(auth) { if(auth.r == 'logged in') { self.username = ('u' in auth ? auth.u : username); if(self.settings.storageMethod) self.storeKey = [self.storageBrowserKey, self.username, ''].join('-'); var existing = self.cookies.get(self.settings.cookieName); self.storage(); // Begin the session $.each(auth.f, function() { self.friends[this.u] = {status: [this.s, ''], group: this.g}; }); self._session(self.friends); self._storeFriends(); $(self).trigger('loginSuccessful', [auth]); return auth; } else { $(self).trigger('loginError', [auth]); } return false; } ); }, // === {{{AjaxIM.}}}**{{{logout()}}}** === // // Logs the user out and removes his/her session cookie. As well, it // will close all existing chat windows, clear the storage engine, and // remove the IM bar. logout: function() { AjaxIM.request( this.actions.logout, {}, function(done) { if(done) { self.cookies.erase(self.settings.cookieName); self._clearSession(); $(self).trigger('logoutSuccessful'); return true; } else { $(self).trigger('logoutError'); return false; } } ); }, // === {{{AjaxIM.}}}**{{{resume()}}}** === // // Resumes an existing session based on a session ID stored in the // server-set cookie. This function is called //automatically// upon IM // engine (re-)initialization, so the user does not need to re-login // should a session already exist. resume: function() { var session = this.cookies.get(this.settings.cookieName); if(session && session.sid) { this.username = session.user; this.storeKey = [this.storageBrowserKey, this.username, ''].join('-'); var friends = $.jStore.store(this.storeKey + 'friends') || []; if(self.settings.checkResume) { AjaxIM.request( this.actions.resume, {}, function(response) { if(response.r == 'connected') { self._session(friends); self.storage(); } else { var username = this.username; self._clearSession(); $(self).trigger('sessionNotResumed', [username]); } } ); } else { self._session(friends); self.storage(); } } else { $(self).trigger('noSession'); } }, // === //private// {{{AjaxIM.}}}**{{{_session(friends)}}}** === // // Restores session data (username, friends) and begins polling the server. // Called only by {{{AjaxIM.resume()}}}. // // ==== Parameters ==== // * {{{friends}}} is a list of "friend" objects, e.g.:\\ // {{{[{u: 'friend', s: 1, g: 'group'}, ...]}}} // ** {{{u}}} being the friend's username. // ** {{{s}}} being one of the available status codes (see {{{AjaxIM.statuses}}}), depending on the friend's current status. // ** {{{g}}} being the group that the friend is in. _session: function(friends) { $('#imjs-friends-panel .imjs-header span').html(this.username); $('#imjs-friends').removeClass('imjs-not-connected'); $.each(friends, function(friend, info) { self.addFriend.apply(self, [friend, info.status, info.group]); }); self._storeFriends(); $(self).trigger('sessionResumed', [this.username]); setTimeout(function() { self.poll(); }, 0); }, // === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** === // // Clears all session data from the last known user. _clearSession: function() { if(self.settings.storageMethod && self.storageReady) { $.jStore.remove(self.storeKey + 'chats'); $.jStore.remove(self.storeKey + 'friends'); $.jStore.remove(self.storeKey + 'activeTab'); } self.chats = {}; $('.imjs-tab').not('.imjs-tab.imjs-default').remove(); self.cookies.erase('ajaxim_session'); delete self.username; }, // === {{{AjaxIM.}}}**{{{form(element)}}}** === // // Loads a login and registration form into the specified element // or, if no element is supplied, to the location on the page from // which this function was called. form: function(element) { $(element).load(this.settings.theme + '/theme.html #imjs-lr', function() { $('#imjs-lr .error').hide(); if(self.username) { $('#imjs-register, #imjs-login fieldset').hide(); $('#imjs-logged-in') .show() .html($('#imjs-logged-in').html().replace('{username}', self.username)); } else { $('#imjs-logged-in').hide(); } // Handle logout success $(self).bind('logoutSuccessful', function() { $('#imjs-login fieldset').slideDown(); $('#imjs-register').slideDown(); $('#imjs-logged-in') .html($('#imjs-logged-in strong').html('{username}')) .slideUp(); }); $('#imjs-logged-in a').click(function() { self.logout(); return false; }); // Handle login error or success $(self).bind('loginError', function() { $('#imjs-login .error').html(AjaxIM.i18n.authInvalid).slideDown('fast'); $('#imjs-login input') .addClass('imjs-lr-error') .blur(function() { $(this).removeClass('imjs-lr-error'); }); }).bind('loginSuccessful', function() { $('#imjs-login fieldset').slideUp(); $('#imjs-register').slideUp(); $('#imjs-logged-in') .html($('#imjs-logged-in').html().replace('{username}', self.username)) .slideDown(); }); var login = function() { self.login($('#imjs-login-username').val(), $('#imjs-login-password').val()); return false; }; $('#imjs-login').submit(login); $('#imjs-login-submit').click(login); var regIssues = false; var regError = function(error, fields) { $('#imjs-register .error') .append(AjaxIM.i18n['register' + error] + ' ') .slideDown(); $(fields) .addClass('imjs-lr-error') .blur(function() { $(this).removeClass('imjs-lr-error'); }); regIssues = true; }; var register = function() { $('#imjs-register .error').empty(); regIssues = false; var username = $('#imjs-register-username').val(); var password = $('#imjs-register-password').val(); if(password.length < 4) regError('PasswordLength', '#imjs-register-password'); if(password != $('#imjs-register-cpassword').val()) regError('PasswordMatch', '#imjs-register-password, #imjs-register-cpassword'); if(username.length <= 2 || !$('#imjs-register-username').val().match(/^[A-Za-z0-9_.]+$/)) regError('UsernameLength', '#imjs-register-username'); if(!regIssues) { AjaxIM.request( self.actions.register, {username: username, password: password}, function(response) { if(response.r == 'registered') { self.login(username, password); } else if(response.r == 'error') { switch(response.e) { case 'unknown': regError('Unknown', ''); break; case 'invalid password': regError('PasswordLength', '#imjs-register-password'); break; case 'invalid username': regError('UsernameLength', '#imjs-register-username'); break; case 'username taken': regError('UsernameTaken', '#imjs-register-username'); break; } } }, function(error) { regError('Unknown', ''); } ); } return false; }; $('#imjs-register').submit(register); $('#imjs-register-submit').click(register); }); }, // === {{{AjaxIM.}}}**{{{poll()}}}** === // // Queries the server for new messages. If a 'long' or 'short' poll // type is used, jQuery's {{{$.post}}} or {{{$.getJSON}}} will be // used. If 'comet' is used, the server connection will be deferred // to the comet set of functions. poll: function() { if(/^(short|long)$/.test(this.settings.pollType)) { AjaxIM.request( this.actions.poll, {}, function(response) { if(!response['e']) { if(response.length) self._parseMessages(response); setTimeout(function() { self.poll(); }, 0); } else { switch(response.e) { case 'no session found': self._notConnected(); break; } $(self).trigger('pollFailed', [response.e]); } }, function(error) { self._notConnected(); $(self).trigger('pollFailed', ['not connected']); // try reconnecting? } ); } else if(this.settings.pollType == 'comet') { this.comet.connect(); } }, // === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** === // // Handles an incoming message array:\\ // {{{[{t: 'type', s: 'sender', r: 'recipient', m: 'message'}, ...]}}} // // * {{{t}}} (message type) is one of: // ** {{{m}}} — a standard message // ** {{{s}}} — a user's status update // ** {{{b}}} — a broadcasted message (sent to many users simultaneously) // * {{{s}}} is the sender of the message. // * {{{r}}} is the intended recipient of the message. Most of the time, this will // simply be the logged in user, however, a broadcasted message may not specify // a recipient or may specify a different recipient. Also provides future // compatability for chatrooms. // * {{{m}}} is the actual message. For something such as a status update, this can // be a JSON object or parsable string; e.g. {{{"2:I'm away."}}} // // ==== Parameters ==== // * {{{messages}}} is the message array _parseMessages: function(messages) { if($.isArray(messages)) { $.each(messages, function() { $(self).trigger('parseMessage', [this]); switch(this.t) { case 'm': self.incoming(this.s, this.m); break; case 's': var status = this.m.split(':'); if(this['g']) self.addFriend(this.s, status, this.g); self._friendUpdate(this.s, status[0], status.slice(1).join(':')); self._storeFriends(); break; case 'b': break; default: break; } }); } }, // === {{{AjaxIM.}}}**{{{incoming(from, message)}}}** === // // Handles a new message from another user. If a chatbox for that // user does not yet exist, one is created. If it does exist, but // is minimized, the user is notified but the chatbox is not brought // to the front. This function also stores the message, if a storage // method is set. // // ==== Parameters ==== // * {{{from}}} is the username of the sender. // * {{{message}}} is the body. incoming: function(from, message) { // check if IM exists, otherwise create new window // TODO: If friend is not on the buddylist, // should add them to a temp list? var chatbox = this._createChatbox(from); if(!$('#imjs-bar .imjs-selected').length) { chatbox.data('tab').click(); } else if(chatbox.data('tab').data('state') != 'active') { this.bar.notification(chatbox.data('tab')); } var time = this._addMessage('b', chatbox, from, message); this._storeMessage('b', chatbox, from, message, time); }, // === {{{AjaxIM.}}}**{{{addFriend(username, group)}}}** === // // Inserts a new friend into the friends list. If the group specified // doesn't exist, it is created. If the friend is already in this group, // they aren't added again, however, the friend item is returned. // // ==== Parameters ==== // * {{{username}}} is the username of the new friend. // * {{{status}}} is the current status of the friend. // * {{{group}}} is the user group to which the friend should be added. addFriend: function(username, status, group) { var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status[0] == val) { status_name = key; return false; } }); var group_id = 'imjs-group-' + $.md5(group); if(!(group_item = $('#' + group_id)).length) { var group_item = $('.imjs-friend-group.imjs-default').clone() .removeClass('imjs-default') .attr('id', group_id) .data('group', group) .appendTo('#imjs-friends-list'); var group_header = group_item.find('.imjs-friend-group-header'); group_header.html(group_header.html().replace('{group}', group)); } var user_id = 'imjs-friend-' + $.md5(username + group); if(!$('#' + user_id).length) { var user_item = group_item.find('ul li.imjs-default').clone() .removeClass('imjs-default') .addClass('imjs-' + status_name) .attr('id', user_id) .data('friend', username) .appendTo(group_item.find('ul')); if(status[0] == 0) user_item.hide(); user_item.html(user_item.html().replace('{username}', username)); } this.friends[username] = {'status': status, group: group}; this._updateFriendCount(); return this.friends[username]; }, // === //private// {{{AjaxIM.}}}**{{{_updateFriendCount()}}}** === // // Counts the number of online friends and updates the friends count // in the friend tab. _updateFriendCount: function() { var friendsLength = 0; for(var f in this.friends) { if(this.friends[f].status[0] != 0) friendsLength++; } $('#imjs-friends .imjs-tab-text span span').html(friendsLength); }, // === //private// {{{AjaxIM.}}}**{{{_storeFriends()}}}** === // // If a storage method is enabled, the current state of the // user's friends list is stored. _storeFriends: function() { if(this.settings.storageMethod && this.storageReady) $.jStore.store(this.storeKey + 'friends', this.friends); }, // === //private// {{{AjaxIM.}}}**{{{_createChatbox(username)}}}** === // // Builds a chatbox based on the default chatbox HTML and CSS defined // in the current theme. Should a chatbox for this user already exist, // a new one is not created. Instead, it is either given focus (should // no other windows already have focus), or a notification is issued. // // As well, if the chatbox does not exist, an associated tab will be // created. // // ==== Parameters ==== // * {{{username}}} is the name of the user for whom the chatbox is intended // for. // * {{{no_stamp}}} sets whther or not to add a date stamp to the chatbox // upon creation. // // //Note:// New chatboxes are given an automatically generated ID in the // format of {{{#imjs-[md5 of username]}}}. _createChatbox: function(username, no_stamp) { var chatbox_id = 'imjs-' + $.md5(username); if(!(chatbox = $('#' + chatbox_id)).size()) { // add a tab var tab = this.bar.addTab(username, '#' + chatbox_id); var chatbox = tab.find('.imjs-chatbox'); chatbox.attr('id', chatbox_id); chatbox.data('tab', tab); // remove default items from the message log var message_log = chatbox.find('.imjs-msglog').empty(); // setup the chatbox header var cb_header = chatbox.find('.imjs-header'); cb_header.html(cb_header.html().replace('{username}', username)); if(!no_stamp) { // add a date stamp var time = this._addDateStamp(chatbox); this._storeNonMessage('datestamp', username, null, time); } // associate the username with the object and vice-versa this.chats[username] = chatbox; chatbox.data('username', username); // did this chatbox fall down? this.bar._scrollers(); if(username in this.friends) { status = this.friends[username].status; var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status[0] == val) { status_name = key; return false; } }); tab.addClass('imjs-' + status_name); } // store inputbox height //var input = chatbox.find('.imjs-input'); //input.data('height', input.height()); } else if(chatbox.data('tab').data('state') == 'closed') { chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old'); var tab = chatbox.data('tab'); if(tab.css('display') == 'none') tab.css('display', '').removeClass('imjs-selected') .appendTo('#imjs-bar'); if(!no_stamp) { // possibly add a date stamp var time = this._addDateStamp(chatbox); this._storeNonMessage('datestamp', username, null, time); } if(!$('#imjs-bar .imjs-selected').length) { tab.click(); } else { this.bar.notification(tab); } } return chatbox; }, // === //private// {{{AjaxIM.}}}**{{{_addDateStamp(chatbox)}}}** // // // Adds a date/time notifier to a chatbox. These are generally // inserted upon creation of a chatbox, or upon the date changing // since the last time a date stamp was added. If a date stamp for // the current date already exists, a new one will not be added. // // ==== Parameters ==== // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{time}}} is the date/time the date stamp will show. It is specified // in milliseconds since the Unix Epoch. This is //only// defined when // date stamps are being restored from storage; if not specified, the // current computer time will be used. _addDateStamp: function(chatbox, time) { var message_log = $(chatbox).find('.imjs-msglog'); if(!time) time = (new Date()).getTime(); var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); var date_stamp_time = date_stamp.find('.imjs-msg-time'); if(date_stamp_time.length) date_stamp_time.html(AjaxIM.dateFormat(time, date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date'); var formatted_date = AjaxIM.dateFormat(time, date_stamp_date.html()); if(chatbox.data('lastDateStamp') != formatted_date) { if(date_stamp_date.length) date_stamp_date.html(AjaxIM.dateFormat(time, date_stamp_date.html())); chatbox.data('lastDateStamp', formatted_date); date_stamp.appendTo(message_log); } else { //$('
').appendTo(message_log); } return time; }, // === //private// {{{AjaxIM.}}}**{{{_addError(chatbox, error)}}}** // // // Adds an error to a chatbox. These are generally inserted after // a user sends a message unsuccessfully. If an error message // was already added, another one will be added anyway. // // ==== Parameters ==== // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{error}}} is the error message string. // * {{{time}}} is the date/time the error occurred. It is specified in // milliseconds since the Unix Epoch. This is //only// defined when // errors are being restored from storage; if not specified, the current // computer time will be used. _addError: function(chatbox, error, time) { var message_log = $(chatbox).find('.imjs-msglog'); var error_item = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-error').clone(); var error_item_time = error_item.find('.imjs-msg-time'); if(error_item_time.length) { if(!time) time = (new Date()).getTime(); error_item_time.html(AjaxIM.dateFormat(time, error_item_time.html())); } error_item.find('.imjs-error-error').html(error); error_item.appendTo(message_log); message_log[0].scrollTop = message_log[0].scrollHeight; }, // === //private// {{{AjaxIM.}}}**{{{_addMessage(ab, chatbox, username, message, time)}}}** // // // Adds a message to a chatbox. Depending on the {{{ab}}} value, // the color of the username may change as a way of visually // identifying users (however, this depends on the theme's CSS). // A timestamp is added to the message, and the chatbox is scrolled // to the bottom, such that the new message is visible. // // Messages will be automatically tag-escaped, so as to prevent // any potential cross-site scripting problems. Additionally, // URLs will be automatically linked. // // ==== Parameters ==== // * {{{ab}}} refers to whether the user is "a" or "b" in a conversation. // For the general case, "you" are "a" and "they" are "b". // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{username}}} is the username of the user who sent the message. // * {{{time}}} is the time the message was sent in milliseconds since // the Unix Epoch. This is //only// defined when messages are being // restored from storage. For new messages, the current computer // time is automatically used. _addMessage: function(ab, chatbox, username, message, time) { var last_message = chatbox.find('.imjs-msglog > *:last-child'); if(last_message.hasClass('imjs-msg-' + ab)) { // Last message was from the same person, so let's just add another imjs-msg-*-msg var message_container = (last_message.hasClass('imjs-msg-' + ab + '-container') ? last_message : last_message.find('.imjs-msg-' + ab + '-container')); var single_message = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-msg-' + ab + '-msg') .clone().appendTo(message_container); single_message.html(single_message.html().replace('{username}', username)); } else if(!last_message.length || !last_message.hasClass('imjs-msg-' + ab)) { var message_group = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msg-' + ab) .clone().appendTo(chatbox.find('.imjs-msglog')); message_group.html(message_group.html().replace('{username}', username)); var single_message = message_group.find('.imjs-msg-' + ab + '-msg'); } // clean up the message message = message.replace(//g, '>') .replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1$2$3'); // autolink URLs message = message.replace( new RegExp('([A-Za-z][A-Za-z0-9+.-]{1,120}:[A-Za-z0-9/]' + '(([A-Za-z0-9$_.+!*,;/?:@&~=-])|%[A-Fa-f0-9]{2}){1,333}' + '(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*,;/?:@&~=%-]{0,1000}))?)', 'g'), '$1'); // insert the message single_message.html(single_message.html().replace('{message}', message)); // set the message time var msgtime = single_message.find('.imjs-msg-time'); if(!time) time = new Date(); if(typeof time != 'string') time = AjaxIM.dateFormat(time, msgtime.html()); msgtime.html(time); var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; return time; }, // === //private// {{{AjaxIM.}}}**{{{_storeNonMessage(type, username, data, time)}}}** === // // **Redundant?**\\ // Similar to {{{AjaxIM._storeMessage}}}, but stores items that aren't messages, // such as datestamps and errors. // // ==== Parameters ==== // * {{{type}}} is the type of non-message (error, datestamp). // * {{{username}}} is the username of the user being chatted with. // * {{{data}}} is the (optional) data of the non-message to be stored. // * {{{time}}} is the time of the message. _storeNonMessage: function(type, username, data, time) { // If storage is on & ready, store the non-message if(this.settings.storageMethod) { if(!this.chatstore) this.chatstore = {}; if(!(username in this.chatstore)) this.chatstore[username] = []; // If the chat store gets too long, it becomes slow to load. if(this.chatstore[username].length > 300) this.chatstore[username].shift(); // For some reason, if we don't encode and decode the message, it *will* break // (at least) the Flash storage engine's retrieval. Gah! this.chatstore[username].push( [type, username, encodeURIComponent(data), time]); if(this.storageReady) $.jStore.store(self.storeKey + 'chats', this.chatstore); } }, // === //private// {{{AjaxIM.}}}**{{{_storeMessage(ab, chatbox, username, message, time)}}}** === // // Taking the same arguments as {{{AjaxIM._addMessage}}}, {{{_storeMessage}}} pushes a message // into the storage hash, if storage is enabled. // // Messages are added to a message array, by username. The entire chat hash is stored as // a {{{'chats'}}} object in whatever storage engine is enabled. _storeMessage: function(ab, chatbox, username, message, time) { // If storage is on & ready, store the message if(this.settings.storageMethod) { if(!this.chatstore) this.chatstore = {}; message = message.replace(//g, '>'); if(!(username in this.chatstore)) { this.chatstore[username] = []; } else if(this.chatstore[username].length > 300) { // If the chat store gets too long, it becomes slow to load. this.chatstore[username].shift(); } // For some reason, if we don't encode and decode the message, it *will* break // (at least) the Flash storage engine's retrieval. Gah! this.chatstore[chatbox.data('username')].push( [ab, username, encodeURIComponent(message), time]); if(this.storageReady) $.jStore.store(this.storeKey + 'chats', this.chatstore); } }, // === //private// {{{AjaxIM.}}}**{{{_friendUpdate(friend, status, statusMessage)}}}** === // // Called when a friend's status is updated. This function will update all locations // where a status icon is displayed (chat tab, friends list), as well as insert // a notification, should a chatbox be open. // // ==== Parameters ==== // * {{{friend}}} is the username of the friend. // * {{{status}}} is the new status code. See {{{AjaxIM.statuses}}} for a list of available // codes. //Note: If an invalid status is specified, no action will be taken.// // * {{{statusMessage}}} is a message that was, optionally, specified by the user. It will be // used should "you" send the user an IM while they are away, or if their status is viewed // in another way (such as via the friends list [**not yet implemented**]). _friendUpdate: function(friend, status, statusMessage) { // add friend to buddylist, update their status, etc. var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status == val) { status_name = key; return false; } }); if(this.chats[friend]) { var tab = this.chats[friend].data('tab'); var tab_class = 'imjs-tab'; if(tab.data('state') == 'active') tab_class += ' imjs-selected'; tab_class += ' imjs-' + status_name; tab.attr('class', tab_class); // display the status in the chatbox var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); var date_stamp_time = date_stamp.find('.imjs-msg-time'); if(date_stamp_time.length) date_stamp_time.html(AjaxIM.dateFormat(date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date').html( AjaxIM.i18n[ 'chat' + status_name[0].toUpperCase() + status_name.slice(1) ].replace(/%s/g, friend)); var msglog = this.chats[friend].find('.imjs-msglog'); date_stamp.appendTo(msglog); msglog[0].scrollTop = msglog[0].scrollHeight; } if(this.friends[friend]) { var friend_id = 'imjs-friend-' + $.md5(friend + this.friends[friend].group); $('#' + friend_id).attr('class', 'imjs-friend imjs-' + status_name); if(status == 0) { $('#' + friend_id + ':visible').slideUp(); $('#' + friend_id + ':hidden').hide(); } else if(!$('#' + friend_id + ':visible').length) { $('#' + friend_id).slideDown(); } this.friends[friend].status = [status, statusMessage]; this._updateFriendCount(); } }, // === //private// {{{AjaxIM.}}}**{{{_notConnected()}}}** === // // Puts the user into a visible state of disconnection. Sets the // friends list to "not connected" and empties it; disallows new messages // to be sent. _notConnected: function() { $('#imjs-friends').addClass('imjs-not-connected').unbind('click', this.activateTab); }, // === {{{AjaxIM.}}}**{{{send(to, message)}}}** === // // Sends a message to another user. The message will be added // to the chatbox before it is actually sent, however, if an // error occurs during sending, that will be indicated immediately // afterward. // // After sending the message, one of three status codes should be // returned as a JSON object, e.g. {{{{r: 'code'}}}}: // * {{{ok}}} — Message was sent successfully. // * {{{offline}}} — The user is offline or unavailable to // receive messages. // * {{{error}}} — a problem occurred, unrelated to the user // being unavailable. // // ==== Parameters ==== // * {{{to}}} is the username of the recipient. // * {{{message}}} is the content to be sent. send: function(to, message) { if(!message) return; if(this.chats[to]) { // REMOVE ME? // possibly add a datestamp var time = self._addDateStamp(this.chats[to]); this._storeNonMessage('datestamp', to, null, time); time = this._addMessage('a', this.chats[to], this.username, message); this._storeMessage('a', this.chats[to], this.username, message, time); } $(self).trigger('sendingMessage', [to, message]); AjaxIM.request( this.actions.send, {'to': to, 'message': message}, function(result) { switch(result.r) { case 'ok': $(self).trigger('sendMessageSuccessful', [to, message]); break; case 'offline': $(self).trigger('sendMessageFailed', ['offline', to, message]); break; case 'error': default: if(result.e == 'no session found') { self._notConnected(); self._addError(self.chats[to], AjaxIM.i18n.notConnected); self._storeNonMessage('error', to, AjaxIM.i18n.notConnected, (new Date()).getTime()); } $(self).trigger('sendMessageFailed', [result.e, to, message]); break; } }, function(error) { self._notConnected(); self._addError(self.chats[to], AjaxIM.i18n.notConnected); self._storeNonMessage('error', to, AjaxIM.i18n.notConnected, (new Date()).getTime()); $(self).trigger('sendMessageFailed', ['not connected', to, message]); } ); }, // === {{{AjaxIM.}}}**{{{status(s, message)}}}** === // // Sets the user's status and status message. It is possible to not // set a status message by setting it to an empty string. The status // will be sent to the server, where upon the server will broadcast // the update to all individuals with "you" on their friends list. // // ==== Parameters ==== // * {{{s}}} is the status code, as defined by {{{AjaxIM.statuses}}}. // * {{{message}}} is the custom status message. status: function(s, message) { // update status icon(s) if(!this.statuses[s]) return; $('#imjs-friends').attr('class', 'imjs-' + s); $(self).trigger('changingStatus', [s, message]); AjaxIM.request( this.actions.status, {'status': this.statuses[s], 'message': message}, function(result) { switch(result.r) { case 'ok': $(self).trigger('changeStatusSuccessful', [s, message]); break; case 'error': default: $(self).trigger('changeStatusFailed', [result.e, s, message]); break; } }, function(error) { $(self).trigger('changeStatusFailed', ['not connected', s, message]); } ); }, // === {{{AjaxIM.}}}**{{{statuses}}}** === // // These are the available status codes and their associated identities: // * {{{offline}}} (0) — Only used when signing out/when another // user has signed out, as once this status is set, the user is removed // from the server and friends will be unable to contact the user. // * {{{available}}} (1) — The user is online and ready to be messaged. // * {{{away}}} (2) — The user is online but is not available. Others // may still contact this user, however, the user may not respond. Anyone // contacting an away user will receive a notice stating that the user is away, // and (if one is set) their custom status message. // * {{{invisible}}} (3; **not yet implemented**) — The user is online, // but other users are made unaware, and the user will be represented // as being offline. It is still possible to contact this user, and for this // user to contact others; no status message or notice will be sent to others // messaging this user. statuses: {offline: 0, available: 1, away: 2, invisible: 3}, // == Footer bar == // // The footer bar is the bar that sits at the bottom of the page, in a fixed // position. It contains a tab for the friends list, and tabs for any open // chat boxes. It is also possible to add custom tabs for other functionality. bar: { // === {{{AjaxIM.}}}**{{{bar.initialize()}}}** === // // Setup the footer bar and enable tab actions. This function // uses {{{jQuery.live}}} to set hooks on any bar tabs created // in the future. initialize: function() { // Set up your standard tab actions $('.imjs-tab') .live('click', this.activateTab); $('.imjs-tab .imjs-close') .live('click', this.closeTab); // Set up the friends list actions var self = this; $(document).click(function(e) { if(e.target.id == 'imjs-friends' || $(e.target).parents('#imjs-friends').length) { return; } if($('#imjs-friends').data('state') == 'active') self.activateTab.call($('#imjs-friends')); }); $('#imjs-friends') .data('state', 'minimized') .click(function(e) { if(!$(this).hasClass('imjs-not-connected') && e.target.id != 'imjs-friends-panel' && !$(e.target).parents('#imjs-friends-panel').length) self.activateTab.call(this); }) .mouseenter(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip').css('display', 'block'); $('.imjs-tooltip p').html(AjaxIM.i18n.notConnectedTip); var tip_left = $(this).offset().left - $('.imjs-tooltip').outerWidth() + ($(this).outerWidth() / 2); var tip_top = $(this).offset().top - $('.imjs-tooltip').outerHeight(true); $('.imjs-tooltip').css({ left: tip_left, top: tip_top }); } }) .mouseleave(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip').css('display', ''); } }); $('#imjs-friends-panel') .data('tab', $('#imjs-friends')) .css('display', 'none'); }, // === {{{AjaxIM.}}}**{{{bar.activateTab()}}}** === // // Activate a tab by setting it to the 'active' state and // showing any related chatbox. If a chatbox is available // for this tab, also focus the input box. // // //Note:// {{{this}}}, here, refers to the tab DOM element. activateTab: function() { var chatbox = $(this).find('.imjs-chatbox') || false; if($(this).data('state') != 'active') { if($(this).attr('id') != 'imjs-friends') { $('#imjs-bar > li') .not($(this)) .not('#imjs-friends') .removeClass('imjs-selected') .each(function() { if($(this).data('state') != 'closed') { $(this).data('state', 'minimized'); var chatbox = $(this).find('.imjs-chatbox'); if(chatbox.length) chatbox.css('display', 'none'); } }); } if(chatbox && chatbox.css('display') == 'none') chatbox.css('display', ''); // set the tab to active... var tab = $(this).addClass('imjs-selected').data('state', 'active'); // ...and hide and reset the notification icon tab.find('.imjs-notification').css('display', 'none') .data('count', 0); if(self.settings.storageMethod && self.storageReady && chatbox && (username = chatbox.data('username'))) { $.jStore.store(self.storeKey + 'activeTab', [username]); } $(self).trigger('tabToggled', ['activated', tab]); } else { var tab = $(this).removeClass('imjs-selected').data('state', 'minimized'); if(chatbox && chatbox.css('display') != 'none') chatbox.css('display', 'none'); if(self.settings.storageMethod && self.storageReady) { $.jStore.store(self.storeKey + 'activeTab', ['*']); } $(self).trigger('tabToggled', ['minimized', tab]); } if(chatbox) { if(!(input = chatbox.find('.imjs-input')).data('height')) { // store the height for resizing later input.data('height', input.height()); } try { var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } catch(e) {} try { chatbox.find('.imjs-input').focus(); } catch(e) {} } }, // === {{{AjaxIM.}}}**{{{bar.closeTab()}}}** === // // Close a tab and hide any related chatbox, such that // the chatbox can not be reopened without reinitializing // the tab. // // //Note:// {{{this}}}, here, refers to the tab DOM element. closeTab: function() { var tab = $(this).parents('.imjs-tab'); tab.css('display', 'none').data('state', 'closed'); if(self.settings.storageMethod && self.storageReady) { delete self.chatstore[tab.find('.imjs-chatbox').data('username')]; if(self.storageReady) $.jStore.store(self.storeKey + 'chats', self.chatstore); } $(self).trigger('tabToggled', ['closed', tab]); self.bar._scrollers(); return false; }, // === {{{AjaxIM.}}}**{{{bar.addTab(label, action, closable)}}}** === // // Adds a tab to the tab bar, with the label {{{label}}}. When // clicked, it will call a callback function, {{{action}}}. If // {{{action}}} is a string, it is assumed that the string is // referring to a chatbox ID. // // ==== Parameters ==== // * {{{label}}} is the text that will be displayed on the tab.\\ // * {{{action}}} is the callback function, if it is a non-chatbox // tab, or a string if it //is// a chatbox tab.\\ // * {{{closable}}} is a boolean value that determines whether or not // it is possible for a user to close this tab. // // //Note:// New tabs are given an automatically generated ID // in the format of {{{#imjs-tab-[md5 of label]}}}. addTab: function(label, action, closable) { var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-right'); tab.removeClass('imjs-default') .attr('id', 'imjs-tab-' + $.md5(label)) .html(tab.html().replace('{label}', label)) .data('state', 'minimized'); var notification = tab.find('.imjs-notification'); notification.css('display', 'none') .data('count', 0) .data('default-text', notification.html()) .html(notification.html().replace('{count}', '0')); if(closable === false) tab.find('.imjs-close').eq(0).remove(); if(typeof action == 'string') { //tab.data('chatbox', action); } else { tab.find('.imjs-chatbox').remove(); tab.click(action); } return tab; }, // === {{{AjaxIM.}}}**{{{bar.notification(tab)}}}** === // // Displays a notification on a tab. Generally, this is called when // a tab is minimized to let the user know that there is an update // for them. The way the notification is displayed depends on the // theme CSS. // // ==== Parameters ==== // * {{{tab}}} is the jQuery-selected tab DOM element. notification: function(tab) { var notify = tab.find('.imjs-notification'); var notify_count = notify.data('count') + 1; notify.data('count', notify_count) .html(notify.data('default-text').replace('{count}', notify_count)) .css('display', ''); }, // === //private// {{{AjaxIM.}}}**{{{bar._scrollers()}}}** === // // Document me! _scrollers: function() { var needScrollers = false; $('.imjs-tab').filter(function() { return $(this).data('state') != 'closed' }).css('display', ''); $.each(self.chats, function(username, chatbox) { var tab = chatbox.data('tab'); if(tab.data('state') == 'closed') return true; if(tab.position().top > $('#imjs-bar').height()) { $('.imjs-scroll').css('display', ''); tab.css('display', 'none'); needScrollers = true; } else { tab.css('display', ''); } }); if(!needScrollers) { $('.imjs-scroll').css('display', 'none'); } if($('#imjs-scroll-left').css('display') != 'none' && $('#imjs-scroll-left').position().top > $('#imjs-bar').height()) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); } var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').slice(-1) .nextAll('#imjs-bar li.imjs-tab:hidden') .not('.imjs-default') .filter(function() { return $(this).data('state') != 'closed' }).length; var hiddenRight = $('#imjs-bar li.imjs-tab:visible').eq(0) .prevAll('#imjs-bar li.imjs-tab:hidden') .not('.imjs-default') .filter(function() { return $(this).data('state') != 'closed' }).length; $('#imjs-scroll-left').html(hiddenLeft); $('#imjs-scroll-right').html(hiddenRight); } }, // == Comet == // // Comet, or HTTP streaming, holds open a connection between the client and // the server indefinitely. As the server receives new messages or events, // they are passed down to the client in a {{{<script>}}} // tag which calls the {{{AjaxIM.incoming()}}} function. The connection is // opened using either an {{{iframe}}} (in Opera or Internet Explorer) or // an {{{XMLHTTPRequest}}} object. Due to the extra flexibility necessary // with the {{{XMLHTTPRequest}}} object, jQuery's {{{$.ajax}}} is not used. comet: { type: '', obj: null, onbeforeunload: null, onreadystatechange: null, iframe: function() { if(/loaded|complete/i.test(this.obj.readyState)) throw new Error("IM server not available!"); }, // === {{{AjaxIM.}}}**{{{comet.connect()}}}** === // // Creates and initializes the object and connection between the server // and the client. For Internet Explorer and Opera, we use an // {{{<iframe>}}} element; for all other browsers, we create an // {{{XMLHTTPRequest}}} object. The server connects to the URI defined // as the "poll" action. This function is called automatically, when // the IM engine is initialized and the {{{AjaxIM.poll()}}} function // is called. connect: function() { var self = _instance; if($.browser.opera || $.browser.msie) { var iframe = $(''); with(iframe) { css({ position: 'absolute', visibility: 'visible', display: 'block', left: '-10000px', top: '-10000px', width: '1px', height: '1px' }); attr('src', self.actions.poll[1]); appendTo('body'); bind('readystatechange', self.comet.onreadystatechange = function() { self.comet.iframe() }); bind('beforeunload', self.comet.onbeforeunload = function() { self.comet.disconnect() }); } self.comet.type = 'iframe'; self.comet.obj = iframe; } else { var xhr = new XMLHttpRequest; var length = 1029; var code = /^\s*]*>parent\.(.+);<\/script>$/; xhr.open('get', self.actions.poll[1], true); xhr.onreadystatechange = function(){ if(xhr.readyState > 2) { if(xhr.status == 200) { responseText = xhr.responseText.substring(length); length = xhr.responseText.length; if(responseText != ' ') eval(responseText.replace(code, "$1")); } // We need an "else" here. If the state changes to // "loaded", the user needs to know they're // disconnected. } }; self.comet.type = 'xhr'; self.comet.obj = xhr; addEventListener('beforeunload', self.comet.beforeunload = function() { self.comet.disconnect(); }, false); setTimeout(function() { xhr.send(null) }, 10); } }, // === {{{AjaxIM.}}}**{{{comet.disconnect()}}}** === // // Disconnect from the server and destroy the connection object. This // function is called before the page unloads, so that we plug up and // potential leaks and free memory. disconnect: function() { var self = _instance.comet; if(!this.type || !this.obj) return; if(this.type == 'iframe') { detachEvent('onreadystatechange', this.onreadystatechange); detachEvent('onbeforeunload', this.onbeforeunload); this.obj.src = '.'; $(this.obj).remove(); } else { removeEventListener('beforeunload', this.onbeforeunload, false); this.obj.onreadystatechange = function(){}; this.obj.abort(); } delete this.obj; } } }) self.bar.initialize(); if(prequeue.length) { $.each(prequeue, function() { var func = this[0].split('.'); if(func.length > 1) self[func[0]][func[1]].apply(self, this[1]); else self[func[0]].apply(self, this[1]); }); } }); // == Static functions and variables == // // The following functions and variables are available outside of an initialized // {{{AjaxIM}}} object. // === {{{AjaxIM.}}}**{{{client}}}** === // // Once {{{AjaxIM.init()}}} is called, this will be set to the active AjaxIM // object. Only one AjaxIM object instance can exist at a time. This variable // can and should be accessed directly. AjaxIM.client = null; // === {{{AjaxIM.}}}**{{{init(options, actions)}}}** === // // Initialize the AjaxIM client object and engine. Here, you can define your // options and actions as outlined at the top of this documentation. // // ==== Parameters ==== // * {{{options}}} is the hash of custom settings to initialize Ajax IM with. // * {{{actions}}} is the hash of any custom action URLs. AjaxIM.init = function(options, actions) { if(!_instance) { _instance = new AjaxIM(options, actions); AjaxIM.client = _instance; } return _instance; } // === {{{AjaxIM.}}}**{{{request(url, data, successFunc, failureFunc)}}}** === // // Wrapper around {{{$.jsonp}}}, the JSON-P library for jQuery, and {{{$.ajax}}}, // jQuery's ajax library. Allows either function to be called, automatically, // depending on the request's URL array (see {{{AjaxIM.actions}}}). // // ==== Parameters ==== // {{{url}}} is the URL of the request. // {{{data}}} are any arguments that go along with the request. // {{{success}}} is a callback function called when a request has completed // without issue. // {{{_ignore_}}} is simply to provide compatability with {{{$.post}}}. // {{{failure}}} is a callback function called when a request hasn't not // completed successfully. AjaxIM.request = function(url, data, successFunc, failureFunc) { if(typeof failureFunc != 'function'); failureFunc = function(){}; $[url[0]]({ 'url': url[1], 'data': data, dataType: (url[0] == 'ajax' ? 'json' : 'jsonp'), type: 'POST', cache: false, timeout: 60000, callback: 'jsonp' + (new Date()).getTime(), success: function(json, textStatus) { successFunc(json); }, error: function(xOptions, error) { failureFunc(error); } }); // This prevents Firefox from spinning indefinitely // while it waits for a response. Why? Fuck if I know. if(url[0] == 'jsonp' && $.browser.mozilla) { $.jsonp({ 'url': 'about:', timeout: 0 }); } }; // === {{{AjaxIM.}}}**{{{incoming(data)}}}** === // // Never call this directly. It is used as a connecting function between // client and server for Comet. // // //Note:// There are two {{{AjaxIM.incoming()}}} functions. This one is a // static function called outside of the initialized AjaxIM object; the other // is only called within the initalized AjaxIM object. AjaxIM.incoming = function(data) { if(!_instance) return false; if(data.length) _instance._parseMessages(data); } // === {{{AjaxIM.}}}**{{{loaded}}}** === // // If Ajax IM has been loaded with the im.load.js file, this function will be // called when the library is finally loaded and ready for use. Similar to // jQuery's $(document).ready(), but for Ajax IM. AjaxIM.loaded = function() { if(typeof AjaxIMLoadedFunction == 'function') { AjaxIMLoadedFunction(); delete AjaxIMLoadedFunction; // clean up the global namespace } }; // === {{{AjaxIM.}}}**{{{dateFormat([date,] [mask,] utc)}}}** === // // Date Format 1.2.3\\ // © 2007-2009 Steven Levithan ([[http://blog.stevenlevithan.com/archives/date-time-format|stevenlevithan.com]])\\ // MIT license // // Includes enhancements by Scott Trenda // and Kris Kowal ([[http://cixar.com/~kris.kowal/|cixar.com/~kris.kowal/]]) // // Accepts a date, a mask, or a date and a mask and returns a formatted version // of the given date. // // ==== Parameters ==== // * {{{date}}} is a {{{Date()}}} object. If not specified, the date defaults to the // current date/time. // * {{{mask}}} is a string that defines the formatting of the date. Formatting // options can be found in the // [[http://blog.stevenlevithan.com/archives/date-time-format|Date Format]] // documentation. If not specified, the mask defaults to {{{dateFormat.masks.default}}}. AjaxIM.dateFormat = function () { var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, timezone = new RegExp('\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) ' + '(?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b', 'g'), timezoneClip = /[^-+\dA-Z]/g, pad = function (val, len) { val = String(val); len = len || 2; while (val.length < len) val = "0" + val; return val; }; // Regexes and supporting functions are cached through closure return function (date, mask, utc) { var dF = AjaxIM.dateFormat; // You can't provide utc if you skip other args (use the "UTC:" mask prefix) if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { mask = date; date = undefined; } // Passing date through Date applies Date.parse, if necessary date = date ? new Date(date) : new Date; if (isNaN(date)) throw SyntaxError("invalid date"); mask = String(dF.masks[mask] || mask || dF.masks["default"]); // Allow setting the utc argument via the mask if (mask.slice(0, 4) == "UTC:") { mask = mask.slice(4); utc = true; } var _ = utc ? "getUTC" : "get", d = date[_ + "Date"](), D = date[_ + "Day"](), m = date[_ + "Month"](), y = date[_ + "FullYear"](), H = date[_ + "Hours"](), M = date[_ + "Minutes"](), s = date[_ + "Seconds"](), L = date[_ + "Milliseconds"](), o = utc ? 0 : date.getTimezoneOffset(), flags = { d: d, dd: pad(d), ddd: AjaxIM.i18n.dayNames[D], dddd: AjaxIM.i18n.dayNames[D + 7], m: m + 1, mm: pad(m + 1), mmm: AjaxIM.i18n.monthNames[m], mmmm: AjaxIM.i18n.monthNames[m + 12], yy: String(y).slice(2), yyyy: y, h: H % 12 || 12, hh: pad(H % 12 || 12), H: H, HH: pad(H), M: M, MM: pad(M), s: s, ss: pad(s), l: pad(L, 3), L: pad(L > 99 ? Math.round(L / 10) : L), t: H < 12 ? "a" : "p", tt: H < 12 ? "am" : "pm", T: H < 12 ? "A" : "P", TT: H < 12 ? "AM" : "PM", Z: utc ? "UTC" : (String(date).match(timezone) || [""]) .pop().replace(timezoneClip, ""), o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] }; return mask.replace(token, function ($0) { return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); }); }; }(); // Some common format strings AjaxIM.dateFormat.masks = { "default": "ddd mmm dd yyyy HH:MM:ss", shortDate: "m/d/yy", mediumDate: "mmm d, yyyy", longDate: "mmmm d, yyyy", fullDate: "dddd, mmmm d, yyyy", shortTime: "h:MM TT", mediumTime: "h:MM:ss TT", longTime: "h:MM:ss TT Z", isoDate: "yyyy-mm-dd", isoTime: "HH:MM:ss", isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" }; // === {{{AjaxIM.}}}**{{{i18n}}}** === // // Text strings used by Ajax IM. Should you want to translate Ajax IM into // another language, merely change these strings. // // {{{%s}}} denotes text that will be automatically replaced when the string is // used. AjaxIM.i18n = { dayNames: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], monthNames: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], chatOffline: '%s signed off.', chatAvailable: '%s became available.', chatAway: '%s went away.', notConnected: 'You are currently not connected or the server is not available. ' + 'Please ensure that you are signed in and try again.', notConnectedTip: 'You are currently not connected.', authInvalid: 'Invalid username or password.', registerPasswordLength: 'Passwords must be more than 4 characters in length.', registerUsernameLength: 'Usernames must be more than 2 characters in length and ' + ' contain only A-Z, a-z, 0-9, underscores (_) and periods (.).', registerPasswordMatch: 'Entered passwords do not match.', registerUsernameTaken: 'The chosen username is already in use; please choose another.', registerUnknown: 'An unknown error occurred. Please try again.' } })(jQuery, false); \ No newline at end of file From 43a6970d7a71cc89b70aa0bb5f74d541239cf2ea Mon Sep 17 00:00:00 2001 From: "noz (OpenERP)" Date: Mon, 11 Apr 2011 10:53:53 +0530 Subject: [PATCH 08/86] [FIX] httprequest, jsonrequest conflicts testing. bzr revid: noz@tinyerp.com-20110411052353-45ftq6rb57zhxtdo --- addons/web_chat/controllers/main.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/addons/web_chat/controllers/main.py b/addons/web_chat/controllers/main.py index 456fd0f5340..2beccdcda0c 100644 --- a/addons/web_chat/controllers/main.py +++ b/addons/web_chat/controllers/main.py @@ -133,10 +133,10 @@ class PollServer(openerpweb.Controller): """ for i in range(60): - received_msg = mq.read('Guest2', i); + received_msg = mq.read('Guest1', i); if received_msg: msg = self._pollParseMessages(received_msg) - print "============ msg...", msg + time.sleep(2) else: time.sleep(2) @@ -149,12 +149,11 @@ class PollServer(openerpweb.Controller): # else # return emptylist - - # it's http://localhost:8002/web_chat/pollserver/poll?method=long?callback=jsonp1302147330483&_1302147330483= +# print "==============poll receive...", kw.get('callback') +# # it's http://localhost:8002/web_chat/pollserver/poll?method=long?callback=jsonp1302147330483&_1302147330483= return '%s([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"xxxxxx"}]);'%kw.get('callback','') - return None - @openerpweb.httprequest + @openerpweb.jsonrequest def send(self, req, **kw): print "========= send ========", kw @@ -187,9 +186,10 @@ class PollServer(openerpweb.Controller): return dict(r='error', e='no_recipient') if message: - mq.write(m_type="text", m_from=req.applicationsession['current_user'], m_to=to, m_message=message, m_group="Users") - - return {'r': 'sent'} + mq.write(m_type="m", m_from=req.applicationsession['current_user'], m_to=to, m_message=message, m_group="Users") + return {'r': 'sent'} + else: + return {'r': 'error', 'e': 'send error'} @openerpweb.httprequest def status(self, req, **kw): @@ -215,9 +215,7 @@ class PollServer(openerpweb.Controller): def _pollParseMessages(self, messages): msg_arr = [] - print "=========== messages...", messages for msg in messages: - print "=========== msg..", msg msg_arr.append({'t': msg['type'], 's': msg['from'], 'r': msg['to'], 'm': msg['message']}) return msg_arr From 75bf28388b9c2904576aedc43b61620665ee9cd0 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 11 Apr 2011 08:25:41 +0200 Subject: [PATCH 09/86] [IMP] improvements to View.normalize_attrs bzr revid: xmo@openerp.com-20110411062541-mjib0mm8wk7ujlyy --- addons/base/controllers/main.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 96782c1cec4..efc960168c4 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -424,19 +424,16 @@ class View(openerpweb.Controller): :param dict context: evaluation context """ # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads - attrs = eval(elem.attrib.get('attrs', '{}')) + attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}')) if 'states' in elem.attrib: - if 'invisible' not in attrs: - attrs['invisible'] = [] - # This should be done by the server - attrs['invisible'].append(('state', 'not in', elem.attrib['states'].split(','))) - del(elem.attrib['states']) + attrs.setdefault('invisible', [])\ + .append(('state', 'not in', elem.attrib.pop('states').split(','))) if attrs: - elem.attrib['attrs'] = simplejson.dumps(attrs) + elem.set('attrs', simplejson.dumps(attrs)) for a in ['invisible', 'readonly', 'required']: if a in elem.attrib: # In the XML we trust - avalue = bool(eval(elem.attrib.get(a, 'False'), + avalue = bool(eval(elem.get(a, 'False'), {'context': context or {}})) if not avalue: del elem.attrib[a] From 5a901eef839cf82dae9886c2e5d7929234e9d86d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 11 Apr 2011 08:27:25 +0200 Subject: [PATCH 10/86] [FIX] dataset tests bzr revid: xmo@openerp.com-20110411062725-7eh31m9vtuasmsjz --- addons/base/test/test_dataset.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/base/test/test_dataset.py b/addons/base/test/test_dataset.py index 80d1e52561b..27d2169bf1b 100644 --- a/addons/base/test/test_dataset.py +++ b/addons/base/test/test_dataset.py @@ -14,13 +14,13 @@ class TestDataSetController(unittest2.TestCase): self.search.return_value = [] self.read.return_value = [] - self.assertFalse(self.dataset.do_find(self.request, 'fake.model')) + self.assertFalse(self.dataset.do_search_read(self.request, 'fake.model')) self.read.assert_called_once_with([], False) def test_regular_find(self): self.search.return_value = [1, 2, 3] - self.dataset.do_find(self.request, 'fake.model') + self.dataset.do_search_read(self.request, 'fake.model') self.read.assert_called_once_with([1, 2, 3], False) def test_ids_shortcut(self): @@ -32,7 +32,7 @@ class TestDataSetController(unittest2.TestCase): ] self.assertEqual( - self.dataset.do_find(self.request, 'fake.model', ['id']), + self.dataset.do_search_read(self.request, 'fake.model', ['id']), [{'id': 1}, {'id': 2}, {'id': 3}]) self.assertFalse(self.read.called) @@ -46,7 +46,7 @@ class TestDataSetController(unittest2.TestCase): result = self.dataset.do_get( self.request, 'fake.model', [3, 2, 1]) self.read.assert_called_once_with( - [3, 2, 1]) + [3, 2, 1], False) self.assertFalse(self.search.called) self.assertEqual( From 669b109bde7786c406b6141f2954677f52668852 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 11 Apr 2011 08:30:54 +0200 Subject: [PATCH 11/86] [FIX] docstring for DataSet.do_search_read: missing 'fields' param bzr revid: xmo@openerp.com-20110411063054-jybr8z734xy3qndp --- addons/base/controllers/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index efc960168c4..c8614c0f5cb 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -363,6 +363,9 @@ class DataSet(openerpweb.Controller): :type model: str :param ids: a list of identifiers :type ids: list + :param fields: a list of fields to fetch, ``False`` or empty to fetch + all fields in the model + :type fields: list | False :returns: a list of records, in the same order as the list of ids :rtype: list """ From f21bb336d3aca07eb135c891dfcaf20818fcebd4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 11 Apr 2011 09:58:46 +0200 Subject: [PATCH 12/86] [FIX] dataset search, 'context' is popped from kwargs into the request object, and should be taken from there for RPC calls bzr revid: xmo@openerp.com-20110411075846-9qxzabjav0y1l47n --- addons/base/controllers/main.py | 5 ++--- addons/base/test/test_dataset.py | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index c8614c0f5cb..28806083d61 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -334,18 +334,17 @@ class DataSet(openerpweb.Controller): :param int offset: from which index should the results start being returned :param int limit: the maximum number of records to return :param list domain: the search domain for the query - :param dict context: the context in which the search should be executed :param list sort: sorting directives :returns: a list of result records :rtype: list """ Model = request.session.model(model) ids = Model.search(domain or [], offset or 0, limit or False, - sort or False, context or False) + sort or False, request.context) if fields and fields == ['id']: # shortcut read if we only want the ids return map(lambda id: {'id': id}, ids) - return Model.read(ids, fields or False) + return Model.read(ids, fields or False, request.context) @openerpweb.jsonrequest def get(self, request, model, ids, fields=False): diff --git a/addons/base/test/test_dataset.py b/addons/base/test/test_dataset.py index 27d2169bf1b..702c63b0899 100644 --- a/addons/base/test/test_dataset.py +++ b/addons/base/test/test_dataset.py @@ -15,13 +15,14 @@ class TestDataSetController(unittest2.TestCase): self.read.return_value = [] self.assertFalse(self.dataset.do_search_read(self.request, 'fake.model')) - self.read.assert_called_once_with([], False) + self.read.assert_called_once_with([], False, self.request.context) def test_regular_find(self): self.search.return_value = [1, 2, 3] self.dataset.do_search_read(self.request, 'fake.model') - self.read.assert_called_once_with([1, 2, 3], False) + self.read.assert_called_once_with([1, 2, 3], False, + self.request.context) def test_ids_shortcut(self): self.search.return_value = [1, 2, 3] From d99bacbd60615ab9e1a6e18e437d13fc2e949821 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 11 Apr 2011 10:42:59 +0200 Subject: [PATCH 13/86] [TEST] add test for normalize_attrs of states bzr revid: xmo@openerp.com-20110411084259-gpkw2nzpw8ctuhy5 --- addons/base/test/test_view.py | 53 ++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/addons/base/test/test_view.py b/addons/base/test/test_view.py index e5c6162dae0..f20f9e12f61 100644 --- a/addons/base/test/test_view.py +++ b/addons/base/test/test_view.py @@ -2,33 +2,16 @@ import xml.etree.ElementTree import mock import unittest2 +import simplejson import base.controllers.main import openerpweb.nonliterals import openerpweb.openerpweb #noinspection PyCompatibility -class ViewTest(unittest2.TestCase): +class DomainsAndContextsTest(unittest2.TestCase): def setUp(self): self.view = base.controllers.main.View() - def test_identity(self): - base_view = """ -
- - - - - - - """ - - pristine = xml.etree.ElementTree.fromstring(base_view) - transformed = self.view.transform_view(base_view, None) - - self.assertEqual( - xml.etree.ElementTree.tostring(transformed), - xml.etree.ElementTree.tostring(pristine) - ) def test_convert_literal_domain(self): e = xml.etree.ElementTree.Element( @@ -115,3 +98,35 @@ class ViewTest(unittest2.TestCase): openerpweb.nonliterals.Context( session, key=e.get('context').key).get_context_string(), context_string) + +class AttrsNormalizationTest(unittest2.TestCase): + def setUp(self): + self.view = base.controllers.main.View() + + def test_identity(self): + base_view = """ +
+ + + + + + + """ + + pristine = xml.etree.ElementTree.fromstring(base_view) + transformed = self.view.transform_view(base_view, None) + + self.assertEqual( + xml.etree.ElementTree.tostring(transformed), + xml.etree.ElementTree.tostring(pristine) + ) + def test_transform_states(self): + element = xml.etree.ElementTree.Element( + 'field', states="open,closed") + self.view.normalize_attrs(element, {}) + + self.assertIsNone(element.get('states')) + self.assertEqual( + simplejson.loads(element.get('attrs')), + {'invisible': [['state', 'not in', ['open', 'closed']]]}) From 3b7d2dd1c3090b793f14adbc63d034732c1c409c Mon Sep 17 00:00:00 2001 From: "noz (OpenERP)" Date: Mon, 11 Apr 2011 14:38:02 +0530 Subject: [PATCH 14/86] [FIX] Fixed dynamic return for poll method. bzr revid: noz@tinyerp.com-20110411090802-zzrsexmojj6w9zan --- addons/web_chat/controllers/main.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/addons/web_chat/controllers/main.py b/addons/web_chat/controllers/main.py index 2beccdcda0c..ff5b95d6579 100644 --- a/addons/web_chat/controllers/main.py +++ b/addons/web_chat/controllers/main.py @@ -132,13 +132,16 @@ class PollServer(openerpweb.Controller): """ - for i in range(60): + msg = [] + + for i in range(5): received_msg = mq.read('Guest1', i); if received_msg: msg = self._pollParseMessages(received_msg) - time.sleep(2) + time.sleep(1) else: - time.sleep(2) + msg = [] + time.sleep(1) # for i in range(60): #r = mq.read(username,timestamp) @@ -148,12 +151,14 @@ class PollServer(openerpweb.Controller): # sleep 2 # else # return emptylist - -# print "==============poll receive...", kw.get('callback') + + msg = '[{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"xxxxxx"}]' + # # it's http://localhost:8002/web_chat/pollserver/poll?method=long?callback=jsonp1302147330483&_1302147330483= - return '%s([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"xxxxxx"}]);'%kw.get('callback','') - @openerpweb.jsonrequest + return '%s'%kw.get('callback', '') + '(' + msg + ');' + + @openerpweb.httprequest def send(self, req, **kw): print "========= send ========", kw From c2865075b16014ace644d6f0cb17e1ccc2cc4dcf Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 11 Apr 2011 11:26:46 +0200 Subject: [PATCH 15/86] web_chat im.js eol 0x0d to 0x0a bzr revid: al@openerp.com-20110411092646-dykbpwndmn5gawf1 --- addons/web_chat/static/lib/AjaxIM/js/im.js | 2102 +++++++++++++++++++- 1 file changed, 2101 insertions(+), 1 deletion(-) diff --git a/addons/web_chat/static/lib/AjaxIM/js/im.js b/addons/web_chat/static/lib/AjaxIM/js/im.js index ef80481a246..42c3f743204 100644 --- a/addons/web_chat/static/lib/AjaxIM/js/im.js +++ b/addons/web_chat/static/lib/AjaxIM/js/im.js @@ -1 +1,2101 @@ -// = im.js = // // **Copyright © 2005 – 2010 Joshua Gross**\\ // //MIT Licensed// // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // This is the main library for Ajax IM. It encompasses the UI controls, // and connecting with the server. It does //not// handle registration or // account management. (function($, _instance) { AjaxIM = function(options, actions) { if(this instanceof AjaxIM) { var self = this; // === {{{ defaults }}} === // // These are the available settings for Ajax IM, and the associated // defaults: // // * {{{pollType}}} determines the way in which the client will talk to // the server. // ** {{{comet}}} will use http streaming, wherein a connection to the server will be held open indefinitely, and the server will push down new events (messages, status updates) as they occur. // ** {{{long}}} will hold open a connection with the server for as long as possible, or until a new event is received. Upon the server sending an event or closing the connection, the client will automatically—and immediately—reconnect. // ** {{{short}}} will open a connection, and the server (if this method is supported) will //immediately// provide a response as to whether or not there are any new events. Once a response is received, the client will wait 5 seconds, and then reconnect. // * {{{pollServer}}} is the default URL to which all actions refer. It is // possible to specify certain action URLs separately (as is used with the // NodeJS server). // * {{{cookieName}}} is the name of the session cookie used by the server. // If this is not set properly, the IM engine will not be able to automatically // reinitialize sessions. // * {{{theme}}} is the name of the theme folder that defines the HTML and // CSS of the IM bar and chat boxes. Usually, themes are deposited in the // provided "themes" folder and specified by that path, e.g. {{{themes/default}}}. // Theme files within the theme folder must be named {{{theme.html}}} and // {{{theme.css}}}. // * {{{storageMethod}}} defines the way in which data (chat sessions) are // temporarily stored client-side. By default, {{{"flash"}}} is used because // it is the most widely supported method. However, // [[http://eric.garside.name/docs.html?p=jstore#js-engines|other storage engines]] // are available, with their respective up- and down-sides // outlined, on the jStore website. // * {{{storeSession}}} (**not implemented**) sets the number of days to // retain stored chat session data before it should be deleted. // * {{{checkResume}}} is a flag that sets whether or not the client should // make a call to the server before resuming the session (such as on a page // reload). This will ensure that the session has not expired. If set to {{{false}}}, // a call to the server will not be made, and the session will be assumed to // be active. var defaults = { pollType: 'long', pollServer: './ajaxim.php', cookieName: 'ajaxim_session', theme: 'themes/default', storageMethod: 'auto', flashStorage: 'js/jStore.Flash.html', storeSession: 5, // number of days to store chat data (0 for current session only) checkResume: true }; // === {{{AjaxIM.}}}**{{{settings}}}** === // // These are the settings for the IM. If particular options are not specified, // the defaults (see above) will be used. //These options will be defined // upon calling the initialization function, and not set directly.// this.settings = $.extend(defaults, options); // === {{{AjaxIM.}}}**{{{actions}}}** === // // Each individual action that the IM engine can execute is predefined here. // By default, it merely appends the action onto the end of the {{{pollServer}}} url, // however, it is possible to define actions individually. //The alternative actions // will be defined upon calling the initialization function, and not set directly.// // // Should you define an action at a different URL, Ajax IM will determine whether // or not this URL is within the current domain. If it is within a subdomain of // the current domain, it will set the document.domain variable for you, // to match a broader hostname scope; the action will continue to use {{{$.post}}} // (the default AJAX method for Ajax IM). // // On the other hand, should you choose a URL outside the current domain // Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid // cross-domain scripting issues. This means that a server on a different // port or at a different address will need to be able to handle GET // requests rather than POST requests (such as how the Node.JS Ajax IM // server works). this.actions = $.extend({ login: this.settings.pollServer + '/login', logout: this.settings.pollServer + '/logout', register: this.settings.pollServer + '/register', poll: this.settings.pollServer + '/poll?method=' + this.settings.pollType, send: this.settings.pollServer + '/send', status: this.settings.pollServer + '/status', resume: this.settings.pollServer + '/resume' }, actions); var httpRx = new RegExp('^http://', 'i'), slashslashRx = new RegExp('^//', 'i'), queryStrRx = new RegExp('[?](.+)$'), subdomainRx = new RegExp('((http[s]?:)?//)?(.+?)[.]' + window.location.host, 'i'); $.each(this.actions, function(name, action) { if(name == 'poll' || name == 'send') { if(self.settings.pollType != 'comet') action += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; action = ['jsonp', action]; } else { action = ['ajax', action]; if(subdomainRx.test(action[1])) { document.domain = '.' + window.location.host; } else if((http = httpRx.test(action[1])) || slashslashRx.test(action[1])) { if(!(new RegExp('//' + window.location.host + '/', 'i')).test(action[1])) { action[1] += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; action = ['jsonp', action[1]]; } } } self.actions[name] = action; }); // We load the theme dynamically based on the passed // settings. If the theme is set to false, we assume // that the user is going to load it himself. this.themeLoaded = false; if(this.settings.theme) { $('
').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip', function() { self.themeLoaded = true; setup.apply(self); } ); if(typeof document.createStyleSheet == 'function') document.createStyleSheet(this.settings.theme + '/theme.css'); else $('body').append(''); } else { this.themeLoaded = true; setup.apply(this); } // Client-side storage for keeping track of // conversation states, active tabs, etc. // The default is flash storage, however, other // options are available via the jStore library. if(this.settings.storageMethod) { this.storageBrowserKey = 'unknown'; this.storeKey = ''; $.each($.browser, function(k, v) { if(k == 'version') return true; else if(v == true) { self.storageBrowserKey = k; return false; } }); if(this.settings.storageMethod == 'auto') { $.each(['ie', 'html5', 'local', 'flash'], function() { if($.jStore.Availability[this]()) { self.settings.storageMethod = this; return false; } }); } $.extend($.jStore.defaults, { project: 'im.js', engine: this.settings.storageMethod, flash: this.settings.flashStorage }); this.storageReady = false; if(this.settings.storageMethod == 'flash') { $.jStore.ready(function(engine) { $.jStore.flashReady(function() { self.storageReady = true; setup.apply(self); }); }) } else { $.jStore.ready(function(engine) { self.storageReady = true; setup.apply(self); }); } $.jStore.load(); } else { this.storageReady = true; setup.apply(this); } // Allow a chatbox to me minimized $('.imjs-chatbox').live('click', function(e) { e.preventDefault(); return false; }); $('.imjs-chatbox .imjs-minimize').live('click', function() { $(this).parents('.imjs-chatbox').data('tab').click(); }); // Allow a chatbox to be closed $('.imjs-chatbox .imjs-close').live('click', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.data('tab') .data('state', 'closed').css('display', 'none'); if(self.settings.storageMethod && self.storageReady) { delete self.chatstore[chatbox.data('username')]; if(self.storageReady) { $.jStore.store( self.storageBrowserKey + '-' + self.username + '-chats', self.chatstore); } } }); // Setup message sending for all chatboxes $('.imjs-chatbox .imjs-input').live('keydown', function(event) { var obj = $(this); if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) { self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } }).live('keyup', function(event) { if(event.keyCode == 13) { if($.browser.msie && $.browser.version < 8) { var obj = $(this); self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } var obj = $(this); obj.val(''); obj.height(obj.data('height')); } }).live('keypress', function(e) { var obj = $(this); if(!($.browser.msie && $.browser.opera)) obj.height(0); if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) { obj.height(this.scrollHeight); } }); $('.imjs-msglog').live('click', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.find('.imjs-input').focus(); }); // Create a chatbox when a buddylist item is clicked $('.imjs-friend').live('click', function() { var chatbox = self._createChatbox($(this).data('friend')); if(chatbox.data('tab').data('state') != 'active') chatbox.data('tab').click(); chatbox.find('.imjs-input').focus(); }); // Setup and hide the scrollers $('.imjs-scroll').css('display', 'none'); $('#imjs-scroll-left').live('click', function() { var hiddenTab = $('#imjs-bar li.imjs-tab:visible').slice(-1) .next('#imjs-bar li.imjs-tab:hidden') .filter(function() { return $(this).data('state') != 'closed' }) .not('.imjs-default').slice(-1).css('display', ''); if(hiddenTab.length) { $('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none'); $(this).html(parseInt($(this).html()) - 1); $('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1); } return false; }); $('#imjs-scroll-right').live('click', function() { var hiddenTab = $('#imjs-bar li.imjs-tab:visible').eq(0) .prev('#imjs-bar li.imjs-tab:hidden') .filter(function() { return $(this).data('state') != 'closed' }) .not('.imjs-default').slice(-1).css('display', ''); if(hiddenTab.length) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); $(this).html(parseInt($(this).html()) - 1); $('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1); } return false; }); // Initialize the chatbox hash this.chats = {}; // Try to resume any existing session this.resume(); $(window).resize(function() { self.bar._scrollers(); }); } else return AjaxIM.init(options); }; // We predefine all public functions here... // If they are called before everything (theme, storage engine) has loaded, // then they get put into a "prequeue" and run when everything *does* // finally load. // // This ensures that nothing loads without all of the principal components // being pre-loaded. If that were to occur (without this prequeue), things // would surely break. var prequeue = []; var empty = function() { var func = this; return function() { prequeue.push([func, arguments]) }; }; $.extend(AjaxIM.prototype, { settings: {}, friends: {}, chats: {}, storage: empty.apply('storage'), login: empty.apply('login'), logout: empty.apply('logout'), resume: empty.apply('resume'), form: empty.apply('form'), poll: empty.apply('poll'), incoming: empty.apply('incoming'), send: empty.apply('send'), status: empty.apply('status'), statuses: {}, bar: { initialize: empty.apply('bar.initialize'), activateTab: empty.apply('bar.activateTab'), closeTab: empty.apply('bar.closeTab'), addTab: empty.apply('bar.addTab'), notification: empty.apply('bar.notification'), _scrollers: empty.apply('bar._scrollers') } }); setup = (function() { var self = this; if(!this.storageReady || !this.themeLoaded) return; $(self).trigger('loadComplete'); $(window).resize(); $.extend(AjaxIM.prototype, { // == Cookies == // // The "cookies" functions can be used to set, get, and erase JSON-based cookies. // These functions are primarily used to manage and read the server-set cookie // that handles the user's chat session ID. cookies: { // === {{{AjaxIM.}}}**{{{cookies.set(name, value, days)}}}** === // // Sets a cookie, stringifying the JSON value upon storing it. // // ==== Parameters ==== // * {{{name}}} is the cookie name.\\ // * {{{value}}} is the cookie data that you would like to store.\\ // * {{{days}}} is the number of days that the cookie will be stored for. set: function(name, value, days) { if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "; expires=" + date.toGMTString(); } else var expires = ""; document.cookie = name + "=" + $.compactJSON(value) + expires + "; path=/"; }, // === {{{AjaxIM.}}}**{{{cookies.get(name)}}}** === // // Gets a cookie, decoding the JSON value before returning the data. // // ==== Parameters ==== // * {{{name}}} is the cookie name that you would like to retrieve. get: function(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) { var cval = decodeURIComponent(c.substring(nameEQ.length, c.length)); return $.secureEvalJSON(cval); } } return null; }, // === {{{AjaxIM.}}}**{{{cookies.erase(name)}}}** === // // Deletes a cookie. // // {{{name}}} is the existing cookie that you would like to delete. erase: function(name) { self.cookies.set(name, '', -1); } }, // == Main == // === {{{AjaxIM.}}}**{{{storage()}}}** === // // Retrieves chat session data from whatever storage engine is enabled // (provided that one is enabled at all). If a page reloads, this function // is called to restore the user's chat state (existing conversations, active tab). // This function is called //automatically//, upon initialization of the IM engine. storage: function() { if(!self.storeKey.length) return; try { var chatstore = $.jStore.store(self.storeKey + 'chats') || {}; } catch(e) { $.jStore.remove(self.storeKey + 'chats'); var chatstore = {}; } if(this.chatstore) { $.each(this.chatstore, function(username, convo) { if(username in chatstore) chatstore[username] = $.merge(chatstore[username], self.chatstore[username]); else chatstore[username] = self.chatstore[username]; }); this.chatstore = chatstore; $.jStore.store(self.storeKey + 'chats', chatstore); } else { this.chatstore = chatstore; } $.each(this.chatstore, function(username, convo) { if(!convo.length) return; var chatbox = self._createChatbox(username, true); chatbox.data('lastDateStamp', null).css('display', 'none'); // Remove the automatic date stamp chatbox.find('.imjs-msglog').empty(); // Restore all messages, date stamps, and errors $.each(convo, function() { switch(this[0]) { case 'error': self._addError(chatbox, decodeURIComponent(this[2]), this[3]); break; case 'datestamp': self._addDateStamp(chatbox, this[3]); break; case 'a': case 'b': self._addMessage(this[0], chatbox, this[1], decodeURIComponent(this[2]), this[3]); break; } }); $(self).trigger('chatRestored', [username, chatbox]); }); var activeTab = $.jStore.store(self.storeKey + 'activeTab') || []; if(activeTab.length && (activeTab = activeTab[0]) && activeTab in this.chats) { this.chats[activeTab].data('tab').click(); var msglog = this.chats[activeTab].find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } }, // === {{{AjaxIM.}}}**{{{login(username, password)}}}** === // // Authenticates a user and initializes the IM engine. If the user is // already logged in, [s]he is logged out, the session is cleared, and // the new user is logged in. // // Returns the user's properly formatted username, session ID, and online // friends in JSON, if successful; e.g.:\\ // {{{ {u: 'username', s: 'longsessionid', f: [{u: 'friend', s: 1, g: 'group'}]} }}} // // If unsuccessful, {{{false}}} is returned. // // ==== Parameters ==== // * {{{username}}} is the user's username.\\ // * {{{password}}} is the user's password. This password will be MD5 hashed // before it is sent to the server. login: function(username, password) { if(!username) username = ''; if(!password) password = ''; if(this.username) return true; // Already logged in! // hash password before sending it to the server password = $.md5(password); // authenticate AjaxIM.request( this.actions.login, {'username': username, 'password': password}, function(auth) { if(auth.r == 'logged in') { self.username = ('u' in auth ? auth.u : username); if(self.settings.storageMethod) self.storeKey = [self.storageBrowserKey, self.username, ''].join('-'); var existing = self.cookies.get(self.settings.cookieName); self.storage(); // Begin the session $.each(auth.f, function() { self.friends[this.u] = {status: [this.s, ''], group: this.g}; }); self._session(self.friends); self._storeFriends(); $(self).trigger('loginSuccessful', [auth]); return auth; } else { $(self).trigger('loginError', [auth]); } return false; } ); }, // === {{{AjaxIM.}}}**{{{logout()}}}** === // // Logs the user out and removes his/her session cookie. As well, it // will close all existing chat windows, clear the storage engine, and // remove the IM bar. logout: function() { AjaxIM.request( this.actions.logout, {}, function(done) { if(done) { self.cookies.erase(self.settings.cookieName); self._clearSession(); $(self).trigger('logoutSuccessful'); return true; } else { $(self).trigger('logoutError'); return false; } } ); }, // === {{{AjaxIM.}}}**{{{resume()}}}** === // // Resumes an existing session based on a session ID stored in the // server-set cookie. This function is called //automatically// upon IM // engine (re-)initialization, so the user does not need to re-login // should a session already exist. resume: function() { var session = this.cookies.get(this.settings.cookieName); if(session && session.sid) { this.username = session.user; this.storeKey = [this.storageBrowserKey, this.username, ''].join('-'); var friends = $.jStore.store(this.storeKey + 'friends') || []; if(self.settings.checkResume) { AjaxIM.request( this.actions.resume, {}, function(response) { if(response.r == 'connected') { self._session(friends); self.storage(); } else { var username = this.username; self._clearSession(); $(self).trigger('sessionNotResumed', [username]); } } ); } else { self._session(friends); self.storage(); } } else { $(self).trigger('noSession'); } }, // === //private// {{{AjaxIM.}}}**{{{_session(friends)}}}** === // // Restores session data (username, friends) and begins polling the server. // Called only by {{{AjaxIM.resume()}}}. // // ==== Parameters ==== // * {{{friends}}} is a list of "friend" objects, e.g.:\\ // {{{[{u: 'friend', s: 1, g: 'group'}, ...]}}} // ** {{{u}}} being the friend's username. // ** {{{s}}} being one of the available status codes (see {{{AjaxIM.statuses}}}), depending on the friend's current status. // ** {{{g}}} being the group that the friend is in. _session: function(friends) { $('#imjs-friends-panel .imjs-header span').html(this.username); $('#imjs-friends').removeClass('imjs-not-connected'); $.each(friends, function(friend, info) { self.addFriend.apply(self, [friend, info.status, info.group]); }); self._storeFriends(); $(self).trigger('sessionResumed', [this.username]); setTimeout(function() { self.poll(); }, 0); }, // === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** === // // Clears all session data from the last known user. _clearSession: function() { if(self.settings.storageMethod && self.storageReady) { $.jStore.remove(self.storeKey + 'chats'); $.jStore.remove(self.storeKey + 'friends'); $.jStore.remove(self.storeKey + 'activeTab'); } self.chats = {}; $('.imjs-tab').not('.imjs-tab.imjs-default').remove(); self.cookies.erase('ajaxim_session'); delete self.username; }, // === {{{AjaxIM.}}}**{{{form(element)}}}** === // // Loads a login and registration form into the specified element // or, if no element is supplied, to the location on the page from // which this function was called. form: function(element) { $(element).load(this.settings.theme + '/theme.html #imjs-lr', function() { $('#imjs-lr .error').hide(); if(self.username) { $('#imjs-register, #imjs-login fieldset').hide(); $('#imjs-logged-in') .show() .html($('#imjs-logged-in').html().replace('{username}', self.username)); } else { $('#imjs-logged-in').hide(); } // Handle logout success $(self).bind('logoutSuccessful', function() { $('#imjs-login fieldset').slideDown(); $('#imjs-register').slideDown(); $('#imjs-logged-in') .html($('#imjs-logged-in strong').html('{username}')) .slideUp(); }); $('#imjs-logged-in a').click(function() { self.logout(); return false; }); // Handle login error or success $(self).bind('loginError', function() { $('#imjs-login .error').html(AjaxIM.i18n.authInvalid).slideDown('fast'); $('#imjs-login input') .addClass('imjs-lr-error') .blur(function() { $(this).removeClass('imjs-lr-error'); }); }).bind('loginSuccessful', function() { $('#imjs-login fieldset').slideUp(); $('#imjs-register').slideUp(); $('#imjs-logged-in') .html($('#imjs-logged-in').html().replace('{username}', self.username)) .slideDown(); }); var login = function() { self.login($('#imjs-login-username').val(), $('#imjs-login-password').val()); return false; }; $('#imjs-login').submit(login); $('#imjs-login-submit').click(login); var regIssues = false; var regError = function(error, fields) { $('#imjs-register .error') .append(AjaxIM.i18n['register' + error] + ' ') .slideDown(); $(fields) .addClass('imjs-lr-error') .blur(function() { $(this).removeClass('imjs-lr-error'); }); regIssues = true; }; var register = function() { $('#imjs-register .error').empty(); regIssues = false; var username = $('#imjs-register-username').val(); var password = $('#imjs-register-password').val(); if(password.length < 4) regError('PasswordLength', '#imjs-register-password'); if(password != $('#imjs-register-cpassword').val()) regError('PasswordMatch', '#imjs-register-password, #imjs-register-cpassword'); if(username.length <= 2 || !$('#imjs-register-username').val().match(/^[A-Za-z0-9_.]+$/)) regError('UsernameLength', '#imjs-register-username'); if(!regIssues) { AjaxIM.request( self.actions.register, {username: username, password: password}, function(response) { if(response.r == 'registered') { self.login(username, password); } else if(response.r == 'error') { switch(response.e) { case 'unknown': regError('Unknown', ''); break; case 'invalid password': regError('PasswordLength', '#imjs-register-password'); break; case 'invalid username': regError('UsernameLength', '#imjs-register-username'); break; case 'username taken': regError('UsernameTaken', '#imjs-register-username'); break; } } }, function(error) { regError('Unknown', ''); } ); } return false; }; $('#imjs-register').submit(register); $('#imjs-register-submit').click(register); }); }, // === {{{AjaxIM.}}}**{{{poll()}}}** === // // Queries the server for new messages. If a 'long' or 'short' poll // type is used, jQuery's {{{$.post}}} or {{{$.getJSON}}} will be // used. If 'comet' is used, the server connection will be deferred // to the comet set of functions. poll: function() { if(/^(short|long)$/.test(this.settings.pollType)) { AjaxIM.request( this.actions.poll, {}, function(response) { if(!response['e']) { if(response.length) self._parseMessages(response); setTimeout(function() { self.poll(); }, 0); } else { switch(response.e) { case 'no session found': self._notConnected(); break; } $(self).trigger('pollFailed', [response.e]); } }, function(error) { self._notConnected(); $(self).trigger('pollFailed', ['not connected']); // try reconnecting? } ); } else if(this.settings.pollType == 'comet') { this.comet.connect(); } }, // === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** === // // Handles an incoming message array:\\ // {{{[{t: 'type', s: 'sender', r: 'recipient', m: 'message'}, ...]}}} // // * {{{t}}} (message type) is one of: // ** {{{m}}} — a standard message // ** {{{s}}} — a user's status update // ** {{{b}}} — a broadcasted message (sent to many users simultaneously) // * {{{s}}} is the sender of the message. // * {{{r}}} is the intended recipient of the message. Most of the time, this will // simply be the logged in user, however, a broadcasted message may not specify // a recipient or may specify a different recipient. Also provides future // compatability for chatrooms. // * {{{m}}} is the actual message. For something such as a status update, this can // be a JSON object or parsable string; e.g. {{{"2:I'm away."}}} // // ==== Parameters ==== // * {{{messages}}} is the message array _parseMessages: function(messages) { if($.isArray(messages)) { $.each(messages, function() { $(self).trigger('parseMessage', [this]); switch(this.t) { case 'm': self.incoming(this.s, this.m); break; case 's': var status = this.m.split(':'); if(this['g']) self.addFriend(this.s, status, this.g); self._friendUpdate(this.s, status[0], status.slice(1).join(':')); self._storeFriends(); break; case 'b': break; default: break; } }); } }, // === {{{AjaxIM.}}}**{{{incoming(from, message)}}}** === // // Handles a new message from another user. If a chatbox for that // user does not yet exist, one is created. If it does exist, but // is minimized, the user is notified but the chatbox is not brought // to the front. This function also stores the message, if a storage // method is set. // // ==== Parameters ==== // * {{{from}}} is the username of the sender. // * {{{message}}} is the body. incoming: function(from, message) { // check if IM exists, otherwise create new window // TODO: If friend is not on the buddylist, // should add them to a temp list? var chatbox = this._createChatbox(from); if(!$('#imjs-bar .imjs-selected').length) { chatbox.data('tab').click(); } else if(chatbox.data('tab').data('state') != 'active') { this.bar.notification(chatbox.data('tab')); } var time = this._addMessage('b', chatbox, from, message); this._storeMessage('b', chatbox, from, message, time); }, // === {{{AjaxIM.}}}**{{{addFriend(username, group)}}}** === // // Inserts a new friend into the friends list. If the group specified // doesn't exist, it is created. If the friend is already in this group, // they aren't added again, however, the friend item is returned. // // ==== Parameters ==== // * {{{username}}} is the username of the new friend. // * {{{status}}} is the current status of the friend. // * {{{group}}} is the user group to which the friend should be added. addFriend: function(username, status, group) { var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status[0] == val) { status_name = key; return false; } }); var group_id = 'imjs-group-' + $.md5(group); if(!(group_item = $('#' + group_id)).length) { var group_item = $('.imjs-friend-group.imjs-default').clone() .removeClass('imjs-default') .attr('id', group_id) .data('group', group) .appendTo('#imjs-friends-list'); var group_header = group_item.find('.imjs-friend-group-header'); group_header.html(group_header.html().replace('{group}', group)); } var user_id = 'imjs-friend-' + $.md5(username + group); if(!$('#' + user_id).length) { var user_item = group_item.find('ul li.imjs-default').clone() .removeClass('imjs-default') .addClass('imjs-' + status_name) .attr('id', user_id) .data('friend', username) .appendTo(group_item.find('ul')); if(status[0] == 0) user_item.hide(); user_item.html(user_item.html().replace('{username}', username)); } this.friends[username] = {'status': status, group: group}; this._updateFriendCount(); return this.friends[username]; }, // === //private// {{{AjaxIM.}}}**{{{_updateFriendCount()}}}** === // // Counts the number of online friends and updates the friends count // in the friend tab. _updateFriendCount: function() { var friendsLength = 0; for(var f in this.friends) { if(this.friends[f].status[0] != 0) friendsLength++; } $('#imjs-friends .imjs-tab-text span span').html(friendsLength); }, // === //private// {{{AjaxIM.}}}**{{{_storeFriends()}}}** === // // If a storage method is enabled, the current state of the // user's friends list is stored. _storeFriends: function() { if(this.settings.storageMethod && this.storageReady) $.jStore.store(this.storeKey + 'friends', this.friends); }, // === //private// {{{AjaxIM.}}}**{{{_createChatbox(username)}}}** === // // Builds a chatbox based on the default chatbox HTML and CSS defined // in the current theme. Should a chatbox for this user already exist, // a new one is not created. Instead, it is either given focus (should // no other windows already have focus), or a notification is issued. // // As well, if the chatbox does not exist, an associated tab will be // created. // // ==== Parameters ==== // * {{{username}}} is the name of the user for whom the chatbox is intended // for. // * {{{no_stamp}}} sets whther or not to add a date stamp to the chatbox // upon creation. // // //Note:// New chatboxes are given an automatically generated ID in the // format of {{{#imjs-[md5 of username]}}}. _createChatbox: function(username, no_stamp) { var chatbox_id = 'imjs-' + $.md5(username); if(!(chatbox = $('#' + chatbox_id)).size()) { // add a tab var tab = this.bar.addTab(username, '#' + chatbox_id); var chatbox = tab.find('.imjs-chatbox'); chatbox.attr('id', chatbox_id); chatbox.data('tab', tab); // remove default items from the message log var message_log = chatbox.find('.imjs-msglog').empty(); // setup the chatbox header var cb_header = chatbox.find('.imjs-header'); cb_header.html(cb_header.html().replace('{username}', username)); if(!no_stamp) { // add a date stamp var time = this._addDateStamp(chatbox); this._storeNonMessage('datestamp', username, null, time); } // associate the username with the object and vice-versa this.chats[username] = chatbox; chatbox.data('username', username); // did this chatbox fall down? this.bar._scrollers(); if(username in this.friends) { status = this.friends[username].status; var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status[0] == val) { status_name = key; return false; } }); tab.addClass('imjs-' + status_name); } // store inputbox height //var input = chatbox.find('.imjs-input'); //input.data('height', input.height()); } else if(chatbox.data('tab').data('state') == 'closed') { chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old'); var tab = chatbox.data('tab'); if(tab.css('display') == 'none') tab.css('display', '').removeClass('imjs-selected') .appendTo('#imjs-bar'); if(!no_stamp) { // possibly add a date stamp var time = this._addDateStamp(chatbox); this._storeNonMessage('datestamp', username, null, time); } if(!$('#imjs-bar .imjs-selected').length) { tab.click(); } else { this.bar.notification(tab); } } return chatbox; }, // === //private// {{{AjaxIM.}}}**{{{_addDateStamp(chatbox)}}}** // // // Adds a date/time notifier to a chatbox. These are generally // inserted upon creation of a chatbox, or upon the date changing // since the last time a date stamp was added. If a date stamp for // the current date already exists, a new one will not be added. // // ==== Parameters ==== // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{time}}} is the date/time the date stamp will show. It is specified // in milliseconds since the Unix Epoch. This is //only// defined when // date stamps are being restored from storage; if not specified, the // current computer time will be used. _addDateStamp: function(chatbox, time) { var message_log = $(chatbox).find('.imjs-msglog'); if(!time) time = (new Date()).getTime(); var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); var date_stamp_time = date_stamp.find('.imjs-msg-time'); if(date_stamp_time.length) date_stamp_time.html(AjaxIM.dateFormat(time, date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date'); var formatted_date = AjaxIM.dateFormat(time, date_stamp_date.html()); if(chatbox.data('lastDateStamp') != formatted_date) { if(date_stamp_date.length) date_stamp_date.html(AjaxIM.dateFormat(time, date_stamp_date.html())); chatbox.data('lastDateStamp', formatted_date); date_stamp.appendTo(message_log); } else { //$('
').appendTo(message_log); } return time; }, // === //private// {{{AjaxIM.}}}**{{{_addError(chatbox, error)}}}** // // // Adds an error to a chatbox. These are generally inserted after // a user sends a message unsuccessfully. If an error message // was already added, another one will be added anyway. // // ==== Parameters ==== // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{error}}} is the error message string. // * {{{time}}} is the date/time the error occurred. It is specified in // milliseconds since the Unix Epoch. This is //only// defined when // errors are being restored from storage; if not specified, the current // computer time will be used. _addError: function(chatbox, error, time) { var message_log = $(chatbox).find('.imjs-msglog'); var error_item = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-error').clone(); var error_item_time = error_item.find('.imjs-msg-time'); if(error_item_time.length) { if(!time) time = (new Date()).getTime(); error_item_time.html(AjaxIM.dateFormat(time, error_item_time.html())); } error_item.find('.imjs-error-error').html(error); error_item.appendTo(message_log); message_log[0].scrollTop = message_log[0].scrollHeight; }, // === //private// {{{AjaxIM.}}}**{{{_addMessage(ab, chatbox, username, message, time)}}}** // // // Adds a message to a chatbox. Depending on the {{{ab}}} value, // the color of the username may change as a way of visually // identifying users (however, this depends on the theme's CSS). // A timestamp is added to the message, and the chatbox is scrolled // to the bottom, such that the new message is visible. // // Messages will be automatically tag-escaped, so as to prevent // any potential cross-site scripting problems. Additionally, // URLs will be automatically linked. // // ==== Parameters ==== // * {{{ab}}} refers to whether the user is "a" or "b" in a conversation. // For the general case, "you" are "a" and "they" are "b". // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{username}}} is the username of the user who sent the message. // * {{{time}}} is the time the message was sent in milliseconds since // the Unix Epoch. This is //only// defined when messages are being // restored from storage. For new messages, the current computer // time is automatically used. _addMessage: function(ab, chatbox, username, message, time) { var last_message = chatbox.find('.imjs-msglog > *:last-child'); if(last_message.hasClass('imjs-msg-' + ab)) { // Last message was from the same person, so let's just add another imjs-msg-*-msg var message_container = (last_message.hasClass('imjs-msg-' + ab + '-container') ? last_message : last_message.find('.imjs-msg-' + ab + '-container')); var single_message = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-msg-' + ab + '-msg') .clone().appendTo(message_container); single_message.html(single_message.html().replace('{username}', username)); } else if(!last_message.length || !last_message.hasClass('imjs-msg-' + ab)) { var message_group = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msg-' + ab) .clone().appendTo(chatbox.find('.imjs-msglog')); message_group.html(message_group.html().replace('{username}', username)); var single_message = message_group.find('.imjs-msg-' + ab + '-msg'); } // clean up the message message = message.replace(//g, '>') .replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1$2$3'); // autolink URLs message = message.replace( new RegExp('([A-Za-z][A-Za-z0-9+.-]{1,120}:[A-Za-z0-9/]' + '(([A-Za-z0-9$_.+!*,;/?:@&~=-])|%[A-Fa-f0-9]{2}){1,333}' + '(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*,;/?:@&~=%-]{0,1000}))?)', 'g'), '$1'); // insert the message single_message.html(single_message.html().replace('{message}', message)); // set the message time var msgtime = single_message.find('.imjs-msg-time'); if(!time) time = new Date(); if(typeof time != 'string') time = AjaxIM.dateFormat(time, msgtime.html()); msgtime.html(time); var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; return time; }, // === //private// {{{AjaxIM.}}}**{{{_storeNonMessage(type, username, data, time)}}}** === // // **Redundant?**\\ // Similar to {{{AjaxIM._storeMessage}}}, but stores items that aren't messages, // such as datestamps and errors. // // ==== Parameters ==== // * {{{type}}} is the type of non-message (error, datestamp). // * {{{username}}} is the username of the user being chatted with. // * {{{data}}} is the (optional) data of the non-message to be stored. // * {{{time}}} is the time of the message. _storeNonMessage: function(type, username, data, time) { // If storage is on & ready, store the non-message if(this.settings.storageMethod) { if(!this.chatstore) this.chatstore = {}; if(!(username in this.chatstore)) this.chatstore[username] = []; // If the chat store gets too long, it becomes slow to load. if(this.chatstore[username].length > 300) this.chatstore[username].shift(); // For some reason, if we don't encode and decode the message, it *will* break // (at least) the Flash storage engine's retrieval. Gah! this.chatstore[username].push( [type, username, encodeURIComponent(data), time]); if(this.storageReady) $.jStore.store(self.storeKey + 'chats', this.chatstore); } }, // === //private// {{{AjaxIM.}}}**{{{_storeMessage(ab, chatbox, username, message, time)}}}** === // // Taking the same arguments as {{{AjaxIM._addMessage}}}, {{{_storeMessage}}} pushes a message // into the storage hash, if storage is enabled. // // Messages are added to a message array, by username. The entire chat hash is stored as // a {{{'chats'}}} object in whatever storage engine is enabled. _storeMessage: function(ab, chatbox, username, message, time) { // If storage is on & ready, store the message if(this.settings.storageMethod) { if(!this.chatstore) this.chatstore = {}; message = message.replace(//g, '>'); if(!(username in this.chatstore)) { this.chatstore[username] = []; } else if(this.chatstore[username].length > 300) { // If the chat store gets too long, it becomes slow to load. this.chatstore[username].shift(); } // For some reason, if we don't encode and decode the message, it *will* break // (at least) the Flash storage engine's retrieval. Gah! this.chatstore[chatbox.data('username')].push( [ab, username, encodeURIComponent(message), time]); if(this.storageReady) $.jStore.store(this.storeKey + 'chats', this.chatstore); } }, // === //private// {{{AjaxIM.}}}**{{{_friendUpdate(friend, status, statusMessage)}}}** === // // Called when a friend's status is updated. This function will update all locations // where a status icon is displayed (chat tab, friends list), as well as insert // a notification, should a chatbox be open. // // ==== Parameters ==== // * {{{friend}}} is the username of the friend. // * {{{status}}} is the new status code. See {{{AjaxIM.statuses}}} for a list of available // codes. //Note: If an invalid status is specified, no action will be taken.// // * {{{statusMessage}}} is a message that was, optionally, specified by the user. It will be // used should "you" send the user an IM while they are away, or if their status is viewed // in another way (such as via the friends list [**not yet implemented**]). _friendUpdate: function(friend, status, statusMessage) { // add friend to buddylist, update their status, etc. var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status == val) { status_name = key; return false; } }); if(this.chats[friend]) { var tab = this.chats[friend].data('tab'); var tab_class = 'imjs-tab'; if(tab.data('state') == 'active') tab_class += ' imjs-selected'; tab_class += ' imjs-' + status_name; tab.attr('class', tab_class); // display the status in the chatbox var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); var date_stamp_time = date_stamp.find('.imjs-msg-time'); if(date_stamp_time.length) date_stamp_time.html(AjaxIM.dateFormat(date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date').html( AjaxIM.i18n[ 'chat' + status_name[0].toUpperCase() + status_name.slice(1) ].replace(/%s/g, friend)); var msglog = this.chats[friend].find('.imjs-msglog'); date_stamp.appendTo(msglog); msglog[0].scrollTop = msglog[0].scrollHeight; } if(this.friends[friend]) { var friend_id = 'imjs-friend-' + $.md5(friend + this.friends[friend].group); $('#' + friend_id).attr('class', 'imjs-friend imjs-' + status_name); if(status == 0) { $('#' + friend_id + ':visible').slideUp(); $('#' + friend_id + ':hidden').hide(); } else if(!$('#' + friend_id + ':visible').length) { $('#' + friend_id).slideDown(); } this.friends[friend].status = [status, statusMessage]; this._updateFriendCount(); } }, // === //private// {{{AjaxIM.}}}**{{{_notConnected()}}}** === // // Puts the user into a visible state of disconnection. Sets the // friends list to "not connected" and empties it; disallows new messages // to be sent. _notConnected: function() { $('#imjs-friends').addClass('imjs-not-connected').unbind('click', this.activateTab); }, // === {{{AjaxIM.}}}**{{{send(to, message)}}}** === // // Sends a message to another user. The message will be added // to the chatbox before it is actually sent, however, if an // error occurs during sending, that will be indicated immediately // afterward. // // After sending the message, one of three status codes should be // returned as a JSON object, e.g. {{{{r: 'code'}}}}: // * {{{ok}}} — Message was sent successfully. // * {{{offline}}} — The user is offline or unavailable to // receive messages. // * {{{error}}} — a problem occurred, unrelated to the user // being unavailable. // // ==== Parameters ==== // * {{{to}}} is the username of the recipient. // * {{{message}}} is the content to be sent. send: function(to, message) { if(!message) return; if(this.chats[to]) { // REMOVE ME? // possibly add a datestamp var time = self._addDateStamp(this.chats[to]); this._storeNonMessage('datestamp', to, null, time); time = this._addMessage('a', this.chats[to], this.username, message); this._storeMessage('a', this.chats[to], this.username, message, time); } $(self).trigger('sendingMessage', [to, message]); AjaxIM.request( this.actions.send, {'to': to, 'message': message}, function(result) { switch(result.r) { case 'ok': $(self).trigger('sendMessageSuccessful', [to, message]); break; case 'offline': $(self).trigger('sendMessageFailed', ['offline', to, message]); break; case 'error': default: if(result.e == 'no session found') { self._notConnected(); self._addError(self.chats[to], AjaxIM.i18n.notConnected); self._storeNonMessage('error', to, AjaxIM.i18n.notConnected, (new Date()).getTime()); } $(self).trigger('sendMessageFailed', [result.e, to, message]); break; } }, function(error) { self._notConnected(); self._addError(self.chats[to], AjaxIM.i18n.notConnected); self._storeNonMessage('error', to, AjaxIM.i18n.notConnected, (new Date()).getTime()); $(self).trigger('sendMessageFailed', ['not connected', to, message]); } ); }, // === {{{AjaxIM.}}}**{{{status(s, message)}}}** === // // Sets the user's status and status message. It is possible to not // set a status message by setting it to an empty string. The status // will be sent to the server, where upon the server will broadcast // the update to all individuals with "you" on their friends list. // // ==== Parameters ==== // * {{{s}}} is the status code, as defined by {{{AjaxIM.statuses}}}. // * {{{message}}} is the custom status message. status: function(s, message) { // update status icon(s) if(!this.statuses[s]) return; $('#imjs-friends').attr('class', 'imjs-' + s); $(self).trigger('changingStatus', [s, message]); AjaxIM.request( this.actions.status, {'status': this.statuses[s], 'message': message}, function(result) { switch(result.r) { case 'ok': $(self).trigger('changeStatusSuccessful', [s, message]); break; case 'error': default: $(self).trigger('changeStatusFailed', [result.e, s, message]); break; } }, function(error) { $(self).trigger('changeStatusFailed', ['not connected', s, message]); } ); }, // === {{{AjaxIM.}}}**{{{statuses}}}** === // // These are the available status codes and their associated identities: // * {{{offline}}} (0) — Only used when signing out/when another // user has signed out, as once this status is set, the user is removed // from the server and friends will be unable to contact the user. // * {{{available}}} (1) — The user is online and ready to be messaged. // * {{{away}}} (2) — The user is online but is not available. Others // may still contact this user, however, the user may not respond. Anyone // contacting an away user will receive a notice stating that the user is away, // and (if one is set) their custom status message. // * {{{invisible}}} (3; **not yet implemented**) — The user is online, // but other users are made unaware, and the user will be represented // as being offline. It is still possible to contact this user, and for this // user to contact others; no status message or notice will be sent to others // messaging this user. statuses: {offline: 0, available: 1, away: 2, invisible: 3}, // == Footer bar == // // The footer bar is the bar that sits at the bottom of the page, in a fixed // position. It contains a tab for the friends list, and tabs for any open // chat boxes. It is also possible to add custom tabs for other functionality. bar: { // === {{{AjaxIM.}}}**{{{bar.initialize()}}}** === // // Setup the footer bar and enable tab actions. This function // uses {{{jQuery.live}}} to set hooks on any bar tabs created // in the future. initialize: function() { // Set up your standard tab actions $('.imjs-tab') .live('click', this.activateTab); $('.imjs-tab .imjs-close') .live('click', this.closeTab); // Set up the friends list actions var self = this; $(document).click(function(e) { if(e.target.id == 'imjs-friends' || $(e.target).parents('#imjs-friends').length) { return; } if($('#imjs-friends').data('state') == 'active') self.activateTab.call($('#imjs-friends')); }); $('#imjs-friends') .data('state', 'minimized') .click(function(e) { if(!$(this).hasClass('imjs-not-connected') && e.target.id != 'imjs-friends-panel' && !$(e.target).parents('#imjs-friends-panel').length) self.activateTab.call(this); }) .mouseenter(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip').css('display', 'block'); $('.imjs-tooltip p').html(AjaxIM.i18n.notConnectedTip); var tip_left = $(this).offset().left - $('.imjs-tooltip').outerWidth() + ($(this).outerWidth() / 2); var tip_top = $(this).offset().top - $('.imjs-tooltip').outerHeight(true); $('.imjs-tooltip').css({ left: tip_left, top: tip_top }); } }) .mouseleave(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip').css('display', ''); } }); $('#imjs-friends-panel') .data('tab', $('#imjs-friends')) .css('display', 'none'); }, // === {{{AjaxIM.}}}**{{{bar.activateTab()}}}** === // // Activate a tab by setting it to the 'active' state and // showing any related chatbox. If a chatbox is available // for this tab, also focus the input box. // // //Note:// {{{this}}}, here, refers to the tab DOM element. activateTab: function() { var chatbox = $(this).find('.imjs-chatbox') || false; if($(this).data('state') != 'active') { if($(this).attr('id') != 'imjs-friends') { $('#imjs-bar > li') .not($(this)) .not('#imjs-friends') .removeClass('imjs-selected') .each(function() { if($(this).data('state') != 'closed') { $(this).data('state', 'minimized'); var chatbox = $(this).find('.imjs-chatbox'); if(chatbox.length) chatbox.css('display', 'none'); } }); } if(chatbox && chatbox.css('display') == 'none') chatbox.css('display', ''); // set the tab to active... var tab = $(this).addClass('imjs-selected').data('state', 'active'); // ...and hide and reset the notification icon tab.find('.imjs-notification').css('display', 'none') .data('count', 0); if(self.settings.storageMethod && self.storageReady && chatbox && (username = chatbox.data('username'))) { $.jStore.store(self.storeKey + 'activeTab', [username]); } $(self).trigger('tabToggled', ['activated', tab]); } else { var tab = $(this).removeClass('imjs-selected').data('state', 'minimized'); if(chatbox && chatbox.css('display') != 'none') chatbox.css('display', 'none'); if(self.settings.storageMethod && self.storageReady) { $.jStore.store(self.storeKey + 'activeTab', ['*']); } $(self).trigger('tabToggled', ['minimized', tab]); } if(chatbox) { if(!(input = chatbox.find('.imjs-input')).data('height')) { // store the height for resizing later input.data('height', input.height()); } try { var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } catch(e) {} try { chatbox.find('.imjs-input').focus(); } catch(e) {} } }, // === {{{AjaxIM.}}}**{{{bar.closeTab()}}}** === // // Close a tab and hide any related chatbox, such that // the chatbox can not be reopened without reinitializing // the tab. // // //Note:// {{{this}}}, here, refers to the tab DOM element. closeTab: function() { var tab = $(this).parents('.imjs-tab'); tab.css('display', 'none').data('state', 'closed'); if(self.settings.storageMethod && self.storageReady) { delete self.chatstore[tab.find('.imjs-chatbox').data('username')]; if(self.storageReady) $.jStore.store(self.storeKey + 'chats', self.chatstore); } $(self).trigger('tabToggled', ['closed', tab]); self.bar._scrollers(); return false; }, // === {{{AjaxIM.}}}**{{{bar.addTab(label, action, closable)}}}** === // // Adds a tab to the tab bar, with the label {{{label}}}. When // clicked, it will call a callback function, {{{action}}}. If // {{{action}}} is a string, it is assumed that the string is // referring to a chatbox ID. // // ==== Parameters ==== // * {{{label}}} is the text that will be displayed on the tab.\\ // * {{{action}}} is the callback function, if it is a non-chatbox // tab, or a string if it //is// a chatbox tab.\\ // * {{{closable}}} is a boolean value that determines whether or not // it is possible for a user to close this tab. // // //Note:// New tabs are given an automatically generated ID // in the format of {{{#imjs-tab-[md5 of label]}}}. addTab: function(label, action, closable) { var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-right'); tab.removeClass('imjs-default') .attr('id', 'imjs-tab-' + $.md5(label)) .html(tab.html().replace('{label}', label)) .data('state', 'minimized'); var notification = tab.find('.imjs-notification'); notification.css('display', 'none') .data('count', 0) .data('default-text', notification.html()) .html(notification.html().replace('{count}', '0')); if(closable === false) tab.find('.imjs-close').eq(0).remove(); if(typeof action == 'string') { //tab.data('chatbox', action); } else { tab.find('.imjs-chatbox').remove(); tab.click(action); } return tab; }, // === {{{AjaxIM.}}}**{{{bar.notification(tab)}}}** === // // Displays a notification on a tab. Generally, this is called when // a tab is minimized to let the user know that there is an update // for them. The way the notification is displayed depends on the // theme CSS. // // ==== Parameters ==== // * {{{tab}}} is the jQuery-selected tab DOM element. notification: function(tab) { var notify = tab.find('.imjs-notification'); var notify_count = notify.data('count') + 1; notify.data('count', notify_count) .html(notify.data('default-text').replace('{count}', notify_count)) .css('display', ''); }, // === //private// {{{AjaxIM.}}}**{{{bar._scrollers()}}}** === // // Document me! _scrollers: function() { var needScrollers = false; $('.imjs-tab').filter(function() { return $(this).data('state') != 'closed' }).css('display', ''); $.each(self.chats, function(username, chatbox) { var tab = chatbox.data('tab'); if(tab.data('state') == 'closed') return true; if(tab.position().top > $('#imjs-bar').height()) { $('.imjs-scroll').css('display', ''); tab.css('display', 'none'); needScrollers = true; } else { tab.css('display', ''); } }); if(!needScrollers) { $('.imjs-scroll').css('display', 'none'); } if($('#imjs-scroll-left').css('display') != 'none' && $('#imjs-scroll-left').position().top > $('#imjs-bar').height()) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); } var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').slice(-1) .nextAll('#imjs-bar li.imjs-tab:hidden') .not('.imjs-default') .filter(function() { return $(this).data('state') != 'closed' }).length; var hiddenRight = $('#imjs-bar li.imjs-tab:visible').eq(0) .prevAll('#imjs-bar li.imjs-tab:hidden') .not('.imjs-default') .filter(function() { return $(this).data('state') != 'closed' }).length; $('#imjs-scroll-left').html(hiddenLeft); $('#imjs-scroll-right').html(hiddenRight); } }, // == Comet == // // Comet, or HTTP streaming, holds open a connection between the client and // the server indefinitely. As the server receives new messages or events, // they are passed down to the client in a {{{<script>}}} // tag which calls the {{{AjaxIM.incoming()}}} function. The connection is // opened using either an {{{iframe}}} (in Opera or Internet Explorer) or // an {{{XMLHTTPRequest}}} object. Due to the extra flexibility necessary // with the {{{XMLHTTPRequest}}} object, jQuery's {{{$.ajax}}} is not used. comet: { type: '', obj: null, onbeforeunload: null, onreadystatechange: null, iframe: function() { if(/loaded|complete/i.test(this.obj.readyState)) throw new Error("IM server not available!"); }, // === {{{AjaxIM.}}}**{{{comet.connect()}}}** === // // Creates and initializes the object and connection between the server // and the client. For Internet Explorer and Opera, we use an // {{{<iframe>}}} element; for all other browsers, we create an // {{{XMLHTTPRequest}}} object. The server connects to the URI defined // as the "poll" action. This function is called automatically, when // the IM engine is initialized and the {{{AjaxIM.poll()}}} function // is called. connect: function() { var self = _instance; if($.browser.opera || $.browser.msie) { var iframe = $(''); with(iframe) { css({ position: 'absolute', visibility: 'visible', display: 'block', left: '-10000px', top: '-10000px', width: '1px', height: '1px' }); attr('src', self.actions.poll[1]); appendTo('body'); bind('readystatechange', self.comet.onreadystatechange = function() { self.comet.iframe() }); bind('beforeunload', self.comet.onbeforeunload = function() { self.comet.disconnect() }); } self.comet.type = 'iframe'; self.comet.obj = iframe; } else { var xhr = new XMLHttpRequest; var length = 1029; var code = /^\s*]*>parent\.(.+);<\/script>$/; xhr.open('get', self.actions.poll[1], true); xhr.onreadystatechange = function(){ if(xhr.readyState > 2) { if(xhr.status == 200) { responseText = xhr.responseText.substring(length); length = xhr.responseText.length; if(responseText != ' ') eval(responseText.replace(code, "$1")); } // We need an "else" here. If the state changes to // "loaded", the user needs to know they're // disconnected. } }; self.comet.type = 'xhr'; self.comet.obj = xhr; addEventListener('beforeunload', self.comet.beforeunload = function() { self.comet.disconnect(); }, false); setTimeout(function() { xhr.send(null) }, 10); } }, // === {{{AjaxIM.}}}**{{{comet.disconnect()}}}** === // // Disconnect from the server and destroy the connection object. This // function is called before the page unloads, so that we plug up and // potential leaks and free memory. disconnect: function() { var self = _instance.comet; if(!this.type || !this.obj) return; if(this.type == 'iframe') { detachEvent('onreadystatechange', this.onreadystatechange); detachEvent('onbeforeunload', this.onbeforeunload); this.obj.src = '.'; $(this.obj).remove(); } else { removeEventListener('beforeunload', this.onbeforeunload, false); this.obj.onreadystatechange = function(){}; this.obj.abort(); } delete this.obj; } } }) self.bar.initialize(); if(prequeue.length) { $.each(prequeue, function() { var func = this[0].split('.'); if(func.length > 1) self[func[0]][func[1]].apply(self, this[1]); else self[func[0]].apply(self, this[1]); }); } }); // == Static functions and variables == // // The following functions and variables are available outside of an initialized // {{{AjaxIM}}} object. // === {{{AjaxIM.}}}**{{{client}}}** === // // Once {{{AjaxIM.init()}}} is called, this will be set to the active AjaxIM // object. Only one AjaxIM object instance can exist at a time. This variable // can and should be accessed directly. AjaxIM.client = null; // === {{{AjaxIM.}}}**{{{init(options, actions)}}}** === // // Initialize the AjaxIM client object and engine. Here, you can define your // options and actions as outlined at the top of this documentation. // // ==== Parameters ==== // * {{{options}}} is the hash of custom settings to initialize Ajax IM with. // * {{{actions}}} is the hash of any custom action URLs. AjaxIM.init = function(options, actions) { if(!_instance) { _instance = new AjaxIM(options, actions); AjaxIM.client = _instance; } return _instance; } // === {{{AjaxIM.}}}**{{{request(url, data, successFunc, failureFunc)}}}** === // // Wrapper around {{{$.jsonp}}}, the JSON-P library for jQuery, and {{{$.ajax}}}, // jQuery's ajax library. Allows either function to be called, automatically, // depending on the request's URL array (see {{{AjaxIM.actions}}}). // // ==== Parameters ==== // {{{url}}} is the URL of the request. // {{{data}}} are any arguments that go along with the request. // {{{success}}} is a callback function called when a request has completed // without issue. // {{{_ignore_}}} is simply to provide compatability with {{{$.post}}}. // {{{failure}}} is a callback function called when a request hasn't not // completed successfully. AjaxIM.request = function(url, data, successFunc, failureFunc) { if(typeof failureFunc != 'function'); failureFunc = function(){}; $[url[0]]({ 'url': url[1], 'data': data, dataType: (url[0] == 'ajax' ? 'json' : 'jsonp'), type: 'POST', cache: false, timeout: 60000, callback: 'jsonp' + (new Date()).getTime(), success: function(json, textStatus) { successFunc(json); }, error: function(xOptions, error) { failureFunc(error); } }); // This prevents Firefox from spinning indefinitely // while it waits for a response. Why? Fuck if I know. if(url[0] == 'jsonp' && $.browser.mozilla) { $.jsonp({ 'url': 'about:', timeout: 0 }); } }; // === {{{AjaxIM.}}}**{{{incoming(data)}}}** === // // Never call this directly. It is used as a connecting function between // client and server for Comet. // // //Note:// There are two {{{AjaxIM.incoming()}}} functions. This one is a // static function called outside of the initialized AjaxIM object; the other // is only called within the initalized AjaxIM object. AjaxIM.incoming = function(data) { if(!_instance) return false; if(data.length) _instance._parseMessages(data); } // === {{{AjaxIM.}}}**{{{loaded}}}** === // // If Ajax IM has been loaded with the im.load.js file, this function will be // called when the library is finally loaded and ready for use. Similar to // jQuery's $(document).ready(), but for Ajax IM. AjaxIM.loaded = function() { if(typeof AjaxIMLoadedFunction == 'function') { AjaxIMLoadedFunction(); delete AjaxIMLoadedFunction; // clean up the global namespace } }; // === {{{AjaxIM.}}}**{{{dateFormat([date,] [mask,] utc)}}}** === // // Date Format 1.2.3\\ // © 2007-2009 Steven Levithan ([[http://blog.stevenlevithan.com/archives/date-time-format|stevenlevithan.com]])\\ // MIT license // // Includes enhancements by Scott Trenda // and Kris Kowal ([[http://cixar.com/~kris.kowal/|cixar.com/~kris.kowal/]]) // // Accepts a date, a mask, or a date and a mask and returns a formatted version // of the given date. // // ==== Parameters ==== // * {{{date}}} is a {{{Date()}}} object. If not specified, the date defaults to the // current date/time. // * {{{mask}}} is a string that defines the formatting of the date. Formatting // options can be found in the // [[http://blog.stevenlevithan.com/archives/date-time-format|Date Format]] // documentation. If not specified, the mask defaults to {{{dateFormat.masks.default}}}. AjaxIM.dateFormat = function () { var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, timezone = new RegExp('\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) ' + '(?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b', 'g'), timezoneClip = /[^-+\dA-Z]/g, pad = function (val, len) { val = String(val); len = len || 2; while (val.length < len) val = "0" + val; return val; }; // Regexes and supporting functions are cached through closure return function (date, mask, utc) { var dF = AjaxIM.dateFormat; // You can't provide utc if you skip other args (use the "UTC:" mask prefix) if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { mask = date; date = undefined; } // Passing date through Date applies Date.parse, if necessary date = date ? new Date(date) : new Date; if (isNaN(date)) throw SyntaxError("invalid date"); mask = String(dF.masks[mask] || mask || dF.masks["default"]); // Allow setting the utc argument via the mask if (mask.slice(0, 4) == "UTC:") { mask = mask.slice(4); utc = true; } var _ = utc ? "getUTC" : "get", d = date[_ + "Date"](), D = date[_ + "Day"](), m = date[_ + "Month"](), y = date[_ + "FullYear"](), H = date[_ + "Hours"](), M = date[_ + "Minutes"](), s = date[_ + "Seconds"](), L = date[_ + "Milliseconds"](), o = utc ? 0 : date.getTimezoneOffset(), flags = { d: d, dd: pad(d), ddd: AjaxIM.i18n.dayNames[D], dddd: AjaxIM.i18n.dayNames[D + 7], m: m + 1, mm: pad(m + 1), mmm: AjaxIM.i18n.monthNames[m], mmmm: AjaxIM.i18n.monthNames[m + 12], yy: String(y).slice(2), yyyy: y, h: H % 12 || 12, hh: pad(H % 12 || 12), H: H, HH: pad(H), M: M, MM: pad(M), s: s, ss: pad(s), l: pad(L, 3), L: pad(L > 99 ? Math.round(L / 10) : L), t: H < 12 ? "a" : "p", tt: H < 12 ? "am" : "pm", T: H < 12 ? "A" : "P", TT: H < 12 ? "AM" : "PM", Z: utc ? "UTC" : (String(date).match(timezone) || [""]) .pop().replace(timezoneClip, ""), o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] }; return mask.replace(token, function ($0) { return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); }); }; }(); // Some common format strings AjaxIM.dateFormat.masks = { "default": "ddd mmm dd yyyy HH:MM:ss", shortDate: "m/d/yy", mediumDate: "mmm d, yyyy", longDate: "mmmm d, yyyy", fullDate: "dddd, mmmm d, yyyy", shortTime: "h:MM TT", mediumTime: "h:MM:ss TT", longTime: "h:MM:ss TT Z", isoDate: "yyyy-mm-dd", isoTime: "HH:MM:ss", isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" }; // === {{{AjaxIM.}}}**{{{i18n}}}** === // // Text strings used by Ajax IM. Should you want to translate Ajax IM into // another language, merely change these strings. // // {{{%s}}} denotes text that will be automatically replaced when the string is // used. AjaxIM.i18n = { dayNames: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], monthNames: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], chatOffline: '%s signed off.', chatAvailable: '%s became available.', chatAway: '%s went away.', notConnected: 'You are currently not connected or the server is not available. ' + 'Please ensure that you are signed in and try again.', notConnectedTip: 'You are currently not connected.', authInvalid: 'Invalid username or password.', registerPasswordLength: 'Passwords must be more than 4 characters in length.', registerUsernameLength: 'Usernames must be more than 2 characters in length and ' + ' contain only A-Z, a-z, 0-9, underscores (_) and periods (.).', registerPasswordMatch: 'Entered passwords do not match.', registerUsernameTaken: 'The chosen username is already in use; please choose another.', registerUnknown: 'An unknown error occurred. Please try again.' } })(jQuery, false); \ No newline at end of file +// = im.js = +// +// **Copyright © 2005 – 2010 Joshua Gross**\\ +// //MIT Licensed// +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// This is the main library for Ajax IM. It encompasses the UI controls, +// and connecting with the server. It does //not// handle registration or +// account management. + +(function($, _instance) { + AjaxIM = function(options, actions) { + if(this instanceof AjaxIM) { + var self = this; + + // === {{{ defaults }}} === + // + // These are the available settings for Ajax IM, and the associated + // defaults: + // + // * {{{pollType}}} determines the way in which the client will talk to + // the server. + // ** {{{comet}}} will use http streaming, wherein a connection to the server will be held open indefinitely, and the server will push down new events (messages, status updates) as they occur. + // ** {{{long}}} will hold open a connection with the server for as long as possible, or until a new event is received. Upon the server sending an event or closing the connection, the client will automatically—and immediately—reconnect. + // ** {{{short}}} will open a connection, and the server (if this method is supported) will //immediately// provide a response as to whether or not there are any new events. Once a response is received, the client will wait 5 seconds, and then reconnect. + // * {{{pollServer}}} is the default URL to which all actions refer. It is + // possible to specify certain action URLs separately (as is used with the + // NodeJS server). + // * {{{cookieName}}} is the name of the session cookie used by the server. + // If this is not set properly, the IM engine will not be able to automatically + // reinitialize sessions. + // * {{{theme}}} is the name of the theme folder that defines the HTML and + // CSS of the IM bar and chat boxes. Usually, themes are deposited in the + // provided "themes" folder and specified by that path, e.g. {{{themes/default}}}. + // Theme files within the theme folder must be named {{{theme.html}}} and + // {{{theme.css}}}. + // * {{{storageMethod}}} defines the way in which data (chat sessions) are + // temporarily stored client-side. By default, {{{"flash"}}} is used because + // it is the most widely supported method. However, + // [[http://eric.garside.name/docs.html?p=jstore#js-engines|other storage engines]] + // are available, with their respective up- and down-sides + // outlined, on the jStore website. + // * {{{storeSession}}} (**not implemented**) sets the number of days to + // retain stored chat session data before it should be deleted. + // * {{{checkResume}}} is a flag that sets whether or not the client should + // make a call to the server before resuming the session (such as on a page + // reload). This will ensure that the session has not expired. If set to {{{false}}}, + // a call to the server will not be made, and the session will be assumed to + // be active. + var defaults = { + pollType: 'long', + pollServer: './ajaxim.php', + cookieName: 'ajaxim_session', + theme: 'themes/default', + storageMethod: 'auto', + flashStorage: 'js/jStore.Flash.html', + storeSession: 5, // number of days to store chat data (0 for current session only) + checkResume: true + }; + + // === {{{AjaxIM.}}}**{{{settings}}}** === + // + // These are the settings for the IM. If particular options are not specified, + // the defaults (see above) will be used. //These options will be defined + // upon calling the initialization function, and not set directly.// + this.settings = $.extend(defaults, options); + + // === {{{AjaxIM.}}}**{{{actions}}}** === + // + // Each individual action that the IM engine can execute is predefined here. + // By default, it merely appends the action onto the end of the {{{pollServer}}} url, + // however, it is possible to define actions individually. //The alternative actions + // will be defined upon calling the initialization function, and not set directly.// + // + // Should you define an action at a different URL, Ajax IM will determine whether + // or not this URL is within the current domain. If it is within a subdomain of + // the current domain, it will set the document.domain variable for you, + // to match a broader hostname scope; the action will continue to use {{{$.post}}} + // (the default AJAX method for Ajax IM). + // + // On the other hand, should you choose a URL outside the current domain + // Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid + // cross-domain scripting issues. This means that a server on a different + // port or at a different address will need to be able to handle GET + // requests rather than POST requests (such as how the Node.JS Ajax IM + // server works). + this.actions = $.extend({ + login: this.settings.pollServer + '/login', + logout: this.settings.pollServer + '/logout', + register: this.settings.pollServer + '/register', + poll: this.settings.pollServer + '/poll?method=' + this.settings.pollType, + send: this.settings.pollServer + '/send', + status: this.settings.pollServer + '/status', + resume: this.settings.pollServer + '/resume' + }, actions); + + var httpRx = new RegExp('^http://', 'i'), + slashslashRx = new RegExp('^//', 'i'), + queryStrRx = new RegExp('[?](.+)$'), + subdomainRx = new RegExp('((http[s]?:)?//)?(.+?)[.]' + window.location.host, 'i'); + $.each(this.actions, function(name, action) { + if(name == 'poll') { + if(self.settings.pollType != 'comet') + action += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; + + action = ['jsonp', action]; + } else { + action = ['ajax', action]; + if(subdomainRx.test(action[1])) { + document.domain = '.' + window.location.host; + } else if((http = httpRx.test(action[1])) || slashslashRx.test(action[1])) { + if(!(new RegExp('//' + window.location.host + '/', 'i')).test(action[1])) { + action[1] += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; + action = ['jsonp', action[1]]; + } + } + } + + self.actions[name] = action; + }); + + // We load the theme dynamically based on the passed + // settings. If the theme is set to false, we assume + // that the user is going to load it himself. + this.themeLoaded = false; + if(this.settings.theme) { + $('
').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip', + function() { + self.themeLoaded = true; + setup.apply(self); + } + ); + if(typeof document.createStyleSheet == 'function') + document.createStyleSheet(this.settings.theme + '/theme.css'); + else + $('body').append(''); + } else { + this.themeLoaded = true; + setup.apply(this); + } + + // Client-side storage for keeping track of + // conversation states, active tabs, etc. + // The default is flash storage, however, other + // options are available via the jStore library. + if(this.settings.storageMethod) { + this.storageBrowserKey = 'unknown'; + this.storeKey = ''; + + $.each($.browser, function(k, v) { + if(k == 'version') return true; + else if(v == true) { + self.storageBrowserKey = k; + return false; + } + }); + + if(this.settings.storageMethod == 'auto') { + $.each(['ie', 'html5', 'local', 'flash'], function() { + if($.jStore.Availability[this]()) { + self.settings.storageMethod = this; + return false; + } + }); + } + + $.extend($.jStore.defaults, { + project: 'im.js', + engine: this.settings.storageMethod, + flash: this.settings.flashStorage + }); + + this.storageReady = false; + + if(this.settings.storageMethod == 'flash') { + $.jStore.ready(function(engine) { + $.jStore.flashReady(function() { + self.storageReady = true; + setup.apply(self); + }); + }) + } else { + $.jStore.ready(function(engine) { + self.storageReady = true; + setup.apply(self); + }); + } + + $.jStore.load(); + } else { + this.storageReady = true; + setup.apply(this); + } + + // Allow a chatbox to me minimized + $('.imjs-chatbox').live('click', function(e) { + e.preventDefault(); + return false; + }); + + $('.imjs-chatbox .imjs-minimize').live('click', function() { + $(this).parents('.imjs-chatbox').data('tab').click(); + }); + + // Allow a chatbox to be closed + $('.imjs-chatbox .imjs-close').live('click', function() { + var chatbox = $(this).parents('.imjs-chatbox'); + chatbox.data('tab') + .data('state', 'closed').css('display', 'none'); + + if(self.settings.storageMethod && self.storageReady) { + delete self.chatstore[chatbox.data('username')]; + if(self.storageReady) { + $.jStore.store( + self.storageBrowserKey + '-' + + self.username + '-chats', + self.chatstore); + } + } + }); + + // Setup message sending for all chatboxes + $('.imjs-chatbox .imjs-input').live('keydown', function(event) { + var obj = $(this); + if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) { + self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); + } + }).live('keyup', function(event) { + if(event.keyCode == 13) { + if($.browser.msie && $.browser.version < 8) { + var obj = $(this); + self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); + } + + var obj = $(this); + obj.val(''); + obj.height(obj.data('height')); + } + }).live('keypress', function(e) { + var obj = $(this); + if(!($.browser.msie && $.browser.opera)) obj.height(0); + if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) { + obj.height(this.scrollHeight); + } + }); + + $('.imjs-msglog').live('click', function() { + var chatbox = $(this).parents('.imjs-chatbox'); + chatbox.find('.imjs-input').focus(); + }); + + // Create a chatbox when a buddylist item is clicked + $('.imjs-friend').live('click', function() { + var chatbox = self._createChatbox($(this).data('friend')); + + if(chatbox.data('tab').data('state') != 'active') + chatbox.data('tab').click(); + + chatbox.find('.imjs-input').focus(); + }); + + // Setup and hide the scrollers + $('.imjs-scroll').css('display', 'none'); + $('#imjs-scroll-left').live('click', function() { + var hiddenTab = $('#imjs-bar li.imjs-tab:visible').slice(-1) + .next('#imjs-bar li.imjs-tab:hidden') + .filter(function() { + return $(this).data('state') != 'closed' + }) + .not('.imjs-default').slice(-1).css('display', ''); + + if(hiddenTab.length) { + $('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none'); + $(this).html(parseInt($(this).html()) - 1); + $('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1); + } + + return false; + }); + $('#imjs-scroll-right').live('click', function() { + var hiddenTab = $('#imjs-bar li.imjs-tab:visible').eq(0) + .prev('#imjs-bar li.imjs-tab:hidden') + .filter(function() { + return $(this).data('state') != 'closed' + }) + .not('.imjs-default').slice(-1).css('display', ''); + + if(hiddenTab.length) { + $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); + $(this).html(parseInt($(this).html()) - 1); + $('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1); + } + + return false; + }); + + // Initialize the chatbox hash + this.chats = {}; + + // Try to resume any existing session + this.resume(); + + $(window).resize(function() { + self.bar._scrollers(); + }); + } else + return AjaxIM.init(options); + }; + + // We predefine all public functions here... + // If they are called before everything (theme, storage engine) has loaded, + // then they get put into a "prequeue" and run when everything *does* + // finally load. + // + // This ensures that nothing loads without all of the principal components + // being pre-loaded. If that were to occur (without this prequeue), things + // would surely break. + var prequeue = []; + var empty = function() { + var func = this; + return function() { prequeue.push([func, arguments]) }; + }; + $.extend(AjaxIM.prototype, { + settings: {}, + friends: {}, + chats: {}, + + storage: empty.apply('storage'), + login: empty.apply('login'), + logout: empty.apply('logout'), + resume: empty.apply('resume'), + form: empty.apply('form'), + poll: empty.apply('poll'), + incoming: empty.apply('incoming'), + send: empty.apply('send'), + status: empty.apply('status'), + statuses: {}, + bar: { + initialize: empty.apply('bar.initialize'), + activateTab: empty.apply('bar.activateTab'), + closeTab: empty.apply('bar.closeTab'), + addTab: empty.apply('bar.addTab'), + notification: empty.apply('bar.notification'), + _scrollers: empty.apply('bar._scrollers') + } + }); + + setup = (function() { + var self = this; + if(!this.storageReady || !this.themeLoaded) return; + + $(self).trigger('loadComplete'); + $(window).resize(); + + $.extend(AjaxIM.prototype, { + // == Cookies == + // + // The "cookies" functions can be used to set, get, and erase JSON-based cookies. + // These functions are primarily used to manage and read the server-set cookie + // that handles the user's chat session ID. + cookies: { + // === {{{AjaxIM.}}}**{{{cookies.set(name, value, days)}}}** === + // + // Sets a cookie, stringifying the JSON value upon storing it. + // + // ==== Parameters ==== + // * {{{name}}} is the cookie name.\\ + // * {{{value}}} is the cookie data that you would like to store.\\ + // * {{{days}}} is the number of days that the cookie will be stored for. + set: function(name, value, days) { + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + var expires = "; expires=" + date.toGMTString(); + } else var expires = ""; + document.cookie = name + "=" + $.compactJSON(value) + expires + "; path=/"; + }, + + // === {{{AjaxIM.}}}**{{{cookies.get(name)}}}** === + // + // Gets a cookie, decoding the JSON value before returning the data. + // + // ==== Parameters ==== + // * {{{name}}} is the cookie name that you would like to retrieve. + get: function(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) { + var cval = decodeURIComponent(c.substring(nameEQ.length, c.length)); + return $.secureEvalJSON(cval); + } + } + return null; + }, + + // === {{{AjaxIM.}}}**{{{cookies.erase(name)}}}** === + // + // Deletes a cookie. + // + // {{{name}}} is the existing cookie that you would like to delete. + erase: function(name) { + self.cookies.set(name, '', -1); + } + }, + + // == Main == + + // === {{{AjaxIM.}}}**{{{storage()}}}** === + // + // Retrieves chat session data from whatever storage engine is enabled + // (provided that one is enabled at all). If a page reloads, this function + // is called to restore the user's chat state (existing conversations, active tab). + // This function is called //automatically//, upon initialization of the IM engine. + storage: function() { + if(!self.storeKey.length) return; + + try { + var chatstore = $.jStore.store(self.storeKey + 'chats') || {}; + } catch(e) { + $.jStore.remove(self.storeKey + 'chats'); + var chatstore = {}; + } + + if(this.chatstore) { + $.each(this.chatstore, function(username, convo) { + if(username in chatstore) + chatstore[username] = $.merge(chatstore[username], self.chatstore[username]); + else + chatstore[username] = self.chatstore[username]; + }); + + this.chatstore = chatstore; + $.jStore.store(self.storeKey + 'chats', chatstore); + } else { + this.chatstore = chatstore; + } + + $.each(this.chatstore, function(username, convo) { + if(!convo.length) return; + + var chatbox = self._createChatbox(username, true); + chatbox.data('lastDateStamp', null).css('display', 'none'); + + // Remove the automatic date stamp + chatbox.find('.imjs-msglog').empty(); + + // Restore all messages, date stamps, and errors + $.each(convo, function() { + switch(this[0]) { + case 'error': + self._addError(chatbox, decodeURIComponent(this[2]), this[3]); + break; + + case 'datestamp': + self._addDateStamp(chatbox, this[3]); + break; + + case 'a': + case 'b': + self._addMessage(this[0], chatbox, this[1], + decodeURIComponent(this[2]), this[3]); + break; + } + }); + + $(self).trigger('chatRestored', [username, chatbox]); + }); + + var activeTab = $.jStore.store(self.storeKey + 'activeTab') || []; + if(activeTab.length && (activeTab = activeTab[0]) && activeTab in this.chats) { + this.chats[activeTab].data('tab').click(); + var msglog = this.chats[activeTab].find('.imjs-msglog'); + msglog[0].scrollTop = msglog[0].scrollHeight; + } + }, + + // === {{{AjaxIM.}}}**{{{login(username, password)}}}** === + // + // Authenticates a user and initializes the IM engine. If the user is + // already logged in, [s]he is logged out, the session is cleared, and + // the new user is logged in. + // + // Returns the user's properly formatted username, session ID, and online + // friends in JSON, if successful; e.g.:\\ + // {{{ {u: 'username', s: 'longsessionid', f: [{u: 'friend', s: 1, g: 'group'}]} }}} + // + // If unsuccessful, {{{false}}} is returned. + // + // ==== Parameters ==== + // * {{{username}}} is the user's username.\\ + // * {{{password}}} is the user's password. This password will be MD5 hashed + // before it is sent to the server. + login: function(username, password) { + if(!username) username = ''; + if(!password) password = ''; + + if(this.username) + return true; // Already logged in! + + // hash password before sending it to the server + password = $.md5(password); + + // authenticate + AjaxIM.request( + this.actions.login, + {'username': username, 'password': password}, + function(auth) { + if(auth.r == 'logged in') { + self.username = ('u' in auth ? auth.u : username); + + if(self.settings.storageMethod) + self.storeKey = [self.storageBrowserKey, self.username, ''].join('-'); + + var existing = self.cookies.get(self.settings.cookieName); + self.storage(); + + // Begin the session + $.each(auth.f, function() { + self.friends[this.u] = {status: [this.s, ''], group: this.g}; + }); + self._session(self.friends); + self._storeFriends(); + + $(self).trigger('loginSuccessful', [auth]); + + return auth; + } else { + $(self).trigger('loginError', [auth]); + } + + return false; + } + ); + }, + + // === {{{AjaxIM.}}}**{{{logout()}}}** === + // + // Logs the user out and removes his/her session cookie. As well, it + // will close all existing chat windows, clear the storage engine, and + // remove the IM bar. + logout: function() { + AjaxIM.request( + this.actions.logout, + {}, + function(done) { + if(done) { + self.cookies.erase(self.settings.cookieName); + self._clearSession(); + $(self).trigger('logoutSuccessful'); + return true; + } else { + $(self).trigger('logoutError'); + return false; + } + } + ); + }, + + // === {{{AjaxIM.}}}**{{{resume()}}}** === + // + // Resumes an existing session based on a session ID stored in the + // server-set cookie. This function is called //automatically// upon IM + // engine (re-)initialization, so the user does not need to re-login + // should a session already exist. + resume: function() { + var session = this.cookies.get(this.settings.cookieName); + + if(session && session.sid) { + this.username = session.user; + + this.storeKey = [this.storageBrowserKey, this.username, ''].join('-'); + + var friends = $.jStore.store(this.storeKey + 'friends') || []; + if(self.settings.checkResume) { + AjaxIM.request( + this.actions.resume, + {}, + function(response) { + if(response.r == 'connected') { + self._session(friends); + self.storage(); + } else { + var username = this.username; + self._clearSession(); + $(self).trigger('sessionNotResumed', [username]); + } + } + ); + } else { + self._session(friends); + self.storage(); + } + } else { + $(self).trigger('noSession'); + } + }, + + // === //private// {{{AjaxIM.}}}**{{{_session(friends)}}}** === + // + // Restores session data (username, friends) and begins polling the server. + // Called only by {{{AjaxIM.resume()}}}. + // + // ==== Parameters ==== + // * {{{friends}}} is a list of "friend" objects, e.g.:\\ + // {{{[{u: 'friend', s: 1, g: 'group'}, ...]}}} + // ** {{{u}}} being the friend's username. + // ** {{{s}}} being one of the available status codes (see {{{AjaxIM.statuses}}}), depending on the friend's current status. + // ** {{{g}}} being the group that the friend is in. + _session: function(friends) { + $('#imjs-friends-panel .imjs-header span').html(this.username); + $('#imjs-friends').removeClass('imjs-not-connected'); + + $.each(friends, function(friend, info) { + self.addFriend.apply(self, [friend, info.status, info.group]); + }); + self._storeFriends(); + + $(self).trigger('sessionResumed', [this.username]); + + setTimeout(function() { self.poll(); }, 0); + }, + + // === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** === + // + // Clears all session data from the last known user. + _clearSession: function() { + if(self.settings.storageMethod && self.storageReady) { + $.jStore.remove(self.storeKey + 'chats'); + $.jStore.remove(self.storeKey + 'friends'); + $.jStore.remove(self.storeKey + 'activeTab'); + } + + self.chats = {}; + $('.imjs-tab').not('.imjs-tab.imjs-default').remove(); + + self.cookies.erase('ajaxim_session'); + delete self.username; + }, + + // === {{{AjaxIM.}}}**{{{form(element)}}}** === + // + // Loads a login and registration form into the specified element + // or, if no element is supplied, to the location on the page from + // which this function was called. + form: function(element) { + $(element).load(this.settings.theme + '/theme.html #imjs-lr', function() { + $('#imjs-lr .error').hide(); + + if(self.username) { + $('#imjs-register, #imjs-login fieldset').hide(); + $('#imjs-logged-in') + .show() + .html($('#imjs-logged-in').html().replace('{username}', self.username)); + } else { + $('#imjs-logged-in').hide(); + } + + // Handle logout success + $(self).bind('logoutSuccessful', function() { + $('#imjs-login fieldset').slideDown(); + $('#imjs-register').slideDown(); + + $('#imjs-logged-in') + .html($('#imjs-logged-in strong').html('{username}')) + .slideUp(); + }); + $('#imjs-logged-in a').click(function() { + self.logout(); + return false; + }); + + // Handle login error or success + $(self).bind('loginError', function() { + $('#imjs-login .error').html(AjaxIM.i18n.authInvalid).slideDown('fast'); + $('#imjs-login input') + .addClass('imjs-lr-error') + .blur(function() { $(this).removeClass('imjs-lr-error'); }); + }).bind('loginSuccessful', function() { + $('#imjs-login fieldset').slideUp(); + $('#imjs-register').slideUp(); + + $('#imjs-logged-in') + .html($('#imjs-logged-in').html().replace('{username}', self.username)) + .slideDown(); + }); + var login = function() { + self.login($('#imjs-login-username').val(), $('#imjs-login-password').val()); + return false; + }; + $('#imjs-login').submit(login); + $('#imjs-login-submit').click(login); + + var regIssues = false; + var regError = function(error, fields) { + $('#imjs-register .error') + .append(AjaxIM.i18n['register' + error] + ' ') + .slideDown(); + $(fields) + .addClass('imjs-lr-error') + .blur(function() { $(this).removeClass('imjs-lr-error'); }); + regIssues = true; + }; + + var register = function() { + $('#imjs-register .error').empty(); + + regIssues = false; + + var username = $('#imjs-register-username').val(); + var password = $('#imjs-register-password').val(); + + if(password.length < 4) + regError('PasswordLength', '#imjs-register-password'); + + if(password != $('#imjs-register-cpassword').val()) + regError('PasswordMatch', '#imjs-register-password, #imjs-register-cpassword'); + + if(username.length <= 2 || + !$('#imjs-register-username').val().match(/^[A-Za-z0-9_.]+$/)) + regError('UsernameLength', '#imjs-register-username'); + + if(!regIssues) { + AjaxIM.request( + self.actions.register, + {username: username, password: password}, + function(response) { + if(response.r == 'registered') { + self.login(username, password); + } else if(response.r == 'error') { + switch(response.e) { + case 'unknown': + regError('Unknown', ''); + break; + + case 'invalid password': + regError('PasswordLength', '#imjs-register-password'); + break; + + case 'invalid username': + regError('UsernameLength', '#imjs-register-username'); + break; + + case 'username taken': + regError('UsernameTaken', '#imjs-register-username'); + break; + } + } + }, + function(error) { + regError('Unknown', ''); + } + ); + } + + return false; + }; + $('#imjs-register').submit(register); + $('#imjs-register-submit').click(register); + }); + }, + + // === {{{AjaxIM.}}}**{{{poll()}}}** === + // + // Queries the server for new messages. If a 'long' or 'short' poll + // type is used, jQuery's {{{$.post}}} or {{{$.getJSON}}} will be + // used. If 'comet' is used, the server connection will be deferred + // to the comet set of functions. + poll: function() { + if(/^(short|long)$/.test(this.settings.pollType)) { + AjaxIM.request( + this.actions.poll, + {}, + function(response) { + if(!response['e']) { + if(response.length) + self._parseMessages(response); + + setTimeout(function() { self.poll(); }, 0); + } else { + switch(response.e) { + case 'no session found': + self._notConnected(); + break; + } + + $(self).trigger('pollFailed', [response.e]); + } + }, + function(error) { + self._notConnected(); + $(self).trigger('pollFailed', ['not connected']); + // try reconnecting? + } + ); + } else if(this.settings.pollType == 'comet') { + this.comet.connect(); + } + }, + + // === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** === + // + // Handles an incoming message array:\\ + // {{{[{t: 'type', s: 'sender', r: 'recipient', m: 'message'}, ...]}}} + // + // * {{{t}}} (message type) is one of: + // ** {{{m}}} — a standard message + // ** {{{s}}} — a user's status update + // ** {{{b}}} — a broadcasted message (sent to many users simultaneously) + // * {{{s}}} is the sender of the message. + // * {{{r}}} is the intended recipient of the message. Most of the time, this will + // simply be the logged in user, however, a broadcasted message may not specify + // a recipient or may specify a different recipient. Also provides future + // compatability for chatrooms. + // * {{{m}}} is the actual message. For something such as a status update, this can + // be a JSON object or parsable string; e.g. {{{"2:I'm away."}}} + // + // ==== Parameters ==== + // * {{{messages}}} is the message array + _parseMessages: function(messages) { + if($.isArray(messages)) { + $.each(messages, function() { + $(self).trigger('parseMessage', [this]); + + switch(this.t) { + case 'm': + self.incoming(this.s, this.m); + break; + + case 's': + var status = this.m.split(':'); + if(this['g']) + self.addFriend(this.s, status, this.g); + self._friendUpdate(this.s, status[0], status.slice(1).join(':')); + self._storeFriends(); + break; + + case 'b': + break; + + default: + break; + } + }); + } + }, + + // === {{{AjaxIM.}}}**{{{incoming(from, message)}}}** === + // + // Handles a new message from another user. If a chatbox for that + // user does not yet exist, one is created. If it does exist, but + // is minimized, the user is notified but the chatbox is not brought + // to the front. This function also stores the message, if a storage + // method is set. + // + // ==== Parameters ==== + // * {{{from}}} is the username of the sender. + // * {{{message}}} is the body. + incoming: function(from, message) { + // check if IM exists, otherwise create new window + // TODO: If friend is not on the buddylist, + // should add them to a temp list? + var chatbox = this._createChatbox(from); + + if(!$('#imjs-bar .imjs-selected').length) { + chatbox.data('tab').click(); + } else if(chatbox.data('tab').data('state') != 'active') { + this.bar.notification(chatbox.data('tab')); + } + + var time = this._addMessage('b', chatbox, from, message); + this._storeMessage('b', chatbox, from, message, time); + }, + + // === {{{AjaxIM.}}}**{{{addFriend(username, group)}}}** === + // + // Inserts a new friend into the friends list. If the group specified + // doesn't exist, it is created. If the friend is already in this group, + // they aren't added again, however, the friend item is returned. + // + // ==== Parameters ==== + // * {{{username}}} is the username of the new friend. + // * {{{status}}} is the current status of the friend. + // * {{{group}}} is the user group to which the friend should be added. + addFriend: function(username, status, group) { + var status_name = 'available'; + $.each(this.statuses, + function(key, val) { if(status[0] == val) { status_name = key; return false; } }); + + var group_id = 'imjs-group-' + $.md5(group); + + if(!(group_item = $('#' + group_id)).length) { + var group_item = $('.imjs-friend-group.imjs-default').clone() + .removeClass('imjs-default') + .attr('id', group_id) + .data('group', group) + .appendTo('#imjs-friends-list'); + + var group_header = group_item.find('.imjs-friend-group-header'); + group_header.html(group_header.html().replace('{group}', group)); + } + + var user_id = 'imjs-friend-' + $.md5(username + group); + + if(!$('#' + user_id).length) { + var user_item = group_item.find('ul li.imjs-default').clone() + .removeClass('imjs-default') + .addClass('imjs-' + status_name) + .attr('id', user_id) + .data('friend', username) + .appendTo(group_item.find('ul')); + if(status[0] == 0) user_item.hide(); + user_item.html(user_item.html().replace('{username}', username)); + } + + this.friends[username] = {'status': status, group: group}; + + this._updateFriendCount(); + + return this.friends[username]; + }, + + // === //private// {{{AjaxIM.}}}**{{{_updateFriendCount()}}}** === + // + // Counts the number of online friends and updates the friends count + // in the friend tab. + _updateFriendCount: function() { + var friendsLength = 0; + for(var f in this.friends) { + if(this.friends[f].status[0] != 0) + friendsLength++; + } + $('#imjs-friends .imjs-tab-text span span').html(friendsLength); + }, + + // === //private// {{{AjaxIM.}}}**{{{_storeFriends()}}}** === + // + // If a storage method is enabled, the current state of the + // user's friends list is stored. + _storeFriends: function() { + if(this.settings.storageMethod && this.storageReady) + $.jStore.store(this.storeKey + 'friends', this.friends); + }, + + // === //private// {{{AjaxIM.}}}**{{{_createChatbox(username)}}}** === + // + // Builds a chatbox based on the default chatbox HTML and CSS defined + // in the current theme. Should a chatbox for this user already exist, + // a new one is not created. Instead, it is either given focus (should + // no other windows already have focus), or a notification is issued. + // + // As well, if the chatbox does not exist, an associated tab will be + // created. + // + // ==== Parameters ==== + // * {{{username}}} is the name of the user for whom the chatbox is intended + // for. + // * {{{no_stamp}}} sets whther or not to add a date stamp to the chatbox + // upon creation. + // + // //Note:// New chatboxes are given an automatically generated ID in the + // format of {{{#imjs-[md5 of username]}}}. + _createChatbox: function(username, no_stamp) { + var chatbox_id = 'imjs-' + $.md5(username); + if(!(chatbox = $('#' + chatbox_id)).size()) { + // add a tab + var tab = this.bar.addTab(username, '#' + chatbox_id); + var chatbox = tab.find('.imjs-chatbox'); + chatbox.attr('id', chatbox_id); + + chatbox.data('tab', tab); + + // remove default items from the message log + var message_log = chatbox.find('.imjs-msglog').empty(); + + // setup the chatbox header + var cb_header = chatbox.find('.imjs-header'); + cb_header.html(cb_header.html().replace('{username}', username)); + + if(!no_stamp) { + // add a date stamp + var time = this._addDateStamp(chatbox); + this._storeNonMessage('datestamp', username, null, time); + } + + // associate the username with the object and vice-versa + this.chats[username] = chatbox; + chatbox.data('username', username); + + // did this chatbox fall down? + this.bar._scrollers(); + + if(username in this.friends) { + status = this.friends[username].status; + var status_name = 'available'; + $.each(this.statuses, + function(key, val) { if(status[0] == val) { status_name = key; return false; } }); + tab.addClass('imjs-' + status_name); + } + + // store inputbox height + //var input = chatbox.find('.imjs-input'); + //input.data('height', input.height()); + } else if(chatbox.data('tab').data('state') == 'closed') { + chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old'); + + var tab = chatbox.data('tab'); + if(tab.css('display') == 'none') + tab.css('display', '').removeClass('imjs-selected') + .appendTo('#imjs-bar'); + + if(!no_stamp) { + // possibly add a date stamp + var time = this._addDateStamp(chatbox); + this._storeNonMessage('datestamp', username, null, time); + } + + if(!$('#imjs-bar .imjs-selected').length) { + tab.click(); + } else { + this.bar.notification(tab); + } + } + + return chatbox; + }, + + // === //private// {{{AjaxIM.}}}**{{{_addDateStamp(chatbox)}}}** // + // + // Adds a date/time notifier to a chatbox. These are generally + // inserted upon creation of a chatbox, or upon the date changing + // since the last time a date stamp was added. If a date stamp for + // the current date already exists, a new one will not be added. + // + // ==== Parameters ==== + // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. + // * {{{time}}} is the date/time the date stamp will show. It is specified + // in milliseconds since the Unix Epoch. This is //only// defined when + // date stamps are being restored from storage; if not specified, the + // current computer time will be used. + _addDateStamp: function(chatbox, time) { + var message_log = $(chatbox).find('.imjs-msglog'); + if(!time) + time = (new Date()).getTime(); + + var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); + var date_stamp_time = date_stamp.find('.imjs-msg-time'); + if(date_stamp_time.length) + date_stamp_time.html(AjaxIM.dateFormat(time, date_stamp_time.html())); + + var date_stamp_date = date_stamp.find('.imjs-date-date'); + var formatted_date = AjaxIM.dateFormat(time, date_stamp_date.html()); + if(chatbox.data('lastDateStamp') != formatted_date) { + if(date_stamp_date.length) + date_stamp_date.html(AjaxIM.dateFormat(time, date_stamp_date.html())); + + chatbox.data('lastDateStamp', formatted_date); + date_stamp.appendTo(message_log); + } else { + //$('
').appendTo(message_log); + } + + return time; + }, + + // === //private// {{{AjaxIM.}}}**{{{_addError(chatbox, error)}}}** // + // + // Adds an error to a chatbox. These are generally inserted after + // a user sends a message unsuccessfully. If an error message + // was already added, another one will be added anyway. + // + // ==== Parameters ==== + // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. + // * {{{error}}} is the error message string. + // * {{{time}}} is the date/time the error occurred. It is specified in + // milliseconds since the Unix Epoch. This is //only// defined when + // errors are being restored from storage; if not specified, the current + // computer time will be used. + _addError: function(chatbox, error, time) { + var message_log = $(chatbox).find('.imjs-msglog'); + + var error_item = + $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-error').clone(); + + var error_item_time = error_item.find('.imjs-msg-time'); + if(error_item_time.length) { + if(!time) + time = (new Date()).getTime(); + error_item_time.html(AjaxIM.dateFormat(time, error_item_time.html())); + } + + error_item.find('.imjs-error-error').html(error); + error_item.appendTo(message_log); + + message_log[0].scrollTop = message_log[0].scrollHeight; + }, + + // === //private// {{{AjaxIM.}}}**{{{_addMessage(ab, chatbox, username, message, time)}}}** // + // + // Adds a message to a chatbox. Depending on the {{{ab}}} value, + // the color of the username may change as a way of visually + // identifying users (however, this depends on the theme's CSS). + // A timestamp is added to the message, and the chatbox is scrolled + // to the bottom, such that the new message is visible. + // + // Messages will be automatically tag-escaped, so as to prevent + // any potential cross-site scripting problems. Additionally, + // URLs will be automatically linked. + // + // ==== Parameters ==== + // * {{{ab}}} refers to whether the user is "a" or "b" in a conversation. + // For the general case, "you" are "a" and "they" are "b". + // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. + // * {{{username}}} is the username of the user who sent the message. + // * {{{time}}} is the time the message was sent in milliseconds since + // the Unix Epoch. This is //only// defined when messages are being + // restored from storage. For new messages, the current computer + // time is automatically used. + _addMessage: function(ab, chatbox, username, message, time) { + var last_message = chatbox.find('.imjs-msglog > *:last-child'); + if(last_message.hasClass('imjs-msg-' + ab)) { + // Last message was from the same person, so let's just add another imjs-msg-*-msg + var message_container = (last_message.hasClass('imjs-msg-' + ab + '-container') ? + last_message : + last_message.find('.imjs-msg-' + ab + '-container')); + + var single_message = + $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-msg-' + ab + '-msg') + .clone().appendTo(message_container); + + single_message.html(single_message.html().replace('{username}', username)); + } else if(!last_message.length || !last_message.hasClass('imjs-msg-' + ab)) { + var message_group = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msg-' + ab) + .clone().appendTo(chatbox.find('.imjs-msglog')); + message_group.html(message_group.html().replace('{username}', username)); + + var single_message = message_group.find('.imjs-msg-' + ab + '-msg'); + } + + // clean up the message + message = message.replace(//g, '>') + .replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1$2$3'); + + // autolink URLs + message = message.replace( + new RegExp('([A-Za-z][A-Za-z0-9+.-]{1,120}:[A-Za-z0-9/]' + + '(([A-Za-z0-9$_.+!*,;/?:@&~=-])|%[A-Fa-f0-9]{2}){1,333}' + + '(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*,;/?:@&~=%-]{0,1000}))?)', 'g'), + '$1'); + + // insert the message + single_message.html(single_message.html().replace('{message}', message)); + + // set the message time + var msgtime = single_message.find('.imjs-msg-time'); + if(!time) + time = new Date(); + + if(typeof time != 'string') + time = AjaxIM.dateFormat(time, msgtime.html()); + + msgtime.html(time); + + var msglog = chatbox.find('.imjs-msglog'); + msglog[0].scrollTop = msglog[0].scrollHeight; + + return time; + }, + + // === //private// {{{AjaxIM.}}}**{{{_storeNonMessage(type, username, data, time)}}}** === + // + // **Redundant?**\\ + // Similar to {{{AjaxIM._storeMessage}}}, but stores items that aren't messages, + // such as datestamps and errors. + // + // ==== Parameters ==== + // * {{{type}}} is the type of non-message (error, datestamp). + // * {{{username}}} is the username of the user being chatted with. + // * {{{data}}} is the (optional) data of the non-message to be stored. + // * {{{time}}} is the time of the message. + _storeNonMessage: function(type, username, data, time) { + // If storage is on & ready, store the non-message + if(this.settings.storageMethod) { + if(!this.chatstore) this.chatstore = {}; + if(!(username in this.chatstore)) this.chatstore[username] = []; + + // If the chat store gets too long, it becomes slow to load. + if(this.chatstore[username].length > 300) + this.chatstore[username].shift(); + + // For some reason, if we don't encode and decode the message, it *will* break + // (at least) the Flash storage engine's retrieval. Gah! + this.chatstore[username].push( + [type, username, encodeURIComponent(data), time]); + + if(this.storageReady) $.jStore.store(self.storeKey + 'chats', this.chatstore); + } + }, + + // === //private// {{{AjaxIM.}}}**{{{_storeMessage(ab, chatbox, username, message, time)}}}** === + // + // Taking the same arguments as {{{AjaxIM._addMessage}}}, {{{_storeMessage}}} pushes a message + // into the storage hash, if storage is enabled. + // + // Messages are added to a message array, by username. The entire chat hash is stored as + // a {{{'chats'}}} object in whatever storage engine is enabled. + _storeMessage: function(ab, chatbox, username, message, time) { + // If storage is on & ready, store the message + if(this.settings.storageMethod) { + if(!this.chatstore) this.chatstore = {}; + + message = message.replace(//g, '>'); + + if(!(username in this.chatstore)) { + this.chatstore[username] = []; + } else if(this.chatstore[username].length > 300) { + // If the chat store gets too long, it becomes slow to load. + this.chatstore[username].shift(); + } + + // For some reason, if we don't encode and decode the message, it *will* break + // (at least) the Flash storage engine's retrieval. Gah! + this.chatstore[chatbox.data('username')].push( + [ab, username, encodeURIComponent(message), time]); + + if(this.storageReady) $.jStore.store(this.storeKey + 'chats', this.chatstore); + } + }, + + // === //private// {{{AjaxIM.}}}**{{{_friendUpdate(friend, status, statusMessage)}}}** === + // + // Called when a friend's status is updated. This function will update all locations + // where a status icon is displayed (chat tab, friends list), as well as insert + // a notification, should a chatbox be open. + // + // ==== Parameters ==== + // * {{{friend}}} is the username of the friend. + // * {{{status}}} is the new status code. See {{{AjaxIM.statuses}}} for a list of available + // codes. //Note: If an invalid status is specified, no action will be taken.// + // * {{{statusMessage}}} is a message that was, optionally, specified by the user. It will be + // used should "you" send the user an IM while they are away, or if their status is viewed + // in another way (such as via the friends list [**not yet implemented**]). + _friendUpdate: function(friend, status, statusMessage) { + // add friend to buddylist, update their status, etc. + var status_name = 'available'; + $.each(this.statuses, + function(key, val) { if(status == val) { status_name = key; return false; } }); + + if(this.chats[friend]) { + var tab = this.chats[friend].data('tab'); + var tab_class = 'imjs-tab'; + if(tab.data('state') == 'active') tab_class += ' imjs-selected'; + tab_class += ' imjs-' + status_name; + + tab.attr('class', tab_class); + + // display the status in the chatbox + var date_stamp = + $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); + + var date_stamp_time = date_stamp.find('.imjs-msg-time'); + if(date_stamp_time.length) + date_stamp_time.html(AjaxIM.dateFormat(date_stamp_time.html())); + + var date_stamp_date = date_stamp.find('.imjs-date-date').html( + AjaxIM.i18n[ + 'chat' + status_name[0].toUpperCase() + status_name.slice(1) + ].replace(/%s/g, friend)); + + var msglog = this.chats[friend].find('.imjs-msglog'); + date_stamp.appendTo(msglog); + msglog[0].scrollTop = msglog[0].scrollHeight; + } + + if(this.friends[friend]) { + var friend_id = 'imjs-friend-' + $.md5(friend + this.friends[friend].group); + $('#' + friend_id).attr('class', 'imjs-friend imjs-' + status_name); + + if(status == 0) { + $('#' + friend_id + ':visible').slideUp(); + $('#' + friend_id + ':hidden').hide(); + } else if(!$('#' + friend_id + ':visible').length) { + $('#' + friend_id).slideDown(); + } + + this.friends[friend].status = [status, statusMessage]; + this._updateFriendCount(); + } + }, + + // === //private// {{{AjaxIM.}}}**{{{_notConnected()}}}** === + // + // Puts the user into a visible state of disconnection. Sets the + // friends list to "not connected" and empties it; disallows new messages + // to be sent. + _notConnected: function() { + $('#imjs-friends').addClass('imjs-not-connected').unbind('click', this.activateTab); + }, + + // === {{{AjaxIM.}}}**{{{send(to, message)}}}** === + // + // Sends a message to another user. The message will be added + // to the chatbox before it is actually sent, however, if an + // error occurs during sending, that will be indicated immediately + // afterward. + // + // After sending the message, one of three status codes should be + // returned as a JSON object, e.g. {{{{r: 'code'}}}}: + // * {{{ok}}} — Message was sent successfully. + // * {{{offline}}} — The user is offline or unavailable to + // receive messages. + // * {{{error}}} — a problem occurred, unrelated to the user + // being unavailable. + // + // ==== Parameters ==== + // * {{{to}}} is the username of the recipient. + // * {{{message}}} is the content to be sent. + send: function(to, message) { + if(!message) return; + + if(this.chats[to]) { // REMOVE ME? + // possibly add a datestamp + var time = self._addDateStamp(this.chats[to]); + this._storeNonMessage('datestamp', to, null, time); + + time = this._addMessage('a', this.chats[to], this.username, message); + this._storeMessage('a', this.chats[to], this.username, message, time); + } + + $(self).trigger('sendingMessage', [to, message]); + + AjaxIM.request( + this.actions.send, + {'to': to, 'message': message}, + function(result) { + switch(result.r) { + case 'ok': + $(self).trigger('sendMessageSuccessful', [to, message]); + break; + + case 'offline': + $(self).trigger('sendMessageFailed', ['offline', to, message]); + break; + + case 'error': + default: + if(result.e == 'no session found') { + self._notConnected(); + self._addError(self.chats[to], AjaxIM.i18n.notConnected); + self._storeNonMessage('error', to, + AjaxIM.i18n.notConnected, (new Date()).getTime()); + } + + $(self).trigger('sendMessageFailed', [result.e, to, message]); + break; + } + }, + function(error) { + self._notConnected(); + self._addError(self.chats[to], AjaxIM.i18n.notConnected); + self._storeNonMessage('error', to, + AjaxIM.i18n.notConnected, (new Date()).getTime()); + + $(self).trigger('sendMessageFailed', ['not connected', to, message]); + } + ); + }, + + // === {{{AjaxIM.}}}**{{{status(s, message)}}}** === + // + // Sets the user's status and status message. It is possible to not + // set a status message by setting it to an empty string. The status + // will be sent to the server, where upon the server will broadcast + // the update to all individuals with "you" on their friends list. + // + // ==== Parameters ==== + // * {{{s}}} is the status code, as defined by {{{AjaxIM.statuses}}}. + // * {{{message}}} is the custom status message. + status: function(s, message) { + // update status icon(s) + if(!this.statuses[s]) + return; + + $('#imjs-friends').attr('class', 'imjs-' + s); + + $(self).trigger('changingStatus', [s, message]); + + AjaxIM.request( + this.actions.status, + {'status': this.statuses[s], 'message': message}, + function(result) { + switch(result.r) { + case 'ok': + $(self).trigger('changeStatusSuccessful', [s, message]); + break; + + case 'error': + default: + $(self).trigger('changeStatusFailed', [result.e, s, message]); + break; + } + }, + function(error) { + $(self).trigger('changeStatusFailed', ['not connected', s, message]); + } + ); + }, + + // === {{{AjaxIM.}}}**{{{statuses}}}** === + // + // These are the available status codes and their associated identities: + // * {{{offline}}} (0) — Only used when signing out/when another + // user has signed out, as once this status is set, the user is removed + // from the server and friends will be unable to contact the user. + // * {{{available}}} (1) — The user is online and ready to be messaged. + // * {{{away}}} (2) — The user is online but is not available. Others + // may still contact this user, however, the user may not respond. Anyone + // contacting an away user will receive a notice stating that the user is away, + // and (if one is set) their custom status message. + // * {{{invisible}}} (3; **not yet implemented**) — The user is online, + // but other users are made unaware, and the user will be represented + // as being offline. It is still possible to contact this user, and for this + // user to contact others; no status message or notice will be sent to others + // messaging this user. + statuses: {offline: 0, available: 1, away: 2, invisible: 3}, + + // == Footer bar == + // + // The footer bar is the bar that sits at the bottom of the page, in a fixed + // position. It contains a tab for the friends list, and tabs for any open + // chat boxes. It is also possible to add custom tabs for other functionality. + bar: { + // === {{{AjaxIM.}}}**{{{bar.initialize()}}}** === + // + // Setup the footer bar and enable tab actions. This function + // uses {{{jQuery.live}}} to set hooks on any bar tabs created + // in the future. + initialize: function() { + // Set up your standard tab actions + $('.imjs-tab') + .live('click', this.activateTab); + + $('.imjs-tab .imjs-close') + .live('click', this.closeTab); + + // Set up the friends list actions + var self = this; + $(document).click(function(e) { + if(e.target.id == 'imjs-friends' || + $(e.target).parents('#imjs-friends').length) { + return; + } + + if($('#imjs-friends').data('state') == 'active') + self.activateTab.call($('#imjs-friends')); + }); + $('#imjs-friends') + .data('state', 'minimized') + .click(function(e) { + if(!$(this).hasClass('imjs-not-connected') && + e.target.id != 'imjs-friends-panel' && + !$(e.target).parents('#imjs-friends-panel').length) + self.activateTab.call(this); + }) + .mouseenter(function() { + if($(this).hasClass('imjs-not-connected')) { + $('.imjs-tooltip').css('display', 'block'); + $('.imjs-tooltip p').html(AjaxIM.i18n.notConnectedTip); + + var tip_left = $(this).offset().left - + $('.imjs-tooltip').outerWidth() + + ($(this).outerWidth() / 2); + var tip_top = $(this).offset().top - + $('.imjs-tooltip').outerHeight(true); + + $('.imjs-tooltip').css({ + left: tip_left, + top: tip_top + }); + } + }) + .mouseleave(function() { + if($(this).hasClass('imjs-not-connected')) { + $('.imjs-tooltip').css('display', ''); + } + }); + $('#imjs-friends-panel') + .data('tab', $('#imjs-friends')) + .css('display', 'none'); + }, + + // === {{{AjaxIM.}}}**{{{bar.activateTab()}}}** === + // + // Activate a tab by setting it to the 'active' state and + // showing any related chatbox. If a chatbox is available + // for this tab, also focus the input box. + // + // //Note:// {{{this}}}, here, refers to the tab DOM element. + activateTab: function() { + var chatbox = $(this).find('.imjs-chatbox') || false; + + if($(this).data('state') != 'active') { + if($(this).attr('id') != 'imjs-friends') { + $('#imjs-bar > li') + .not($(this)) + .not('#imjs-friends') + .removeClass('imjs-selected') + .each(function() { + if($(this).data('state') != 'closed') { + $(this).data('state', 'minimized'); + var chatbox = $(this).find('.imjs-chatbox'); + if(chatbox.length) + chatbox.css('display', 'none'); + } + }); + } + + if(chatbox && chatbox.css('display') == 'none') + chatbox.css('display', ''); + + // set the tab to active... + var tab = $(this).addClass('imjs-selected').data('state', 'active'); + + // ...and hide and reset the notification icon + tab.find('.imjs-notification').css('display', 'none') + .data('count', 0); + + if(self.settings.storageMethod && self.storageReady && + chatbox && (username = chatbox.data('username'))) { + $.jStore.store(self.storeKey + 'activeTab', [username]); + } + + $(self).trigger('tabToggled', ['activated', tab]); + } else { + var tab = $(this).removeClass('imjs-selected').data('state', 'minimized'); + + if(chatbox && chatbox.css('display') != 'none') + chatbox.css('display', 'none'); + + if(self.settings.storageMethod && self.storageReady) { + $.jStore.store(self.storeKey + 'activeTab', ['*']); + } + + $(self).trigger('tabToggled', ['minimized', tab]); + } + + if(chatbox) { + if(!(input = chatbox.find('.imjs-input')).data('height')) { + // store the height for resizing later + input.data('height', input.height()); + } + + try { + var msglog = chatbox.find('.imjs-msglog'); + msglog[0].scrollTop = msglog[0].scrollHeight; + } catch(e) {} + + try { chatbox.find('.imjs-input').focus(); } catch(e) {} + } + }, + + // === {{{AjaxIM.}}}**{{{bar.closeTab()}}}** === + // + // Close a tab and hide any related chatbox, such that + // the chatbox can not be reopened without reinitializing + // the tab. + // + // //Note:// {{{this}}}, here, refers to the tab DOM element. + closeTab: function() { + var tab = $(this).parents('.imjs-tab'); + tab.css('display', 'none').data('state', 'closed'); + + if(self.settings.storageMethod && self.storageReady) { + delete self.chatstore[tab.find('.imjs-chatbox').data('username')]; + if(self.storageReady) $.jStore.store(self.storeKey + 'chats', self.chatstore); + } + + $(self).trigger('tabToggled', ['closed', tab]); + + self.bar._scrollers(); + + return false; + }, + + // === {{{AjaxIM.}}}**{{{bar.addTab(label, action, closable)}}}** === + // + // Adds a tab to the tab bar, with the label {{{label}}}. When + // clicked, it will call a callback function, {{{action}}}. If + // {{{action}}} is a string, it is assumed that the string is + // referring to a chatbox ID. + // + // ==== Parameters ==== + // * {{{label}}} is the text that will be displayed on the tab.\\ + // * {{{action}}} is the callback function, if it is a non-chatbox + // tab, or a string if it //is// a chatbox tab.\\ + // * {{{closable}}} is a boolean value that determines whether or not + // it is possible for a user to close this tab. + // + // //Note:// New tabs are given an automatically generated ID + // in the format of {{{#imjs-tab-[md5 of label]}}}. + addTab: function(label, action, closable) { + var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-right'); + tab.removeClass('imjs-default') + .attr('id', 'imjs-tab-' + $.md5(label)) + .html(tab.html().replace('{label}', label)) + .data('state', 'minimized'); + + var notification = tab.find('.imjs-notification'); + notification.css('display', 'none') + .data('count', 0) + .data('default-text', notification.html()) + .html(notification.html().replace('{count}', '0')); + + if(closable === false) + tab.find('.imjs-close').eq(0).remove(); + + if(typeof action == 'string') { + //tab.data('chatbox', action); + } else { + tab.find('.imjs-chatbox').remove(); + tab.click(action); + } + + return tab; + }, + + // === {{{AjaxIM.}}}**{{{bar.notification(tab)}}}** === + // + // Displays a notification on a tab. Generally, this is called when + // a tab is minimized to let the user know that there is an update + // for them. The way the notification is displayed depends on the + // theme CSS. + // + // ==== Parameters ==== + // * {{{tab}}} is the jQuery-selected tab DOM element. + notification: function(tab) { + var notify = tab.find('.imjs-notification'); + var notify_count = notify.data('count') + 1; + + notify.data('count', notify_count) + .html(notify.data('default-text').replace('{count}', notify_count)) + .css('display', ''); + }, + + // === //private// {{{AjaxIM.}}}**{{{bar._scrollers()}}}** === + // + // Document me! + _scrollers: function() { + var needScrollers = false; + $('.imjs-tab').filter(function() { + return $(this).data('state') != 'closed' + }).css('display', ''); + + $.each(self.chats, function(username, chatbox) { + var tab = chatbox.data('tab'); + if(tab.data('state') == 'closed') return true; + + if(tab.position().top > $('#imjs-bar').height()) { + $('.imjs-scroll').css('display', ''); + tab.css('display', 'none'); + needScrollers = true; + } else { + tab.css('display', ''); + } + }); + + if(!needScrollers) { + $('.imjs-scroll').css('display', 'none'); + } + + if($('#imjs-scroll-left').css('display') != 'none' && + $('#imjs-scroll-left').position().top > $('#imjs-bar').height()) { + $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); + } + + var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').slice(-1) + .nextAll('#imjs-bar li.imjs-tab:hidden') + .not('.imjs-default') + .filter(function() { + return $(this).data('state') != 'closed' + }).length; + + var hiddenRight = $('#imjs-bar li.imjs-tab:visible').eq(0) + .prevAll('#imjs-bar li.imjs-tab:hidden') + .not('.imjs-default') + .filter(function() { + return $(this).data('state') != 'closed' + }).length; + + $('#imjs-scroll-left').html(hiddenLeft); + $('#imjs-scroll-right').html(hiddenRight); + } + }, + + // == Comet == + // + // Comet, or HTTP streaming, holds open a connection between the client and + // the server indefinitely. As the server receives new messages or events, + // they are passed down to the client in a {{{<script>}}} + // tag which calls the {{{AjaxIM.incoming()}}} function. The connection is + // opened using either an {{{iframe}}} (in Opera or Internet Explorer) or + // an {{{XMLHTTPRequest}}} object. Due to the extra flexibility necessary + // with the {{{XMLHTTPRequest}}} object, jQuery's {{{$.ajax}}} is not used. + comet: { + type: '', + obj: null, + + onbeforeunload: null, + onreadystatechange: null, + + iframe: function() { + if(/loaded|complete/i.test(this.obj.readyState)) + throw new Error("IM server not available!"); + }, + + // === {{{AjaxIM.}}}**{{{comet.connect()}}}** === + // + // Creates and initializes the object and connection between the server + // and the client. For Internet Explorer and Opera, we use an + // {{{<iframe>}}} element; for all other browsers, we create an + // {{{XMLHTTPRequest}}} object. The server connects to the URI defined + // as the "poll" action. This function is called automatically, when + // the IM engine is initialized and the {{{AjaxIM.poll()}}} function + // is called. + connect: function() { + var self = _instance; + + if($.browser.opera || $.browser.msie) { + var iframe = $(''); + with(iframe) { + css({ + position: 'absolute', + visibility: 'visible', + display: 'block', + left: '-10000px', + top: '-10000px', + width: '1px', + height: '1px' + }); + + attr('src', self.actions.poll[1]); + appendTo('body'); + + bind('readystatechange', + self.comet.onreadystatechange = function() { self.comet.iframe() }); + bind('beforeunload', + self.comet.onbeforeunload = function() { self.comet.disconnect() }); + } + + self.comet.type = 'iframe'; + self.comet.obj = iframe; + } else { + var xhr = new XMLHttpRequest; + var length = 1029; + var code = /^\s*]*>parent\.(.+);<\/script>$/; + + xhr.open('get', self.actions.poll[1], true); + xhr.onreadystatechange = function(){ + if(xhr.readyState > 2) { + if(xhr.status == 200) { + responseText = xhr.responseText.substring(length); + length = xhr.responseText.length; + if(responseText != ' ') + eval(responseText.replace(code, "$1")); + } + // We need an "else" here. If the state changes to + // "loaded", the user needs to know they're + // disconnected. + } + }; + + self.comet.type = 'xhr'; + self.comet.obj = xhr; + + addEventListener('beforeunload', + self.comet.beforeunload = function() { self.comet.disconnect(); }, false); + setTimeout(function() { xhr.send(null) }, 10); + } + }, + + // === {{{AjaxIM.}}}**{{{comet.disconnect()}}}** === + // + // Disconnect from the server and destroy the connection object. This + // function is called before the page unloads, so that we plug up and + // potential leaks and free memory. + disconnect: function() { + var self = _instance.comet; + + if(!this.type || !this.obj) + return; + + if(this.type == 'iframe') { + detachEvent('onreadystatechange', this.onreadystatechange); + detachEvent('onbeforeunload', this.onbeforeunload); + this.obj.src = '.'; + $(this.obj).remove(); + } else { + removeEventListener('beforeunload', this.onbeforeunload, false); + this.obj.onreadystatechange = function(){}; + this.obj.abort(); + } + + delete this.obj; + } + } + }) + + self.bar.initialize(); + + if(prequeue.length) { + $.each(prequeue, function() { + var func = this[0].split('.'); + if(func.length > 1) + self[func[0]][func[1]].apply(self, this[1]); + else + self[func[0]].apply(self, this[1]); + }); + } + }); + + // == Static functions and variables == + // + // The following functions and variables are available outside of an initialized + // {{{AjaxIM}}} object. + + // === {{{AjaxIM.}}}**{{{client}}}** === + // + // Once {{{AjaxIM.init()}}} is called, this will be set to the active AjaxIM + // object. Only one AjaxIM object instance can exist at a time. This variable + // can and should be accessed directly. + AjaxIM.client = null; + + // === {{{AjaxIM.}}}**{{{init(options, actions)}}}** === + // + // Initialize the AjaxIM client object and engine. Here, you can define your + // options and actions as outlined at the top of this documentation. + // + // ==== Parameters ==== + // * {{{options}}} is the hash of custom settings to initialize Ajax IM with. + // * {{{actions}}} is the hash of any custom action URLs. + AjaxIM.init = function(options, actions) { + if(!_instance) { + _instance = new AjaxIM(options, actions); + AjaxIM.client = _instance; + } + + return _instance; + } + + + // === {{{AjaxIM.}}}**{{{request(url, data, successFunc, failureFunc)}}}** === + // + // Wrapper around {{{$.jsonp}}}, the JSON-P library for jQuery, and {{{$.ajax}}}, + // jQuery's ajax library. Allows either function to be called, automatically, + // depending on the request's URL array (see {{{AjaxIM.actions}}}). + // + // ==== Parameters ==== + // {{{url}}} is the URL of the request. + // {{{data}}} are any arguments that go along with the request. + // {{{success}}} is a callback function called when a request has completed + // without issue. + // {{{_ignore_}}} is simply to provide compatability with {{{$.post}}}. + // {{{failure}}} is a callback function called when a request hasn't not + // completed successfully. + AjaxIM.request = function(url, data, successFunc, failureFunc) { + if(typeof failureFunc != 'function'); + failureFunc = function(){}; + + $[url[0]]({ + 'url': url[1], + 'data': data, + dataType: (url[0] == 'ajax' ? 'json' : 'jsonp'), + type: 'POST', + cache: false, + timeout: 60000, + callback: 'jsonp' + (new Date()).getTime(), + success: function(json, textStatus) { + successFunc(json); + }, + error: function(xOptions, error) { + failureFunc(error); + } + }); + + // This prevents Firefox from spinning indefinitely + // while it waits for a response. Why? Fuck if I know. + if(url[0] == 'jsonp' && $.browser.mozilla) { + $.jsonp({ + 'url': 'about:', + timeout: 0 + }); + } + }; + + // === {{{AjaxIM.}}}**{{{incoming(data)}}}** === + // + // Never call this directly. It is used as a connecting function between + // client and server for Comet. + // + // //Note:// There are two {{{AjaxIM.incoming()}}} functions. This one is a + // static function called outside of the initialized AjaxIM object; the other + // is only called within the initalized AjaxIM object. + AjaxIM.incoming = function(data) { + if(!_instance) + return false; + + if(data.length) + _instance._parseMessages(data); + } + + // === {{{AjaxIM.}}}**{{{loaded}}}** === + // + // If Ajax IM has been loaded with the im.load.js file, this function will be + // called when the library is finally loaded and ready for use. Similar to + // jQuery's $(document).ready(), but for Ajax IM. + AjaxIM.loaded = function() { + if(typeof AjaxIMLoadedFunction == 'function') { + AjaxIMLoadedFunction(); + delete AjaxIMLoadedFunction; // clean up the global namespace + } + }; + + // === {{{AjaxIM.}}}**{{{dateFormat([date,] [mask,] utc)}}}** === + // + // Date Format 1.2.3\\ + // © 2007-2009 Steven Levithan ([[http://blog.stevenlevithan.com/archives/date-time-format|stevenlevithan.com]])\\ + // MIT license + // + // Includes enhancements by Scott Trenda + // and Kris Kowal ([[http://cixar.com/~kris.kowal/|cixar.com/~kris.kowal/]]) + // + // Accepts a date, a mask, or a date and a mask and returns a formatted version + // of the given date. + // + // ==== Parameters ==== + // * {{{date}}} is a {{{Date()}}} object. If not specified, the date defaults to the + // current date/time. + // * {{{mask}}} is a string that defines the formatting of the date. Formatting + // options can be found in the + // [[http://blog.stevenlevithan.com/archives/date-time-format|Date Format]] + // documentation. If not specified, the mask defaults to {{{dateFormat.masks.default}}}. + + AjaxIM.dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = new RegExp('\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) ' + + '(?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b', + 'g'), + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; + + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = AjaxIM.dateFormat; + + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == + "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } + + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); + + mask = String(dF.masks[mask] || mask || dF.masks["default"]); + + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } + + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: AjaxIM.i18n.dayNames[D], + dddd: AjaxIM.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: AjaxIM.i18n.monthNames[m], + mmmm: AjaxIM.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : + (String(date).match(timezone) || [""]) + .pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? + 0 : + (d % 100 - d % 10 != 10) * d % 10] + }; + + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; + }(); + + // Some common format strings + AjaxIM.dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" + }; + + // === {{{AjaxIM.}}}**{{{i18n}}}** === + // + // Text strings used by Ajax IM. Should you want to translate Ajax IM into + // another language, merely change these strings. + // + // {{{%s}}} denotes text that will be automatically replaced when the string is + // used. + AjaxIM.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", + "October", "November", "December" + ], + + chatOffline: '%s signed off.', + chatAvailable: '%s became available.', + chatAway: '%s went away.', + + notConnected: 'You are currently not connected or the server is not available. ' + + 'Please ensure that you are signed in and try again.', + notConnectedTip: 'You are currently not connected.', + + authInvalid: 'Invalid username or password.', + + registerPasswordLength: 'Passwords must be more than 4 characters in length.', + registerUsernameLength: 'Usernames must be more than 2 characters in length and ' + + ' contain only A-Z, a-z, 0-9, underscores (_) and periods (.).', + registerPasswordMatch: 'Entered passwords do not match.', + registerUsernameTaken: 'The chosen username is already in use; please choose another.', + registerUnknown: 'An unknown error occurred. Please try again.' + } +})(jQuery, false); From 3412b89d95c1febe139e5184ba0295c85bf3a785 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 11 Apr 2011 12:31:18 +0200 Subject: [PATCH 16/86] [IMP] add a bunch of tests for transformation of invisible fields, make View.fields_view_get take a request so it can use the request context for its evaluations bzr revid: xmo@openerp.com-20110411103118-msx5plh2lnz5obps --- addons/base/controllers/main.py | 26 +++++++------ addons/base/test/test_view.py | 48 ++++++++++++++++++++++++ addons/base_calendar/controllers/main.py | 3 +- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 28806083d61..a8dc58625fa 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -403,16 +403,20 @@ class DataSet(openerpweb.Controller): return {'result': r} class View(openerpweb.Controller): - def fields_view_get(self, session, model, view_id, view_type, transform=True, toolbar=False, submenu=False): - Model = session.model(model) - r = Model.fields_view_get(view_id, view_type, {}, toolbar, submenu) + def fields_view_get(self, request, model, view_id, view_type, + transform=True, toolbar=False, submenu=False): + Model = request.session.model(model) + fvg = Model.fields_view_get(view_id, view_type, request.context, + toolbar, submenu) if transform: - context = {} # TODO: dict(ctx_sesssion, **ctx_action) - xml = self.transform_view(r['arch'], session, context) + evaluation_context = request.session.evaluation_context( + request.context or {}) + xml = self.transform_view( + fvg['arch'], request.session, evaluation_context) else: - xml = ElementTree.fromstring(r['arch']) - r['arch'] = Xml2Json.convert_element(xml) - return r + xml = ElementTree.fromstring(fvg['arch']) + fvg['arch'] = Xml2Json.convert_element(xml) + return fvg def normalize_attrs(self, elem, context): """ Normalize @attrs, @invisible, @required, @readonly and @states, so @@ -507,7 +511,7 @@ class FormView(View): @openerpweb.jsonrequest def load(self, req, model, view_id, toolbar=False): - fields_view = self.fields_view_get(req.session, model, view_id, 'form', toolbar=toolbar) + fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar) return {'fields_view': fields_view} class ListView(View): @@ -515,7 +519,7 @@ class ListView(View): @openerpweb.jsonrequest def load(self, req, model, view_id, toolbar=False): - fields_view = self.fields_view_get(req.session, model, view_id, 'tree', toolbar=toolbar) + fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar) return {'fields_view': fields_view} class SearchView(View): @@ -523,7 +527,7 @@ class SearchView(View): @openerpweb.jsonrequest def load(self, req, model, view_id): - fields_view = self.fields_view_get(req.session, model, view_id, 'search') + fields_view = self.fields_view_get(req, model, view_id, 'search') return {'fields_view': fields_view} class Action(openerpweb.Controller): diff --git a/addons/base/test/test_view.py b/addons/base/test/test_view.py index f20f9e12f61..d8f65ccff99 100644 --- a/addons/base/test/test_view.py +++ b/addons/base/test/test_view.py @@ -1,3 +1,4 @@ +import copy import xml.etree.ElementTree import mock @@ -8,6 +9,11 @@ import base.controllers.main import openerpweb.nonliterals import openerpweb.openerpweb +def field_attrs(fields_view_get, fieldname): + (field,) = filter(lambda f: f['attrs'].get('name') == fieldname, + fields_view_get['arch']['children']) + return field['attrs'] + #noinspection PyCompatibility class DomainsAndContextsTest(unittest2.TestCase): def setUp(self): @@ -130,3 +136,45 @@ class AttrsNormalizationTest(unittest2.TestCase): self.assertEqual( simplejson.loads(element.get('attrs')), {'invisible': [['state', 'not in', ['open', 'closed']]]}) + + def test_transform_invisible(self): + element = xml.etree.ElementTree.Element( + 'field', invisible="context.get('invisible_country', False)") + + empty_context = copy.deepcopy(element) + self.view.normalize_attrs(empty_context, {}) + self.assertEqual(empty_context.get('invisible'), None) + + full_context = copy.deepcopy(element) + self.view.normalize_attrs(full_context, {'invisible_country': True}) + self.assertEqual(full_context.get('invisible'), '1') + + def test_transform_invisible_list_column(self): + req = mock.Mock() + req.context = {'set_editable':True, 'set_visible':True, + 'gtd_visible':True, 'user_invisible':True} + req.session.evaluation_context = \ + openerpweb.openerpweb.OpenERPSession().evaluation_context + req.session.model('project.task').fields_view_get.return_value = { + 'arch': ''' + + + + + + + + + '''} + parsed_view = base.controllers.main.View().fields_view_get( + req, 'project.task', 42, 'tree') + + self.assertTrue(field_attrs(parsed_view, 'sequence')['invisible']) + self.assertTrue(field_attrs(parsed_view, 'user_id')['invisible']) + self.assertTrue( + field_attrs(parsed_view, 'delegated_user_id')['invisible']) + self.assertTrue(field_attrs(parsed_view, 'total_hours')['invisible']) + self.assertTrue( + field_attrs(parsed_view, 'date_deadline')['invisible']) + self.assertTrue(field_attrs(parsed_view, 'type_id')['invisible']) + diff --git a/addons/base_calendar/controllers/main.py b/addons/base_calendar/controllers/main.py index 41081bc7cae..a54d75c5b9b 100644 --- a/addons/base_calendar/controllers/main.py +++ b/addons/base_calendar/controllers/main.py @@ -57,7 +57,7 @@ class CalendarView(View): @openerpweb.jsonrequest def load(self, req, model, view_id): - fields_view = self.fields_view_get(req.session, model, view_id, 'calendar') + fields_view = self.fields_view_get(req, model, view_id, 'calendar') return {'fields_view':fields_view} def convert(self, event): @@ -354,4 +354,3 @@ class CalendarView(View): title = title.strip() description = ', '.join(description).strip() return {'id': event['id'], 'start_date': str(DT.datetime(*starts[:6])), 'end_date': str(DT.datetime(*ends[:6])), 'text': title, 'title': description, 'color': self.colors[event[self.color_field]][-1]} - \ No newline at end of file From 911369822abb9779193137301326e61242b9daa7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 11 Apr 2011 13:14:51 +0200 Subject: [PATCH 17/86] [ADD] processing of context['set_editable'] on list view bzr revid: xmo@openerp.com-20110411111451-5j6ve9f1wjiuzjfx --- addons/base/controllers/main.py | 49 +++++++++++++++++++++++++++++++ addons/base/static/src/js/list.js | 11 ++++++- addons/base/test/test_view.py | 27 +++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index a8dc58625fa..36e9b0feaee 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import glob, os +import pprint from xml.etree import ElementTree from cStringIO import StringIO @@ -522,6 +523,54 @@ class ListView(View): fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar) return {'fields_view': fields_view} + def fields_view_get(self, request, model, view_id, view_type="tree", + transform=True, toolbar=False, submenu=False): + """ Sets @editable on the view's arch if it isn't already set and + ``set_editable`` is present in the request context + """ + view = super(ListView, self).fields_view_get( + request, model, view_id, view_type, transform, toolbar, submenu) + + view_attributes = view['arch']['attrs'] + if request.context.get('set_editable')\ + and 'editable' not in view_attributes: + view_attributes['editable'] = 'bottom' + return view + + @openerpweb.jsonrequest + def fill(self, request, model, id, domain, + offset=0, limit=False): + return self.do_fill(request, model, id, domain, offset, limit) + + def do_fill(self, request, model, id, domain, + offset=0, limit=False): + """ Returns all information needed to fill a table: + + * view with processed ``editable`` flag + * fields (columns) with processed ``invisible`` flag + * rows with processed ``attrs`` and ``colors`` + + .. note:: context is passed through ``request`` parameter + + :param request: OpenERP request + :type request: openerpweb.openerpweb.JsonRequest + :type str model: OpenERP model for this list view + :type int id: view_id, or False if none provided + :param list domain: the search domain to search for + :param int offset: search offset, for pagination + :param int limit: search limit, for pagination + :returns: hell if I have any idea yet + """ + view = self.fields_view_get(request, model, id) + + rows = DataSet().do_search_read(request, model, + offset=offset, limit=limit, + domain=domain) + # rows pipe + # process_attrs + # process_colors + return rows + class SearchView(View): _cp_path = "/base/searchview" diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 4fc3ef07720..247b2d535d9 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -160,7 +160,16 @@ openerp.base.ListView = openerp.base.Controller.extend( // TODO: handle non-empty results.group_by with read_group self.dataset.context = results.context; self.dataset.domain = results.domain; - self.dataset.read_slice(self.dataset.fields, 0, self.limit, self.do_fill_table); + // TODO: need to do 5 billion tons of pre-processing, bypass + // DataSet for now + //self.dataset.read_slice(self.dataset.fields, 0, self.limit, + // self.do_fill_table); + self.rpc('/base/listview/fill', { + 'model': self.dataset.model, + 'id': self.view_id, + 'context': results.context, + 'domain': results.domain + }, self.do_fill_table); }); }, do_update: function () { diff --git a/addons/base/test/test_view.py b/addons/base/test/test_view.py index d8f65ccff99..141bbd5e0db 100644 --- a/addons/base/test/test_view.py +++ b/addons/base/test/test_view.py @@ -178,3 +178,30 @@ class AttrsNormalizationTest(unittest2.TestCase): field_attrs(parsed_view, 'date_deadline')['invisible']) self.assertTrue(field_attrs(parsed_view, 'type_id')['invisible']) +class ListViewTest(unittest2.TestCase): + def setUp(self): + self.view = base.controllers.main.ListView() + self.request = mock.Mock() + self.request.context = {'set_editable': True} + def test_no_editable_editable_context(self): + self.request.session.model('fake').fields_view_get.return_value = \ + {'arch': ''} + view = self.view.fields_view_get(self.request, 'fake', False) + + self.assertEqual(view['arch']['attrs']['editable'], + 'bottom') + def test_editable_top_editable_context(self): + self.request.session.model('fake').fields_view_get.return_value = \ + {'arch': ''} + view = self.view.fields_view_get(self.request, 'fake', False) + + self.assertEqual(view['arch']['attrs']['editable'], + 'top') + + def test_editable_bottom_editable_context(self): + self.request.session.model('fake').fields_view_get.return_value = \ + {'arch': ''} + view = self.view.fields_view_get(self.request, 'fake', False) + + self.assertEqual(view['arch']['attrs']['editable'], + 'bottom') From 8ca75e0b11eb712d549d91064358bb816aeef0ec Mon Sep 17 00:00:00 2001 From: "noz (OpenERP)" Date: Mon, 11 Apr 2011 16:45:38 +0530 Subject: [PATCH 18/86] [FIX] Sent first chat message to self. bzr revid: noz@tinyerp.com-20110411111538-yxvexkoyldb5xrid --- addons/web_chat/controllers/main.py | 33 ++++++++-------------- addons/web_chat/static/lib/AjaxIM/js/im.js | 4 +-- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/addons/web_chat/controllers/main.py b/addons/web_chat/controllers/main.py index ff5b95d6579..adb32c3a7a3 100644 --- a/addons/web_chat/controllers/main.py +++ b/addons/web_chat/controllers/main.py @@ -67,6 +67,7 @@ class PollServer(openerpweb.Controller): } """ mq = req.applicationsession.setdefault("web_chat", PollServerMessageQueue()) + mq.messages = [] #r = 'logged in' #u = generate random.randint(0,2**32) @@ -106,9 +107,9 @@ class PollServer(openerpweb.Controller): def poll(self, req, **kw): mq = req.applicationsession.setdefault("web_chat", PollServerMessageQueue()) - print "================ poll ======= ", kw - # Long Polling - method = kw.get('method') + + # Method: Long Poll + """ --> GET http://im.ajaxim.com/poll?callback=jsonp1302138663582&_1302138663582= <-- 200 OK @@ -132,7 +133,7 @@ class PollServer(openerpweb.Controller): """ - msg = [] + msg = '[]' for i in range(5): received_msg = mq.read('Guest1', i); @@ -140,27 +141,14 @@ class PollServer(openerpweb.Controller): msg = self._pollParseMessages(received_msg) time.sleep(1) else: - msg = [] + msg = '[]' time.sleep(1) - - # for i in range(60): - #r = mq.read(username,timestamp) - # if messages - # return r - # if no message sleep 2s and retry - # sleep 2 - # else - # return emptylist - msg = '[{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"xxxxxx"}]' - -# # it's http://localhost:8002/web_chat/pollserver/poll?method=long?callback=jsonp1302147330483&_1302147330483= - - return '%s'%kw.get('callback', '') + '(' + msg + ');' + # it's http://localhost:8002/web_chat/pollserver/poll?callback=jsonp1302147330483&_1302147330483= + return '%s'%kw.get('callback', '') + '(' + str(msg) + ');' @openerpweb.httprequest def send(self, req, **kw): - print "========= send ========", kw to = kw.get('to') message = kw.get('message') @@ -192,7 +180,7 @@ class PollServer(openerpweb.Controller): if message: mq.write(m_type="m", m_from=req.applicationsession['current_user'], m_to=to, m_message=message, m_group="Users") - return {'r': 'sent'} + return '%s'%kw.get('callback', '') + '(' + simplejson.dumps({'r': 'sent'}) + ')' else: return {'r': 'error', 'e': 'send error'} @@ -221,7 +209,8 @@ class PollServer(openerpweb.Controller): def _pollParseMessages(self, messages): msg_arr = [] for msg in messages: - msg_arr.append({'t': msg['type'], 's': msg['from'], 'r': msg['to'], 'm': msg['message']}) + msg_arr.append({"t": str(msg['type']), "s": str(msg['from']), "r": str(msg['to']), "m": str(msg['message'])}) + return msg_arr def _sanitize(self, message): diff --git a/addons/web_chat/static/lib/AjaxIM/js/im.js b/addons/web_chat/static/lib/AjaxIM/js/im.js index 42c3f743204..9ab195fd6b6 100644 --- a/addons/web_chat/static/lib/AjaxIM/js/im.js +++ b/addons/web_chat/static/lib/AjaxIM/js/im.js @@ -105,7 +105,7 @@ login: this.settings.pollServer + '/login', logout: this.settings.pollServer + '/logout', register: this.settings.pollServer + '/register', - poll: this.settings.pollServer + '/poll?method=' + this.settings.pollType, + poll: this.settings.pollServer + '/poll', send: this.settings.pollServer + '/send', status: this.settings.pollServer + '/status', resume: this.settings.pollServer + '/resume' @@ -116,7 +116,7 @@ queryStrRx = new RegExp('[?](.+)$'), subdomainRx = new RegExp('((http[s]?:)?//)?(.+?)[.]' + window.location.host, 'i'); $.each(this.actions, function(name, action) { - if(name == 'poll') { + if(name == 'poll' || name == 'send') { if(self.settings.pollType != 'comet') action += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; From 31ff2c17723f0eb0b34f6c050a2890fe7880b7a2 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 11 Apr 2011 13:48:10 +0200 Subject: [PATCH 19/86] add doctype to disable quirks mode bzr revid: al@openerp.com-20110411114810-6vvo1jmu7rg5bo8z --- addons/web_chat/static/src/web_chat.html | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/web_chat/static/src/web_chat.html b/addons/web_chat/static/src/web_chat.html index 5f11f01ee7b..a1070ab475a 100644 --- a/addons/web_chat/static/src/web_chat.html +++ b/addons/web_chat/static/src/web_chat.html @@ -1,3 +1,4 @@ + From 5fc7311f1a6bea35701d4aa7c2a68ee1f3ad1a2e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 11 Apr 2011 14:54:50 +0200 Subject: [PATCH 20/86] [ADD] row-wise colors bzr revid: xmo@openerp.com-20110411125450-3h0ay8fskd1s6nz5 --- addons/base/controllers/main.py | 34 +++++++++++++++++++++++++---- addons/base/static/src/xml/base.xml | 13 +++++++---- addons/base/test/test_view.py | 33 ++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 36e9b0feaee..4d75b063a35 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -566,10 +566,36 @@ class ListView(View): rows = DataSet().do_search_read(request, model, offset=offset, limit=limit, domain=domain) - # rows pipe - # process_attrs - # process_colors - return rows + eval_context = request.session.evaluation_context( + request.context) + return [ + {'data': row, + 'attrs': self.process_attrs(view, row, eval_context), + 'color': self.process_colors(view, row, eval_context)} + for row in rows + ] + + def process_attrs(self, view, row, context): + pass + + def process_colors(self, view, row, context): + colors = view['arch']['attrs'].get('colors') + + if not colors: + return None + + color = [ + pair.split(':')[0] + for pair in colors.split(';') + if eval(pair.split(':')[1], dict(context, **row)) + ] + + if not color: + return None + elif len(color) == 1: + return color[0] + return 'maroon' + class SearchView(View): _cp_path = "/base/searchview" diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index caf463cf4e0..8a71b8ebc7a 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -197,9 +197,14 @@
- + + + + + + - + @@ -208,8 +213,8 @@ - - + + - From 8fb463acaee3cc17b50113aa0e1bfa4d4f5fbff6 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 09:23:56 +0200 Subject: [PATCH 30/86] [FIX] don't forget the action's domain when performing a search bzr revid: xmo@openerp.com-20110412072356-gcbuv9ybi6ohx9uz --- addons/base/static/src/js/views.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index 1e3851c218f..e70495e3491 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -113,8 +113,9 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ this.searchview.stop(); } this.searchview = new openerp.base.SearchView(this, this.session, this.element_id + "_search", this.dataset, view_id, search_defaults); - this.searchview.on_search.add(function() { - self.views[self.active_view].controller.do_search.apply(self, arguments); + this.searchview.on_search.add(function(domains, contexts, groupbys) { + self.views[self.active_view].controller.do_search.call( + self, domains.concat(self.domains()), contexts, groupbys); }); return this.searchview.start(); }, @@ -128,6 +129,15 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ on_remove: function() { }, on_edit: function() { + }, + /** + * Domains added on searches by the view manager, to override in subsequent + * view manager in order to add new pieces of domains to searches + * + * @returns an empty list + */ + domains: function () { + return []; } }); @@ -175,6 +185,14 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ this.sidebar.stop(); } this._super(); + }, + /** + * adds action domain to the search domains + * + * @returns the action's domain + */ + domains: function () { + return [this.action.domain]; } }); From ef9880931cb4284258ea2c07f846b6fba0b60794 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 09:27:50 +0200 Subject: [PATCH 31/86] [FIX] add the action's context to the search contexts as well bzr revid: xmo@openerp.com-20110412072750-8338yfl549glz5wz --- addons/base/static/src/js/views.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index e70495e3491..e4deb375a55 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -115,7 +115,8 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ this.searchview = new openerp.base.SearchView(this, this.session, this.element_id + "_search", this.dataset, view_id, search_defaults); this.searchview.on_search.add(function(domains, contexts, groupbys) { self.views[self.active_view].controller.do_search.call( - self, domains.concat(self.domains()), contexts, groupbys); + self, domains.concat(self.domains()), + contexts.concat(self.contexts()), groupbys); }); return this.searchview.start(); }, @@ -138,6 +139,14 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ */ domains: function () { return []; + }, + /** + * Contexts added on searches by the view manager. + * + * @returns an empty list + */ + contexts: function () { + return []; } }); @@ -193,6 +202,14 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ */ domains: function () { return [this.action.domain]; + }, + /** + * adds action context to the search contexts + * + * @returns the action's context + */ + contexts: function () { + return [this.action.context]; } }); From fa02db060dde1e1fd5fbf8dbc8a7c737e2870467 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 09:45:29 +0200 Subject: [PATCH 32/86] [FIX] don't send a domain called 'false' if the action has no domain... bzr revid: xmo@openerp.com-20110412074529-ganfpzhailq2k1zs --- addons/base/static/src/js/views.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index e4deb375a55..74e058c2c79 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -201,6 +201,9 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ * @returns the action's domain */ domains: function () { + if (!this.action.domain) { + return []; + } return [this.action.domain]; }, /** @@ -209,6 +212,9 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ * @returns the action's context */ contexts: function () { + if (!this.action.context) { + return []; + } return [this.action.context]; } }); From 40f26b7327f991fde5071bfc581adf62ba4b08ec Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 09:50:37 +0200 Subject: [PATCH 33/86] [FIX] handling of search views when not provided with an id (still need to go and get a search view, just one with the id 'false' to get the default search view for the model) bzr revid: xmo@openerp.com-20110412075037-6az2uudgp5ktfp8c --- addons/base/static/src/js/views.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index 74e058c2c79..5e6e3d72745 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -168,8 +168,6 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ this.sidebar.start(); } - // init search view - var view_id = this.action.search_view_id ? this.action.search_view_id[0] || false : false; var search_defaults = {}; _.each(this.action.context, function (value, key) { var match = /^search_default_(.*)$/.exec(key); @@ -177,10 +175,12 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ search_defaults[match[1]] = value; } }); - var searchview_loaded = null; - if (view_id) { - searchview_loaded = this.setup_search_view(view_id,search_defaults); - } + + // init search view + var searchview_id = this.action.search_view_id && this.action.search_view_id[0]; + + var searchview_loaded = this.setup_search_view( + searchview_id || false, search_defaults); // schedule auto_search if (searchview_loaded != null && this.action['auto_search']) { From 5d7dae6e6f48b5c29ab9a0e964c4fd2404b431f0 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 10:20:28 +0200 Subject: [PATCH 34/86] [FIX] id fetching for deletion signal in list views bzr revid: xmo@openerp.com-20110412082028-c3yfg1ay14xcrmfh --- addons/base/static/src/js/list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index e70b442cac0..7827bbad2a2 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -204,7 +204,7 @@ openerp.base.ListView = openerp.base.Controller.extend( // don't link to forms e.stopImmediatePropagation(); this.dataset.unlink( - [this.rows[$(e.currentTarget).closest('tr').prevAll().length].id]); + [this.rows[$(e.currentTarget).closest('tr').prevAll().length].data.id.value]); }, /** * Handles deletion of all selected lines @@ -226,7 +226,7 @@ openerp.base.ListView = openerp.base.Controller.extend( var rows = this.rows; return this.$element.find('th.oe-record-selector input:checked') .closest('tr').map(function () { - return rows[$(this).prevAll().length].id; + return rows[$(this).prevAll().length].data.id.value; }).get(); } }); From 9f780dd615953b28fcc2d7f388f005daaf554b0e Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Tue, 12 Apr 2011 10:38:31 +0200 Subject: [PATCH 35/86] [FIX] form: use boolean attributes (readonly, required,...) from field too bzr revid: fme@openerp.com-20110412083831-1a8e37ozysgrs6y1 --- addons/base/static/src/js/form.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index ce6a203a52d..65cb5498c54 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -435,12 +435,12 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ } } }, - handle_node: function(n) { - var type = this.view.fields_view.fields[n.attrs.name] || {}; - var widget_type = n.attrs.widget || type.type || n.tag; - var widget = new (openerp.base.form.widgets.get_object(widget_type)) (this.view, n); - if (n.tag == 'field' && n.attrs.nolabel != '1') { - var label = new (openerp.base.form.widgets.get_object('label')) (this.view, n); + handle_node: function(node) { + var type = this.view.fields_view.fields[node.attrs.name] || {}; + var widget_type = node.attrs.widget || type.type || node.tag; + var widget = new (openerp.base.form.widgets.get_object(widget_type)) (this.view, node); + if (node.tag == 'field' && node.attrs.nolabel != '1') { + var label = new (openerp.base.form.widgets.get_object('label')) (this.view, node); label["for"] = widget; this.add_widget(label); } @@ -579,12 +579,13 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ if (node.attrs.nolabel != '1' && this.colspan > 1) { this.colspan--; } - this.field = view.fields_view.fields[node.attrs.name]; + this.field = view.fields_view.fields[node.attrs.name] || {}; this.string = node.attrs.string || this.field.string; this.help = node.attrs.help || this.field.help; - this.nolabel = (node.attrs.nolabel == '1'); - this.readonly = (node.attrs.readonly == '1'); - this.required = (node.attrs.required == '1'); + this.invisible = (this.invisible || this.field.invisible == '1'); + this.nolabel = (this.field.nolabel || node.attrs.nolabel) == '1'; + this.readonly = (this.field.readonly || node.attrs.readonly) == '1'; + this.required = (this.field.required || node.attrs.required) == '1'; this.invalid = false; this.touched = false; }, From 091f817b14ccb9fb0ca38a2b45fdf008ac33e53a Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Tue, 12 Apr 2011 11:07:16 +0200 Subject: [PATCH 36/86] [ADD] Add t-opentag="bool" attribute in order to force an empty tag to be opened (IE issue) bzr revid: fme@openerp.com-20110412090716-s3jy1ct6pz3qfrvn --- addons/base/static/lib/qweb/qweb.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/base/static/lib/qweb/qweb.js b/addons/base/static/lib/qweb/qweb.js index 59bcd12ef9b..05dd9d65fe2 100644 --- a/addons/base/static/lib/qweb/qweb.js +++ b/addons/base/static/lib/qweb/qweb.js @@ -166,7 +166,9 @@ var QWeb = { for (var an in g_att) { att += " " + an + '="' + this.escape_att(g_att[an]) + '"'; } - return inner.length ? "<" + e.tagName + att + ">" + inner + "" : "<" + e.tagName + att + "/>"; + // Some IE versions have problems with closed tags + var opentag = !!t_att['opentag'] && this.eval_bool(t_att["opentag"], v); + return inner.length || opentag ? "<" + e.tagName + att + ">" + inner + "" : "<" + e.tagName + att + "/>"; }, render_att_att:function(e, t_att, g_att, v, ext, av) { if (ext) { From e14997c7bcdd34b35e940dbb8123140edf40dd43 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 11:10:36 +0200 Subject: [PATCH 37/86] [FIX] unit tests for list view following change in records topology bzr revid: xmo@openerp.com-20110412091036-w7i9dnh7svqkpa3u --- addons/base/static/test/list.js | 38 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/addons/base/static/test/list.js b/addons/base/static/test/list.js index 4a5bb3795fe..c0a34aa71be 100644 --- a/addons/base/static/test/list.js +++ b/addons/base/static/test/list.js @@ -24,7 +24,9 @@ $(document).ready(function () { openerp = window.openerp.init(true); window.openerp.base.chrome(openerp); // views loader stuff + window.openerp.base.data(openerp); window.openerp.base.views(openerp); + window.openerp.base.form(openerp); window.openerp.base.list(openerp); } }); @@ -36,7 +38,11 @@ $(document).ready(function () { listview.on_loaded(fvg); - listview.do_fill_table([{}, {}, {}]).then(function () { + listview.do_fill_table([ + {data: {id: {value: null}}}, + {data: {id: {value: null}}}, + {data: {id: {value: null}}} + ]).then(function () { ok(are(listview.$element.find('tbody th'), '.oe-record-selector')); ok(are(listview.$element.find('tbody th input'), @@ -52,7 +58,11 @@ $(document).ready(function () { listview.on_loaded(fvg); - listview.do_fill_table([{}, {}, {}]).then(function () { + listview.do_fill_table([ + {data: {id: {value: null}}}, + {data: {id: {value: null}}}, + {data: {id: {value: null}}} + ]).then(function () { equal(listview.$element.find('tbody th').length, 0); start(); }); @@ -62,7 +72,11 @@ $(document).ready(function () { {}, null, 'qunit-fixture', {model: null}); listview.on_loaded(fvg); - listview.do_fill_table([{id: 1}, {id: 2}, {id: 3}]).then(function () { + listview.do_fill_table([ + {data: {id: {value: 1}}}, + {data: {id: {value: 2}}}, + {data: {id: {value: 3}}} + ]).then(function () { listview.$element.find('tbody th input:eq(2)') .attr('checked', true); deepEqual(listview.get_selection(), [3]); @@ -78,7 +92,11 @@ $(document).ready(function () { listview.on_loaded(fvg); - listview.do_fill_table([{id: 1}, {id: 2}, {id: 3}]).then(function () { + listview.do_fill_table([ + {data: {id: {value: null}}}, + {data: {id: {value: null}}}, + {data: {id: {value: null}}} + ]).then(function () { equal( listview.$element.find('tbody tr td.oe-record-delete button').length, 3); @@ -95,7 +113,11 @@ $(document).ready(function () { listview.on_loaded(fvg); - listview.do_fill_table([{id: 1}, {id: 2}, {id: 3}]).then(function () { + listview.do_fill_table([ + {data: {id: {value: 1}}}, + {data: {id: {value: 2}}}, + {data: {id: {value: 3}}} + ]).then(function () { listview.$element.find('tbody td.oe-record-delete:eq(2) button').click(); deepEqual(deleted, [3]); listview.$element.find('tbody td.oe-record-delete:eq(0) button').click(); @@ -112,7 +134,11 @@ $(document).ready(function () { listview.on_loaded(fvg); - listview.do_fill_table([{id: 1}, {id: 2}, {id: 3}]).then(function () { + listview.do_fill_table([ + {data: {id: {value: 1}}}, + {data: {id: {value: 2}}}, + {data: {id: {value: 3}}} + ]).then(function () { listview.$element.find('tbody th input:eq(2)') .attr('checked', true); listview.$element.find('tbody th input:eq(1)') From 5ed6be00fbf0aa5ee0afe0a6a61114433696b66b Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 11:22:22 +0200 Subject: [PATCH 38/86] [IMP] basic styling cleanup for listview table bzr revid: xmo@openerp.com-20110412092222-8htdqnzygqa3fsv6 --- addons/base/static/src/css/base.css | 7 +++++++ addons/base/static/src/js/list.js | 4 ++-- addons/base/static/src/xml/base.xml | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 7b686f53fb4..f6d8dac3bee 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -414,6 +414,13 @@ body.openerp { padding-left: 10px; } +/* List */ +.openerp .oe-listview table { + clear: right; + width: 100%; + border-spacing: 0; + border-collapse: collapse; +} /* Form */ .openerp .required.error { diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 7827bbad2a2..bbc69689851 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -35,7 +35,7 @@ openerp.base.ListView = openerp.base.Controller.extend( this.options = _.extend({}, this.defaults, options || {}); }, start: function() { - //this.log('Starting ListView '+this.model+this.view_id) + this.$element.addClass('oe-listview'); return this.rpc("/base/listview/load", {"model": this.model, "view_id":this.view_id, toolbar:!!this.view_manager.sidebar}, this.on_loaded); }, @@ -119,7 +119,7 @@ openerp.base.ListView = openerp.base.Controller.extend( var PAGE_SIZE = 50, bodies_count = Math.ceil(this.rows.length / PAGE_SIZE), body = 0, - $body = $('').appendTo($table); + $body = $('').appendTo($table); var rendered = $.Deferred(); var render_body = function () { diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 7e2c5f9eec7..e0d51d105a7 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -175,7 +175,7 @@ - +
- +
From 1755727dd3ecd0758795e3b3b6f4ab9b0b3c115c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 12:46:59 +0200 Subject: [PATCH 44/86] [FIX] no need for cellspacing bzr revid: xmo@openerp.com-20110412104659-3vp3w2pjq3xfx664 --- addons/base/static/src/xml/base.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 650c26c46eb..97f656378bf 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -170,7 +170,7 @@ - +
From 4e1b38312e6a97522051a4c8548bb480bff480a8 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 12 Apr 2011 13:07:03 +0200 Subject: [PATCH 45/86] [IMP] highlight rows on hover bzr revid: xmo@openerp.com-20110412110703-3chpip0vrcdwg1s3 --- addons/base/static/src/css/base.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index eebf8a54a00..535d152839f 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -425,6 +425,12 @@ body.openerp { .openerp .oe-listview tr.odd { background-color: #f3f3f3; } +.openerp .oe-listview tbody tr:hover { + background-color: #ecebf2; +} +.openerp .oe-listview tbody tr:hover { + background-color: #eae9f0; +} .openerp .oe-listview .oe-field-cell button { padding: 0; From e4636f1e7e71666c28ebb9aa4aad5e545c1411dc Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 12 Apr 2011 13:14:56 +0200 Subject: [PATCH 46/86] [IMP] Css improvements for sidebar. bzr revid: nicolas.vanhoren@openerp.com-20110412111456-zms7v8qapa2f1r50 --- addons/base/static/src/css/base.css | 28 ++++++++++++++ addons/base/static/src/img/toggle-a-bg.png | Bin 0 -> 412 bytes addons/base/static/src/js/views.js | 5 +++ addons/base/static/src/xml/base.xml | 41 ++++++++++++--------- 4 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 addons/base/static/src/img/toggle-a-bg.png diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index dde45195c6e..b7a9d6c7d60 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -559,6 +559,10 @@ body.openerp { } .openerp .sidebar-main-div { + height:100%; +} + +.openerp .sidebar-sub-div { padding:0; margin:0; width:180px; @@ -570,6 +574,14 @@ body.openerp { font-size: 0.9em; } +.openerp .closed-sidebar .sidebar-sub-div { + width:22px; +} + +.openerp .closed-sidebar .sidebar-displaying-div { + display:none; +} + .openerp .sidebar-main-div a { color: #555; text-decoration: none; @@ -609,3 +621,19 @@ body.openerp { padding: 3px 3px 3px 10px; } +.openerp .toggle-sidebar { + border-color: #D2CFCF; + border-style: solid; + border-width: 1px; + display: block; + background: url(../img/toggle-a-bg.png); + width: 21px; + height: 21px; + position: absolute; + z-index: 10; +} +.openerp .open-sidebar .toggle-sidebar { + margin-left: 158px; + background-position: 21px 0; +} + diff --git a/addons/base/static/src/img/toggle-a-bg.png b/addons/base/static/src/img/toggle-a-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..b9837ab233e5ef387e09fc655e8fc81d8bea286f GIT binary patch literal 412 zcmV;N0b~A&P){r&v>`uh6%`T6+x`1SMi?d$9G^78ie_4oJp@$c{c{{H{}{}P0$7XSbN zzDYzuRCwCO$~O|iKmbEg**K6fIVa<|1OxX!91JUl0u^+;PJe{^&;EbCse04bpP{9y zjL1}#5qW00>#(^Ua4-(G&2a5zgfzES{c(36a4-h@?zq3o3H9`)4Zr5qQesS>$w!+5 z_2ebOq1Txt#F$Jvy#pc+IE582&1_fDJMLy?85abnw5ISwZ^Oa3@j@TdlHeGtFm6)0 z95SM;K1@Y2qPz$)VsZS|kP*RIkP*YHAR|U6K}O84f{YJy6qvEHn2#p_0000 -