From f08392f5921af76f2ad6197cfc97884f706ec30e Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 3 Apr 2013 17:27:13 +0200 Subject: [PATCH 01/12] GraphViewer: NodeShaper now supports colouring by attribute value --- html/admin/js/graphViewer/graph/colourMapper.js | 2 +- html/admin/js/graphViewer/graph/nodeShaper.js | 7 +++++++ .../js/graphViewer/jasmine_test/runnerNodeShaper.html | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/html/admin/js/graphViewer/graph/colourMapper.js b/html/admin/js/graphViewer/graph/colourMapper.js index 7a76bafaee..78a92fe3ef 100644 --- a/html/admin/js/graphViewer/graph/colourMapper.js +++ b/html/admin/js/graphViewer/graph/colourMapper.js @@ -38,7 +38,7 @@ function ColourMapper() { colours.push("navy"); colours.push("green"); colours.push("gold"); - colours.push("indigo"); + colours.push("red"); colours.push("saddlebrown"); colours.push("skyblue"); colours.push("olive"); diff --git a/html/admin/js/graphViewer/graph/nodeShaper.js b/html/admin/js/graphViewer/graph/nodeShaper.js index d6a34f94e1..d1b28bed13 100644 --- a/html/admin/js/graphViewer/graph/nodeShaper.js +++ b/html/admin/js/graphViewer/graph/nodeShaper.js @@ -1,5 +1,6 @@ /*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */ /*global $, _, d3*/ +/*global ColourMapper*/ //////////////////////////////////////////////////////////////////////////////// /// @brief Graph functionality /// @@ -66,6 +67,7 @@ function NodeShaper(parent, flags, idfunc) { noop = function (node) { }, + colourMapper = new ColourMapper(), events = { click: noop, dblclick: noop, @@ -225,6 +227,11 @@ function NodeShaper(parent, flags, idfunc) { }; break; case "attribute": + addColor = function (g) { + g.attr("fill", function(n) { + return colourMapper.getColour(n[color.key]); + }); + }; break; default: throw "Sorry given colour-scheme not known"; diff --git a/html/admin/js/graphViewer/jasmine_test/runnerNodeShaper.html b/html/admin/js/graphViewer/jasmine_test/runnerNodeShaper.html index e6aac9d75d..b2f45cdd24 100644 --- a/html/admin/js/graphViewer/jasmine_test/runnerNodeShaper.html +++ b/html/admin/js/graphViewer/jasmine_test/runnerNodeShaper.html @@ -20,6 +20,7 @@ + From c62230782ad6510f3fe507173c265640156bdf58 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 3 Apr 2013 17:36:59 +0200 Subject: [PATCH 02/12] GraphViewer: EdgeShaper now supports colouring by attribute value --- html/admin/js/graphViewer/graph/edgeShaper.js | 7 +++++++ .../graphViewer/jasmine_test/runnerEdgeShaper.html | 1 + .../jasmine_test/specEdgeShaper/edgeShaperSpec.js | 12 ++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/html/admin/js/graphViewer/graph/edgeShaper.js b/html/admin/js/graphViewer/graph/edgeShaper.js index 2a740ab591..e67dd8cbdb 100644 --- a/html/admin/js/graphViewer/graph/edgeShaper.js +++ b/html/admin/js/graphViewer/graph/edgeShaper.js @@ -1,5 +1,6 @@ /*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */ /*global _, $, d3*/ +/*global ColourMapper*/ //////////////////////////////////////////////////////////////////////////////// /// @brief Graph functionality /// @@ -55,6 +56,7 @@ function EdgeShaper(parent, flags, idfunc) { noop = function (line, g) { }, + colourMapper = new ColourMapper(), events = { click: noop, dblclick: noop, @@ -261,6 +263,11 @@ function EdgeShaper(parent, flags, idfunc) { }; break; case "attribute": + addColor = function (line, g) { + g.attr("stroke", function(e) { + return colourMapper.getColour(e[color.key]); + }); + }; break; default: throw "Sorry given colour-scheme not known"; diff --git a/html/admin/js/graphViewer/jasmine_test/runnerEdgeShaper.html b/html/admin/js/graphViewer/jasmine_test/runnerEdgeShaper.html index 0b8ca1f98d..3b9ced9785 100644 --- a/html/admin/js/graphViewer/jasmine_test/runnerEdgeShaper.html +++ b/html/admin/js/graphViewer/jasmine_test/runnerEdgeShaper.html @@ -20,6 +20,7 @@ + diff --git a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js index 215a422975..ea782555b3 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js @@ -301,17 +301,17 @@ { color: { type: "attribute", - value: "label" + key: "label" } } ), c1,c2,c3,c4; shaper.drawEdges(edges); - c1 = $("#1-2 line").attr("stroke"); - c2 = $("#2-3 line").attr("stroke"); - c3 = $("#3-4 line").attr("stroke"); - c4 = $("#4-1 line").attr("stroke"); + c1 = $("#1-2").attr("stroke"); + c2 = $("#2-3").attr("stroke"); + c3 = $("#3-4").attr("stroke"); + c4 = $("#4-1").attr("stroke"); expect(c1).toBeDefined(); expect(c2).toBeDefined(); @@ -735,7 +735,7 @@ expect($("svg #3-4")[0]).toBeUndefined(); }); - it('should be able to add some edges and remove other egdes', function () { + it('should be able to add some edges and remove other edges', function () { edges.splice(2, 1); edges.splice(0, 1); edges.push( From 5c9c408cb4b1296e1e19aacbf655432799c416e8 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 3 Apr 2013 17:46:30 +0200 Subject: [PATCH 03/12] GraphViewer: Added tests for Node-Colour change controls --- .../specNodeShaper/nodeShaperUISpec.js | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperUISpec.js b/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperUISpec.js index 351216f676..a1acd63d22 100644 --- a/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperUISpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperUISpec.js @@ -155,6 +155,83 @@ }); + it('should be able to add a switch single colour control to the list', function() { + runs(function() { + shaperUI.addControlOpticSingleColour(); + + expect($("#control_list #control_singlecolour").length).toEqual(1); + + helper.simulateMouseEvent("click", "control_singlecolour"); + $("#control_singlecolour_fill").attr("value", "#123456"); + $("#control_singlecolour_stroke").attr("value", "#654321"); + helper.simulateMouseEvent("click", "control_singlecolour_submit"); + + expect(shaper.changeTo).toHaveBeenCalledWith({ + color: { + type: "single", + fill: "#123456", + stroke: "#654321" + } + }); + }); + + waitsFor(function() { + return $("#control_singlecolour_modal").length === 0; + }, 2000, "The modal dialog should disappear."); + + }); + + it('should be able to add a switch colour on attribute control to the list', function() { + runs(function() { + shaperUI.addControlOpticAttributeColour(); + + expect($("#control_list #control_attributecolour").length).toEqual(1); + + helper.simulateMouseEvent("click", "control_attributecolour"); + $("#control_attributecolour_key").attr("value", "label"); + helper.simulateMouseEvent("click", "control_attributecolour_submit"); + + expect(shaper.changeTo).toHaveBeenCalledWith({ + color: { + type: "attribute", + key: "label" + } + }); + }); + + waitsFor(function() { + return $("#control_attributecolour_modal").length === 0; + }, 2000, "The modal dialog should disappear."); + + }); + + it('should be able to add a switch colour on expand status control to the list', function() { + runs(function() { + shaperUI.addControlOpticExpandColour(); + + expect($("#control_list #control_expandcolour").length).toEqual(1); + + helper.simulateMouseEvent("click", "control_expandcolour"); + $("#control_expandcolour_expanded").attr("value", "#123456"); + $("#control_expandcolour_collapsed").attr("value", "#654321"); + helper.simulateMouseEvent("click", "control_expandcolour_submit"); + + expect(shaper.changeTo).toHaveBeenCalledWith({ + color: { + type: "expand", + expanded: "#123456", + collapsed: "#654321" + } + }); + }); + + waitsFor(function() { + return $("#control_expandcolour_modal").length === 0; + }, 2000, "The modal dialog should disappear."); + + }); + + it('should be able to add all optic controls to the list', function () { shaperUI.addAllOptics(); @@ -162,7 +239,9 @@ expect($("#control_list #control_circle").length).toEqual(1); expect($("#control_list #control_rect").length).toEqual(1); expect($("#control_list #control_label").length).toEqual(1); - + expect($("#control_list #control_singlecolour").length).toEqual(1); + expect($("#control_list #control_attributecolour").length).toEqual(1); + expect($("#control_list #control_expandcolour").length).toEqual(1); }); it('should be able to add all action controls to the list', function () { @@ -177,7 +256,9 @@ expect($("#control_list #control_circle").length).toEqual(1); expect($("#control_list #control_rect").length).toEqual(1); expect($("#control_list #control_label").length).toEqual(1); - + expect($("#control_list #control_singlecolour").length).toEqual(1); + expect($("#control_list #control_attributecolour").length).toEqual(1); + expect($("#control_list #control_expandcolour").length).toEqual(1); }); }); From 8a0b79b42906e455218121c3fc11eba3587492e6 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 3 Apr 2013 18:06:35 +0200 Subject: [PATCH 04/12] GraphViewer: NodeShaper now creates UI elements to change the colouring --- .../js/graphViewer/ui/nodeShaperControls.js | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/html/admin/js/graphViewer/ui/nodeShaperControls.js b/html/admin/js/graphViewer/ui/nodeShaperControls.js index 549b0cab04..115887c9ab 100644 --- a/html/admin/js/graphViewer/ui/nodeShaperControls.js +++ b/html/admin/js/graphViewer/ui/nodeShaperControls.js @@ -140,11 +140,109 @@ function NodeShaperControls(list, shaper) { button.onclick = callback; }; + ////////////////////////////////////////////////////////////////// + // Colour Buttons + ////////////////////////////////////////////////////////////////// + + this.addControlOpticSingleColour = function() { + var prefix = "control_singlecolour", + idprefix = prefix + "_", + callback = function() { + modalDialogHelper.createModalDialog("Switch to Colour", + idprefix, [{ + type: "text", + id: "fill" + },{ + type: "text", + id: "stroke" + }], function () { + var fill = $("#" + idprefix + "fill").attr("value"), + stroke = $("#" + idprefix + "stroke").attr("value"); + shaper.changeTo({ + color: { + type: "single", + fill: fill, + stroke: stroke + } + }); + } + ); + }, + button = document.createElement("li"); + button.className = "graph_control " + prefix; + button.id = prefix; + button.appendChild(document.createTextNode("Single Colour")); + list.appendChild(button); + button.onclick = callback; + }; + + this.addControlOpticAttributeColour = function() { + var prefix = "control_attributecolour", + idprefix = prefix + "_", + callback = function() { + modalDialogHelper.createModalDialog("Display colour by attribute", + idprefix, [{ + type: "text", + id: "key" + }], function () { + var key = $("#" + idprefix + "key").attr("value"); + shaper.changeTo({ + color: { + type: "attribute", + key: key + } + }); + } + ); + }, + button = document.createElement("li"); + button.className = "graph_control " + prefix; + button.id = prefix; + button.appendChild(document.createTextNode("Colour by Attribute")); + list.appendChild(button); + button.onclick = callback; + }; + + this.addControlOpticExpandColour = function() { + var prefix = "control_expandcolour", + idprefix = prefix + "_", + callback = function() { + modalDialogHelper.createModalDialog("Display colours for expansion", + idprefix, [{ + type: "text", + id: "expanded" + },{ + type: "text", + id: "collapsed" + }], function () { + var expanded = $("#" + idprefix + "expanded").attr("value"), + collapsed = $("#" + idprefix + "collapsed").attr("value"); + shaper.changeTo({ + color: { + type: "expand", + expanded: expanded, + collapsed: collapsed + } + }); + } + ); + }, + button = document.createElement("li"); + button.className = "graph_control " + prefix; + button.id = prefix; + button.appendChild(document.createTextNode("Expansion Colour")); + list.appendChild(button); + button.onclick = callback; + }; + this.addAllOptics = function () { self.addControlOpticShapeNone(); self.addControlOpticShapeCircle(); self.addControlOpticShapeRect(); self.addControlOpticLabel(); + self.addControlOpticSingleColour(); + self.addControlOpticAttributeColour(); + self.addControlOpticExpandColour(); }; this.addAllActions = function () { From 8ab12a75b547f46e8d19c5335b1e4c5f58b58a8e Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 4 Apr 2013 00:16:49 +0200 Subject: [PATCH 05/12] GraphViewer: Node colouring added to the demo.html. Decided to forcefully set the TextColour to black, as it inherits the user defined colour-scheme otherwise and this gets unreadable quite fast --- html/admin/js/graphViewer/demo.html | 3 ++- html/admin/js/graphViewer/graph/nodeShaper.js | 4 ++++ html/admin/js/graphViewer/graphViewer.js | 4 ++-- .../graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js | 2 ++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/html/admin/js/graphViewer/demo.html b/html/admin/js/graphViewer/demo.html index 61fc8dde80..1a650abdd5 100644 --- a/html/admin/js/graphViewer/demo.html +++ b/html/admin/js/graphViewer/demo.html @@ -3,13 +3,14 @@ - + + diff --git a/html/admin/js/graphViewer/graph/nodeShaper.js b/html/admin/js/graphViewer/graph/nodeShaper.js index d1b28bed13..dda5fd0c06 100644 --- a/html/admin/js/graphViewer/graph/nodeShaper.js +++ b/html/admin/js/graphViewer/graph/nodeShaper.js @@ -189,12 +189,16 @@ function NodeShaper(parent, flags, idfunc) { addLabel = function (node) { node.append("text") // Append a label for the node .attr("text-anchor", "middle") // Define text-anchor + .attr("fill", "black") + .attr("stroke", "black") .text(label); }; } else { addLabel = function (node) { node.append("text") // Append a label for the node .attr("text-anchor", "middle") // Define text-anchor + .attr("fill", "black") + .attr("stroke", "black") .text(function(d) { return d[label] !== undefined ? d[label] : ""; // Which value should be used as label }); diff --git a/html/admin/js/graphViewer/graphViewer.js b/html/admin/js/graphViewer/graphViewer.js index 3312718d4c..8620fb81eb 100644 --- a/html/admin/js/graphViewer/graphViewer.js +++ b/html/admin/js/graphViewer/graphViewer.js @@ -276,11 +276,11 @@ function GraphViewer(svg, width, height, //TODO REMOVE //HACK to view the Controls in the Demo - /* + var edgeShaperControls = new EdgeShaperControls($("#controls")[0], edgeShaper); edgeShaperControls.addAll(); var nodeShaperControls = new NodeShaperControls($("#controls")[0], nodeShaper); nodeShaperControls.addAll(); - */ + } \ No newline at end of file diff --git a/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js b/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js index 1a21b10d6b..92da6a7110 100644 --- a/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js @@ -503,6 +503,8 @@ expect($("svg .node text")[0]).toBeDefined(); expect($("svg .node text").length).toEqual(1); expect($("svg .node text")[0].textContent).toEqual("MyLabel"); + expect($("svg .node text").attr("stroke")).toEqual("black"); + expect($("svg .node text").attr("fill")).toEqual("black"); }); it('should ignore other attributes', function () { From f5c523ea562f3b41adc32144a8b9fb85aa9d57bd Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 4 Apr 2013 00:22:24 +0200 Subject: [PATCH 06/12] GraphViewer: Implemented test for edge colouring dialogs --- .../specEdgeShaper/edgeShaperUISpec.js | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperUISpec.js b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperUISpec.js index 5910623e50..9278c8314a 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperUISpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperUISpec.js @@ -120,12 +120,93 @@ }); + it('should be able to add a switch single colour control to the list', function() { + runs(function() { + shaperUI.addControlOpticSingleColour(); + + expect($("#control_list #control_singlecolour").length).toEqual(1); + + helper.simulateMouseEvent("click", "control_singlecolour"); + $("#control_singlecolour_stroke").attr("value", "#123456"); + helper.simulateMouseEvent("click", "control_singlecolour_submit"); + + expect(shaper.changeTo).toHaveBeenCalledWith({ + color: { + type: "single", + stroke: "#123456" + } + }); + }); + + waitsFor(function() { + return $("#control_singlecolour_modal").length === 0; + }, 2000, "The modal dialog should disappear."); + + }); + + it('should be able to add a switch colour on attribute control to the list', function() { + runs(function() { + shaperUI.addControlOpticAttributeColour(); + + expect($("#control_list #control_attributecolour").length).toEqual(1); + + helper.simulateMouseEvent("click", "control_attributecolour"); + $("#control_attributecolour_key").attr("value", "label"); + helper.simulateMouseEvent("click", "control_attributecolour_submit"); + + expect(shaper.changeTo).toHaveBeenCalledWith({ + color: { + type: "attribute", + key: "label" + } + }); + }); + + waitsFor(function() { + return $("#control_attributecolour_modal").length === 0; + }, 2000, "The modal dialog should disappear."); + + }); + + it('should be able to add a switch colour to gradient control to the list', function() { + runs(function() { + shaperUI.addControlOpticGradientColour(); + + expect($("#control_list #control_gradientcolour").length).toEqual(1); + + helper.simulateMouseEvent("click", "control_gradientcolour"); + $("#control_gradientcolour_source").attr("value", "#123456"); + $("#control_gradientcolour_target").attr("value", "#654321"); + helper.simulateMouseEvent("click", "control_expandcolour_submit"); + + expect(shaper.changeTo).toHaveBeenCalledWith({ + color: { + type: "gradient", + source: "#123456", + target: "#654321" + } + }); + }); + + waitsFor(function() { + return $("#control_gradientcolour_modal").length === 0; + }, 2000, "The modal dialog should disappear."); + + }); + + + + + it('should be able to add all optic controls to the list', function () { shaperUI.addAllOptics(); expect($("#control_list #control_none").length).toEqual(1); expect($("#control_list #control_arrow").length).toEqual(1); expect($("#control_list #control_label").length).toEqual(1); + expect($("#control_list #control_singlecolour").length).toEqual(1); + expect($("#control_list #control_attributecolour").length).toEqual(1); + expect($("#control_list #control_gradientcolour").length).toEqual(1); }); @@ -140,6 +221,9 @@ expect($("#control_list #control_none").length).toEqual(1); expect($("#control_list #control_arrow").length).toEqual(1); expect($("#control_list #control_label").length).toEqual(1); + expect($("#control_list #control_singlecolour").length).toEqual(1); + expect($("#control_list #control_attributecolour").length).toEqual(1); + expect($("#control_list #control_gradientcolour").length).toEqual(1); }); }); From c208a94f52b313a0845d1a20254e83eeba177452 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 4 Apr 2013 00:30:54 +0200 Subject: [PATCH 07/12] GraphViewer: Implemented UI Dialogs for edge colouring --- .../specEdgeShaper/edgeShaperUISpec.js | 2 +- .../js/graphViewer/ui/edgeShaperControls.js | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperUISpec.js b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperUISpec.js index 9278c8314a..c3005d802d 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperUISpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperUISpec.js @@ -177,7 +177,7 @@ helper.simulateMouseEvent("click", "control_gradientcolour"); $("#control_gradientcolour_source").attr("value", "#123456"); $("#control_gradientcolour_target").attr("value", "#654321"); - helper.simulateMouseEvent("click", "control_expandcolour_submit"); + helper.simulateMouseEvent("click", "control_gradientcolour_submit"); expect(shaper.changeTo).toHaveBeenCalledWith({ color: { diff --git a/html/admin/js/graphViewer/ui/edgeShaperControls.js b/html/admin/js/graphViewer/ui/edgeShaperControls.js index f41efec2d7..7937a42d64 100644 --- a/html/admin/js/graphViewer/ui/edgeShaperControls.js +++ b/html/admin/js/graphViewer/ui/edgeShaperControls.js @@ -101,10 +101,102 @@ function EdgeShaperControls(list, shaper) { button.onclick = callback; }; + + + + this.addControlOpticSingleColour = function() { + var prefix = "control_singlecolour", + idprefix = prefix + "_", + callback = function() { + modalDialogHelper.createModalDialog("Switch to Colour", + idprefix, [{ + type: "text", + id: "stroke" + }], function () { + var stroke = $("#" + idprefix + "stroke").attr("value"); + shaper.changeTo({ + color: { + type: "single", + stroke: stroke + } + }); + } + ); + }, + button = document.createElement("li"); + button.className = "graph_control " + prefix; + button.id = prefix; + button.appendChild(document.createTextNode("Single Colour")); + list.appendChild(button); + button.onclick = callback; + }; + + this.addControlOpticAttributeColour = function() { + var prefix = "control_attributecolour", + idprefix = prefix + "_", + callback = function() { + modalDialogHelper.createModalDialog("Display colour by attribute", + idprefix, [{ + type: "text", + id: "key" + }], function () { + var key = $("#" + idprefix + "key").attr("value"); + shaper.changeTo({ + color: { + type: "attribute", + key: key + } + }); + } + ); + }, + button = document.createElement("li"); + button.className = "graph_control " + prefix; + button.id = prefix; + button.appendChild(document.createTextNode("Colour by Attribute")); + list.appendChild(button); + button.onclick = callback; + }; + + this.addControlOpticGradientColour = function() { + var prefix = "control_gradientcolour", + idprefix = prefix + "_", + callback = function() { + modalDialogHelper.createModalDialog("Change colours for gradient", + idprefix, [{ + type: "text", + id: "source" + },{ + type: "text", + id: "target" + }], function () { + var source = $("#" + idprefix + "source").attr("value"), + target = $("#" + idprefix + "target").attr("value"); + shaper.changeTo({ + color: { + type: "gradient", + source: source, + target: target + } + }); + } + ); + }, + button = document.createElement("li"); + button.className = "graph_control " + prefix; + button.id = prefix; + button.appendChild(document.createTextNode("Gradient Colour")); + list.appendChild(button); + button.onclick = callback; + }; + this.addAllOptics = function () { self.addControlOpticShapeNone(); self.addControlOpticShapeArrow(); self.addControlOpticLabel(); + self.addControlOpticSingleColour(); + self.addControlOpticAttributeColour(); + self.addControlOpticGradientColour(); }; this.addAllActions = function () { From 159c55be39ba73d45d42d963fb814ea1b8ea5c33 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 4 Apr 2013 00:46:41 +0200 Subject: [PATCH 08/12] GraphViewer: EdgeColouring tested in demo.html, again the label is set to black forcefully --- html/admin/js/graphViewer/graph/edgeShaper.js | 4 +++- .../jasmine_test/specEdgeShaper/edgeShaperSpec.js | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/html/admin/js/graphViewer/graph/edgeShaper.js b/html/admin/js/graphViewer/graph/edgeShaper.js index e67dd8cbdb..90b9123f1c 100644 --- a/html/admin/js/graphViewer/graph/edgeShaper.js +++ b/html/admin/js/graphViewer/graph/edgeShaper.js @@ -202,12 +202,14 @@ function EdgeShaper(parent, flags, idfunc) { addLabel = function (line, g) { g.append("text") // Append a label for the edge .attr("text-anchor", "middle") // Define text-anchor + .attr("stroke", "black") .text(label); }; } else { addLabel = function (line, g) { g.append("text") // Append a label for the edge .attr("text-anchor", "middle") // Define text-anchor + .attr("stroke", "black") .text(function(d) { return d[label] !== undefined ? d[label] : ""; // Which value should be used as label }); @@ -234,7 +236,7 @@ function EdgeShaper(parent, flags, idfunc) { switch (color.type) { case "single": addColor = function (line, g) { - line.attr("stroke", color.value); + line.attr("stroke", color.stroke); }; break; case "gradient": diff --git a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js index ea782555b3..209facbf72 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js @@ -265,7 +265,7 @@ { color: { type: "single", - value: "#123456" + stroke: "#123456" } } ); @@ -539,6 +539,13 @@ expect($("#2-3 text")[0].textContent).toEqual("second"); expect($("#3-4 text")[0].textContent).toEqual("third"); expect($("#4-1 text")[0].textContent).toEqual("fourth"); + + // All labels should be printed in black + expect($("#1-2 text").attr("stroke")).toEqual("black"); + expect($("#2-3 text").attr("stroke")).toEqual("black"); + expect($("#3-4 text").attr("stroke")).toEqual("black"); + expect($("#4-1 text").attr("stroke")).toEqual("black"); + }); it('should display the label at the correct position', function() { From 2affe643d3a5d2e20f2c0bd8faad566cd06ecb2e Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 4 Apr 2013 01:04:58 +0200 Subject: [PATCH 09/12] GraphViewer: Enlarged the Arrow Pointer, has to be changed to be relative to nodesize --- html/admin/js/graphViewer/graph/edgeShaper.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/html/admin/js/graphViewer/graph/edgeShaper.js b/html/admin/js/graphViewer/graph/edgeShaper.js index 90b9123f1c..2053ef1908 100644 --- a/html/admin/js/graphViewer/graph/edgeShaper.js +++ b/html/admin/js/graphViewer/graph/edgeShaper.js @@ -183,11 +183,11 @@ function EdgeShaper(parent, flags, idfunc) { .select("defs") .append("marker") .attr("id", "arrow") - .attr("viewBox", "0 0 10 10") - .attr("refX", "0") + .attr("refX", "22") .attr("refY", "5") .attr("markerUnits", "strokeWidth") - .attr("markerHeight", "3") + .attr("markerHeight", "10") + .attr("markerWidth", "10") .attr("orient", "auto") .append("path") .attr("d", "M 0 0 L 10 5 L 0 10 z"); From ea1d88b04b058160390fd30dde0f183e7ec90df5 Mon Sep 17 00:00:00 2001 From: Lucas Dohmen Date: Thu, 4 Apr 2013 14:16:01 +0200 Subject: [PATCH 10/12] Crazy Documentation Fixes. Doxygen is so awesome. /thx @jsteemann --- Documentation/UserManual/Foxx.md | 2 -- js/server/modules/org/arangodb/foxx.js | 47 +++++++++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Documentation/UserManual/Foxx.md b/Documentation/UserManual/Foxx.md index 81968dc61b..d1a44bd9d1 100644 --- a/Documentation/UserManual/Foxx.md +++ b/Documentation/UserManual/Foxx.md @@ -237,8 +237,6 @@ A repository is a gateway to the database. It gets data from the database, updat #### new Foxx.Repository @copydetails JSF_foxx_repository_initializer -#### Foxx.Repository.extend -@copydetails JSF_foxx_repository_extend #### Foxx.Repository#collection @copydetails JSF_foxx_repository_collection diff --git a/js/server/modules/org/arangodb/foxx.js b/js/server/modules/org/arangodb/foxx.js index 6201b2d602..95aacfbfd5 100644 --- a/js/server/modules/org/arangodb/foxx.js +++ b/js/server/modules/org/arangodb/foxx.js @@ -43,6 +43,11 @@ var Application, foxxManager = require("org/arangodb/foxx-manager"), internal = {}; +//////////////////////////////////////////////////////////////////////////////// +/// @addtogroup ArangoAPI +/// @{ +//////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// /// @fn JSF_foxx_createUrlObject /// @brief create a new url object @@ -174,6 +179,7 @@ _.extend(Application.prototype, { /// //handle the request /// }); //////////////////////////////////////////////////////////////////////////////// + handleRequest: function (method, route, callback) { 'use strict'; var newRoute = { @@ -295,6 +301,7 @@ _.extend(Application.prototype, { /// // Take this request and deal with it! /// }); //////////////////////////////////////////////////////////////////////////////// + 'delete': function (route, callback) { 'use strict'; return this.handleRequest("delete", route, callback); @@ -321,6 +328,7 @@ _.extend(Application.prototype, { /// //Do some crazy request logging /// }); //////////////////////////////////////////////////////////////////////////////// + before: function (path, func) { 'use strict'; if (_.isUndefined(func)) { @@ -351,6 +359,7 @@ _.extend(Application.prototype, { /// //Do some crazy response logging /// }); //////////////////////////////////////////////////////////////////////////////// + after: function (path, func) { 'use strict'; if (_.isUndefined(func)) { @@ -371,16 +380,17 @@ _.extend(Application.prototype, { /// @brief The ViewHelper concept /// /// If you want to use a function inside your templates, the ViewHelpers -/// will come to rescue you. Define them on your app like this: +/// will come to rescue you. Define them on your app like in the example. +/// +/// Then you can just call this function in your template's JavaScript +/// blocks. /// /// @EXAMPLES /// app.helper("link_to", function (identifier) { /// return urlRepository[identifier]; /// }); -/// -/// Then you can just call this function in your template's JavaScript -/// blocks. //////////////////////////////////////////////////////////////////////////////// + helper: function (name, func) { 'use strict'; this.helperCollection[name] = func; @@ -396,6 +406,7 @@ _.extend(Application.prototype, { /// @EXAMPLES /// app.accepts(["json"], "json"); //////////////////////////////////////////////////////////////////////////////// + accepts: function (allowedFormats, defaultFormat) { 'use strict'; @@ -414,6 +425,7 @@ _.extend(Application.prototype, { /// /// Used for documenting and constraining the routes. //////////////////////////////////////////////////////////////////////////////// + RequestContext = function (route) { 'use strict'; this.route = route; @@ -444,6 +456,7 @@ _.extend(RequestContext.prototype, { /// dataType: "int" /// }); //////////////////////////////////////////////////////////////////////////////// + pathParam: function (paramName, attributes) { 'use strict'; var url = this.route.url, @@ -568,6 +581,7 @@ _.extend(RequestContext.prototype, { /// The `BaseMiddleware` manipulates the request and response /// objects to give you a nicer API. //////////////////////////////////////////////////////////////////////////////// + BaseMiddleware = function (templateCollection, helperCollection) { 'use strict'; var middleware = function (request, response, options, next) { @@ -582,6 +596,7 @@ BaseMiddleware = function (templateCollection, helperCollection) { /// /// Get the body of the request //////////////////////////////////////////////////////////////////////////////// + body: function () { return this.requestBody; }, @@ -599,6 +614,7 @@ BaseMiddleware = function (templateCollection, helperCollection) { /// So for example if the user requested `/test?a=2`, the call /// `params("a")` will return `2`. //////////////////////////////////////////////////////////////////////////////// + params: function (key) { var ps = {}; _.extend(ps, this.urlParameters); @@ -631,14 +647,14 @@ BaseMiddleware = function (templateCollection, helperCollection) { /// response.set("Content-Length", 123); /// response.set("Content-Type", "text/plain"); /// -/// or alternatively: +/// // or alternatively: /// -/// @EXAMPLES /// response.set({ /// "Content-Length": "123", /// "Content-Type": "text/plain" /// }); //////////////////////////////////////////////////////////////////////////////// + set: function (key, value) { var attributes = {}; if (_.isUndefined(value)) { @@ -668,6 +684,7 @@ BaseMiddleware = function (templateCollection, helperCollection) { /// @EXAMPLES /// response.json({'born': 'December 12, 1915'}); //////////////////////////////////////////////////////////////////////////////// + json: function (obj) { this.contentType = "application/json"; this.body = JSON.stringify(obj); @@ -681,7 +698,6 @@ BaseMiddleware = function (templateCollection, helperCollection) { /// you're in luck now. /// It expects documents in the following form in this collection: /// -/// @EXAMPLES /// { /// path: "high/way", /// content: "hallo <%= username %>", @@ -698,8 +714,6 @@ BaseMiddleware = function (templateCollection, helperCollection) { /// into the this collection and search by the path attribute. /// It will then render the template with the given data: /// -/// @EXAMPLES -/// response.render("high/way", {username: 'Application'}) /// /// Which would set the body of the response to `hello Application` with the /// template defined above. It will also set the `contentType` to @@ -708,6 +722,9 @@ BaseMiddleware = function (templateCollection, helperCollection) { /// In addition to the attributes you provided, you also have access to /// all your view helpers. How to define them? Read above in the /// ViewHelper section. +/// +/// @EXAMPLES +/// response.render("high/way", {username: 'Application'}) //////////////////////////////////////////////////////////////////////////////// render: function (templatePath, data) { var template; @@ -839,6 +856,7 @@ FormatMiddleware = function (allowedFormats, defaultFormat) { /// a: 1 /// }); //////////////////////////////////////////////////////////////////////////////// + Model = function (attributes) { 'use strict'; @@ -846,6 +864,7 @@ Model = function (attributes) { /// @fn JSF_foxx_model_attributes /// @brief The attributes property is the internal hash containing the model's state. //////////////////////////////////////////////////////////////////////////////// + this.attributes = attributes || {}; }; @@ -861,6 +880,7 @@ _.extend(Model.prototype, { /// /// instance.get("a"); //////////////////////////////////////////////////////////////////////////////// + get: function (attributeName) { return this.attributes[attributeName]; }, @@ -876,6 +896,7 @@ _.extend(Model.prototype, { /// /// instance.set("a", 2); //////////////////////////////////////////////////////////////////////////////// + set: function (attributeName, value) { this.attributes[attributeName] = value; }, @@ -892,6 +913,7 @@ _.extend(Model.prototype, { /// instance.has("a"); //=> true /// instance.has("b"); //=> false //////////////////////////////////////////////////////////////////////////////// + has: function (attributeName) { return !(_.isUndefined(this.attributes[attributeName]) || _.isNull(this.attributes[attributeName])); @@ -901,6 +923,7 @@ _.extend(Model.prototype, { /// @fn JSF_foxx_model_toJSON /// @brief Return a copy of the model which can be saved into ArangoDB (or send to the client). //////////////////////////////////////////////////////////////////////////////// + toJSON: function () { return this.attributes; } @@ -910,6 +933,7 @@ _.extend(Model.prototype, { /// @fn JSF_foxx_model_extend /// @brief Extend the Model prototype to add or overwrite methods. //////////////////////////////////////////////////////////////////////////////// + Model.extend = backbone_helpers.extend; //////////////////////////////////////////////////////////////////////////////// @@ -922,6 +946,7 @@ Model.extend = backbone_helpers.extend; /// @EXAMPLES /// instance = new Repository(prefix, collection, modelPrototype); //////////////////////////////////////////////////////////////////////////////// + Repository = function (prefix, collection, modelPrototype) { 'use strict'; @@ -929,18 +954,21 @@ Repository = function (prefix, collection, modelPrototype) { /// @fn JSF_foxx_repository_prefix /// @brief The prefix of the application. //////////////////////////////////////////////////////////////////////////////// + this.prefix = prefix; //////////////////////////////////////////////////////////////////////////////// /// @fn JSF_foxx_repository_collection /// @brief The collection object. //////////////////////////////////////////////////////////////////////////////// + this.collection = collection; //////////////////////////////////////////////////////////////////////////////// /// @fn JSF_foxx_repository_modelPrototype /// @brief The prototype of the according model. //////////////////////////////////////////////////////////////////////////////// + this.modelPrototype = modelPrototype; }; @@ -948,6 +976,7 @@ Repository = function (prefix, collection, modelPrototype) { /// @fn JSF_foxx_repository_extend /// @brief Extend the Repository prototype to add or overwrite methods. //////////////////////////////////////////////////////////////////////////////// + Repository.extend = backbone_helpers.extend; exports.Application = Application; From 2e43dbfb14376891cd9ec9b95f3900df22d5fdde Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 4 Apr 2013 15:56:02 +0200 Subject: [PATCH 11/12] Aardvark: Adapted new Foxx interface --- html/admin/js/graphViewer/demo.html | 2 +- html/admin/js/graphViewer/graph/nodeShaper.js | 21 +- .../specNodeShaper/nodeShaperSpec.js | 11 + .../js/graphViewer/style/graphlayout.css | 11 +- js/apps/aardvark/aardvark.js | 36 +- .../assets/collections/foxxCollection.js | 3 - js/apps/aardvark/assets/index.html | 44 - js/apps/aardvark/assets/lib/backbone.js | 1571 --- js/apps/aardvark/assets/lib/jquery.js | 9597 ----------------- js/apps/aardvark/assets/lib/underscore.js | 1226 --- js/apps/aardvark/assets/models/foxx.js | 5 - js/apps/aardvark/assets/router/router.js | 0 js/apps/aardvark/assets/views/foxxListView.js | 16 - js/apps/aardvark/assets/views/foxxView.js | 16 - js/apps/aardvark/manifest.json | 107 +- js/apps/aardvark/models/swagger.js | 81 - .../{models => repositories}/foxxes.js | 77 +- js/apps/aardvark/repositories/swagger.js | 91 + 18 files changed, 194 insertions(+), 12721 deletions(-) delete mode 100644 js/apps/aardvark/assets/collections/foxxCollection.js delete mode 100644 js/apps/aardvark/assets/index.html delete mode 100755 js/apps/aardvark/assets/lib/backbone.js delete mode 100755 js/apps/aardvark/assets/lib/jquery.js delete mode 100755 js/apps/aardvark/assets/lib/underscore.js delete mode 100644 js/apps/aardvark/assets/models/foxx.js delete mode 100644 js/apps/aardvark/assets/router/router.js delete mode 100644 js/apps/aardvark/assets/views/foxxListView.js delete mode 100644 js/apps/aardvark/assets/views/foxxView.js delete mode 100644 js/apps/aardvark/models/swagger.js rename js/apps/aardvark/{models => repositories}/foxxes.js (51%) create mode 100644 js/apps/aardvark/repositories/swagger.js diff --git a/html/admin/js/graphViewer/demo.html b/html/admin/js/graphViewer/demo.html index 1a650abdd5..c70e886355 100644 --- a/html/admin/js/graphViewer/demo.html +++ b/html/admin/js/graphViewer/demo.html @@ -3,7 +3,7 @@ - + diff --git a/html/admin/js/graphViewer/graph/nodeShaper.js b/html/admin/js/graphViewer/graph/nodeShaper.js index dda5fd0c06..8ede4e8d12 100644 --- a/html/admin/js/graphViewer/graph/nodeShaper.js +++ b/html/admin/js/graphViewer/graph/nodeShaper.js @@ -262,7 +262,26 @@ function NodeShaper(parent, flags, idfunc) { if (flags !== undefined) { parseConfig(flags); - } + /* + flags = { + color: { + type: "single", + stroke: "#FF8F35", + fill: "#8AA051" + } + }; + */ + } + /* + if (flags.color === undefined) { + flags.color = { + type: "single", + stroke: "#FF8F35", + fill: "#8AA051" + }; + + } + */ if (_.isFunction(idfunc)) { idFunction = idfunc; diff --git a/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js b/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js index 92da6a7110..1d64bb5d37 100644 --- a/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js @@ -89,6 +89,17 @@ describe('testing for colours', function() { + it('should have a default colouring of no colour flag is given', function() { + var nodes = [{_id: 1}, {_id: 2}], + shaper = new NodeShaper(d3.select("svg")); + shaper.drawNodes(nodes); + + expect($("#1").attr("fill")).toEqual("#8AA051"); + expect($("#1").attr("stroke")).toEqual("#FF8F35"); + expect($("#2").attr("fill")).toEqual("#8AA051"); + expect($("#2").attr("stroke")).toEqual("#FF8F35"); + }); + it('should be able to use the same colour for all nodes', function() { var nodes = [{_id: 1}, {_id: 2}], shaper = new NodeShaper(d3.select("svg"), diff --git a/html/admin/js/graphViewer/style/graphlayout.css b/html/admin/js/graphViewer/style/graphlayout.css index 0de3d6617b..317a175430 100644 --- a/html/admin/js/graphViewer/style/graphlayout.css +++ b/html/admin/js/graphViewer/style/graphlayout.css @@ -8,24 +8,19 @@ stroke: url("#edgegradient"); } -.node circle { +.node { cursor: pointer; fill: #ccc; stroke: #fff; stroke-width: 1px; } -circle.expanded { - fill: #8AA051; -} - -circle.collapsed { - fill: #FF8F35; -} text { font: 10px sans-serif; pointer-events: none; + stroke: #fff; + stroke-width: 1px; } marker#arrow { fill: #666; diff --git a/js/apps/aardvark/aardvark.js b/js/apps/aardvark/aardvark.js index b70a086199..fd0c15751e 100644 --- a/js/apps/aardvark/aardvark.js +++ b/js/apps/aardvark/aardvark.js @@ -33,17 +33,25 @@ "use strict"; // Initialise a new FoxxApplication called app under the urlPrefix: "foxxes". - var FoxxApplication = require("org/arangodb/foxx").FoxxApplication, + var FoxxApplication = require("org/arangodb/foxx").Application, app = new FoxxApplication(); - app.requiresModels = { - foxxes: "foxxes", - swagger: "swagger" - }; - + app.registerRepository( + "foxxes", + { + repository: "repositories/foxxes" + } + ); + + app.registerRepository( + "docus", + { + repository: "repositories/swagger" + } + ); app.del("/foxxes/:key", function (req, res) { - res.json(foxxes.uninstall(req.params("key"))); + res.json(repositories.foxxes.uninstall(req.params("key"))); }).pathParam("key", { description: "The _key attribute, where the information of this Foxx-Install is stored.", dataType: "string", @@ -58,9 +66,9 @@ active = content.active; // TODO: Other changes applied to foxx! e.g. Mount if (active) { - res.json(foxxes.activate()); + res.json(repositories.foxxes.activate()); } else { - res.json(foxxes.deactivate()); + res.json(repositories.foxxes.deactivate()); } }).pathParam("key", { description: "The _key attribute, where the information of this Foxx-Install is stored.", @@ -74,20 +82,20 @@ app.get('/foxxes', function (req, res) { - res.json(foxxes.viewAll()); + res.json(repositories.foxxes.viewAll()); }).nickname("foxxes") .summary("Update a foxx.") .notes("Used to either activate/deactivate a foxx, or change the mount point."); - app.get('/swagger', function (req, res) { - res.json(swagger.list()); + app.get('/docus', function (req, res) { + res.json(repositories.docus.list()); }).nickname("swaggers") .summary("List of all foxxes.") .notes("This function simply returns the list of all running" + " foxxes and supplies the paths for the swagger documentation"); - app.get('/swagger/:appname', function(req, res) { - res.json(swagger.show(req.params("appname"))) + app.get('/docus/:appname', function(req, res) { + res.json(repositories.docus.show(req.params("appname"))) }).pathParam("appname", { description: "The mount point of the App the documentation should be requested for", dataType: "string", diff --git a/js/apps/aardvark/assets/collections/foxxCollection.js b/js/apps/aardvark/assets/collections/foxxCollection.js deleted file mode 100644 index 08344921d5..0000000000 --- a/js/apps/aardvark/assets/collections/foxxCollection.js +++ /dev/null @@ -1,3 +0,0 @@ -var List = Backbone.Collection.extend({ - model: Item -}); diff --git a/js/apps/aardvark/assets/index.html b/js/apps/aardvark/assets/index.html deleted file mode 100644 index 33ea3f8f90..0000000000 --- a/js/apps/aardvark/assets/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - Aardvark - - - - - - - - - - - - - - - - - - - - -
- Header -
-
-
-
- Footer -
- - diff --git a/js/apps/aardvark/assets/lib/backbone.js b/js/apps/aardvark/assets/lib/backbone.js deleted file mode 100755 index 3512d42fb4..0000000000 --- a/js/apps/aardvark/assets/lib/backbone.js +++ /dev/null @@ -1,1571 +0,0 @@ -// Backbone.js 1.0.0 - -// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the MIT license. -// For all details and documentation: -// http://backbonejs.org - -(function(){ - - // Initial Setup - // ------------- - - // Save a reference to the global object (`window` in the browser, `exports` - // on the server). - var root = this; - - // Save the previous value of the `Backbone` variable, so that it can be - // restored later on, if `noConflict` is used. - var previousBackbone = root.Backbone; - - // Create local references to array methods we'll want to use later. - var array = []; - var push = array.push; - var slice = array.slice; - var splice = array.splice; - - // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both the browser and the server. - var Backbone; - if (typeof exports !== 'undefined') { - Backbone = exports; - } else { - Backbone = root.Backbone = {}; - } - - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.0.0'; - - // Require Underscore, if we're on the server, and it's not already present. - var _ = root._; - if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); - - // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns - // the `$` variable. - Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; - - // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable - // to its previous owner. Returns a reference to this Backbone object. - Backbone.noConflict = function() { - root.Backbone = previousBackbone; - return this; - }; - - // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option - // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and - // set a `X-Http-Method-Override` header. - Backbone.emulateHTTP = false; - - // Turn on `emulateJSON` to support legacy servers that can't deal with direct - // `application/json` requests ... will encode the body as - // `application/x-www-form-urlencoded` instead and will send the model in a - // form param named `model`. - Backbone.emulateJSON = false; - - // Backbone.Events - // --------------- - - // A module that can be mixed in to *any object* in order to provide it with - // custom events. You may bind with `on` or remove with `off` callback - // functions to an event; `trigger`-ing an event fires all callbacks in - // succession. - // - // var object = {}; - // _.extend(object, Backbone.Events); - // object.on('expand', function(){ alert('expanded'); }); - // object.trigger('expand'); - // - var Events = Backbone.Events = { - - // Bind an event to a `callback` function. Passing `"all"` will bind - // the callback to all events fired. - on: function(name, callback, context) { - if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; - this._events || (this._events = {}); - var events = this._events[name] || (this._events[name] = []); - events.push({callback: callback, context: context, ctx: context || this}); - return this; - }, - - // Bind an event to only be triggered a single time. After the first time - // the callback is invoked, it will be removed. - once: function(name, callback, context) { - if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; - var self = this; - var once = _.once(function() { - self.off(name, once); - callback.apply(this, arguments); - }); - once._callback = callback; - return this.on(name, once, context); - }, - - // Remove one or many callbacks. If `context` is null, removes all - // callbacks with that function. If `callback` is null, removes all - // callbacks for the event. If `name` is null, removes all bound - // callbacks for all events. - off: function(name, callback, context) { - var retain, ev, events, names, i, l, j, k; - if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; - if (!name && !callback && !context) { - this._events = {}; - return this; - } - - names = name ? [name] : _.keys(this._events); - for (i = 0, l = names.length; i < l; i++) { - name = names[i]; - if (events = this._events[name]) { - this._events[name] = retain = []; - if (callback || context) { - for (j = 0, k = events.length; j < k; j++) { - ev = events[j]; - if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || - (context && context !== ev.context)) { - retain.push(ev); - } - } - } - if (!retain.length) delete this._events[name]; - } - } - - return this; - }, - - // Trigger one or many events, firing all bound callbacks. Callbacks are - // passed the same arguments as `trigger` is, apart from the event name - // (unless you're listening on `"all"`, which will cause your callback to - // receive the true name of the event as the first argument). - trigger: function(name) { - if (!this._events) return this; - var args = slice.call(arguments, 1); - if (!eventsApi(this, 'trigger', name, args)) return this; - var events = this._events[name]; - var allEvents = this._events.all; - if (events) triggerEvents(events, args); - if (allEvents) triggerEvents(allEvents, arguments); - return this; - }, - - // Tell this object to stop listening to either specific events ... or - // to every object it's currently listening to. - stopListening: function(obj, name, callback) { - var listeners = this._listeners; - if (!listeners) return this; - var deleteListener = !name && !callback; - if (typeof name === 'object') callback = this; - if (obj) (listeners = {})[obj._listenerId] = obj; - for (var id in listeners) { - listeners[id].off(name, callback, this); - if (deleteListener) delete this._listeners[id]; - } - return this; - } - - }; - - // Regular expression used to split event strings. - var eventSplitter = /\s+/; - - // Implement fancy features of the Events API such as multiple event - // names `"change blur"` and jQuery-style event maps `{change: action}` - // in terms of the existing API. - var eventsApi = function(obj, action, name, rest) { - if (!name) return true; - - // Handle event maps. - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); - } - return false; - } - - // Handle space separated event names. - if (eventSplitter.test(name)) { - var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { - obj[action].apply(obj, [names[i]].concat(rest)); - } - return false; - } - - return true; - }; - - // A difficult-to-believe, but optimized internal dispatch function for - // triggering events. Tries to keep the usual cases speedy (most internal - // Backbone events have 3 arguments). - var triggerEvents = function(events, args) { - var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; - switch (args.length) { - case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; - case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; - case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; - case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); - } - }; - - var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; - - // Inversion-of-control versions of `on` and `once`. Tell *this* object to - // listen to an event in another object ... keeping track of what it's - // listening to. - _.each(listenMethods, function(implementation, method) { - Events[method] = function(obj, name, callback) { - var listeners = this._listeners || (this._listeners = {}); - var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); - listeners[id] = obj; - if (typeof name === 'object') callback = this; - obj[implementation](name, callback, this); - return this; - }; - }); - - // Aliases for backwards compatibility. - Events.bind = Events.on; - Events.unbind = Events.off; - - // Allow the `Backbone` object to serve as a global event bus, for folks who - // want global "pubsub" in a convenient place. - _.extend(Backbone, Events); - - // Backbone.Model - // -------------- - - // Backbone **Models** are the basic data object in the framework -- - // frequently representing a row in a table in a database on your server. - // A discrete chunk of data and a bunch of useful, related methods for - // performing computations and transformations on that data. - - // Create a new model with the specified attributes. A client id (`cid`) - // is automatically generated and assigned for you. - var Model = Backbone.Model = function(attributes, options) { - var defaults; - var attrs = attributes || {}; - options || (options = {}); - this.cid = _.uniqueId('c'); - this.attributes = {}; - _.extend(this, _.pick(options, modelOptions)); - if (options.parse) attrs = this.parse(attrs, options) || {}; - if (defaults = _.result(this, 'defaults')) { - attrs = _.defaults({}, attrs, defaults); - } - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); - }; - - // A list of options to be attached directly to the model, if provided. - var modelOptions = ['url', 'urlRoot', 'collection']; - - // Attach all inheritable methods to the Model prototype. - _.extend(Model.prototype, Events, { - - // A hash of attributes whose current and previous value differ. - changed: null, - - // The value returned during the last failed validation. - validationError: null, - - // The default name for the JSON `id` attribute is `"id"`. MongoDB and - // CouchDB users may want to set this to `"_id"`. - idAttribute: 'id', - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Return a copy of the model's `attributes` object. - toJSON: function(options) { - return _.clone(this.attributes); - }, - - // Proxy `Backbone.sync` by default -- but override this if you need - // custom syncing semantics for *this* particular model. - sync: function() { - return Backbone.sync.apply(this, arguments); - }, - - // Get the value of an attribute. - get: function(attr) { - return this.attributes[attr]; - }, - - // Get the HTML-escaped value of an attribute. - escape: function(attr) { - return _.escape(this.get(attr)); - }, - - // Returns `true` if the attribute contains a value that is not null - // or undefined. - has: function(attr) { - return this.get(attr) != null; - }, - - // Set a hash of model attributes on the object, firing `"change"`. This is - // the core primitive operation of a model, updating the data and notifying - // anyone who needs to know about the change in state. The heart of the beast. - set: function(key, val, options) { - var attr, attrs, unset, changes, silent, changing, prev, current; - if (key == null) return this; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - options || (options = {}); - - // Run validation. - if (!this._validate(attrs, options)) return false; - - // Extract attributes and options. - unset = options.unset; - silent = options.silent; - changes = []; - changing = this._changing; - this._changing = true; - - if (!changing) { - this._previousAttributes = _.clone(this.attributes); - this.changed = {}; - } - current = this.attributes, prev = this._previousAttributes; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - - // For each `set` attribute, update or delete the current value. - for (attr in attrs) { - val = attrs[attr]; - if (!_.isEqual(current[attr], val)) changes.push(attr); - if (!_.isEqual(prev[attr], val)) { - this.changed[attr] = val; - } else { - delete this.changed[attr]; - } - unset ? delete current[attr] : current[attr] = val; - } - - // Trigger all relevant attribute changes. - if (!silent) { - if (changes.length) this._pending = true; - for (var i = 0, l = changes.length; i < l; i++) { - this.trigger('change:' + changes[i], this, current[changes[i]], options); - } - } - - // You might be wondering why there's a `while` loop here. Changes can - // be recursively nested within `"change"` events. - if (changing) return this; - if (!silent) { - while (this._pending) { - this._pending = false; - this.trigger('change', this, options); - } - } - this._pending = false; - this._changing = false; - return this; - }, - - // Remove an attribute from the model, firing `"change"`. `unset` is a noop - // if the attribute doesn't exist. - unset: function(attr, options) { - return this.set(attr, void 0, _.extend({}, options, {unset: true})); - }, - - // Clear all attributes on the model, firing `"change"`. - clear: function(options) { - var attrs = {}; - for (var key in this.attributes) attrs[key] = void 0; - return this.set(attrs, _.extend({}, options, {unset: true})); - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged: function(attr) { - if (attr == null) return !_.isEmpty(this.changed); - return _.has(this.changed, attr); - }, - - // Return an object containing all the attributes that have changed, or - // false if there are no changed attributes. Useful for determining what - // parts of a view need to be updated and/or what attributes need to be - // persisted to the server. Unset attributes will be set to undefined. - // You can also pass an attributes object to diff against the model, - // determining if there *would be* a change. - changedAttributes: function(diff) { - if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; - var val, changed = false; - var old = this._changing ? this._previousAttributes : this.attributes; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous: function(attr) { - if (attr == null || !this._previousAttributes) return null; - return this._previousAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - return _.clone(this._previousAttributes); - }, - - // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overridden, - // triggering a `"change"` event. - fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; - var model = this; - var success = options.success; - options.success = function(resp) { - if (!model.set(model.parse(resp, options), options)) return false; - if (success) success(model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); - }, - - // Set a hash of model attributes, and sync the model to the server. - // If the server returns an attributes hash that differs, the model's - // state will be `set` again. - save: function(key, val, options) { - var attrs, method, xhr, attributes = this.attributes; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. - if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; - - options = _.extend({validate: true}, options); - - // Do not persist invalid models. - if (!this._validate(attrs, options)) return false; - - // Set temporary attributes if `{wait: true}`. - if (attrs && options.wait) { - this.attributes = _.extend({}, attributes, attrs); - } - - // After a successful server-side save, the client is (optionally) - // updated with the server-side state. - if (options.parse === void 0) options.parse = true; - var model = this; - var success = options.success; - options.success = function(resp) { - // Ensure attributes are restored during synchronous saves. - model.attributes = attributes; - var serverAttrs = model.parse(resp, options); - if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); - if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { - return false; - } - if (success) success(model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - - method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); - if (method === 'patch') options.attrs = attrs; - xhr = this.sync(method, this, options); - - // Restore attributes. - if (attrs && options.wait) this.attributes = attributes; - - return xhr; - }, - - // Destroy this model on the server if it was already persisted. - // Optimistically removes the model from its collection, if it has one. - // If `wait: true` is passed, waits for the server to respond before removal. - destroy: function(options) { - options = options ? _.clone(options) : {}; - var model = this; - var success = options.success; - - var destroy = function() { - model.trigger('destroy', model, model.collection, options); - }; - - options.success = function(resp) { - if (options.wait || model.isNew()) destroy(); - if (success) success(model, resp, options); - if (!model.isNew()) model.trigger('sync', model, resp, options); - }; - - if (this.isNew()) { - options.success(); - return false; - } - wrapError(this, options); - - var xhr = this.sync('delete', this, options); - if (!options.wait) destroy(); - return xhr; - }, - - // Default URL for the model's representation on the server -- if you're - // using Backbone's restful methods, override this to change the endpoint - // that will be called. - url: function() { - var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); - if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); - }, - - // **parse** converts a response into the hash of attributes to be `set` on - // the model. The default implementation is just to pass the response along. - parse: function(resp, options) { - return resp; - }, - - // Create a new model with identical attributes to this one. - clone: function() { - return new this.constructor(this.attributes); - }, - - // A model is new if it has never been saved to the server, and lacks an id. - isNew: function() { - return this.id == null; - }, - - // Check if the model is currently in a valid state. - isValid: function(options) { - return this._validate({}, _.extend(options || {}, { validate: true })); - }, - - // Run validation against the next complete set of model attributes, - // returning `true` if all is well. Otherwise, fire an `"invalid"` event. - _validate: function(attrs, options) { - if (!options.validate || !this.validate) return true; - attrs = _.extend({}, this.attributes, attrs); - var error = this.validationError = this.validate(attrs, options) || null; - if (!error) return true; - this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); - return false; - } - - }); - - // Underscore methods that we want to implement on the Model. - var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; - - // Mix in each Underscore method as a proxy to `Model#attributes`. - _.each(modelMethods, function(method) { - Model.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.attributes); - return _[method].apply(_, args); - }; - }); - - // Backbone.Collection - // ------------------- - - // If models tend to represent a single row of data, a Backbone Collection is - // more analagous to a table full of data ... or a small slice or page of that - // table, or a collection of rows that belong together for a particular reason - // -- all of the messages in this particular folder, all of the documents - // belonging to this particular author, and so on. Collections maintain - // indexes of their models, both in order, and for lookup by `id`. - - // Create a new **Collection**, perhaps to contain a specific type of `model`. - // If a `comparator` is specified, the Collection will maintain - // its models in sort order, as they're added and removed. - var Collection = Backbone.Collection = function(models, options) { - options || (options = {}); - if (options.url) this.url = options.url; - if (options.model) this.model = options.model; - if (options.comparator !== void 0) this.comparator = options.comparator; - this._reset(); - this.initialize.apply(this, arguments); - if (models) this.reset(models, _.extend({silent: true}, options)); - }; - - // Default options for `Collection#set`. - var setOptions = {add: true, remove: true, merge: true}; - var addOptions = {add: true, merge: false, remove: false}; - - // Define the Collection's inheritable methods. - _.extend(Collection.prototype, Events, { - - // The default model for a collection is just a **Backbone.Model**. - // This should be overridden in most cases. - model: Model, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // The JSON representation of a Collection is an array of the - // models' attributes. - toJSON: function(options) { - return this.map(function(model){ return model.toJSON(options); }); - }, - - // Proxy `Backbone.sync` by default. - sync: function() { - return Backbone.sync.apply(this, arguments); - }, - - // Add a model, or list of models to the set. - add: function(models, options) { - return this.set(models, _.defaults(options || {}, addOptions)); - }, - - // Remove a model, or a list of models from the set. - remove: function(models, options) { - models = _.isArray(models) ? models.slice() : [models]; - options || (options = {}); - var i, l, index, model; - for (i = 0, l = models.length; i < l; i++) { - model = this.get(models[i]); - if (!model) continue; - delete this._byId[model.id]; - delete this._byId[model.cid]; - index = this.indexOf(model); - this.models.splice(index, 1); - this.length--; - if (!options.silent) { - options.index = index; - model.trigger('remove', model, this, options); - } - this._removeReference(model); - } - return this; - }, - - // Update a collection by `set`-ing a new list of models, adding new ones, - // removing models that are no longer present, and merging models that - // already exist in the collection, as necessary. Similar to **Model#set**, - // the core operation for updating the data contained by the collection. - set: function(models, options) { - options = _.defaults(options || {}, setOptions); - if (options.parse) models = this.parse(models, options); - if (!_.isArray(models)) models = models ? [models] : []; - var i, l, model, attrs, existing, sort; - var at = options.at; - var sortable = this.comparator && (at == null) && options.sort !== false; - var sortAttr = _.isString(this.comparator) ? this.comparator : null; - var toAdd = [], toRemove = [], modelMap = {}; - - // Turn bare objects into model references, and prevent invalid models - // from being added. - for (i = 0, l = models.length; i < l; i++) { - if (!(model = this._prepareModel(models[i], options))) continue; - - // If a duplicate is found, prevent it from being added and - // optionally merge it into the existing model. - if (existing = this.get(model)) { - if (options.remove) modelMap[existing.cid] = true; - if (options.merge) { - existing.set(model.attributes, options); - if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; - } - - // This is a new model, push it to the `toAdd` list. - } else if (options.add) { - toAdd.push(model); - - // Listen to added models' events, and index models for lookup by - // `id` and by `cid`. - model.on('all', this._onModelEvent, this); - this._byId[model.cid] = model; - if (model.id != null) this._byId[model.id] = model; - } - } - - // Remove nonexistent models if appropriate. - if (options.remove) { - for (i = 0, l = this.length; i < l; ++i) { - if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); - } - if (toRemove.length) this.remove(toRemove, options); - } - - // See if sorting is needed, update `length` and splice in new models. - if (toAdd.length) { - if (sortable) sort = true; - this.length += toAdd.length; - if (at != null) { - splice.apply(this.models, [at, 0].concat(toAdd)); - } else { - push.apply(this.models, toAdd); - } - } - - // Silently sort the collection if appropriate. - if (sort) this.sort({silent: true}); - - if (options.silent) return this; - - // Trigger `add` events. - for (i = 0, l = toAdd.length; i < l; i++) { - (model = toAdd[i]).trigger('add', model, this, options); - } - - // Trigger `sort` if the collection was sorted. - if (sort) this.trigger('sort', this, options); - return this; - }, - - // When you have more items than you want to add or remove individually, - // you can reset the entire set with a new list of models, without firing - // any granular `add` or `remove` events. Fires `reset` when finished. - // Useful for bulk operations and optimizations. - reset: function(models, options) { - options || (options = {}); - for (var i = 0, l = this.models.length; i < l; i++) { - this._removeReference(this.models[i]); - } - options.previousModels = this.models; - this._reset(); - this.add(models, _.extend({silent: true}, options)); - if (!options.silent) this.trigger('reset', this, options); - return this; - }, - - // Add a model to the end of the collection. - push: function(model, options) { - model = this._prepareModel(model, options); - this.add(model, _.extend({at: this.length}, options)); - return model; - }, - - // Remove a model from the end of the collection. - pop: function(options) { - var model = this.at(this.length - 1); - this.remove(model, options); - return model; - }, - - // Add a model to the beginning of the collection. - unshift: function(model, options) { - model = this._prepareModel(model, options); - this.add(model, _.extend({at: 0}, options)); - return model; - }, - - // Remove a model from the beginning of the collection. - shift: function(options) { - var model = this.at(0); - this.remove(model, options); - return model; - }, - - // Slice out a sub-array of models from the collection. - slice: function(begin, end) { - return this.models.slice(begin, end); - }, - - // Get a model from the set by id. - get: function(obj) { - if (obj == null) return void 0; - return this._byId[obj.id != null ? obj.id : obj.cid || obj]; - }, - - // Get the model at the given index. - at: function(index) { - return this.models[index]; - }, - - // Return models with matching attributes. Useful for simple cases of - // `filter`. - where: function(attrs, first) { - if (_.isEmpty(attrs)) return first ? void 0 : []; - return this[first ? 'find' : 'filter'](function(model) { - for (var key in attrs) { - if (attrs[key] !== model.get(key)) return false; - } - return true; - }); - }, - - // Return the first model with matching attributes. Useful for simple cases - // of `find`. - findWhere: function(attrs) { - return this.where(attrs, true); - }, - - // Force the collection to re-sort itself. You don't need to call this under - // normal circumstances, as the set will maintain sort order as each item - // is added. - sort: function(options) { - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); - options || (options = {}); - - // Run sort based on type of `comparator`. - if (_.isString(this.comparator) || this.comparator.length === 1) { - this.models = this.sortBy(this.comparator, this); - } else { - this.models.sort(_.bind(this.comparator, this)); - } - - if (!options.silent) this.trigger('sort', this, options); - return this; - }, - - // Figure out the smallest index at which a model should be inserted so as - // to maintain order. - sortedIndex: function(model, value, context) { - value || (value = this.comparator); - var iterator = _.isFunction(value) ? value : function(model) { - return model.get(value); - }; - return _.sortedIndex(this.models, model, iterator, context); - }, - - // Pluck an attribute from each model in the collection. - pluck: function(attr) { - return _.invoke(this.models, 'get', attr); - }, - - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `reset: true` is passed, the response - // data will be passed through the `reset` method instead of `set`. - fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; - var success = options.success; - var collection = this; - options.success = function(resp) { - var method = options.reset ? 'reset' : 'set'; - collection[method](resp, options); - if (success) success(collection, resp, options); - collection.trigger('sync', collection, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); - }, - - // Create a new instance of a model in this collection. Add the model to the - // collection immediately, unless `wait: true` is passed, in which case we - // wait for the server to agree. - create: function(model, options) { - options = options ? _.clone(options) : {}; - if (!(model = this._prepareModel(model, options))) return false; - if (!options.wait) this.add(model, options); - var collection = this; - var success = options.success; - options.success = function(resp) { - if (options.wait) collection.add(model, options); - if (success) success(model, resp, options); - }; - model.save(null, options); - return model; - }, - - // **parse** converts a response into a list of models to be added to the - // collection. The default implementation is just to pass it through. - parse: function(resp, options) { - return resp; - }, - - // Create a new collection with an identical list of models as this one. - clone: function() { - return new this.constructor(this.models); - }, - - // Private method to reset all internal state. Called when the collection - // is first initialized or reset. - _reset: function() { - this.length = 0; - this.models = []; - this._byId = {}; - }, - - // Prepare a hash of attributes (or other model) to be added to this - // collection. - _prepareModel: function(attrs, options) { - if (attrs instanceof Model) { - if (!attrs.collection) attrs.collection = this; - return attrs; - } - options || (options = {}); - options.collection = this; - var model = new this.model(attrs, options); - if (!model._validate(attrs, options)) { - this.trigger('invalid', this, attrs, options); - return false; - } - return model; - }, - - // Internal method to sever a model's ties to a collection. - _removeReference: function(model) { - if (this === model.collection) delete model.collection; - model.off('all', this._onModelEvent, this); - }, - - // Internal method called every time a model in the set fires an event. - // Sets need to update their indexes when models change ids. All other - // events simply proxy through. "add" and "remove" events that originate - // in other collections are ignored. - _onModelEvent: function(event, model, collection, options) { - if ((event === 'add' || event === 'remove') && collection !== this) return; - if (event === 'destroy') this.remove(model, options); - if (model && event === 'change:' + model.idAttribute) { - delete this._byId[model.previous(model.idAttribute)]; - if (model.id != null) this._byId[model.id] = model; - } - this.trigger.apply(this, arguments); - } - - }); - - // Underscore methods that we want to implement on the Collection. - // 90% of the core usefulness of Backbone Collections is actually implemented - // right here: - var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', - 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', - 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', - 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', - 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', - 'isEmpty', 'chain']; - - // Mix in each Underscore method as a proxy to `Collection#models`. - _.each(methods, function(method) { - Collection.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.models); - return _[method].apply(_, args); - }; - }); - - // Underscore methods that take a property name as an argument. - var attributeMethods = ['groupBy', 'countBy', 'sortBy']; - - // Use attributes instead of properties. - _.each(attributeMethods, function(method) { - Collection.prototype[method] = function(value, context) { - var iterator = _.isFunction(value) ? value : function(model) { - return model.get(value); - }; - return _[method](this.models, iterator, context); - }; - }); - - // Backbone.View - // ------------- - - // Backbone Views are almost more convention than they are actual code. A View - // is simply a JavaScript object that represents a logical chunk of UI in the - // DOM. This might be a single item, an entire list, a sidebar or panel, or - // even the surrounding frame which wraps your whole app. Defining a chunk of - // UI as a **View** allows you to define your DOM events declaratively, without - // having to worry about render order ... and makes it easy for the view to - // react to specific changes in the state of your models. - - // Creating a Backbone.View creates its initial element outside of the DOM, - // if an existing element is not provided... - var View = Backbone.View = function(options) { - this.cid = _.uniqueId('view'); - this._configure(options || {}); - this._ensureElement(); - this.initialize.apply(this, arguments); - this.delegateEvents(); - }; - - // Cached regex to split keys for `delegate`. - var delegateEventSplitter = /^(\S+)\s*(.*)$/; - - // List of view options to be merged as properties. - var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; - - // Set up all inheritable **Backbone.View** properties and methods. - _.extend(View.prototype, Events, { - - // The default `tagName` of a View's element is `"div"`. - tagName: 'div', - - // jQuery delegate for element lookup, scoped to DOM elements within the - // current view. This should be prefered to global lookups where possible. - $: function(selector) { - return this.$el.find(selector); - }, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // **render** is the core function that your view should override, in order - // to populate its element (`this.el`), with the appropriate HTML. The - // convention is for **render** to always return `this`. - render: function() { - return this; - }, - - // Remove this view by taking the element out of the DOM, and removing any - // applicable Backbone.Events listeners. - remove: function() { - this.$el.remove(); - this.stopListening(); - return this; - }, - - // Change the view's element (`this.el` property), including event - // re-delegation. - setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); - this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); - this.el = this.$el[0]; - if (delegate !== false) this.delegateEvents(); - return this; - }, - - // Set callbacks, where `this.events` is a hash of - // - // *{"event selector": "callback"}* - // - // { - // 'mousedown .title': 'edit', - // 'click .button': 'save' - // 'click .open': function(e) { ... } - // } - // - // pairs. Callbacks will be bound to the view, with `this` set properly. - // Uses event delegation for efficiency. - // Omitting the selector binds the event to `this.el`. - // This only works for delegate-able events: not `focus`, `blur`, and - // not `change`, `submit`, and `reset` in Internet Explorer. - delegateEvents: function(events) { - if (!(events || (events = _.result(this, 'events')))) return this; - this.undelegateEvents(); - for (var key in events) { - var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; - if (!method) continue; - - var match = key.match(delegateEventSplitter); - var eventName = match[1], selector = match[2]; - method = _.bind(method, this); - eventName += '.delegateEvents' + this.cid; - if (selector === '') { - this.$el.on(eventName, method); - } else { - this.$el.on(eventName, selector, method); - } - } - return this; - }, - - // Clears all callbacks previously bound to the view with `delegateEvents`. - // You usually don't need to use this, but may wish to if you have multiple - // Backbone views attached to the same DOM element. - undelegateEvents: function() { - this.$el.off('.delegateEvents' + this.cid); - return this; - }, - - // Performs the initial configuration of a View with a set of options. - // Keys with special meaning *(e.g. model, collection, id, className)* are - // attached directly to the view. See `viewOptions` for an exhaustive - // list. - _configure: function(options) { - if (this.options) options = _.extend({}, _.result(this, 'options'), options); - _.extend(this, _.pick(options, viewOptions)); - this.options = options; - }, - - // Ensure that the View has a DOM element to render into. - // If `this.el` is a string, pass it through `$()`, take the first - // matching element, and re-assign it to `el`. Otherwise, create - // an element from the `id`, `className` and `tagName` properties. - _ensureElement: function() { - if (!this.el) { - var attrs = _.extend({}, _.result(this, 'attributes')); - if (this.id) attrs.id = _.result(this, 'id'); - if (this.className) attrs['class'] = _.result(this, 'className'); - var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); - this.setElement($el, false); - } else { - this.setElement(_.result(this, 'el'), false); - } - } - - }); - - // Backbone.sync - // ------------- - - // Override this function to change the manner in which Backbone persists - // models to the server. You will be passed the type of request, and the - // model in question. By default, makes a RESTful Ajax request - // to the model's `url()`. Some possible customizations could be: - // - // * Use `setTimeout` to batch rapid-fire updates into a single request. - // * Send up the models as XML instead of JSON. - // * Persist models via WebSockets instead of Ajax. - // - // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests - // as `POST`, with a `_method` parameter containing the true HTTP method, - // as well as all requests with the body as `application/x-www-form-urlencoded` - // instead of `application/json` with the model in a param named `model`. - // Useful when interfacing with server-side languages like **PHP** that make - // it difficult to read the body of `PUT` requests. - Backbone.sync = function(method, model, options) { - var type = methodMap[method]; - - // Default options, unless specified. - _.defaults(options || (options = {}), { - emulateHTTP: Backbone.emulateHTTP, - emulateJSON: Backbone.emulateJSON - }); - - // Default JSON-request options. - var params = {type: type, dataType: 'json'}; - - // Ensure that we have a URL. - if (!options.url) { - params.url = _.result(model, 'url') || urlError(); - } - - // Ensure that we have the appropriate request data. - if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { - params.contentType = 'application/json'; - params.data = JSON.stringify(options.attrs || model.toJSON(options)); - } - - // For older servers, emulate JSON by encoding the request into an HTML-form. - if (options.emulateJSON) { - params.contentType = 'application/x-www-form-urlencoded'; - params.data = params.data ? {model: params.data} : {}; - } - - // For older servers, emulate HTTP by mimicking the HTTP method with `_method` - // And an `X-HTTP-Method-Override` header. - if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { - params.type = 'POST'; - if (options.emulateJSON) params.data._method = type; - var beforeSend = options.beforeSend; - options.beforeSend = function(xhr) { - xhr.setRequestHeader('X-HTTP-Method-Override', type); - if (beforeSend) return beforeSend.apply(this, arguments); - }; - } - - // Don't process data on a non-GET request. - if (params.type !== 'GET' && !options.emulateJSON) { - params.processData = false; - } - - // If we're sending a `PATCH` request, and we're in an old Internet Explorer - // that still has ActiveX enabled by default, override jQuery to use that - // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. - if (params.type === 'PATCH' && window.ActiveXObject && - !(window.external && window.external.msActiveXFilteringEnabled)) { - params.xhr = function() { - return new ActiveXObject("Microsoft.XMLHTTP"); - }; - } - - // Make the request, allowing the user to override any Ajax options. - var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); - model.trigger('request', model, xhr, options); - return xhr; - }; - - // Map from CRUD to HTTP for our default `Backbone.sync` implementation. - var methodMap = { - 'create': 'POST', - 'update': 'PUT', - 'patch': 'PATCH', - 'delete': 'DELETE', - 'read': 'GET' - }; - - // Set the default implementation of `Backbone.ajax` to proxy through to `$`. - // Override this if you'd like to use a different library. - Backbone.ajax = function() { - return Backbone.$.ajax.apply(Backbone.$, arguments); - }; - - // Backbone.Router - // --------------- - - // Routers map faux-URLs to actions, and fire events when routes are - // matched. Creating a new one sets its `routes` hash, if not set statically. - var Router = Backbone.Router = function(options) { - options || (options = {}); - if (options.routes) this.routes = options.routes; - this._bindRoutes(); - this.initialize.apply(this, arguments); - }; - - // Cached regular expressions for matching named param parts and splatted - // parts of route strings. - var optionalParam = /\((.*?)\)/g; - var namedParam = /(\(\?)?:\w+/g; - var splatParam = /\*\w+/g; - var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; - - // Set up all inheritable **Backbone.Router** properties and methods. - _.extend(Router.prototype, Events, { - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Manually bind a single named route to a callback. For example: - // - // this.route('search/:query/p:num', 'search', function(query, num) { - // ... - // }); - // - route: function(route, name, callback) { - if (!_.isRegExp(route)) route = this._routeToRegExp(route); - if (_.isFunction(name)) { - callback = name; - name = ''; - } - if (!callback) callback = this[name]; - var router = this; - Backbone.history.route(route, function(fragment) { - var args = router._extractParameters(route, fragment); - callback && callback.apply(router, args); - router.trigger.apply(router, ['route:' + name].concat(args)); - router.trigger('route', name, args); - Backbone.history.trigger('route', router, name, args); - }); - return this; - }, - - // Simple proxy to `Backbone.history` to save a fragment into the history. - navigate: function(fragment, options) { - Backbone.history.navigate(fragment, options); - return this; - }, - - // Bind all defined routes to `Backbone.history`. We have to reverse the - // order of the routes here to support behavior where the most general - // routes can be defined at the bottom of the route map. - _bindRoutes: function() { - if (!this.routes) return; - this.routes = _.result(this, 'routes'); - var route, routes = _.keys(this.routes); - while ((route = routes.pop()) != null) { - this.route(route, this.routes[route]); - } - }, - - // Convert a route string into a regular expression, suitable for matching - // against the current location hash. - _routeToRegExp: function(route) { - route = route.replace(escapeRegExp, '\\$&') - .replace(optionalParam, '(?:$1)?') - .replace(namedParam, function(match, optional){ - return optional ? match : '([^\/]+)'; - }) - .replace(splatParam, '(.*?)'); - return new RegExp('^' + route + '$'); - }, - - // Given a route, and a URL fragment that it matches, return the array of - // extracted decoded parameters. Empty or unmatched parameters will be - // treated as `null` to normalize cross-browser behavior. - _extractParameters: function(route, fragment) { - var params = route.exec(fragment).slice(1); - return _.map(params, function(param) { - return param ? decodeURIComponent(param) : null; - }); - } - - }); - - // Backbone.History - // ---------------- - - // Handles cross-browser history management, based on either - // [pushState](http://diveintohtml5.info/history.html) and real URLs, or - // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) - // and URL fragments. If the browser supports neither (old IE, natch), - // falls back to polling. - var History = Backbone.History = function() { - this.handlers = []; - _.bindAll(this, 'checkUrl'); - - // Ensure that `History` can be used outside of the browser. - if (typeof window !== 'undefined') { - this.location = window.location; - this.history = window.history; - } - }; - - // Cached regex for stripping a leading hash/slash and trailing space. - var routeStripper = /^[#\/]|\s+$/g; - - // Cached regex for stripping leading and trailing slashes. - var rootStripper = /^\/+|\/+$/g; - - // Cached regex for detecting MSIE. - var isExplorer = /msie [\w.]+/; - - // Cached regex for removing a trailing slash. - var trailingSlash = /\/$/; - - // Has the history handling already been started? - History.started = false; - - // Set up all inheritable **Backbone.History** properties and methods. - _.extend(History.prototype, Events, { - - // The default interval to poll for hash changes, if necessary, is - // twenty times a second. - interval: 50, - - // Gets the true hash value. Cannot use location.hash directly due to bug - // in Firefox where location.hash will always be decoded. - getHash: function(window) { - var match = (window || this).location.href.match(/#(.*)$/); - return match ? match[1] : ''; - }, - - // Get the cross-browser normalized URL fragment, either from the URL, - // the hash, or the override. - getFragment: function(fragment, forcePushState) { - if (fragment == null) { - if (this._hasPushState || !this._wantsHashChange || forcePushState) { - fragment = this.location.pathname; - var root = this.root.replace(trailingSlash, ''); - if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); - } else { - fragment = this.getHash(); - } - } - return fragment.replace(routeStripper, ''); - }, - - // Start the hash change handling, returning `true` if the current URL matches - // an existing route, and `false` otherwise. - start: function(options) { - if (History.started) throw new Error("Backbone.history has already been started"); - History.started = true; - - // Figure out the initial configuration. Do we need an iframe? - // Is pushState desired ... is it available? - this.options = _.extend({}, {root: '/'}, this.options, options); - this.root = this.options.root; - this._wantsHashChange = this.options.hashChange !== false; - this._wantsPushState = !!this.options.pushState; - this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); - var fragment = this.getFragment(); - var docMode = document.documentMode; - var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); - - // Normalize root to always include a leading and trailing slash. - this.root = ('/' + this.root + '/').replace(rootStripper, '/'); - - if (oldIE && this._wantsHashChange) { - this.iframe = Backbone.$('