Merge branch 'master' into gh-pages

This commit is contained in:
Rufus Pollock
2012-04-27 16:46:35 +01:00
30 changed files with 1690 additions and 1048 deletions

View File

@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Recline Data Explorer Demo</title> <title>Recline Data Explorer (Built Sources)</title>
<meta name="description" content="A demo of the Recline Data Explorer"> <meta name="description" content="A demo of the Recline Data Explorer">
<meta name="author" content="Rufus Pollock and Max Ogden"> <meta name="author" content="Rufus Pollock and Max Ogden">
@@ -11,39 +11,151 @@
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]--> <![endif]-->
<link rel="stylesheet" href="../vendor/bootstrap/2.0.2/css/bootstrap.css"> <link rel="stylesheet" href="../vendor/bootstrap/2.0.2/css/bootstrap.css">
<link rel="stylesheet" href="../vendor/leaflet/0.3.1/leaflet.css">
<!--[if lte IE 8]>
<link rel="stylesheet" href="../vendor/leaflet/0.3.1/leaflet.ie.css" />
<![endif]-->
<!-- Recline CSS components -->
<link rel="stylesheet" href="../css/data-explorer.css"> <link rel="stylesheet" href="../css/data-explorer.css">
<link rel="stylesheet" href="../css/grid.css">
<link rel="stylesheet" href="../css/graph.css"> <link rel="stylesheet" href="../css/graph.css">
<link rel="stylesheet" href="../css/map.css">
<!-- /Recline CSS components -->
<!-- Custom CSS for the Data Explorer Online App -->
<link rel="stylesheet" href="style/demo.css"> <link rel="stylesheet" href="style/demo.css">
<script type="text/javascript" src="../vendor/jquery-1.7.1.js"></script> <link rel="stylesheet" href="../vendor/bootstrap/2.0.2/css/bootstrap-responsive.css">
<script type="text/javascript" src="../vendor/underscore-1.1.6.js"></script>
<script type="text/javascript" src="../vendor/backbone-0.5.1.js"></script> <!-- 3rd party JS libraries -->
<script type="text/javascript" src="../vendor/jquery-ui-1.8.14.custom.min.js"></script> <script type="text/javascript" src="../vendor/jquery/1.7.1/jquery.js"></script>
<script type="text/javascript" src="../vendor/jquery.flot-0.7.js"></script> <script type="text/javascript" src="../vendor/underscore/1.1.6/underscore.js"></script>
<script type="text/javascript" src="../vendor/backbone/0.5.1/backbone.js"></script>
<script type="text/javascript" src="../vendor/jquery.flot/0.7/jquery.flot.js"></script>
<script type="text/javascript" src="../vendor/jquery.mustache.js"></script> <script type="text/javascript" src="../vendor/jquery.mustache.js"></script>
<script type="text/javascript" src="../vendor/bootstrap/2.0.2/bootstrap.js"></script> <script type="text/javascript" src="../vendor/bootstrap/2.0.2/bootstrap.js"></script>
<script type="text/javascript" src="../vendor/leaflet/0.3.1/leaflet.js"></script>
<!-- recline library -->
<script type="text/javascript" src="../recline.js"></script> <script type="text/javascript" src="../recline.js"></script>
<!-- non-library javascript specific to this demo -->
<script type="text/javascript" src="js/app.js"></script> <script type="text/javascript" src="js/app.js"></script>
</head> </head>
<body> <body>
<div class="recline-app">
<div class="navbar navbar-fixed-top"> <div class="navbar navbar-fixed-top">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container-fluid"> <div class="container-fluid">
<a class="brand" href="#">Recline Data Explorer</a> <a class="brand" href="../">Recline Data Explorer</a>
<ul class="nav pull-right"> <ul class="nav">
<li><a class="set-read-only" title="Put into read-only mode">Read-only</a></li> <li><a href="../#docs">Documentation</a></li>
</ul> </ul>
<form class="webstore-load pull-right navbar-search" title="Update from the specified webstore dataset"> <ul class="nav pull-right">
<input type="text" name="source" size="50" /> <li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle">
Import <b class="caret"></b></a>
<ul class="dropdown-menu js-import">
<li>
<a data-toggle="modal" href=".js-import-dialog-url">Import from URL</a>
</li>
<li>
<a data-toggle="modal" href=".js-import-dialog-file">Import from File</a>
</li>
</ul>
</li>
<li>
<a href=".js-share-and-embed-dialog" data-toggle="modal">
Share and Embed
<i class="icon-share icon-white"></i>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="modal fade in js-import-dialog-url" style="display: none;">
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>Import from URL</h3>
</div>
<div class="modal-body">
<form class="js-import-url form-horizontal">
<div class="control-group">
<label class="control-label">URL</label>
<div class="controls">
<input type="text" name="source" class="input-xlarge" />
</div>
</div>
<div class="control-group">
<label class="control-label">Type of data</label>
<div class="controls">
<select name="backend_type"> <select name="backend_type">
<option value="elasticsearch">ElasticSearch</option> <option value="elasticsearch">ElasticSearch</option>
<option value="dataproxy">DataProxy</option> <option value="dataproxy">CSV or Excel</option>
<option value="gdocs">Google Spreadsheets</option> <option value="gdocs">Google Spreadsheet</option>
</select> </select>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Import &raquo;</button>
</div>
</form> </form>
</div> </div>
</div> </div>
<div class="modal fade in js-import-dialog-file" style="display: none;">
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>Import from File</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label">File</label>
<div class="controls">
<input type="file" name="source" />
</div>
</div>
<div class="control-group">
<label class="control-label">Separator</label>
<div class="controls">
<input type="text" name="separator" value="," class="spam1"/>
</div>
</div>
<div class="control-group">
<label class="control-label">Text delimiter</label>
<div class="controls">
<input type="text" name="delimiter" value='"' />
</div>
</div>
<div class="control-group">
<label class="control-label">Encoding</label>
<div class="controls">
<input type="text" name="encoding" value="UTF-8" />
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Import &raquo;</button>
</div>
</form>
</div>
</div>
<div class="modal fade in js-share-and-embed-dialog" style="display: none;">
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>Share and Embed</h3>
</div>
<div class="modal-body">
<h4>Sharable Link to current View</h4>
<textarea class="view-link" style="width: 100%; height: 100px;"></textarea>
<h4>Embed this View</h4>
<textarea class="view-embed" style="width: 100%; height: 200px;"></textarea>
</div>
</div> </div>
<div class="container-fluid"> <div class="container-fluid">
@@ -51,6 +163,8 @@
<div class="data-explorer-here"></div> <div class="data-explorer-here"></div>
</div> </div>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@@ -32,7 +32,6 @@
<script type="text/javascript" src="../vendor/jquery/1.7.1/jquery.js"></script> <script type="text/javascript" src="../vendor/jquery/1.7.1/jquery.js"></script>
<script type="text/javascript" src="../vendor/underscore/1.1.6/underscore.js"></script> <script type="text/javascript" src="../vendor/underscore/1.1.6/underscore.js"></script>
<script type="text/javascript" src="../vendor/backbone/0.5.1/backbone.js"></script> <script type="text/javascript" src="../vendor/backbone/0.5.1/backbone.js"></script>
<script type="text/javascript" src="../vendor/jquery-ui-1.8.14.custom.min.js"></script>
<script type="text/javascript" src="../vendor/jquery.flot/0.7/jquery.flot.js"></script> <script type="text/javascript" src="../vendor/jquery.flot/0.7/jquery.flot.js"></script>
<script type="text/javascript" src="../vendor/jquery.mustache.js"></script> <script type="text/javascript" src="../vendor/jquery.mustache.js"></script>
<script type="text/javascript" src="../vendor/bootstrap/2.0.2/bootstrap.js"></script> <script type="text/javascript" src="../vendor/bootstrap/2.0.2/bootstrap.js"></script>
@@ -58,6 +57,8 @@
<!-- non-library javascript specific to this demo --> <!-- non-library javascript specific to this demo -->
<script type="text/javascript" src="js/app.js"></script> <script type="text/javascript" src="js/app.js"></script>
<!-- for demo dataset -->
<script type="text/javascript" src="../test/base.js"></script>
</head> </head>
<body> <body>
<div class="recline-app"> <div class="recline-app">
@@ -122,6 +123,50 @@
</div> </div>
</div> </div>
<div class="container-fluid">
<div class="content">
<div class="page-home backbone-page">
<div class="hero-unit">
<h1>Welcome to the Recline Data Explorer</h1>
<p>The Data Explorer is an application for exploring
and working with data built in pure javascript and html. In basic operation it's much like a spreadsheet - though it's
feature set is a little different. In particular, the Data
Explorer provides:
<ul>
<li>Data grid / spreadsheet</li>
<li>Data editing including programmatic data transformation in javascript</li>
<li>Visualizations includes graphs and maps</li>
<li>Import and export from a variety of sources including online sources such as online Excel and CSV files, Google docs and
the <a href="http://datahub.io/">DataHub</a> and offline sources like CSV files on your local machine.</li>
<li>Use online or offline - because the app is built in pure javascript and html you can use it anywhere there's a modern web browser. Using offline is as easy and downloading this web page to your local machine.</li>
</ul>
<div class="row">
<div class="span4">
<h3>View the demo</h3>
<p>Take a look at a local demo dataset.</p>
<p><a class="btn btn-primary" href="?url=demo">View the demo dataset &raquo;</a></p>
</div>
<div class="span4">
<h3>Read the tutorial</h3>
<p>Take a look at the tutorial for using the data explorer:</p>
<a class="btn btn-primary" href="#tutorial">Read the tutorial &raquo;</a>
</div>
<div class="span4">
<h3>Import some data</h3>
<p>Starting working with some data straight away. You can import some data <strong>using the menu at the top right</strong> of this page.</p>
</div>
</div>
</div>
</div>
</div>
<div class="page-explorer backbone-page">
<div class="data-explorer-here"></div>
</div>
</div>
</div>
<!-- modals for menus -->
<div class="modal fade in js-import-dialog-file" style="display: none;"> <div class="modal fade in js-import-dialog-file" style="display: none;">
<div class="modal-header"> <div class="modal-header">
<a class="close" data-dismiss="modal">×</a> <a class="close" data-dismiss="modal">×</a>
@@ -141,6 +186,13 @@
<input type="text" name="separator" value="," class="spam1"/> <input type="text" name="separator" value="," class="spam1"/>
</div> </div>
</div> </div>
<div class="control-group">
<label class="control-label">Text delimiter</label>
<div class="controls">
<input type="text" name="delimiter" value='"' />
</div>
</div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Encoding</label> <label class="control-label">Encoding</label>
<div class="controls"> <div class="controls">
@@ -166,12 +218,6 @@
<textarea class="view-embed" style="width: 100%; height: 200px;"></textarea> <textarea class="view-embed" style="width: 100%; height: 200px;"></textarea>
</div> </div>
</div> </div>
<div class="container-fluid">
<div class="content">
<div class="data-explorer-here"></div>
</div>
</div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -2,7 +2,6 @@ jQuery(function($) {
var app = new ExplorerApp({ var app = new ExplorerApp({
el: $('.recline-app') el: $('.recline-app')
}) })
Backbone.history.start();
}); });
var ExplorerApp = Backbone.View.extend({ var ExplorerApp = Backbone.View.extend({
@@ -15,8 +14,14 @@ var ExplorerApp = Backbone.View.extend({
this.el = $(this.el); this.el = $(this.el);
this.explorer = null; this.explorer = null;
this.explorerDiv = $('.data-explorer-here'); this.explorerDiv = $('.data-explorer-here');
_.bindAll(this, 'viewExplorer', 'viewHome');
var state = recline.View.parseQueryString(window.location.search); this.router = new Backbone.Router();
this.router.route('', 'home', this.viewHome);
this.router.route(/explorer/, 'explorer', this.viewExplorer);
Backbone.history.start();
var state = recline.Util.parseQueryString(window.location.search);
if (state) { if (state) {
_.each(state, function(value, key) { _.each(state, function(value, key) {
try { try {
@@ -30,14 +35,34 @@ var ExplorerApp = Backbone.View.extend({
} }
} }
var dataset = null; var dataset = null;
if (state.dataset || state.url) { // special cases for demo / memory dataset
dataset = recline.Model.Dataset.restore(state); if (state.url === 'demo' || state.backend === 'memory') {
} else {
dataset = localDataset(); dataset = localDataset();
} }
else if (state.dataset || state.url) {
dataset = recline.Model.Dataset.restore(state);
}
if (dataset) {
this.createExplorer(dataset, state); this.createExplorer(dataset, state);
}
}, },
viewHome: function() {
this.switchView('home');
},
viewExplorer: function() {
this.router.navigate('explorer');
this.switchView('explorer');
},
switchView: function(path) {
$('.backbone-page').hide();
var cssClass = path.replace('/', '-');
$('.page-' + cssClass).show();
},
// make Explorer creation / initialization in a function so we can call it // make Explorer creation / initialization in a function so we can call it
// again and again // again and again
createExplorer: function(dataset, state) { createExplorer: function(dataset, state) {
@@ -59,12 +84,7 @@ var ExplorerApp = Backbone.View.extend({
this._setupPermaLink(this.dataExplorer); this._setupPermaLink(this.dataExplorer);
this._setupEmbed(this.dataExplorer); this._setupEmbed(this.dataExplorer);
// HACK (a bit). Issue is that Backbone will not trigger the route this.viewExplorer();
// if you are already at that location so we have to make sure we genuinely switch
if (reload) {
// this.dataExplorer.router.navigate('graph');
// this.dataExplorer.router.navigate('', true);
}
}, },
_setupPermaLink: function(explorer) { _setupPermaLink: function(explorer) {
@@ -92,7 +112,7 @@ var ExplorerApp = Backbone.View.extend({
}, },
makePermaLink: function(state) { makePermaLink: function(state) {
var qs = recline.View.composeQueryString(state.toJSON()); var qs = recline.Util.composeQueryString(state.toJSON());
return window.location.origin + window.location.pathname + qs; return window.location.origin + window.location.pathname + qs;
}, },
@@ -128,6 +148,7 @@ var ExplorerApp = Backbone.View.extend({
var file = $file.files[0]; var file = $file.files[0];
var options = { var options = {
separator : $form.find('input[name="separator"]').val(), separator : $form.find('input[name="separator"]').val(),
delimiter : $form.find('input[name="delimiter"]').val(),
encoding : $form.find('input[name="encoding"]').val() encoding : $form.find('input[name="encoding"]').val()
}; };
recline.Backend.loadFromCSVFile(file, function(dataset) { recline.Backend.loadFromCSVFile(file, function(dataset) {
@@ -140,26 +161,7 @@ var ExplorerApp = Backbone.View.extend({
// provide a demonstration in memory dataset // provide a demonstration in memory dataset
function localDataset() { function localDataset() {
var datasetId = 'test-dataset'; var dataset = Fixture.getDataset();
var inData = {
metadata: {
title: 'My Test Dataset'
, name: '1-my-test-dataset'
, id: datasetId
},
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'},{id: 'lat'},{id: 'lon'}],
documents: [
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40}
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second', lat:54.97, lon:-1.60}
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third', lat:40.00, lon:-75.5}
, {id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth', lat:57.27, lon:-6.20}
, {id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth', lat:51.58, lon:0}
, {id: 5, x: 6, y: 12, z: 18, country: 'DE', label: 'sixth', lat:51.04, lon:7.9}
]
};
var backend = new recline.Backend.Memory();
backend.addDataset(inData);
var dataset = new recline.Model.Dataset({id: datasetId}, backend);
dataset.queryState.addFacet('country'); dataset.queryState.addFacet('country');
return dataset; return dataset;
} }

View File

@@ -13,6 +13,11 @@
line-height: 13px; line-height: 13px;
} }
.recline-graph .graph .alert {
width: 450px;
margin: auto;
}
/********************************************************** /**********************************************************
* Editor * Editor
*********************************************************/ *********************************************************/

View File

@@ -210,10 +210,6 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit {
* Transform Dialog * Transform Dialog
*********************************************************/ *********************************************************/
#expression-preview-tabs .ui-tabs-nav li a {
padding: 0.15em 1em;
}
textarea.expression-preview-code { textarea.expression-preview-code {
font-family: monospace; font-family: monospace;
height: 5em; height: 5em;

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
css/images/icons/white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
css/images/zigzags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -21,3 +21,8 @@
width: 100%; width: 100%;
} }
.recline-map .editor .editor-options {
margin-top: 10px;
border-top: 1px solid gray;
padding: 5px 0;
}

270
css/site.css Normal file
View File

@@ -0,0 +1,270 @@
/*
Theme Name: Recline
Description: Layout and styling for reclinejs.com
Author: Sam Smith
Author URI: http://www.mintcanary.com/
*/
/* --------------------------------------------------
Table of Contents
-----------------------------------------------------
:: General Styles
:: Layout
::
::
::
*/
/* ---------------------------------------------------
General Styles
--------------------------------------------------- */
@import url(http://fonts.googleapis.com/css?family=Open+Sans:400,400italic,700);
h1, h2, h3, h4, h5, h6 {
font-family:'Open Sans', Helvetica, Arial, sans-serif;
}
a {
color: #c7231d;
}
a:hover {
color: #bc130e;
}
a.dotted {
border-bottom-width: 1px;
border-bottom-style: dotted;
border-bottom-color: #333;
color:#333;
}
a.dotted:hover {
text-decoration:none;
}
.btn-info {
background: #545454; /* Old browsers */
background: -moz-linear-gradient(top, #545454 0%, #454545 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#545454), color-stop(100%,#454545)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #545454 0%,#454545 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #545454 0%,#454545 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #545454 0%,#454545 100%); /* IE10+ */
background: linear-gradient(top, #545454 0%,#454545 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#545454', endColorstr='#454545',GradientType=0 ); /* IE6-9 */
border-color: #454545 #454545 #454545;
filter: progid:dximagetransform.microsoft.gradient(enabled=false);
border: 1px solid #454545;
border-bottom-color: #454545;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
color:#FFF;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.btn-info:hover,
.btn-info:active,
.btn-info.active,
.btn-info.disabled,
.btn-info[disabled] {
background-color: #454545;
background-image:none;
color:#FFF;
}
.btn-info:active,
.btn-info.active {
background-color: #454545 \9;
background-image:none;
color:#FFF;
}
.btn-large {
padding: 9px 14px;
-webkit-border-radius: 25px;
-moz-border-radius: 25px;
border-radius: 25px;
margin-right:10px;
}
.btn-primary {
background: #c7231d; /* Old browsers */
background: -moz-linear-gradient(top, #c7231d 0%, #bc130e 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#c7231d), color-stop(100%,#bc130e)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #c7231d 0%,#bc130e 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #c7231d 0%,#bc130e 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #c7231d 0%,#bc130e 100%); /* IE10+ */
background: linear-gradient(top, #c7231d 0%,#bc130e 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#c7231d', endColorstr='#bc130e',GradientType=0 ); /* IE6-9 */
border-color: #0055cc #0055cc #003580;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:dximagetransform.microsoft.gradient(enabled=false);
}
.btn-primary:hover,
.btn-primary:active,
.btn-primary.active,
.btn-primary.disabled,
.btn-primary[disabled] {
background-color: #bc130e;
background-image:none;
}
.btn-primary:active,
.btn-primary.active {
background-color: #bc130e \9;
}
a.btn-large.showtitle[title] {
position:relative;
margin-bottom:26px;
min-width:117px;
}
a.btn-large.showtitle[title]:after {
content: attr(title);
position:absolute;
bottom:-26px;
left:0px;
font-size:12px;
height:26px;
line-height:26px;
min-width:145px;
text-align:center;
}
.btn-large .icon-large {
height:25px;
width:25px;
margin-top:-2px;
margin-left:-25px;
margin-right:7px;
}
.btn-large .icon-white.icon-large {
background-image: url(images/icons/white.png);
background-repeat: no-repeat;
}
.btn-large .icon-white.icon-arrow-down.icon-large {
background-position: 3px 1px;
}
.btn-large .icon-white.icon-graph.icon-large {
background-position: -50px 4px;
}
/* ---------------------------------------------------
Layout
--------------------------------------------------- */
.navbar .brand {
font-family:'Open Sans', Helvetica, Arial, sans-serif;
font-style:italic;
font-size:18px;
font-weight:400;
letter-spacing:-1px;
line-height:40px;
}
.navbar .nav > li > a {
padding: 15px 10px;
}
.navbar-inner {
height:50px;
background: #303030; /* Old browsers */
background: -moz-linear-gradient(top, #303030 0%, #2d2d2d 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#303030), color-stop(100%,#2d2d2d)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #303030 0%,#2d2d2d 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #303030 0%,#2d2d2d 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #303030 0%,#2d2d2d 100%); /* IE10+ */
background: linear-gradient(top, #303030 0%,#2d2d2d 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#303030', endColorstr='#2d2d2d',GradientType=0 ); /* IE6-9 */
-webkit-box-shadow:none;
-moz-box-shadow: none;
box-shadow: none;
}
section {
padding-top:20px;
}
.page-header {
margin-top:50px;
background: #2d2d2d; /* Old browsers */
background: -moz-linear-gradient(top, #2d2d2d 0%, #040404 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#2d2d2d), color-stop(100%,#040404)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #2d2d2d 0%,#040404 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #2d2d2d 0%,#040404 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #2d2d2d 0%,#040404 100%); /* IE10+ */
background: linear-gradient(top, #2d2d2d 0%,#040404 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2d2d2d', endColorstr='#040404',GradientType=0 ); /* IE6-9 */
color:#FFF;
padding:0px;
margin-bottom:0px;
border:none;
font-family:'Open Sans', Helvetica, Arial, sans-serif;
}
.page-header a {
color:#FFF;
}
.page-header a.dotted {
border-color:#FFF;
}
.page-header .container {
background-image: url(images/header-screen.png);
background-repeat: no-repeat;
background-position: -3px 0px;
}
.page-header .inner {
padding:0px 0px 30px 40px;
font-size:16px;
}
.page-header .inner ol {
list-style-type:upper-latin;
font-size:20px;
font-style:italic;
}
.page-header .inner .header-button {
display:inline-block;
}
.page-header:after {
margin-top:-14px;
}
section.grey {
background-color:#f5f5f5;
}
section:after {
content: " ";
height:14px;
display:block;
background-image: url(images/zigzags.png);
background-repeat: repeat-x;
background-position: center 1px;
}
section.grey:after {
background-position: center -50px;
}
.footer {
background-color:#040404;
color:#CCC;
}
.footer:before {
content: " ";
height:14px;
display:block;
background-image: url(images/zigzags.png);
background-repeat: repeat-x;
background-position: center -100px;
margin-top:-34px;
}
.footer:after {
display:none;
}
.footer .row {
margin-top:15px;
margin-bottom:15px;
}
.footer a {
color:#CCC;
}
.footer a.btn {
color:#333333;
}

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
images/screenshot-1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -10,90 +10,55 @@
<![endif]--> <![endif]-->
<link rel="stylesheet" href="vendor/bootstrap/2.0.2/css/bootstrap.css" /> <link rel="stylesheet" href="vendor/bootstrap/2.0.2/css/bootstrap.css" />
<link rel="stylesheet" href="http://opendatahandbook.org/en/_static/bootstrap-sphinx.css" /> <link href="css/site.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="vendor/bootstrap/2.0.2/css/bootstrap-responsive.css" />
<style type="text/css">
html, body {
background-color: #eee;
}
body {
padding-top: 50px;
}
.content {
background-color: #fff;
padding: 20px;
margin: 0 -20px; /* negative indent the amount of the padding to maintain the grid system */
-webkit-border-radius: 0 0 6px 6px;
-moz-border-radius: 0 0 6px 6px;
border-radius: 0 0 6px 6px;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.15);
box-shadow: 0 1px 2px rgba(0,0,0,.15);
}
.page-header {
background-color: #f5f5f5;
padding: 20px 20px 10px;
margin: -20px -20px 20px;
}
.page-header h1 {
font-size: 30px;
}
ul.deps {
font-size: 85%;
}
.getit-btn {
margin: 10px 0px;
}
.getit-btn a {
width: 95%;
text-align: center;
}
</style>
</head> </head>
<body> <body>
<div class="navbar navbar-fixed-top"> <div class="navbar navbar-fixed-top">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container"> <div class="container">
<a class="brand" href="#">Recline Data Explorer and Library</a> <a class="brand" href="#"><strong>Recline</strong> Data Explorer and Library</a>
<ul class="nav"> <ul class="nav pull-right">
<li><a href="app/">Data Explorer</a></li> <li><a href="app/">Data Explorer</a></li>
<li><a href="#docs">Docs</a></li> <li><a href="#docs">Docs</a></li>
<li><a href="http://github.com/okfn/recline/">Code on GitHub</a></li> <li><a href="http://github.com/okfn/recline/">Code on GitHub</a></li>
</ul> </ul>
<a class="nav-logo pull-right" href="http://okfn.org/" title="An Open Knowledge Foundation Project">
<img src="http://assets.okfn.org/p/okfn/img/logo_28x30.png" alt="Open Knowledge Foundation logo" />
</a>
<ul class="nav" style="float: right;">
<li><a href="http://twitter.com/maxogden">@maxogden</a></li>
<li><a href="http://twitter.com/rufuspollock">@rufuspollock</a></li>
</ul>
</div> </div>
</div> </div>
</div> </div>
<section class="page-header">
<div class="container"> <div class="container">
<div class="row"><div class="span9"><div class="content"> <div class="row">
<div class="span8 offset4">
<div class="page-header">
<h1> <h1>
Recline Data Explorer and Library<br /> <img src="images/logo.png" width="455" height="190" alt="Recline Data Explorer and Library">
<small>
A. Powerful data explorer built in pure javascript and html
<br />
B. Suite of data components - grid, graphing and data connectors
<br />
&mdash; All built with <a href="http://backbonejs.org/">Backbone</a></small>
</h1> </h1>
<div class="inner">
<ol style="list-style-type:upper-latin;">
<li>Powerful data explorer built in pure javascript and html</li>
<li>Suite of data components - grid, graphing and data connectors</li>
</ol>
&mdash; All built with <a href="http://backbonejs.org/" class="dotted">Backbone</a>
</div> </div>
<div class="inner">
<p>Recline is two things:</p> <a class="btn btn-large btn-info showtitle" href="app/" title="the data explorer"><i class="icon-graph icon-white icon-large"></i>Use It</a>
<a class="btn btn-large btn-primary showtitle" href="http://github.com/okfn/recline/" title="code on GitHub"><i class="icon-arrow-down icon-white icon-large"></i>Get It</a>
</div>
</div>
</div>
</div>
</section>
<section class="grey">
<div class="container">
<div class="row">
<div class="span12">
<h2>Recline is Two Things</h2>
</div>
</div>
<div class="row">
<div class="span6">
<ul> <ul>
<li>A Data Explorer combining a data grid, Google Refine-style data <li>A Data Explorer combining a data grid, Google Refine-style data
transforms and visualizations all in lightweight javascript and html.</li> transforms and visualizations all in lightweight javascript and html.</li>
@@ -101,12 +66,16 @@
grid, graphing, and data connectors - which you can selectively use and build grid, graphing, and data connectors - which you can selectively use and build
on.</li> on.</li>
</ul> </ul>
</div>
<div class="span6">
<p>The Explorer can be used standalone (just download and use) or can be <p>The Explorer can be used standalone (just download and use) or can be
embedded into your own site. Recline builds on the powerful but lightweight embedded into your own site. Recline builds on the powerful but lightweight
Backbone framework making it extremely easy to extend and adapt. The library's Backbone framework making it extremely easy to extend and adapt. The library's
modular design mean means you only have to take what you need.</p> modular design mean means you only have to take what you need.</p>
</div>
</div>
<div class="row">
<div class="span4">
<h2 id="features">Main Features</h2> <h2 id="features">Main Features</h2>
<ul> <ul>
<li>View and edit your data in a clean grid / table interface</li> <li>View and edit your data in a clean grid / table interface</li>
@@ -122,29 +91,42 @@ modular design mean means you only have to take what you need.</p>
<li>Properly designed model with clean separation of data and presentation</li> <li>Properly designed model with clean separation of data and presentation</li>
<li>Componentized design means you use only what you need</li> <li>Componentized design means you use only what you need</li>
</ul> </ul>
</div>
<div class="span8">
<a href="app/"><img src="images/screenshot-1.jpg" width="632" height="428" alt="Recline Data Explorer Screenshot"></a>
</div>
</div>
</div>
</section>
<h2>Screenshots</h2> <section id="docs">
<p><a href="app/"><img src="http://farm8.staticflickr.com/7020/6847468031_0f474de5f7_b.jpg" alt="Recline Data Explorer Screenshot" style="width: 700px; display: block; margin-bottom: 30px;" /></a></p> <div class="container">
<div class="row">
<h2 id="demo">Demo</h2> <div class="span12">
<p><a href="app/index.html" class="btn">For demo see the Data Explorer &raquo;</a></p> <h2>Data Explorer Documentation</h2>
</div>
<h2 id="docs">Data Explorer Documentation</h2> </div>
<div class="row">
<div class="span6">
<p>Usage instructions are built into the <a href="app/">Data Explorer</a> <p>Usage instructions are built into the <a href="app/">Data Explorer</a>
itself so no specific additional documentation is provided on usage.</p> itself so no specific additional documentation is provided on usage.</p>
<p>To embed the data explorer in another site you can use a simple iframe in <p>To embed the data explorer in another site you can use a simple iframe in
your web page:</p> your web page:</p>
</div>
<div class="span6">
<textarea class="span6">&lt;iframe src="http://okfnlabs.org/recline/app/" width="100%"&gt;&lt;/iframe&gt;</textarea> <textarea class="span6">&lt;iframe src="http://okfnlabs.org/recline/app/" width="100%"&gt;&lt;/iframe&gt;</textarea>
</div>
</div>
<div class="row">
<div class="span12">
<p>Alternatively, you can initialize the explorer yourself from javascript. To <p>Alternatively, you can initialize the explorer yourself from javascript. To
see how to do this just take at look at the Explorer's initialization see how to do this just take at look at the Explorer's initialization
javascript in: <a href="app/js/app.js">app.js</a>.</p> javascript in: <a href="app/js/app.js">app.js</a>.</p>
</div>
</div>
<h2 id="docs">Library Documentation</h2> <div class="row">
<div class="span12">
<h2>Library Documentation</h2>
<h3 id="docs-using">Examples</h3> <h3 id="docs-using">Examples</h3>
@@ -197,8 +179,15 @@ var dataset = recline.Model.Dataset({
backend backend
); );
</pre> </pre>
</div>
</div>
<div class="row">
<div class="span12">
<h3 id="docs-concepts">Concepts and Structure</h3> <h3 id="docs-concepts">Concepts and Structure</h3>
</div>
</div>
<div class="row">
<div class="span6">
<p>Recline has a simple structure layered on top of the basic Model/View <p>Recline has a simple structure layered on top of the basic Model/View
distinction inherent in Backbone.</p> distinction inherent in Backbone.</p>
@@ -229,6 +218,10 @@ distinction inherent in Backbone.</p>
<p>More detail of how these work can be found in the <a <p>More detail of how these work can be found in the <a
href="docs/model.html">Model source docs</a>.</p> href="docs/model.html">Model source docs</a>.</p>
</div>
<div class="span6">
<h4>Backends</h4> <h4>Backends</h4>
<p>Backends connect Dataset and Documents to data from a <p>Backends connect Dataset and Documents to data from a
specific 'Backend' data source. They provide methods for loading and saving specific 'Backend' data source. They provide methods for loading and saving
@@ -259,9 +252,15 @@ are useful:</p>
<li>QueryEditor: a query editor view</li> <li>QueryEditor: a query editor view</li>
<li>FacetViewer: display facets</li> <li>FacetViewer: display facets</li>
</ul> </ul>
</div>
</div>
<div class="row">
<div class="span12">
<h3 id="docs-source">Source Docs (via Docco)</h3> <h3 id="docs-source">Source Docs (via Docco)</h3>
</div>
</div>
<div class="row">
<div class="span6">
<h4>Models and Views (Widgets)</h4> <h4>Models and Views (Widgets)</h4>
<ul> <ul>
<li><a href="docs/model.html">Models</a></li> <li><a href="docs/model.html">Models</a></li>
@@ -270,7 +269,8 @@ are useful:</p>
<li><a href="docs/view-graph.html">Graph View (based on Flot)</a></li> <li><a href="docs/view-graph.html">Graph View (based on Flot)</a></li>
<li><a href="docs/view-map.html">Map View (based on Leaflet)</a></li> <li><a href="docs/view-map.html">Map View (based on Leaflet)</a></li>
</ul> </ul>
</div>
<div class="span6">
<h4>Backends</h4> <h4>Backends</h4>
<ul> <ul>
<li><a href="docs/backend/base.html">Backend: Base (base class providing a template for backends)</a></li> <li><a href="docs/backend/base.html">Backend: Base (base class providing a template for backends)</a></li>
@@ -280,11 +280,21 @@ are useful:</p>
<li><a href="docs/backend/gdocs.html">Backend: Google Docs (Spreadsheet)</a></li> <li><a href="docs/backend/gdocs.html">Backend: Google Docs (Spreadsheet)</a></li>
<li><a href="docs/backend/localcsv.html">Backend: Local CSV file</a></li> <li><a href="docs/backend/localcsv.html">Backend: Local CSV file</a></li>
</ul> </ul>
</div>
</div>
<div class="row">
<div class="span6">
<h2 id="tests">Tests</h2> <h2 id="tests">Tests</h2>
<p><a href="test/index.html">Run the tests online</a>.</p> <p><a href="test/index.html">Run the tests online</a>.</p>
</div>
</div>
<div class="row">
<div class="span12">
<h2 id="history">History</h2> <h2 id="history">History</h2>
</div>
</div>
<div class="row">
<div class="span6">
<p>Max Ogden was developing Recline as the frontend data browser and editor for <p>Max Ogden was developing Recline as the frontend data browser and editor for
his <a href="http://datacouch.com/">http://datacouch.com/</a> project. his <a href="http://datacouch.com/">http://datacouch.com/</a> project.
Meanwhile, Rufus Pollock and the <a href="http://ckan.org/">CKAN team</a> at Meanwhile, Rufus Pollock and the <a href="http://ckan.org/">CKAN team</a> at
@@ -296,21 +306,29 @@ on a <a href="http://github.com/okfn/dataexplorer">Data Explorer</a> for use in
<p>When they met up, they realized that they were pretty much working on the <p>When they met up, they realized that they were pretty much working on the
same thing and so decided to join forces to produce the new Recline Data same thing and so decided to join forces to produce the new Recline Data
Explorer.</p> Explorer.</p>
</div>
<div class="span6">
<p>The new project forked off <a <p>The new project forked off <a
href="https://github.com/maxogden/recline">Max's original recline href="https://github.com/maxogden/recline">Max's original recline
codebase</a> combining some portions of the <a codebase</a> combining some portions of the <a
href="http://github.com/okfn/dataexplorer">original Data Explorer</a>. href="http://github.com/okfn/dataexplorer">original Data Explorer</a>.
However, it has been rewritten from the ground up using Backbone.</p> However, it has been rewritten from the ground up using Backbone.</p>
</div>
</div>
</div>
</section>
</div></div> <!-- /span9 /content --> <section class="footer">
<div class="span3 sidebar"> <div class="container">
<div class="well sidebar-nav"> <div class="row">
<h3 class="nav-header">Use the Explorer</h3> <div class="span3">
<h5>Use the Explorer</h5>
<p class="getit-btn"><a href="app/" class="btn primary">Visit the Data Explorer &raquo;</a></p> <p class="getit-btn"><a href="app/" class="btn primary">Visit the Data Explorer &raquo;</a></p>
<h3 class="nav-header">Get the Library</h3> <h5>Get the Library</h5>
<p class="getit-btn"><a href="recline.js" class="btn primary">Development Version<br />v0.3 (67k)</a></p> <p class="getit-btn"><a href="recline.js" class="btn primary">Development Version<br />v0.3 (67k)</a></p>
<h4>Dependencies</h4> </div>
<div class="span3">
<h5>Dependencies</h5>
<ul class="deps"> <ul class="deps">
<li>JQuery &gt;= 1.6</li> <li>JQuery &gt;= 1.6</li>
<li><a href="http://backbonejs.org/">Backbone</a> &gt;= 0.5.1</li> <li><a href="http://backbonejs.org/">Backbone</a> &gt;= 0.5.1</li>
@@ -320,15 +338,28 @@ However, it has been rewritten from the ground up using Backbone.</p>
<li><a href="http://leaflet.cloudmade.com/">Leaflet &gt;= 0.3.1</a>: (Optional) for mapping</li> <li><a href="http://leaflet.cloudmade.com/">Leaflet &gt;= 0.3.1</a>: (Optional) for mapping</li>
<li><a href="http://twitter.github.com/bootstrap/">Bootstrap</a> &gt;= v2.0: (Optional) for CSS/JS</li> <li><a href="http://twitter.github.com/bootstrap/">Bootstrap</a> &gt;= v2.0: (Optional) for CSS/JS</li>
</ul> </ul>
</div>
<h3 class="nav-header">Documentation</h3> <div class="span3">
<ul class="nav nav-list"> <h5>Documentation</h5>
<ul>
<li><a href="#docs-using">Using it</a></li> <li><a href="#docs-using">Using it</a></li>
<li><a href="#docs-concepts">Concepts and Structure</a></li> <li><a href="#docs-concepts">Concepts and Structure</a></li>
<li><a href="#docs-source">Source Docs (Docco)</a></li> <li><a href="#docs-source">Source Docs (Docco)</a></li>
</ul> </ul>
</div><!--/.well --> </div>
</div><!--/span--> <div class="span3">
</div></div> <!-- /row /container --> <h5>Contacts</h5>
<ul>
<li><a href="http://twitter.com/maxogden">@maxogden</a></li>
<li><a href="http://twitter.com/rufuspollock">@rufuspollock</a></li>
</ul>
<a class="nav-logo" href="http://okfn.org/" title="An Open Knowledge Foundation Project">
<img src="http://assets.okfn.org/p/okfn/img/logo_28x30.png" alt="Open Knowledge Foundation logo" />
</a>
</div>
</div>
</div>
</section>
</body> </body>
</html> </html>

View File

@@ -757,22 +757,13 @@ my.Graph = Backbone.View.extend({
<label>Group Column (x-axis)</label> \ <label>Group Column (x-axis)</label> \
<div class="input editor-group"> \ <div class="input editor-group"> \
<select> \ <select> \
<option value="">Please choose ...</option> \
{{#fields}} \ {{#fields}} \
<option value="{{id}}">{{label}}</option> \ <option value="{{id}}">{{label}}</option> \
{{/fields}} \ {{/fields}} \
</select> \ </select> \
</div> \ </div> \
<div class="editor-series-group"> \ <div class="editor-series-group"> \
<div class="editor-series"> \
<label>Series <span>A (y-axis)</span></label> \
<div class="input"> \
<select> \
{{#fields}} \
<option value="{{id}}">{{label}}</option> \
{{/fields}} \
</select> \
</div> \
</div> \
</div> \ </div> \
</div> \ </div> \
<div class="editor-buttons"> \ <div class="editor-buttons"> \
@@ -784,13 +775,34 @@ my.Graph = Backbone.View.extend({
</div> \ </div> \
</form> \ </form> \
</div> \ </div> \
<div class="panel graph"></div> \ <div class="panel graph"> \
<div class="js-temp-notice alert alert-block"> \
<h3 class="alert-heading">Hey there!</h3> \
<p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
<p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
</div> \
</div> \
</div> \
',
templateSeriesEditor: ' \
<div class="editor-series js-series-{{seriesIndex}}"> \
<label>Series <span>{{seriesName}} (y-axis)</span> \
[<a href="#remove" class="action-remove-series">Remove</a>] \
</label> \
<div class="input"> \
<select> \
<option value="">Please choose ...</option> \
{{#fields}} \
<option value="{{id}}">{{label}}</option> \
{{/fields}} \
</select> \
</div> \
</div> \ </div> \
', ',
events: { events: {
'change form select': 'onEditorSubmit', 'change form select': 'onEditorSubmit',
'click .editor-add': 'addSeries', 'click .editor-add': '_onAddSeries',
'click .action-remove-series': 'removeSeries', 'click .action-remove-series': 'removeSeries',
'click .action-toggle-help': 'toggleHelp' 'click .action-toggle-help': 'toggleHelp'
}, },
@@ -807,7 +819,8 @@ my.Graph = Backbone.View.extend({
this.model.currentDocuments.bind('reset', this.redraw); this.model.currentDocuments.bind('reset', this.redraw);
var stateData = _.extend({ var stateData = _.extend({
group: null, group: null,
series: [], // so that at least one series chooser box shows up
series: [""],
graphType: 'lines-and-points' graphType: 'lines-and-points'
}, },
options.state options.state
@@ -817,21 +830,45 @@ my.Graph = Backbone.View.extend({
}, },
render: function() { render: function() {
htmls = $.mustache(this.template, this.model.toTemplateJSON()); var self = this;
var tmplData = this.model.toTemplateJSON();
var htmls = $.mustache(this.template, tmplData);
$(this.el).html(htmls); $(this.el).html(htmls);
// now set a load of stuff up
this.$graph = this.el.find('.panel.graph'); this.$graph = this.el.find('.panel.graph');
// for use later when adding additional series
// could be simpler just to have a common template! // set up editor from state
this.$seriesClone = this.el.find('.editor-series').clone(); if (this.state.get('graphType')) {
this._updateSeries(); this._selectOption('.editor-type', this.state.get('graphType'));
}
if (this.state.get('group')) {
this._selectOption('.editor-group', this.state.get('group'));
}
_.each(this.state.get('series'), function(series, idx) {
self.addSeries(idx);
self._selectOption('.editor-series.js-series-' + idx, series);
});
return this; return this;
}, },
// Private: Helper function to select an option from a select list
//
_selectOption: function(id,value){
var options = this.el.find(id + ' select > option');
if (options) {
options.each(function(opt){
if (this.value == value) {
$(this).attr('selected','selected');
return false;
}
});
}
},
onEditorSubmit: function(e) { onEditorSubmit: function(e) {
var select = this.el.find('.editor-group select'); var select = this.el.find('.editor-group select');
$editor = this; var $editor = this;
var series = this.$series.map(function () { var $series = this.el.find('.editor-series select');
var series = $series.map(function () {
return $(this).val(); return $(this).val();
}); });
var updatedState = { var updatedState = {
@@ -870,10 +907,20 @@ my.Graph = Backbone.View.extend({
// } // }
}, },
// ### getGraphOptions
//
// Get options for Flot Graph
//
// needs to be function as can depend on state // needs to be function as can depend on state
//
// @param typeId graphType id (lines, lines-and-points etc)
getGraphOptions: function(typeId) { getGraphOptions: function(typeId) {
var self = this; var self = this;
// special tickformatter to show labels rather than numbers // special tickformatter to show labels rather than numbers
// TODO: we should really use tickFormatter and 1 interval ticks if (and
// only if) x-axis values are non-numeric
// However, that is non-trivial to work out from a dataset (datasets may
// have no field type info). Thus at present we only do this for bars.
var tickFormatter = function (val) { var tickFormatter = function (val) {
if (self.model.currentDocuments.models[val]) { if (self.model.currentDocuments.models[val]) {
var out = self.model.currentDocuments.models[val].get(self.state.attributes.group); var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);
@@ -886,20 +933,25 @@ my.Graph = Backbone.View.extend({
} }
return val; return val;
}; };
// TODO: we should really use tickFormatter and 1 interval ticks if (and
// only if) x-axis values are non-numeric var xaxis = {};
// However, that is non-trivial to work out from a dataset (datasets may // check for time series on x-axis
// have no field type info). Thus at present we only do this for bars. if (this.model.fields.get(this.state.get('group')).get('type') === 'date') {
var options = { xaxis.mode = 'time';
xaxis.timeformat = '%y-%b';
}
var optionsPerGraphType = {
lines: { lines: {
series: { series: {
lines: { show: true } lines: { show: true }
} },
xaxis: xaxis
}, },
points: { points: {
series: { series: {
points: { show: true } points: { show: true }
}, },
xaxis: xaxis,
grid: { hoverable: true, clickable: true } grid: { hoverable: true, clickable: true }
}, },
'lines-and-points': { 'lines-and-points': {
@@ -907,6 +959,7 @@ my.Graph = Backbone.View.extend({
points: { show: true }, points: { show: true },
lines: { show: true } lines: { show: true }
}, },
xaxis: xaxis,
grid: { hoverable: true, clickable: true } grid: { hoverable: true, clickable: true }
}, },
bars: { bars: {
@@ -930,7 +983,7 @@ my.Graph = Backbone.View.extend({
} }
} }
}; };
return options[typeId]; return optionsPerGraphType[typeId];
}, },
setupTooltips: function() { setupTooltips: function() {
@@ -987,8 +1040,15 @@ my.Graph = Backbone.View.extend({
_.each(this.state.attributes.series, function(field) { _.each(this.state.attributes.series, function(field) {
var points = []; var points = [];
_.each(self.model.currentDocuments.models, function(doc, index) { _.each(self.model.currentDocuments.models, function(doc, index) {
var x = doc.get(self.state.attributes.group); var xfield = self.model.fields.get(self.state.attributes.group);
var y = doc.get(field); var x = doc.getFieldValue(xfield);
// time series
var isDateTime = xfield.get('type') === 'date';
if (isDateTime) {
x = new Date(x);
}
var yfield = self.model.fields.get(field);
var y = doc.getFieldValue(yfield);
if (typeof x === 'string') { if (typeof x === 'string') {
x = index; x = index;
} }
@@ -1006,23 +1066,25 @@ my.Graph = Backbone.View.extend({
// Public: Adds a new empty series select box to the editor. // Public: Adds a new empty series select box to the editor.
// //
// All but the first select box will have a remove button that allows them // @param [int] idx index of this series in the list of series
// to be removed.
// //
// Returns itself. // Returns itself.
addSeries: function (e) { addSeries: function (idx) {
e.preventDefault(); var data = _.extend({
var element = this.$seriesClone.clone(), seriesIndex: idx,
label = element.find('label'), seriesName: String.fromCharCode(idx + 64 + 1),
index = this.$series.length; }, this.model.toTemplateJSON());
this.el.find('.editor-series-group').append(element); var htmls = $.mustache(this.templateSeriesEditor, data);
this._updateSeries(); this.el.find('.editor-series-group').append(htmls);
label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]');
label.find('span').text(String.fromCharCode(this.$series.length + 64));
return this; return this;
}, },
_onAddSeries: function(e) {
e.preventDefault();
this.addSeries(this.state.get('series').length);
},
// Public: Removes a series list item from the editor. // Public: Removes a series list item from the editor.
// //
// Also updates the labels of the remaining series elements. // Also updates the labels of the remaining series elements.
@@ -1030,26 +1092,12 @@ my.Graph = Backbone.View.extend({
e.preventDefault(); e.preventDefault();
var $el = $(e.target); var $el = $(e.target);
$el.parent().parent().remove(); $el.parent().parent().remove();
this._updateSeries();
this.$series.each(function (index) {
if (index > 0) {
var labelSpan = $(this).prev().find('span');
labelSpan.text(String.fromCharCode(index + 65));
}
});
this.onEditorSubmit(); this.onEditorSubmit();
}, },
toggleHelp: function() { toggleHelp: function() {
this.el.find('.editor-info').toggleClass('editor-hide-info'); this.el.find('.editor-info').toggleClass('editor-hide-info');
}, },
// Private: Resets the series property to reference the select elements.
//
// Returns itself.
_updateSeries: function () {
this.$series = this.el.find('.editor-series select');
}
}); });
})(jQuery, recline.View); })(jQuery, recline.View);
@@ -1199,6 +1247,8 @@ my.Grid = Backbone.View.extend({
var hiddenFields = this.state.get('hiddenFields'); var hiddenFields = this.state.get('hiddenFields');
hiddenFields.push(this.tempState.currentColumn); hiddenFields.push(this.tempState.currentColumn);
this.state.set({hiddenFields: hiddenFields}); this.state.set({hiddenFields: hiddenFields});
// change event not being triggered (because it is an array?) so trigger manually
this.state.trigger('change');
this.render(); this.render();
}, },
@@ -1462,6 +1512,11 @@ my.Map = Backbone.View.extend({
<div class="editor-buttons"> \ <div class="editor-buttons"> \
<button class="btn editor-update-map">Update</button> \ <button class="btn editor-update-map">Update</button> \
</div> \ </div> \
<div class="editor-options" > \
<label class="checkbox"> \
<input type="checkbox" id="editor-auto-zoom" checked="checked" /> \
Auto zoom to features</label> \
</div> \
<input type="hidden" class="editor-id" value="map-1" /> \ <input type="hidden" class="editor-id" value="map-1" /> \
</div> \ </div> \
</form> \ </form> \
@@ -1479,7 +1534,8 @@ my.Map = Backbone.View.extend({
// Define here events for UI elements // Define here events for UI elements
events: { events: {
'click .editor-update-map': 'onEditorSubmit', 'click .editor-update-map': 'onEditorSubmit',
'change .editor-field-type': 'onFieldTypeChange' 'change .editor-field-type': 'onFieldTypeChange',
'change #editor-auto-zoom': 'onAutoZoomChange'
}, },
initialize: function(options) { initialize: function(options) {
@@ -1498,15 +1554,27 @@ my.Map = Backbone.View.extend({
// Listen to changes in the documents // Listen to changes in the documents
this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)}); this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
this.model.currentDocuments.bind('change', function(doc){
self.redraw('remove',doc);
self.redraw('add',doc);
});
this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)}); this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
this.model.currentDocuments.bind('reset', function(){self.redraw('reset')}); this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
this.bind('view:show',function(){
// If the div was hidden, Leaflet needs to recalculate some sizes // If the div was hidden, Leaflet needs to recalculate some sizes
// to display properly // to display properly
this.bind('view:show',function(){
if (self.map){ if (self.map){
self.map.invalidateSize(); self.map.invalidateSize();
if (self._zoomPending && self.autoZoom) {
self._zoomToFeatures();
self._zoomPending = false;
} }
}
self.visible = true;
});
this.bind('view:hide',function(){
self.visible = false;
}); });
var stateData = _.extend({ var stateData = _.extend({
@@ -1518,6 +1586,7 @@ my.Map = Backbone.View.extend({
); );
this.state = new recline.Model.ObjectState(stateData); this.state = new recline.Model.ObjectState(stateData);
this.autoZoom = true;
this.mapReady = false; this.mapReady = false;
this.render(); this.render();
}, },
@@ -1583,6 +1652,13 @@ my.Map = Backbone.View.extend({
this.features.clearLayers(); this.features.clearLayers();
this._add(this.model.currentDocuments.models); this._add(this.model.currentDocuments.models);
} }
if (action != 'reset' && this.autoZoom){
if (this.visible){
this._zoomToFeatures();
} else {
this._zoomPending = true;
}
}
} }
}, },
@@ -1629,6 +1705,10 @@ my.Map = Backbone.View.extend({
} }
}, },
onAutoZoomChange: function(e){
this.autoZoom = !this.autoZoom;
},
// Private: Add one or n features to the map // Private: Add one or n features to the map
// //
// For each document passed, a GeoJSON geometry will be extracted and added // For each document passed, a GeoJSON geometry will be extracted and added
@@ -1656,7 +1736,9 @@ my.Map = Backbone.View.extend({
// TODO: mustache? // TODO: mustache?
html = '' html = ''
for (key in doc.attributes){ for (key in doc.attributes){
html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>' if (!(self.state.get('geomField') && key == self.state.get('geomField'))){
html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>';
}
} }
feature.properties = {popupContent: html}; feature.properties = {popupContent: html};
@@ -1707,19 +1789,22 @@ my.Map = Backbone.View.extend({
_getGeometryFromDocument: function(doc){ _getGeometryFromDocument: function(doc){
if (this.geomReady){ if (this.geomReady){
if (this.state.get('geomField')){ if (this.state.get('geomField')){
var value = doc.get(this.state.get('geomField'));
if (typeof(value) === 'string'){
// We have a GeoJSON string representation
return $.parseJSON(value);
} else {
// We assume that the contents of the field are a valid GeoJSON object // We assume that the contents of the field are a valid GeoJSON object
return doc.attributes[this.state.get('geomField')]; return value;
}
} else if (this.state.get('lonField') && this.state.get('latField')){ } else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields // We'll create a GeoJSON like point object from the two lat/lon fields
var lon = doc.get(this.state.get('lonField')); var lon = doc.get(this.state.get('lonField'));
var lat = doc.get(this.state.get('latField')); var lat = doc.get(this.state.get('latField'));
if (lon && lat) { if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
return { return {
type: 'Point', type: 'Point',
coordinates: [ coordinates: [lon,lat]
doc.attributes[this.state.get('lonField')],
doc.attributes[this.state.get('latField')]
]
}; };
} }
} }
@@ -1761,6 +1846,18 @@ my.Map = Backbone.View.extend({
return null; return null;
}, },
// Private: Zoom to map to current features extent if any, or to the full
// extent if none.
//
_zoomToFeatures: function(){
var bounds = this.features.getBounds();
if (bounds){
this.map.fitBounds(bounds);
} else {
this.map.setView(new L.LatLng(0, 0), 2);
}
},
// Private: Sets up the Leaflet map control and the features layer. // Private: Sets up the Leaflet map control and the features layer.
// //
// The map uses a base layer from [MapQuest](http://www.mapquest.com) based // The map uses a base layer from [MapQuest](http://www.mapquest.com) based
@@ -1785,6 +1882,24 @@ my.Map = Backbone.View.extend({
} }
}); });
// This will be available in the next Leaflet stable release.
// In the meantime we add it manually to our layer.
this.features.getBounds = function(){
var bounds = new L.LatLngBounds();
this._iterateLayers(function (layer) {
if (layer instanceof L.Marker){
bounds.extend(layer.getLatLng());
} else {
if (layer.getBounds){
bounds.extend(layer.getBounds().getNorthEast());
bounds.extend(layer.getBounds().getSouthWest());
}
}
}, this);
return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null;
}
this.map.addLayer(this.features); this.map.addLayer(this.features);
this.map.setView(new L.LatLng(0, 0), 2); this.map.setView(new L.LatLng(0, 0), 2);
@@ -2411,7 +2526,9 @@ my.DataExplorer = Backbone.View.extend({
pageView.view.state.bind('change', function() { pageView.view.state.bind('change', function() {
var update = {}; var update = {};
update['view-' + pageView.id] = pageView.view.state.toJSON(); update['view-' + pageView.id] = pageView.view.state.toJSON();
self.state.set(update); // had problems where change not being triggered for e.g. grid view so let's do it explicitly
self.state.set(update, {silent: true});
self.state.trigger('change');
}); });
} }
}); });
@@ -3404,6 +3521,8 @@ this.recline.Backend = this.recline.Backend || {};
var options = options || {}; var options = options || {};
var trm = options.trim; var trm = options.trim;
var separator = options.separator || ','; var separator = options.separator || ',';
var delimiter = options.delimiter || '"';
var cur = '', // The character we are currently processing. var cur = '', // The character we are currently processing.
inQuote = false, inQuote = false,
@@ -3451,8 +3570,8 @@ this.recline.Backend = this.recline.Backend || {};
field = ''; field = '';
fieldQuoted = false; fieldQuoted = false;
} else { } else {
// If it's not a ", add it to the field buffer // If it's not a delimiter, add it to the field buffer
if (cur !== '"') { if (cur !== delimiter) {
field += cur; field += cur;
} else { } else {
if (!inQuote) { if (!inQuote) {
@@ -3460,9 +3579,9 @@ this.recline.Backend = this.recline.Backend || {};
inQuote = true; inQuote = true;
fieldQuoted = true; fieldQuoted = true;
} else { } else {
// Next char is ", this is an escaped " // Next char is delimiter, this is an escaped delimiter
if (s.charAt(i + 1) === '"') { if (s.charAt(i + 1) === delimiter) {
field += '"'; field += delimiter;
// Skip the next char // Skip the next char
i += 1; i += 1;
} else { } else {

View File

@@ -58,6 +58,8 @@ this.recline.Backend = this.recline.Backend || {};
var options = options || {}; var options = options || {};
var trm = options.trim; var trm = options.trim;
var separator = options.separator || ','; var separator = options.separator || ',';
var delimiter = options.delimiter || '"';
var cur = '', // The character we are currently processing. var cur = '', // The character we are currently processing.
inQuote = false, inQuote = false,
@@ -105,8 +107,8 @@ this.recline.Backend = this.recline.Backend || {};
field = ''; field = '';
fieldQuoted = false; fieldQuoted = false;
} else { } else {
// If it's not a ", add it to the field buffer // If it's not a delimiter, add it to the field buffer
if (cur !== '"') { if (cur !== delimiter) {
field += cur; field += cur;
} else { } else {
if (!inQuote) { if (!inQuote) {
@@ -114,9 +116,9 @@ this.recline.Backend = this.recline.Backend || {};
inQuote = true; inQuote = true;
fieldQuoted = true; fieldQuoted = true;
} else { } else {
// Next char is ", this is an escaped " // Next char is delimiter, this is an escaped delimiter
if (s.charAt(i + 1) === '"') { if (s.charAt(i + 1) === delimiter) {
field += '"'; field += delimiter;
// Skip the next char // Skip the next char
i += 1; i += 1;
} else { } else {

View File

@@ -1,153 +1,79 @@
/*jshint multistr:true */ /*jshint multistr:true */
var util = function() { this.recline = this.recline || {};
var templates = { this.recline.Util = this.recline.Util || {};
transformActions: '<li><a data-action="transform" class="menuAction" href="JavaScript:void(0);">Global transform...</a></li>',
cellEditor: ' \
<div class="menu-container data-table-cell-editor"> \
<textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
<div id="data-table-cell-editor-actions"> \
<div class="data-table-cell-editor-action"> \
<button class="okButton btn primary">Update</button> \
<button class="cancelButton btn danger">Cancel</button> \
</div> \
</div> \
</div> \
',
editPreview: ' \
<div class="expression-preview-table-wrapper"> \
<table> \
<thead> \
<tr> \
<th class="expression-preview-heading"> \
before \
</th> \
<th class="expression-preview-heading"> \
after \
</th> \
</tr> \
</thead> \
<tbody> \
{{#rows}} \
<tr> \
<td class="expression-preview-value"> \
{{before}} \
</td> \
<td class="expression-preview-value"> \
{{after}} \
</td> \
</tr> \
{{/rows}} \
</tbody> \
</table> \
</div> \
'
};
$.fn.serializeObject = function() { (function(my) {
var o = {}; // ## Miscellaneous Utilities
var a = this.serializeArray();
$.each(a, function() { var urlPathRegex = /^([^?]+)(\?.*)?/;
if (o[this.name]) {
if (!o[this.name].push) { // Parse the Hash section of a URL into path and query string
o[this.name] = [o[this.name]]; my.parseHashUrl = function(hashUrl) {
} var parsed = urlPathRegex.exec(hashUrl);
o[this.name].push(this.value || ''); if (parsed === null) {
return {};
} else { } else {
o[this.name] = this.value || '';
}
});
return o;
};
function registerEmitter() {
var Emitter = function(obj) {
this.emit = function(obj, channel) {
if (!channel) channel = 'data';
this.trigger(channel, obj);
};
};
MicroEvent.mixin(Emitter);
return new Emitter();
}
function listenFor(keys) {
var shortcuts = { // from jquery.hotkeys.js
8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del",
96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
};
window.addEventListener("keyup", function(e) {
var pressed = shortcuts[e.keyCode];
if(_.include(keys, pressed)) app.emitter.emit("keyup", pressed);
}, false);
}
function observeExit(elem, callback) {
var cancelButton = elem.find('.cancelButton');
// TODO: remove (commented out as part of Backbon-i-fication
// app.emitter.on('esc', function() {
// cancelButton.click();
// app.emitter.clear('esc');
// });
cancelButton.click(callback);
}
function show( thing ) {
$('.' + thing ).show();
$('.' + thing + '-overlay').show();
}
function hide( thing ) {
$('.' + thing ).hide();
$('.' + thing + '-overlay').hide();
// TODO: remove or replace (commented out as part of Backbon-i-fication
// if (thing === "dialog") app.emitter.clear('esc'); // todo more elegant solution
}
function position( thing, elem, offset ) {
var position = $(elem.target).position();
if (offset) {
if (offset.top) position.top += offset.top;
if (offset.left) position.left += offset.left;
}
$('.' + thing + '-overlay').show().click(function(e) {
$(e.target).hide();
$('.' + thing).hide();
});
$('.' + thing).show().css({top: position.top + $(elem.target).height(), left: position.left});
}
function render( template, target, options ) {
if ( !options ) options = {data: {}};
if ( !options.data ) options = {data: options};
var html = $.mustache( templates[template], options.data );
var targetDom = null;
if (target instanceof jQuery) {
targetDom = target;
} else {
targetDom = $( "." + target + ":first" );
}
if( options.append ) {
targetDom.append( html );
} else {
targetDom.html( html );
}
// TODO: remove (commented out as part of Backbon-i-fication
// if (template in app.after) app.after[template]();
}
return { return {
registerEmitter: registerEmitter, path: parsed[1],
listenFor: listenFor, query: parsed[2] || ''
show: show,
hide: hide,
position: position,
render: render,
observeExit: observeExit
}; };
}(); }
};
// Parse a URL query string (?xyz=abc...) into a dictionary.
my.parseQueryString = function(q) {
if (!q) {
return {};
}
var urlParams = {},
e, d = function (s) {
return unescape(s.replace(/\+/g, " "));
},
r = /([^&=]+)=?([^&]*)/g;
if (q && q.length && q[0] === '?') {
q = q.slice(1);
}
while (e = r.exec(q)) {
// TODO: have values be array as query string allow repetition of keys
urlParams[d(e[1])] = d(e[2]);
}
return urlParams;
};
// Parse the query string out of the URL hash
my.parseHashQueryString = function() {
q = my.parseHashUrl(window.location.hash).query;
return my.parseQueryString(q);
};
// Compse a Query String
my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
if (typeof(value) === 'object') {
value = JSON.stringify(value);
}
items.push(key + '=' + value);
});
queryString += items.join('&');
return queryString;
};
my.getNewHashForQueryString = function(queryParams) {
var queryPart = my.composeQueryString(queryParams);
if (window.location.hash) {
// slice(1) to remove # at start
return window.location.hash.split('?')[0].slice(1) + queryPart;
} else {
return queryPart;
}
};
my.setHashQueryString = function(queryParams) {
window.location.hash = my.getNewHashForQueryString(queryParams);
};
})(this.recline.Util);

View File

@@ -48,22 +48,13 @@ my.Graph = Backbone.View.extend({
<label>Group Column (x-axis)</label> \ <label>Group Column (x-axis)</label> \
<div class="input editor-group"> \ <div class="input editor-group"> \
<select> \ <select> \
<option value="">Please choose ...</option> \
{{#fields}} \ {{#fields}} \
<option value="{{id}}">{{label}}</option> \ <option value="{{id}}">{{label}}</option> \
{{/fields}} \ {{/fields}} \
</select> \ </select> \
</div> \ </div> \
<div class="editor-series-group"> \ <div class="editor-series-group"> \
<div class="editor-series"> \
<label>Series <span>A (y-axis)</span></label> \
<div class="input"> \
<select> \
{{#fields}} \
<option value="{{id}}">{{label}}</option> \
{{/fields}} \
</select> \
</div> \
</div> \
</div> \ </div> \
</div> \ </div> \
<div class="editor-buttons"> \ <div class="editor-buttons"> \
@@ -75,13 +66,34 @@ my.Graph = Backbone.View.extend({
</div> \ </div> \
</form> \ </form> \
</div> \ </div> \
<div class="panel graph"></div> \ <div class="panel graph"> \
<div class="js-temp-notice alert alert-block"> \
<h3 class="alert-heading">Hey there!</h3> \
<p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
<p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
</div> \
</div> \
</div> \
',
templateSeriesEditor: ' \
<div class="editor-series js-series-{{seriesIndex}}"> \
<label>Series <span>{{seriesName}} (y-axis)</span> \
[<a href="#remove" class="action-remove-series">Remove</a>] \
</label> \
<div class="input"> \
<select> \
<option value="">Please choose ...</option> \
{{#fields}} \
<option value="{{id}}">{{label}}</option> \
{{/fields}} \
</select> \
</div> \
</div> \ </div> \
', ',
events: { events: {
'change form select': 'onEditorSubmit', 'change form select': 'onEditorSubmit',
'click .editor-add': 'addSeries', 'click .editor-add': '_onAddSeries',
'click .action-remove-series': 'removeSeries', 'click .action-remove-series': 'removeSeries',
'click .action-toggle-help': 'toggleHelp' 'click .action-toggle-help': 'toggleHelp'
}, },
@@ -98,7 +110,8 @@ my.Graph = Backbone.View.extend({
this.model.currentDocuments.bind('reset', this.redraw); this.model.currentDocuments.bind('reset', this.redraw);
var stateData = _.extend({ var stateData = _.extend({
group: null, group: null,
series: [], // so that at least one series chooser box shows up
series: [""],
graphType: 'lines-and-points' graphType: 'lines-and-points'
}, },
options.state options.state
@@ -108,21 +121,45 @@ my.Graph = Backbone.View.extend({
}, },
render: function() { render: function() {
htmls = $.mustache(this.template, this.model.toTemplateJSON()); var self = this;
var tmplData = this.model.toTemplateJSON();
var htmls = $.mustache(this.template, tmplData);
$(this.el).html(htmls); $(this.el).html(htmls);
// now set a load of stuff up
this.$graph = this.el.find('.panel.graph'); this.$graph = this.el.find('.panel.graph');
// for use later when adding additional series
// could be simpler just to have a common template! // set up editor from state
this.$seriesClone = this.el.find('.editor-series').clone(); if (this.state.get('graphType')) {
this._updateSeries(); this._selectOption('.editor-type', this.state.get('graphType'));
}
if (this.state.get('group')) {
this._selectOption('.editor-group', this.state.get('group'));
}
_.each(this.state.get('series'), function(series, idx) {
self.addSeries(idx);
self._selectOption('.editor-series.js-series-' + idx, series);
});
return this; return this;
}, },
// Private: Helper function to select an option from a select list
//
_selectOption: function(id,value){
var options = this.el.find(id + ' select > option');
if (options) {
options.each(function(opt){
if (this.value == value) {
$(this).attr('selected','selected');
return false;
}
});
}
},
onEditorSubmit: function(e) { onEditorSubmit: function(e) {
var select = this.el.find('.editor-group select'); var select = this.el.find('.editor-group select');
$editor = this; var $editor = this;
var series = this.$series.map(function () { var $series = this.el.find('.editor-series select');
var series = $series.map(function () {
return $(this).val(); return $(this).val();
}); });
var updatedState = { var updatedState = {
@@ -161,10 +198,20 @@ my.Graph = Backbone.View.extend({
// } // }
}, },
// ### getGraphOptions
//
// Get options for Flot Graph
//
// needs to be function as can depend on state // needs to be function as can depend on state
//
// @param typeId graphType id (lines, lines-and-points etc)
getGraphOptions: function(typeId) { getGraphOptions: function(typeId) {
var self = this; var self = this;
// special tickformatter to show labels rather than numbers // special tickformatter to show labels rather than numbers
// TODO: we should really use tickFormatter and 1 interval ticks if (and
// only if) x-axis values are non-numeric
// However, that is non-trivial to work out from a dataset (datasets may
// have no field type info). Thus at present we only do this for bars.
var tickFormatter = function (val) { var tickFormatter = function (val) {
if (self.model.currentDocuments.models[val]) { if (self.model.currentDocuments.models[val]) {
var out = self.model.currentDocuments.models[val].get(self.state.attributes.group); var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);
@@ -177,20 +224,25 @@ my.Graph = Backbone.View.extend({
} }
return val; return val;
}; };
// TODO: we should really use tickFormatter and 1 interval ticks if (and
// only if) x-axis values are non-numeric var xaxis = {};
// However, that is non-trivial to work out from a dataset (datasets may // check for time series on x-axis
// have no field type info). Thus at present we only do this for bars. if (this.model.fields.get(this.state.get('group')).get('type') === 'date') {
var options = { xaxis.mode = 'time';
xaxis.timeformat = '%y-%b';
}
var optionsPerGraphType = {
lines: { lines: {
series: { series: {
lines: { show: true } lines: { show: true }
} },
xaxis: xaxis
}, },
points: { points: {
series: { series: {
points: { show: true } points: { show: true }
}, },
xaxis: xaxis,
grid: { hoverable: true, clickable: true } grid: { hoverable: true, clickable: true }
}, },
'lines-and-points': { 'lines-and-points': {
@@ -198,6 +250,7 @@ my.Graph = Backbone.View.extend({
points: { show: true }, points: { show: true },
lines: { show: true } lines: { show: true }
}, },
xaxis: xaxis,
grid: { hoverable: true, clickable: true } grid: { hoverable: true, clickable: true }
}, },
bars: { bars: {
@@ -221,7 +274,7 @@ my.Graph = Backbone.View.extend({
} }
} }
}; };
return options[typeId]; return optionsPerGraphType[typeId];
}, },
setupTooltips: function() { setupTooltips: function() {
@@ -278,8 +331,15 @@ my.Graph = Backbone.View.extend({
_.each(this.state.attributes.series, function(field) { _.each(this.state.attributes.series, function(field) {
var points = []; var points = [];
_.each(self.model.currentDocuments.models, function(doc, index) { _.each(self.model.currentDocuments.models, function(doc, index) {
var x = doc.get(self.state.attributes.group); var xfield = self.model.fields.get(self.state.attributes.group);
var y = doc.get(field); var x = doc.getFieldValue(xfield);
// time series
var isDateTime = xfield.get('type') === 'date';
if (isDateTime) {
x = new Date(x);
}
var yfield = self.model.fields.get(field);
var y = doc.getFieldValue(yfield);
if (typeof x === 'string') { if (typeof x === 'string') {
x = index; x = index;
} }
@@ -297,23 +357,25 @@ my.Graph = Backbone.View.extend({
// Public: Adds a new empty series select box to the editor. // Public: Adds a new empty series select box to the editor.
// //
// All but the first select box will have a remove button that allows them // @param [int] idx index of this series in the list of series
// to be removed.
// //
// Returns itself. // Returns itself.
addSeries: function (e) { addSeries: function (idx) {
e.preventDefault(); var data = _.extend({
var element = this.$seriesClone.clone(), seriesIndex: idx,
label = element.find('label'), seriesName: String.fromCharCode(idx + 64 + 1),
index = this.$series.length; }, this.model.toTemplateJSON());
this.el.find('.editor-series-group').append(element); var htmls = $.mustache(this.templateSeriesEditor, data);
this._updateSeries(); this.el.find('.editor-series-group').append(htmls);
label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]');
label.find('span').text(String.fromCharCode(this.$series.length + 64));
return this; return this;
}, },
_onAddSeries: function(e) {
e.preventDefault();
this.addSeries(this.state.get('series').length);
},
// Public: Removes a series list item from the editor. // Public: Removes a series list item from the editor.
// //
// Also updates the labels of the remaining series elements. // Also updates the labels of the remaining series elements.
@@ -321,26 +383,12 @@ my.Graph = Backbone.View.extend({
e.preventDefault(); e.preventDefault();
var $el = $(e.target); var $el = $(e.target);
$el.parent().parent().remove(); $el.parent().parent().remove();
this._updateSeries();
this.$series.each(function (index) {
if (index > 0) {
var labelSpan = $(this).prev().find('span');
labelSpan.text(String.fromCharCode(index + 65));
}
});
this.onEditorSubmit(); this.onEditorSubmit();
}, },
toggleHelp: function() { toggleHelp: function() {
this.el.find('.editor-info').toggleClass('editor-hide-info'); this.el.find('.editor-info').toggleClass('editor-hide-info');
}, },
// Private: Resets the series property to reference the select elements.
//
// Returns itself.
_updateSeries: function () {
this.$series = this.el.find('.editor-series select');
}
}); });
})(jQuery, recline.View); })(jQuery, recline.View);

View File

@@ -35,18 +35,6 @@ my.Grid = Backbone.View.extend({
'click .data-table-menu li a': 'onMenuClick' 'click .data-table-menu li a': 'onMenuClick'
}, },
// TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)).
// showDialog: function(template, data) {
// if (!data) data = {};
// util.show('dialog');
// util.render(template, 'dialog-content', data);
// util.observeExit($('.dialog-content'), function() {
// util.hide('dialog');
// })
// $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
// },
// ====================================================== // ======================================================
// Column and row menus // Column and row menus
@@ -81,12 +69,12 @@ my.Grid = Backbone.View.extend({
filter: function() { filter: function() {
self.model.queryState.addTermFilter(self.tempState.currentColumn, ''); self.model.queryState.addTermFilter(self.tempState.currentColumn, '');
}, },
transform: function() { self.showTransformDialog('transform'); },
sortAsc: function() { self.setColumnSort('asc'); }, sortAsc: function() { self.setColumnSort('asc'); },
sortDesc: function() { self.setColumnSort('desc'); }, sortDesc: function() { self.setColumnSort('desc'); },
hideColumn: function() { self.hideColumn(); }, hideColumn: function() { self.hideColumn(); },
showColumn: function() { self.showColumn(e); }, showColumn: function() { self.showColumn(e); },
deleteRow: function() { deleteRow: function() {
var self = this;
var doc = _.find(self.model.currentDocuments.models, function(doc) { var doc = _.find(self.model.currentDocuments.models, function(doc) {
// important this is == as the currentRow will be string (as comes // important this is == as the currentRow will be string (as comes
// from DOM) while id may be int // from DOM) while id may be int
@@ -94,9 +82,9 @@ my.Grid = Backbone.View.extend({
}); });
doc.destroy().then(function() { doc.destroy().then(function() {
self.model.currentDocuments.remove(doc); self.model.currentDocuments.remove(doc);
my.notify("Row deleted successfully"); self.trigger('recline:flash', {message: "Row deleted successfully"});
}).fail(function(err) { }).fail(function(err) {
my.notify("Errorz! " + err); self.trigger('recline:flash', {message: "Errorz! " + err});
}); });
} }
}; };
@@ -104,33 +92,18 @@ my.Grid = Backbone.View.extend({
}, },
showTransformColumnDialog: function() { showTransformColumnDialog: function() {
var $el = $('.dialog-content'); var self = this;
util.show('dialog');
var view = new my.ColumnTransform({ var view = new my.ColumnTransform({
model: this.model model: this.model
}); });
// pass the flash message up the chain
view.bind('recline:flash', function(flash) {
self.trigger('recline:flash', flash);
});
view.state = this.tempState; view.state = this.tempState;
view.render(); view.render();
$el.empty(); this.el.append(view.el);
$el.append(view.el); view.el.modal();
util.observeExit($el, function() {
util.hide('dialog');
});
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
},
showTransformDialog: function() {
var $el = $('.dialog-content');
util.show('dialog');
var view = new recline.View.DataTransform({
});
view.render();
$el.empty();
$el.append(view.el);
util.observeExit($el, function() {
util.hide('dialog');
});
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
}, },
setColumnSort: function(order) { setColumnSort: function(order) {
@@ -143,6 +116,8 @@ my.Grid = Backbone.View.extend({
var hiddenFields = this.state.get('hiddenFields'); var hiddenFields = this.state.get('hiddenFields');
hiddenFields.push(this.tempState.currentColumn); hiddenFields.push(this.tempState.currentColumn);
this.state.set({hiddenFields: hiddenFields}); this.state.set({hiddenFields: hiddenFields});
// change event not being triggered (because it is an array?) so trigger manually
this.state.trigger('change');
this.render(); this.render();
}, },
@@ -291,6 +266,19 @@ my.GridRow = Backbone.View.extend({
// =================== // ===================
// Cell Editor methods // Cell Editor methods
cellEditorTemplate: ' \
<div class="menu-container data-table-cell-editor"> \
<textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
<div id="data-table-cell-editor-actions"> \
<div class="data-table-cell-editor-action"> \
<button class="okButton btn primary">Update</button> \
<button class="cancelButton btn danger">Cancel</button> \
</div> \
</div> \
</div> \
',
onEditClick: function(e) { onEditClick: function(e) {
var editing = this.el.find('.data-table-cell-editor-editor'); var editing = this.el.find('.data-table-cell-editor-editor');
if (editing.length > 0) { if (editing.length > 0) {
@@ -299,10 +287,12 @@ my.GridRow = Backbone.View.extend({
$(e.target).addClass("hidden"); $(e.target).addClass("hidden");
var cell = $(e.target).siblings('.data-table-cell-value'); var cell = $(e.target).siblings('.data-table-cell-value');
cell.data("previousContents", cell.text()); cell.data("previousContents", cell.text());
util.render('cellEditor', cell, {value: cell.text()}); var templated = $.mustache(this.cellEditorTemplate, {value: cell.text()});
cell.html(templated);
}, },
onEditorOK: function(e) { onEditorOK: function(e) {
var self = this;
var cell = $(e.target); var cell = $(e.target);
var rowId = cell.parents('tr').attr('data-id'); var rowId = cell.parents('tr').attr('data-id');
var field = cell.parents('td').attr('data-field'); var field = cell.parents('td').attr('data-field');
@@ -310,12 +300,13 @@ my.GridRow = Backbone.View.extend({
var newData = {}; var newData = {};
newData[field] = newValue; newData[field] = newValue;
this.model.set(newData); this.model.set(newData);
my.notify("Updating row...", {loader: true}); this.trigger('recline:flash', {message: "Updating row...", loader: true});
this.model.save().then(function(response) { this.model.save().then(function(response) {
my.notify("Row updated successfully", {category: 'success'}); this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'});
}) })
.fail(function() { .fail(function() {
my.notify('Error saving row', { this.trigger('recline:flash', {
message: 'Error saving row',
category: 'error', category: 'error',
persist: true persist: true
}); });

View File

@@ -75,6 +75,11 @@ my.Map = Backbone.View.extend({
<div class="editor-buttons"> \ <div class="editor-buttons"> \
<button class="btn editor-update-map">Update</button> \ <button class="btn editor-update-map">Update</button> \
</div> \ </div> \
<div class="editor-options" > \
<label class="checkbox"> \
<input type="checkbox" id="editor-auto-zoom" checked="checked" /> \
Auto zoom to features</label> \
</div> \
<input type="hidden" class="editor-id" value="map-1" /> \ <input type="hidden" class="editor-id" value="map-1" /> \
</div> \ </div> \
</form> \ </form> \
@@ -92,7 +97,8 @@ my.Map = Backbone.View.extend({
// Define here events for UI elements // Define here events for UI elements
events: { events: {
'click .editor-update-map': 'onEditorSubmit', 'click .editor-update-map': 'onEditorSubmit',
'change .editor-field-type': 'onFieldTypeChange' 'change .editor-field-type': 'onFieldTypeChange',
'change #editor-auto-zoom': 'onAutoZoomChange'
}, },
initialize: function(options) { initialize: function(options) {
@@ -111,15 +117,27 @@ my.Map = Backbone.View.extend({
// Listen to changes in the documents // Listen to changes in the documents
this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)}); this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
this.model.currentDocuments.bind('change', function(doc){
self.redraw('remove',doc);
self.redraw('add',doc);
});
this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)}); this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
this.model.currentDocuments.bind('reset', function(){self.redraw('reset')}); this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
this.bind('view:show',function(){
// If the div was hidden, Leaflet needs to recalculate some sizes // If the div was hidden, Leaflet needs to recalculate some sizes
// to display properly // to display properly
this.bind('view:show',function(){
if (self.map){ if (self.map){
self.map.invalidateSize(); self.map.invalidateSize();
if (self._zoomPending && self.autoZoom) {
self._zoomToFeatures();
self._zoomPending = false;
} }
}
self.visible = true;
});
this.bind('view:hide',function(){
self.visible = false;
}); });
var stateData = _.extend({ var stateData = _.extend({
@@ -131,6 +149,7 @@ my.Map = Backbone.View.extend({
); );
this.state = new recline.Model.ObjectState(stateData); this.state = new recline.Model.ObjectState(stateData);
this.autoZoom = true;
this.mapReady = false; this.mapReady = false;
this.render(); this.render();
}, },
@@ -196,6 +215,13 @@ my.Map = Backbone.View.extend({
this.features.clearLayers(); this.features.clearLayers();
this._add(this.model.currentDocuments.models); this._add(this.model.currentDocuments.models);
} }
if (action != 'reset' && this.autoZoom){
if (this.visible){
this._zoomToFeatures();
} else {
this._zoomPending = true;
}
}
} }
}, },
@@ -242,6 +268,10 @@ my.Map = Backbone.View.extend({
} }
}, },
onAutoZoomChange: function(e){
this.autoZoom = !this.autoZoom;
},
// Private: Add one or n features to the map // Private: Add one or n features to the map
// //
// For each document passed, a GeoJSON geometry will be extracted and added // For each document passed, a GeoJSON geometry will be extracted and added
@@ -251,7 +281,6 @@ my.Map = Backbone.View.extend({
// Each feature will have a popup associated with all the document fields. // Each feature will have a popup associated with all the document fields.
// //
_add: function(docs){ _add: function(docs){
var self = this; var self = this;
if (!(docs instanceof Array)) docs = [docs]; if (!(docs instanceof Array)) docs = [docs];
@@ -269,7 +298,9 @@ my.Map = Backbone.View.extend({
// TODO: mustache? // TODO: mustache?
html = '' html = ''
for (key in doc.attributes){ for (key in doc.attributes){
html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>' if (!(self.state.get('geomField') && key == self.state.get('geomField'))){
html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>';
}
} }
feature.properties = {popupContent: html}; feature.properties = {popupContent: html};
@@ -284,13 +315,13 @@ my.Map = Backbone.View.extend({
var msg = 'Wrong geometry value'; var msg = 'Wrong geometry value';
if (except.message) msg += ' (' + except.message + ')'; if (except.message) msg += ' (' + except.message + ')';
if (wrongSoFar <= 10) { if (wrongSoFar <= 10) {
my.notify(msg,{category:'error'}); self.trigger('recline:flash', {message: msg, category:'error'});
} }
} }
} else { } else {
wrongSoFar += 1 wrongSoFar += 1
if (wrongSoFar <= 10) { if (wrongSoFar <= 10) {
my.notify('Wrong geometry value',{category:'error'}); self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'});
} }
} }
return true; return true;
@@ -320,19 +351,22 @@ my.Map = Backbone.View.extend({
_getGeometryFromDocument: function(doc){ _getGeometryFromDocument: function(doc){
if (this.geomReady){ if (this.geomReady){
if (this.state.get('geomField')){ if (this.state.get('geomField')){
var value = doc.get(this.state.get('geomField'));
if (typeof(value) === 'string'){
// We have a GeoJSON string representation
return $.parseJSON(value);
} else {
// We assume that the contents of the field are a valid GeoJSON object // We assume that the contents of the field are a valid GeoJSON object
return doc.attributes[this.state.get('geomField')]; return value;
}
} else if (this.state.get('lonField') && this.state.get('latField')){ } else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields // We'll create a GeoJSON like point object from the two lat/lon fields
var lon = doc.get(this.state.get('lonField')); var lon = doc.get(this.state.get('lonField'));
var lat = doc.get(this.state.get('latField')); var lat = doc.get(this.state.get('latField'));
if (lon && lat) { if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
return { return {
type: 'Point', type: 'Point',
coordinates: [ coordinates: [lon,lat]
doc.attributes[this.state.get('lonField')],
doc.attributes[this.state.get('latField')]
]
}; };
} }
} }
@@ -374,6 +408,18 @@ my.Map = Backbone.View.extend({
return null; return null;
}, },
// Private: Zoom to map to current features extent if any, or to the full
// extent if none.
//
_zoomToFeatures: function(){
var bounds = this.features.getBounds();
if (bounds){
this.map.fitBounds(bounds);
} else {
this.map.setView(new L.LatLng(0, 0), 2);
}
},
// Private: Sets up the Leaflet map control and the features layer. // Private: Sets up the Leaflet map control and the features layer.
// //
// The map uses a base layer from [MapQuest](http://www.mapquest.com) based // The map uses a base layer from [MapQuest](http://www.mapquest.com) based
@@ -398,6 +444,24 @@ my.Map = Backbone.View.extend({
} }
}); });
// This will be available in the next Leaflet stable release.
// In the meantime we add it manually to our layer.
this.features.getBounds = function(){
var bounds = new L.LatLngBounds();
this._iterateLayers(function (layer) {
if (layer instanceof L.Marker){
bounds.extend(layer.getLatLng());
} else {
if (layer.getBounds){
bounds.extend(layer.getBounds().getNorthEast());
bounds.extend(layer.getBounds().getSouthWest());
}
}
}, this);
return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null;
}
this.map.addLayer(this.features); this.map.addLayer(this.features);
this.map.setView(new L.LatLng(0, 0), 2); this.map.setView(new L.LatLng(0, 0), 2);

View File

@@ -6,82 +6,17 @@ this.recline.View = this.recline.View || {};
// Views module following classic module pattern // Views module following classic module pattern
(function($, my) { (function($, my) {
// View (Dialog) for doing data transformations on whole dataset. // ## ColumnTransform
my.DataTransform = Backbone.View.extend({ //
className: 'transform-view',
template: ' \
<div class="dialog-header"> \
Recursive transform on all rows \
</div> \
<div class="dialog-body"> \
<div class="grid-layout layout-full"> \
<p class="info">Traverse and transform objects by visiting every node on a recursive walk using <a href="https://github.com/substack/js-traverse">js-traverse</a>.</p> \
<table> \
<tbody> \
<tr> \
<td colspan="4"> \
<div class="grid-layout layout-tight layout-full"> \
<table rows="4" cols="4"> \
<tbody> \
<tr style="vertical-align: bottom;"> \
<td colspan="4"> \
Expression \
</td> \
</tr> \
<tr> \
<td colspan="3"> \
<div class="input-container"> \
<textarea class="expression-preview-code"></textarea> \
</div> \
</td> \
<td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
No syntax error. \
</td> \
</tr> \
<tr> \
<td colspan="4"> \
<div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
<span>Preview</span> \
<div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
<div class="expression-preview-container" style="width: 652px; "> \
</div> \
</div> \
</div> \
</td> \
</tr> \
</tbody> \
</table> \
</div> \
</td> \
</tr> \
</tbody> \
</table> \
</div> \
</div> \
<div class="dialog-footer"> \
<button class="okButton button">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
<button class="cancelButton button">Cancel</button> \
</div> \
',
initialize: function() {
this.el = $(this.el);
},
render: function() {
this.el.html(this.template);
}
});
// View (Dialog) for doing data transformations (on columns of data). // View (Dialog) for doing data transformations (on columns of data).
my.ColumnTransform = Backbone.View.extend({ my.ColumnTransform = Backbone.View.extend({
className: 'transform-column-view', className: 'transform-column-view modal fade in',
template: ' \ template: ' \
<div class="dialog-header"> \ <div class="modal-header"> \
Functional transform on column {{name}} \ <a class="close" data-dismiss="modal">×</a> \
<h3>Functional transform on column {{name}}</h3> \
</div> \ </div> \
<div class="dialog-body"> \ <div class="modal-body"> \
<div class="grid-layout layout-tight layout-full"> \ <div class="grid-layout layout-tight layout-full"> \
<table> \ <table> \
<tbody> \ <tbody> \
@@ -107,10 +42,10 @@ my.ColumnTransform = Backbone.View.extend({
</tr> \ </tr> \
<tr> \ <tr> \
<td colspan="4"> \ <td colspan="4"> \
<div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \ <div id="expression-preview-tabs"> \
<span>Preview</span> \ <span>Preview</span> \
<div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \ <div id="expression-preview-tabs-preview"> \
<div class="expression-preview-container" style="width: 652px; "> \ <div class="expression-preview-container"> \
</div> \ </div> \
</div> \ </div> \
</div> \ </div> \
@@ -125,7 +60,7 @@ my.ColumnTransform = Backbone.View.extend({
</table> \ </table> \
</div> \ </div> \
</div> \ </div> \
<div class="dialog-footer"> \ <div class="modal-footer"> \
<button class="okButton btn primary">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \ <button class="okButton btn primary">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
<button class="cancelButton btn danger">Cancel</button> \ <button class="cancelButton btn danger">Cancel</button> \
</div> \ </div> \
@@ -158,11 +93,11 @@ my.ColumnTransform = Backbone.View.extend({
var funcText = this.el.find('.expression-preview-code').val(); var funcText = this.el.find('.expression-preview-code').val();
var editFunc = costco.evalFunction(funcText); var editFunc = costco.evalFunction(funcText);
if (editFunc.errorMessage) { if (editFunc.errorMessage) {
my.notify("Error with function! " + editFunc.errorMessage); this.trigger('recline:flash', {message: "Error with function! " + editFunc.errorMessage});
return; return;
} }
util.hide('dialog'); this.el.modal('hide');
my.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true}); this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
var docs = self.model.currentDocuments.map(function(doc) { var docs = self.model.currentDocuments.map(function(doc) {
return doc.toJSON(); return doc.toJSON();
}); });
@@ -172,7 +107,7 @@ my.ColumnTransform = Backbone.View.extend({
function onCompletedUpdate() { function onCompletedUpdate() {
totalToUpdate += -1; totalToUpdate += -1;
if (totalToUpdate === 0) { if (totalToUpdate === 0) {
my.notify(toUpdate.length + " documents updated successfully"); self.trigger('recline:flash', {message: toUpdate.length + " documents updated successfully"});
alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)'); alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
self.remove(); self.remove();
} }
@@ -183,8 +118,38 @@ my.ColumnTransform = Backbone.View.extend({
realDoc.set(editedDoc); realDoc.set(editedDoc);
realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate); realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate);
}); });
this.el.remove();
}, },
editPreviewTemplate: ' \
<div class="expression-preview-table-wrapper"> \
<table class="table table-condensed"> \
<thead> \
<tr> \
<th class="expression-preview-heading"> \
before \
</th> \
<th class="expression-preview-heading"> \
after \
</th> \
</tr> \
</thead> \
<tbody> \
{{#rows}} \
<tr> \
<td class="expression-preview-value"> \
{{before}} \
</td> \
<td class="expression-preview-value"> \
{{after}} \
</td> \
</tr> \
{{/rows}} \
</tbody> \
</table> \
</div> \
',
onEditorKeydown: function(e) { onEditorKeydown: function(e) {
var self = this; var self = this;
// if you don't setTimeout it won't grab the latest character if you call e.target.value // if you don't setTimeout it won't grab the latest character if you call e.target.value
@@ -197,7 +162,9 @@ my.ColumnTransform = Backbone.View.extend({
return doc.toJSON(); return doc.toJSON();
}); });
var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn); var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn);
util.render('editPreview', 'expression-preview-container', {rows: previewData}); var $el = self.el.find('.expression-preview-container');
var templated = $.mustache(self.editPreviewTemplate, {rows: previewData.slice(0,4)});
$el.html(templated);
} else { } else {
errors.text(editFunc.errorMessage); errors.text(editFunc.errorMessage);
} }

View File

@@ -168,12 +168,6 @@ my.DataExplorer = Backbone.View.extend({
<div class="clearfix"></div> \ <div class="clearfix"></div> \
</div> \ </div> \
<div class="data-view-container"></div> \ <div class="data-view-container"></div> \
<div class="dialog-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
<div class="dialog ui-draggable" style="display: none; z-index: 102; top: 101px; "> \
<div class="dialog-frame" style="width: 700px; visibility: visible; "> \
<div class="dialog-content dialog-border"></div> \
</div> \
</div> \
</div> \ </div> \
', ',
events: { events: {
@@ -215,6 +209,7 @@ my.DataExplorer = Backbone.View.extend({
// these must be called after pageViews are created // these must be called after pageViews are created
this.render(); this.render();
this._bindStateChanges(); this._bindStateChanges();
this._bindFlashNotifications();
// now do updates based on state (need to come after render) // now do updates based on state (need to come after render)
if (this.state.get('readOnly')) { if (this.state.get('readOnly')) {
this.setReadOnly(); this.setReadOnly();
@@ -225,24 +220,16 @@ my.DataExplorer = Backbone.View.extend({
this.updateNav(this.pageViews[0].id); this.updateNav(this.pageViews[0].id);
} }
this.router = new Backbone.Router();
this.setupRouting();
this.model.bind('query:start', function() { this.model.bind('query:start', function() {
my.notify('Loading data', {loader: true}); self.notify({message: 'Loading data', loader: true});
}); });
this.model.bind('query:done', function() { this.model.bind('query:done', function() {
my.clearNotifications(); self.clearNotifications();
self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
my.notify('Data loaded', {category: 'success'}); self.notify({message: 'Data loaded', category: 'success'});
// update navigation
var qs = my.parseHashQueryString();
qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
var out = my.getNewHashForQueryString(qs);
// self.router.navigate(out);
}); });
this.model.bind('query:fail', function(error) { this.model.bind('query:fail', function(error) {
my.clearNotifications(); self.clearNotifications();
var msg = ''; var msg = '';
if (typeof(error) == 'string') { if (typeof(error) == 'string') {
msg = error; msg = error;
@@ -256,7 +243,7 @@ my.DataExplorer = Backbone.View.extend({
} else { } else {
msg = 'There was an error querying the backend'; msg = 'There was an error querying the backend';
} }
my.notify(msg, {category: 'error', persist: true}); self.notify({message: msg, category: 'error', persist: true});
}); });
// retrieve basic data like fields etc // retrieve basic data like fields etc
@@ -266,7 +253,7 @@ my.DataExplorer = Backbone.View.extend({
self.model.query(self.state.get('query')); self.model.query(self.state.get('query'));
}) })
.fail(function(error) { .fail(function(error) {
my.notify(error.message, {category: 'error', persist: true}); self.notify({message: error.message, category: 'error', persist: true});
}); });
}, },
@@ -299,21 +286,6 @@ my.DataExplorer = Backbone.View.extend({
this.el.find('.header').append(facetViewer.el); this.el.find('.header').append(facetViewer.el);
}, },
setupRouting: function() {
var self = this;
// Default route
// this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) {
// self.updateNav(self.pageViews[0].id, queryString);
// });
// $.each(this.pageViews, function(idx, view) {
// self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
// self.updateNav(viewId, queryString);
// });
// });
this.router.route(/.*/, 'view', function() {
});
},
updateNav: function(pageName) { updateNav: function(pageName) {
this.el.find('.navigation li').removeClass('active'); this.el.find('.navigation li').removeClass('active');
this.el.find('.navigation li a').removeClass('disabled'); this.el.find('.navigation li a').removeClass('disabled');
@@ -357,7 +329,7 @@ my.DataExplorer = Backbone.View.extend({
_setupState: function(initialState) { _setupState: function(initialState) {
var self = this; var self = this;
// get data from the query string / hash url plus some defaults // get data from the query string / hash url plus some defaults
var qs = my.parseHashQueryString(); var qs = recline.Util.parseHashQueryString();
var query = qs.reclineQuery; var query = qs.reclineQuery;
query = query ? JSON.parse(query) : self.model.queryState.toJSON(); query = query ? JSON.parse(query) : self.model.queryState.toJSON();
// backwards compatability (now named view-graph but was named graph) // backwards compatability (now named view-graph but was named graph)
@@ -391,10 +363,63 @@ my.DataExplorer = Backbone.View.extend({
pageView.view.state.bind('change', function() { pageView.view.state.bind('change', function() {
var update = {}; var update = {};
update['view-' + pageView.id] = pageView.view.state.toJSON(); update['view-' + pageView.id] = pageView.view.state.toJSON();
self.state.set(update); // had problems where change not being triggered for e.g. grid view so let's do it explicitly
self.state.set(update, {silent: true});
self.state.trigger('change');
}); });
} }
}); });
},
_bindFlashNotifications: function() {
var self = this;
_.each(this.pageViews, function(pageView) {
pageView.view.bind('recline:flash', function(flash) {
self.notify(flash);
});
});
},
// ### notify
//
// Create a notification (a div.alert in div.alert-messsages) using provided
// flash object. Flash attributes (all are optional):
//
// * message: message to show.
// * category: warning (default), success, error
// * persist: if true alert is persistent, o/w hidden after 3s (default = false)
// * loader: if true show loading spinner
notify: function(flash) {
var tmplData = _.extend({
message: '',
category: 'warning'
},
flash
);
var _template = ' \
<div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
{{message}} \
{{#loader}} \
<span class="notification-loader">&nbsp;</span> \
{{/loader}} \
</div>';
var _templated = $.mustache(_template, tmplData);
_templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
if (!flash.persist) {
setTimeout(function() {
$(_templated).fadeOut(1000, function() {
$(this).remove();
});
}, 1000);
}
},
// ### clearNotifications
//
// Clear all existing notifications
clearNotifications: function() {
var $notifications = $('.recline-data-explorer .alert-messages .alert');
$notifications.remove();
} }
}); });
@@ -635,118 +660,6 @@ my.FacetViewer = Backbone.View.extend({
} }
}); });
/* ========================================================== */
// ## Miscellaneous Utilities
var urlPathRegex = /^([^?]+)(\?.*)?/;
// Parse the Hash section of a URL into path and query string
my.parseHashUrl = function(hashUrl) {
var parsed = urlPathRegex.exec(hashUrl);
if (parsed === null) {
return {};
} else {
return {
path: parsed[1],
query: parsed[2] || ''
};
}
};
// Parse a URL query string (?xyz=abc...) into a dictionary.
my.parseQueryString = function(q) {
if (!q) {
return {};
}
var urlParams = {},
e, d = function (s) {
return unescape(s.replace(/\+/g, " "));
},
r = /([^&=]+)=?([^&]*)/g;
if (q && q.length && q[0] === '?') {
q = q.slice(1);
}
while (e = r.exec(q)) {
// TODO: have values be array as query string allow repetition of keys
urlParams[d(e[1])] = d(e[2]);
}
return urlParams;
};
// Parse the query string out of the URL hash
my.parseHashQueryString = function() {
q = my.parseHashUrl(window.location.hash).query;
return my.parseQueryString(q);
};
// Compse a Query String
my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
if (typeof(value) === 'object') {
value = JSON.stringify(value);
}
items.push(key + '=' + value);
});
queryString += items.join('&');
return queryString;
};
my.getNewHashForQueryString = function(queryParams) {
var queryPart = my.composeQueryString(queryParams);
if (window.location.hash) {
// slice(1) to remove # at start
return window.location.hash.split('?')[0].slice(1) + queryPart;
} else {
return queryPart;
}
};
my.setHashQueryString = function(queryParams) {
window.location.hash = my.getNewHashForQueryString(queryParams);
};
// ## notify
//
// Create a notification (a div.alert in div.alert-messsages) using provide messages and options. Options are:
//
// * category: warning (default), success, error
// * persist: if true alert is persistent, o/w hidden after 3s (default = false)
// * loader: if true show loading spinner
my.notify = function(message, options) {
if (!options) options = {};
var tmplData = _.extend({
msg: message,
category: 'warning'
},
options);
var _template = ' \
<div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
{{msg}} \
{{#loader}} \
<span class="notification-loader">&nbsp;</span> \
{{/loader}} \
</div>';
var _templated = $.mustache(_template, tmplData);
_templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
if (!options.persist) {
setTimeout(function() {
$(_templated).fadeOut(1000, function() {
$(this).remove();
});
}, 1000);
}
};
// ## clearNotifications
//
// Clear all existing notifications
my.clearNotifications = function() {
var $notifications = $('.recline-data-explorer .alert-messages .alert');
$notifications.remove();
};
})(jQuery, recline.View); })(jQuery, recline.View);

View File

@@ -44,4 +44,20 @@ test("parseCSVsemicolon", function() {
}); });
test("parseCSVdelimiter", function() {
var csv = "'Jones, Jay',10\n" +
"'Xyz \"ABC\" O''Brien',11:35\n" +
"'Other; AN',12:35\n";
var array = recline.Backend.parseCSV(csv, {delimiter:"'"});
var exp = [
["Jones, Jay", 10],
["Xyz \"ABC\" O'Brien", "11:35" ],
["Other; AN", "12:35" ]
];
deepEqual(exp, array);
});
})(this.jQuery); })(this.jQuery);

View File

@@ -1,6 +1,8 @@
var Fixture = { var Fixture = {
getDataset: function() { getDataset: function() {
var fields = [ var fields = [
{id: 'id'},
{id: 'date', type: 'date'},
{id: 'x'}, {id: 'x'},
{id: 'y'}, {id: 'y'},
{id: 'z'}, {id: 'z'},
@@ -10,12 +12,12 @@ var Fixture = {
{id: 'lon'} {id: 'lon'}
]; ];
var documents = [ var documents = [
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40}, {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40},
{id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second', lat:54.97, lon:-1.60}, {id: 1, date: '2011-02-02', x: 2, y: 4, z: 6, country: 'UK', label: 'second', lat:54.97, lon:-1.60},
{id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third', lat:40.00, lon:-75.5}, {id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, country: 'US', label: 'third', lat:40.00, lon:-75.5},
{id: 3, x: 4, y: 8, z: 12, country: 'UK', label: 'fourth', lat:57.27, lon:-6.20}, {id: 3, date: '2011-04-04', x: 4, y: 8, z: 12, country: 'UK', label: 'fourth', lat:57.27, lon:-6.20},
{id: 4, x: 5, y: 10, z: 15, country: 'UK', label: 'fifth', lat:51.58, lon:0}, {id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, country: 'UK', label: 'fifth', lat:51.58, lon:0},
{id: 5, x: 6, y: 12, z: 18, country: 'DE', label: 'sixth', lat:51.04, lon:7.9} {id: 5, date: '2011-06-02', x: 6, y: 12, z: 18, country: 'DE', label: 'sixth', lat:51.04, lon:7.9}
]; ];
var dataset = recline.Backend.createDataset(documents, fields); var dataset = recline.Backend.createDataset(documents, fields);
return dataset; return dataset;

52
test/built.html Normal file
View File

@@ -0,0 +1,52 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Qunit Tests</title>
<link rel="stylesheet" href="qunit/qunit.css" type="text/css" media="screen" />
<!-- need this stylesheet because flot will complain if canvas does not have a height -->
<link rel="stylesheet" href="../css/graph.css" type="text/css" media="screen" />
<script type="text/javascript" src="../vendor/jquery/1.7.1/jquery.js"></script>
<script type="text/javascript" src="../vendor/underscore/1.1.6/underscore.js"></script>
<script type="text/javascript" src="../vendor/backbone/0.5.1/backbone.js"></script>
<script type="text/javascript" src="../vendor/jquery-ui-1.8.14.custom.min.js"></script>
<script type="text/javascript" src="../vendor/jquery.flot/0.7/jquery.flot.js"></script>
<script type="text/javascript" src="../vendor/jquery.mustache.js"></script>
<script type="text/javascript" src="../vendor/bootstrap/2.0.2/bootstrap.js"></script>
<script type="text/javascript" src="../vendor/leaflet/0.3.1/leaflet.js"></script>
<script type="text/javascript" src="qunit/qunit.js"></script>
<script src="sinon/1.1.1/sinon.js"></script>
<script src="sinon-qunit/1.0.0/sinon-qunit.js"></script>
<!-- Link to the built version of recline -->
<script type="text/javascript" src="../recline.js"></script>
<script type="text/javascript" src="base.js"></script>
<script type="text/javascript" src="model.test.js"></script>
<script type="text/javascript" src="backend.test.js"></script>
<script type="text/javascript" src="backend.elasticsearch.test.js"></script>
<script type="text/javascript" src="backend.localcsv.test.js"></script>
<script type="text/javascript" src="view-grid.test.js"></script>
<script type="text/javascript" src="view-graph.test.js"></script>
<script type="text/javascript" src="view-map.test.js"></script>
<script type="text/javascript" src="view.test.js"></script>
<script type="text/javascript" src="util.test.js"></script>
</head>
<body>
<h1 id="qunit-header">Qunit Tests</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div class="fixtures">
<table class="test-datatable">
</table>
<div class="data-explorer-here"></div>
</div>
</body>
</html>

View File

@@ -10,7 +10,6 @@
<script type="text/javascript" src="../vendor/jquery/1.7.1/jquery.js"></script> <script type="text/javascript" src="../vendor/jquery/1.7.1/jquery.js"></script>
<script type="text/javascript" src="../vendor/underscore/1.1.6/underscore.js"></script> <script type="text/javascript" src="../vendor/underscore/1.1.6/underscore.js"></script>
<script type="text/javascript" src="../vendor/backbone/0.5.1/backbone.js"></script> <script type="text/javascript" src="../vendor/backbone/0.5.1/backbone.js"></script>
<script type="text/javascript" src="../vendor/jquery-ui-1.8.14.custom.min.js"></script>
<script type="text/javascript" src="../vendor/jquery.flot/0.7/jquery.flot.js"></script> <script type="text/javascript" src="../vendor/jquery.flot/0.7/jquery.flot.js"></script>
<script type="text/javascript" src="../vendor/jquery.mustache.js"></script> <script type="text/javascript" src="../vendor/jquery.mustache.js"></script>
<script type="text/javascript" src="../vendor/bootstrap/2.0.2/bootstrap.js"></script> <script type="text/javascript" src="../vendor/bootstrap/2.0.2/bootstrap.js"></script>
@@ -22,6 +21,7 @@
<script type="text/javascript" src="base.js"></script> <script type="text/javascript" src="base.js"></script>
<script type="text/javascript" src="../src/util.js"></script>
<script type="text/javascript" src="../src/model.js"></script> <script type="text/javascript" src="../src/model.js"></script>
<script type="text/javascript" src="../src/backend/base.js"></script> <script type="text/javascript" src="../src/backend/base.js"></script>
<script type="text/javascript" src="../src/backend/memory.js"></script> <script type="text/javascript" src="../src/backend/memory.js"></script>
@@ -42,6 +42,7 @@
<script type="text/javascript" src="view-grid.test.js"></script> <script type="text/javascript" src="view-grid.test.js"></script>
<script type="text/javascript" src="view-graph.test.js"></script> <script type="text/javascript" src="view-graph.test.js"></script>
<script type="text/javascript" src="view-map.test.js"></script>
<script type="text/javascript" src="view.test.js"></script> <script type="text/javascript" src="view.test.js"></script>
<script type="text/javascript" src="util.test.js"></script> <script type="text/javascript" src="util.test.js"></script>
</head> </head>

View File

@@ -2,10 +2,10 @@
module("Util"); module("Util");
test('parseHashUrl', function () { test('parseHashUrl', function () {
var out = recline.View.parseHashUrl('graph?x=y'); var out = recline.Util.parseHashUrl('graph?x=y');
equal(out.path, 'graph'); equal(out.path, 'graph');
equal(out.query, '?x=y'); equal(out.query, '?x=y');
var out = recline.View.parseHashUrl('graph'); var out = recline.Util.parseHashUrl('graph');
equal(out.path, 'graph'); equal(out.path, 'graph');
equal(out.query, ''); equal(out.query, '');
}); });
@@ -15,7 +15,7 @@ test('composeQueryString', function () {
x: 'y', x: 'y',
a: 'b' a: 'b'
}; };
var out = recline.View.composeQueryString(params); var out = recline.Util.composeQueryString(params);
equal(out, '?x=y&a=b'); equal(out, '?x=y&a=b');
}); });

View File

@@ -11,3 +11,43 @@ test('basics', function () {
assertPresent('.editor', view.el); assertPresent('.editor', view.el);
view.remove(); view.remove();
}); });
test('initialize', function () {
var dataset = Fixture.getDataset();
var view = new recline.View.Graph({
model: dataset,
state: {
'graphType': 'lines',
'group': 'x',
'series': ['y', 'z']
}
});
$('.fixtures').append(view.el);
equal(view.state.get('graphType'), 'lines');
deepEqual(view.state.get('series'), ['y', 'z']);
// check we have updated editor with state info
equal(view.el.find('.editor-type select').val(), 'lines');
equal(view.el.find('.editor-group select').val(), 'x');
var out = _.map(view.el.find('.editor-series select'), function($el) {
return $($el).val();
});
deepEqual(out, ['y', 'z']);
view.remove();
});
test('dates in graph view', function () {
var dataset = Fixture.getDataset();
var view = new recline.View.Graph({
model: dataset,
state: {
'graphType': 'lines',
'group': 'date',
'series': ['y', 'z']
}
});
$('.fixtures').append(view.el);
view.remove();
});

132
test/view-map.test.js Normal file
View File

@@ -0,0 +1,132 @@
(function ($) {
module("View - Map");
var GeoJSONFixture = {
getDataset: function() {
var fields = [
{id: 'id'},
{id: 'x'},
{id: 'y'},
{id: 'z'},
{id: 'geom'}
];
var documents = [
{id: 0, x: 1, y: 2, z: 3, geom: '{"type":"Point","coordinates":[13.40,52.35]}'},
{id: 1, x: 2, y: 4, z: 6, geom: {type:"Point",coordinates:[13.40,52.35]}},
{id: 2, x: 3, y: 6, z: 9, geom: {type:"LineString",coordinates:[[100.0, 0.0],[101.0, 1.0]]}}
];
var dataset = recline.Backend.createDataset(documents, fields);
return dataset;
}
};
test('basics', function () {
var dataset = Fixture.getDataset();
var view = new recline.View.Map({
model: dataset
});
$('.fixtures').append(view.el);
//Fire query, otherwise the map won't be initialized
dataset.query();
assertPresent('.editor',view.el);
// Check that the Leaflet map was set up
assertPresent('.leaflet-container',view.el);
ok(view.map instanceof L.Map);
ok(view.features instanceof L.GeoJSON);
view.remove();
});
test('Lat/Lon geom fields', function () {
var dataset = Fixture.getDataset();
var view = new recline.View.Map({
model: dataset
});
$('.fixtures').append(view.el);
//Fire query, otherwise the map won't be initialized
dataset.query();
// Check that all markers were created
equal(_getFeaturesCount(view.features),6);
// Delete a document
view.model.currentDocuments.remove(view.model.currentDocuments.get('1'));
equal(_getFeaturesCount(view.features),5);
// Add a new one
view.model.currentDocuments.add({id: 7, x: 7, y: 14, z: 21, country: 'KX', label: 'seventh', lat:13.23, lon:23.56}),
equal(_getFeaturesCount(view.features),6);
view.remove();
});
test('GeoJSON geom field', function () {
var dataset = GeoJSONFixture.getDataset();
var view = new recline.View.Map({
model: dataset
});
$('.fixtures').append(view.el);
//Fire query, otherwise the map won't be initialized
dataset.query();
// Check that all features were created
equal(_getFeaturesCount(view.features),3);
// Delete a document
view.model.currentDocuments.remove(view.model.currentDocuments.get('2'));
equal(_getFeaturesCount(view.features),2);
// Add it back
view.model.currentDocuments.add({id: 2, x: 3, y: 6, z: 9, geom: {type:"LineString",coordinates:[[100.0, 0.0],[101.0, 1.0]]}}),
equal(_getFeaturesCount(view.features),3);
view.remove();
});
test('Popup', function () {
var dataset = GeoJSONFixture.getDataset();
var view = new recline.View.Map({
model: dataset
});
$('.fixtures').append(view.el);
//Fire query, otherwise the map won't be initialized
dataset.query();
var marker = view.el.find('.leaflet-marker-icon').first();
assertPresent(marker);
_.values(view.features._layers)[0].fire('click');
var popup = view.el.find('.leaflet-popup-content');
assertPresent(popup);
var text = popup.text();
ok((text.indexOf('geom') === -1))
_.each(view.model.fields.toJSON(),function(field){
if (field.id != 'geom'){
ok((text.indexOf(field.id) !== -1))
}
});
view.remove();
});
var _getFeaturesCount = function(features){
var cnt = 0;
features._iterateLayers(function(layer){
cnt++;
});
return cnt;
}
})(this.jQuery);

View File

@@ -1,100 +0,0 @@
/*!
* jQuery UI 1.8.14
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI
*/
(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.14",
keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus();
b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,
"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",
function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth,
outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a,"tabindex"),d=isNaN(b);
return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=
0;e<b.length;e++)a.options[b[e][0]]&&b[e][1].apply(a.element,d)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(a,b){if(c(a).css("overflow")==="hidden")return false;b=b&&b==="left"?"scrollLeft":"scrollTop";var d=false;if(a[b]>0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a<b+d},isOver:function(a,b,d,e,h,i){return c.ui.isOverAxis(a,d,h)&&c.ui.isOverAxis(b,e,i)}})}})(jQuery);
;/*!
* jQuery UI Widget 1.8.14
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Widget
*/
(function(b,j){if(b.cleanData){var k=b.cleanData;b.cleanData=function(a){for(var c=0,d;(d=a[c])!=null;c++)b(d).triggerHandler("remove");k(a)}}else{var l=b.fn.remove;b.fn.remove=function(a,c){return this.each(function(){if(!c)if(!a||b.filter(a,[this]).length)b("*",this).add([this]).each(function(){b(this).triggerHandler("remove")});return l.call(b(this),a,c)})}}b.widget=function(a,c,d){var e=a.split(".")[0],f;a=a.split(".")[1];f=e+"-"+a;if(!d){d=c;c=b.Widget}b.expr[":"][f]=function(h){return!!b.data(h,
a)};b[e]=b[e]||{};b[e][a]=function(h,g){arguments.length&&this._createWidget(h,g)};c=new c;c.options=b.extend(true,{},c.options);b[e][a].prototype=b.extend(true,c,{namespace:e,widgetName:a,widgetEventPrefix:b[e][a].prototype.widgetEventPrefix||a,widgetBaseClass:f},d);b.widget.bridge(a,b[e][a])};b.widget.bridge=function(a,c){b.fn[a]=function(d){var e=typeof d==="string",f=Array.prototype.slice.call(arguments,1),h=this;d=!e&&f.length?b.extend.apply(null,[true,d].concat(f)):d;if(e&&d.charAt(0)==="_")return h;
e?this.each(function(){var g=b.data(this,a),i=g&&b.isFunction(g[d])?g[d].apply(g,f):g;if(i!==g&&i!==j){h=i;return false}}):this.each(function(){var g=b.data(this,a);g?g.option(d||{})._init():b.data(this,a,new c(d,this))});return h}};b.Widget=function(a,c){arguments.length&&this._createWidget(a,c)};b.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:false},_createWidget:function(a,c){b.data(c,this.widgetName,this);this.element=b(c);this.options=b.extend(true,{},this.options,
this._getCreateOptions(),a);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()});this._create();this._trigger("create");this._init()},_getCreateOptions:function(){return b.metadata&&b.metadata.get(this.element[0])[this.widgetName]},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled ui-state-disabled")},
widget:function(){return this.element},option:function(a,c){var d=a;if(arguments.length===0)return b.extend({},this.options);if(typeof a==="string"){if(c===j)return this.options[a];d={};d[a]=c}this._setOptions(d);return this},_setOptions:function(a){var c=this;b.each(a,function(d,e){c._setOption(d,e)});return this},_setOption:function(a,c){this.options[a]=c;if(a==="disabled")this.widget()[c?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",c);return this},
enable:function(){return this._setOption("disabled",false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(a,c,d){var e=this.options[a];c=b.Event(c);c.type=(a===this.widgetEventPrefix?a:this.widgetEventPrefix+a).toLowerCase();d=d||{};if(c.originalEvent){a=b.event.props.length;for(var f;a;){f=b.event.props[--a];c[f]=c.originalEvent[f]}}this.element.trigger(c,d);return!(b.isFunction(e)&&e.call(this.element[0],c,d)===false||c.isDefaultPrevented())}}})(jQuery);
;/*!
* jQuery UI Mouse 1.8.14
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Mouse
*
* Depends:
* jquery.ui.widget.js
*/
(function(b){var d=false;b(document).mousedown(function(){d=false});b.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var a=this;this.element.bind("mousedown."+this.widgetName,function(c){return a._mouseDown(c)}).bind("click."+this.widgetName,function(c){if(true===b.data(c.target,a.widgetName+".preventClickEvent")){b.removeData(c.target,a.widgetName+".preventClickEvent");c.stopImmediatePropagation();return false}});this.started=false},_mouseDestroy:function(){this.element.unbind("."+
this.widgetName)},_mouseDown:function(a){if(!d){this._mouseStarted&&this._mouseUp(a);this._mouseDownEvent=a;var c=this,f=a.which==1,g=typeof this.options.cancel=="string"?b(a.target).closest(this.options.cancel).length:false;if(!f||g||!this._mouseCapture(a))return true;this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet)this._mouseDelayTimer=setTimeout(function(){c.mouseDelayMet=true},this.options.delay);if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a)){this._mouseStarted=this._mouseStart(a)!==
false;if(!this._mouseStarted){a.preventDefault();return true}}true===b.data(a.target,this.widgetName+".preventClickEvent")&&b.removeData(a.target,this.widgetName+".preventClickEvent");this._mouseMoveDelegate=function(e){return c._mouseMove(e)};this._mouseUpDelegate=function(e){return c._mouseUp(e)};b(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);a.preventDefault();return d=true}},_mouseMove:function(a){if(b.browser.msie&&
!(document.documentMode>=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=
false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery);
;/*
* jQuery UI Draggable 1.8.14
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Draggables
*
* Depends:
* jquery.ui.core.js
* jquery.ui.mouse.js
* jquery.ui.widget.js
*/
(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper==
"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b=
this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;d(b.iframeFix===true?"iframe":b.iframeFix).each(function(){d('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")});return true},_mouseStart:function(a){var b=this.options;this.helper=
this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});
this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions();d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);d.ui.ddmanager&&d.ui.ddmanager.dragStart(this,a);return true},
_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b=
false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if((!this.element[0]||!this.element[0].parentNode)&&this.options.helper=="original")return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element,b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,
10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},_mouseUp:function(a){this.options.iframeFix===true&&d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)});d.ui.ddmanager&&d.ui.ddmanager.dragStop(this,a);return d.ui.mouse.prototype._mouseUp.call(this,a)},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle||
!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone().removeAttr("id"):this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&&
a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=
this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),
10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),
10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[a.containment=="document"?0:d(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,a.containment=="document"?0:d(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,
(a.containment=="document"?0:d(window).scrollLeft())+d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a.containment=="document"?0:d(window).scrollTop())+(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&&a.containment.constructor!=Array){a=d(a.containment);var b=a[0];if(b){a.offset();var c=d(b).css("overflow")!=
"hidden";this.containment=[(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0),(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0),(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),
10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom];this.relative_container=a}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+
this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&&
!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,h=a.pageY;if(this.originalPosition){var g;if(this.containment){if(this.relative_container){g=this.relative_container.offset();g=[this.containment[0]+g.left,this.containment[1]+g.top,this.containment[2]+g.left,this.containment[3]+g.top]}else g=this.containment;if(a.pageX-this.offset.click.left<g[0])e=g[0]+this.offset.click.left;
if(a.pageY-this.offset.click.top<g[1])h=g[1]+this.offset.click.top;if(a.pageX-this.offset.click.left>g[2])e=g[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>g[3])h=g[3]+this.offset.click.top}if(b.grid){h=b.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/b.grid[1])*b.grid[1]:this.originalPageY;h=g?!(h-this.offset.click.top<g[1]||h-this.offset.click.top>g[3])?h:!(h-this.offset.click.top<g[1])?h-b.grid[1]:h+b.grid[1]:h;e=b.grid[0]?this.originalPageX+Math.round((e-this.originalPageX)/
b.grid[0])*b.grid[0]:this.originalPageX;e=g?!(e-this.offset.click.left<g[0]||e-this.offset.click.left>g[2])?e:!(e-this.offset.click.left<g[0])?e-b.grid[0]:e+b.grid[0]:e}}return{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop()),left:e-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(d.browser.safari&&d.browser.version<
526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging");this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove();this.helper=null;this.cancelHelperRemoval=false},_trigger:function(a,b,c){c=c||this._uiHash();d.ui.plugin.call(this,a,[b,c]);if(a=="drag")this.positionAbs=this._convertPositionTo("absolute");return d.Widget.prototype._trigger.call(this,a,b,
c)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}});d.extend(d.ui.draggable,{version:"1.8.14"});d.ui.plugin.add("draggable","connectToSortable",{start:function(a,b){var c=d(this).data("draggable"),f=c.options,e=d.extend({},b,{item:c.element});c.sortables=[];d(f.connectToSortable).each(function(){var h=d.data(this,"sortable");if(h&&!h.options.disabled){c.sortables.push({instance:h,shouldRevert:h.options.revert});
h.refreshPositions();h._trigger("activate",a,e)}})},stop:function(a,b){var c=d(this).data("draggable"),f=d.extend({},b,{item:c.element});d.each(c.sortables,function(){if(this.instance.isOver){this.instance.isOver=0;c.cancelHelperRemoval=true;this.instance.cancelHelperRemoval=false;if(this.shouldRevert)this.instance.options.revert=true;this.instance._mouseStop(a);this.instance.options.helper=this.instance.options._helper;c.options.helper=="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})}else{this.instance.cancelHelperRemoval=
false;this.instance._trigger("deactivate",a,f)}})},drag:function(a,b){var c=d(this).data("draggable"),f=this;d.each(c.sortables,function(){this.instance.positionAbs=c.positionAbs;this.instance.helperProportions=c.helperProportions;this.instance.offset.click=c.offset.click;if(this.instance._intersectsWith(this.instance.containerCache)){if(!this.instance.isOver){this.instance.isOver=1;this.instance.currentItem=d(f).clone().removeAttr("id").appendTo(this.instance.element).data("sortable-item",true);
this.instance.options._helper=this.instance.options.helper;this.instance.options.helper=function(){return b.helper[0]};a.target=this.instance.currentItem[0];this.instance._mouseCapture(a,true);this.instance._mouseStart(a,true,true);this.instance.offset.click.top=c.offset.click.top;this.instance.offset.click.left=c.offset.click.left;this.instance.offset.parent.left-=c.offset.parent.left-this.instance.offset.parent.left;this.instance.offset.parent.top-=c.offset.parent.top-this.instance.offset.parent.top;
c._trigger("toSortable",a);c.dropped=this.instance.element;c.currentItem=c.element;this.instance.fromOutside=c}this.instance.currentItem&&this.instance._mouseDrag(a)}else if(this.instance.isOver){this.instance.isOver=0;this.instance.cancelHelperRemoval=true;this.instance.options.revert=false;this.instance._trigger("out",a,this.instance._uiHash(this.instance));this.instance._mouseStop(a,true);this.instance.options.helper=this.instance.options._helper;this.instance.currentItem.remove();this.instance.placeholder&&
this.instance.placeholder.remove();c._trigger("fromSortable",a);c.dropped=false}})}});d.ui.plugin.add("draggable","cursor",{start:function(){var a=d("body"),b=d(this).data("draggable").options;if(a.css("cursor"))b._cursor=a.css("cursor");a.css("cursor",b.cursor)},stop:function(){var a=d(this).data("draggable").options;a._cursor&&d("body").css("cursor",a._cursor)}});d.ui.plugin.add("draggable","opacity",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options;if(a.css("opacity"))b._opacity=
a.css("opacity");a.css("opacity",b.opacity)},stop:function(a,b){a=d(this).data("draggable").options;a._opacity&&d(b.helper).css("opacity",a._opacity)}});d.ui.plugin.add("draggable","scroll",{start:function(){var a=d(this).data("draggable");if(a.scrollParent[0]!=document&&a.scrollParent[0].tagName!="HTML")a.overflowOffset=a.scrollParent.offset()},drag:function(a){var b=d(this).data("draggable"),c=b.options,f=false;if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!="HTML"){if(!c.axis||c.axis!=
"x")if(b.overflowOffset.top+b.scrollParent[0].offsetHeight-a.pageY<c.scrollSensitivity)b.scrollParent[0].scrollTop=f=b.scrollParent[0].scrollTop+c.scrollSpeed;else if(a.pageY-b.overflowOffset.top<c.scrollSensitivity)b.scrollParent[0].scrollTop=f=b.scrollParent[0].scrollTop-c.scrollSpeed;if(!c.axis||c.axis!="y")if(b.overflowOffset.left+b.scrollParent[0].offsetWidth-a.pageX<c.scrollSensitivity)b.scrollParent[0].scrollLeft=f=b.scrollParent[0].scrollLeft+c.scrollSpeed;else if(a.pageX-b.overflowOffset.left<
c.scrollSensitivity)b.scrollParent[0].scrollLeft=f=b.scrollParent[0].scrollLeft-c.scrollSpeed}else{if(!c.axis||c.axis!="x")if(a.pageY-d(document).scrollTop()<c.scrollSensitivity)f=d(document).scrollTop(d(document).scrollTop()-c.scrollSpeed);else if(d(window).height()-(a.pageY-d(document).scrollTop())<c.scrollSensitivity)f=d(document).scrollTop(d(document).scrollTop()+c.scrollSpeed);if(!c.axis||c.axis!="y")if(a.pageX-d(document).scrollLeft()<c.scrollSensitivity)f=d(document).scrollLeft(d(document).scrollLeft()-
c.scrollSpeed);else if(d(window).width()-(a.pageX-d(document).scrollLeft())<c.scrollSensitivity)f=d(document).scrollLeft(d(document).scrollLeft()+c.scrollSpeed)}f!==false&&d.ui.ddmanager&&!c.dropBehaviour&&d.ui.ddmanager.prepareOffsets(b,a)}});d.ui.plugin.add("draggable","snap",{start:function(){var a=d(this).data("draggable"),b=a.options;a.snapElements=[];d(b.snap.constructor!=String?b.snap.items||":data(draggable)":b.snap).each(function(){var c=d(this),f=c.offset();this!=a.element[0]&&a.snapElements.push({item:this,
width:c.outerWidth(),height:c.outerHeight(),top:f.top,left:f.left})})},drag:function(a,b){for(var c=d(this).data("draggable"),f=c.options,e=f.snapTolerance,h=b.offset.left,g=h+c.helperProportions.width,n=b.offset.top,o=n+c.helperProportions.height,i=c.snapElements.length-1;i>=0;i--){var j=c.snapElements[i].left,l=j+c.snapElements[i].width,k=c.snapElements[i].top,m=k+c.snapElements[i].height;if(j-e<h&&h<l+e&&k-e<n&&n<m+e||j-e<h&&h<l+e&&k-e<o&&o<m+e||j-e<g&&g<l+e&&k-e<n&&n<m+e||j-e<g&&g<l+e&&k-e<o&&
o<m+e){if(f.snapMode!="inner"){var p=Math.abs(k-o)<=e,q=Math.abs(m-n)<=e,r=Math.abs(j-g)<=e,s=Math.abs(l-h)<=e;if(p)b.position.top=c._convertPositionTo("relative",{top:k-c.helperProportions.height,left:0}).top-c.margins.top;if(q)b.position.top=c._convertPositionTo("relative",{top:m,left:0}).top-c.margins.top;if(r)b.position.left=c._convertPositionTo("relative",{top:0,left:j-c.helperProportions.width}).left-c.margins.left;if(s)b.position.left=c._convertPositionTo("relative",{top:0,left:l}).left-c.margins.left}var t=
p||q||r||s;if(f.snapMode!="outer"){p=Math.abs(k-n)<=e;q=Math.abs(m-o)<=e;r=Math.abs(j-h)<=e;s=Math.abs(l-g)<=e;if(p)b.position.top=c._convertPositionTo("relative",{top:k,left:0}).top-c.margins.top;if(q)b.position.top=c._convertPositionTo("relative",{top:m-c.helperProportions.height,left:0}).top-c.margins.top;if(r)b.position.left=c._convertPositionTo("relative",{top:0,left:j}).left-c.margins.left;if(s)b.position.left=c._convertPositionTo("relative",{top:0,left:l-c.helperProportions.width}).left-c.margins.left}if(!c.snapElements[i].snapping&&
(p||q||r||s||t))c.options.snap.snap&&c.options.snap.snap.call(c.element,a,d.extend(c._uiHash(),{snapItem:c.snapElements[i].item}));c.snapElements[i].snapping=p||q||r||s||t}else{c.snapElements[i].snapping&&c.options.snap.release&&c.options.snap.release.call(c.element,a,d.extend(c._uiHash(),{snapItem:c.snapElements[i].item}));c.snapElements[i].snapping=false}}}});d.ui.plugin.add("draggable","stack",{start:function(){var a=d(this).data("draggable").options;a=d.makeArray(d(a.stack)).sort(function(c,f){return(parseInt(d(c).css("zIndex"),
10)||0)-(parseInt(d(f).css("zIndex"),10)||0)});if(a.length){var b=parseInt(a[0].style.zIndex)||0;d(a).each(function(c){this.style.zIndex=b+c});this[0].style.zIndex=b+a.length}}});d.ui.plugin.add("draggable","zIndex",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options;if(a.css("zIndex"))b._zIndex=a.css("zIndex");a.css("zIndex",b.zIndex)},stop:function(a,b){a=d(this).data("draggable").options;a._zIndex&&d(b.helper).css("zIndex",a._zIndex)}})})(jQuery);
;