Merge branch 'master' into gh-pages

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

View File

@ -2,7 +2,7 @@
<html>
<head>
<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="author" content="Rufus Pollock and Max Ogden">
@ -11,46 +11,160 @@
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<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/grid.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">
<script type="text/javascript" src="../vendor/jquery-1.7.1.js"></script>
<script type="text/javascript" src="../vendor/underscore-1.1.6.js"></script>
<script type="text/javascript" src="../vendor/backbone-0.5.1.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.js"></script>
<link rel="stylesheet" href="../vendor/bootstrap/2.0.2/css/bootstrap-responsive.css">
<!-- 3rd party JS libraries -->
<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.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>
<!-- recline library -->
<script type="text/javascript" src="../recline.js"></script>
<!-- non-library javascript specific to this demo -->
<script type="text/javascript" src="js/app.js"></script>
</head>
</head>
<body>
<div class="recline-app">
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="brand" href="#">Recline Data Explorer</a>
<ul class="nav pull-right">
<li><a class="set-read-only" title="Put into read-only mode">Read-only</a></li>
<a class="brand" href="../">Recline Data Explorer</a>
<ul class="nav">
<li><a href="../#docs">Documentation</a></li>
</ul>
<ul class="nav pull-right">
<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>
<form class="webstore-load pull-right navbar-search" title="Update from the specified webstore dataset">
<input type="text" name="source" size="50" />
<select name="backend_type">
<option value="elasticsearch">ElasticSearch</option>
<option value="dataproxy">DataProxy</option>
<option value="gdocs">Google Spreadsheets</option>
</select>
</form>
</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">
<option value="elasticsearch">ElasticSearch</option>
<option value="dataproxy">CSV or Excel</option>
<option value="gdocs">Google Spreadsheet</option>
</select>
</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-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 class="container-fluid">
<div class="content">
<div class="data-explorer-here"></div>
</div>
</div>
</div>
</body>
</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/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>
@ -58,6 +57,8 @@
<!-- non-library javascript specific to this demo -->
<script type="text/javascript" src="js/app.js"></script>
<!-- for demo dataset -->
<script type="text/javascript" src="../test/base.js"></script>
</head>
<body>
<div class="recline-app">
@ -122,6 +123,50 @@
</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-header">
<a class="close" data-dismiss="modal">×</a>
@ -141,6 +186,13 @@
<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">
@ -166,12 +218,6 @@
<textarea class="view-embed" style="width: 100%; height: 200px;"></textarea>
</div>
</div>
<div class="container-fluid">
<div class="content">
<div class="data-explorer-here"></div>
</div>
</div>
</div>
</body>
</html>

View File

@ -2,7 +2,6 @@ jQuery(function($) {
var app = new ExplorerApp({
el: $('.recline-app')
})
Backbone.history.start();
});
var ExplorerApp = Backbone.View.extend({
@ -15,8 +14,14 @@ var ExplorerApp = Backbone.View.extend({
this.el = $(this.el);
this.explorer = null;
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) {
_.each(state, function(value, key) {
try {
@ -30,14 +35,34 @@ var ExplorerApp = Backbone.View.extend({
}
}
var dataset = null;
if (state.dataset || state.url) {
dataset = recline.Model.Dataset.restore(state);
} else {
// special cases for demo / memory dataset
if (state.url === 'demo' || state.backend === 'memory') {
dataset = localDataset();
}
this.createExplorer(dataset, state);
else if (state.dataset || state.url) {
dataset = recline.Model.Dataset.restore(state);
}
if (dataset) {
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
// again and again
createExplorer: function(dataset, state) {
@ -59,12 +84,7 @@ var ExplorerApp = Backbone.View.extend({
this._setupPermaLink(this.dataExplorer);
this._setupEmbed(this.dataExplorer);
// HACK (a bit). Issue is that Backbone will not trigger the route
// 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);
}
this.viewExplorer();
},
_setupPermaLink: function(explorer) {
@ -92,7 +112,7 @@ var ExplorerApp = Backbone.View.extend({
},
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;
},
@ -128,6 +148,7 @@ var ExplorerApp = Backbone.View.extend({
var file = $file.files[0];
var options = {
separator : $form.find('input[name="separator"]').val(),
delimiter : $form.find('input[name="delimiter"]').val(),
encoding : $form.find('input[name="encoding"]').val()
};
recline.Backend.loadFromCSVFile(file, function(dataset) {
@ -140,26 +161,7 @@ var ExplorerApp = Backbone.View.extend({
// provide a demonstration in memory dataset
function localDataset() {
var datasetId = 'test-dataset';
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);
var dataset = Fixture.getDataset();
dataset.queryState.addFacet('country');
return dataset;
}

View File

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

View File

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

@ -18,6 +18,11 @@
}
.recline-map .editor select {
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,325 +10,356 @@
<![endif]-->
<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 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;
}
<link href="css/site.css" rel="stylesheet" type="text/css" />
.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>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="#">Recline Data Explorer and Library</a>
<ul class="nav">
<a class="brand" href="#"><strong>Recline</strong> Data Explorer and Library</a>
<ul class="nav pull-right">
<li><a href="app/">Data Explorer</a></li>
<li><a href="#docs">Docs</a></li>
<li><a href="http://github.com/okfn/recline/">Code on GitHub</a></li>
</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 class="container">
<div class="row"><div class="span9"><div class="content">
<div class="page-header">
<h1>
Recline Data Explorer and Library<br />
<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>
</div>
<section class="page-header">
<div class="container">
<div class="row">
<div class="span8 offset4">
<h1>
<img src="images/logo.png" width="455" height="190" alt="Recline Data Explorer and Library">
</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 class="inner">
<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>
<li>A Data Explorer combining a data grid, Google Refine-style data
transforms and visualizations all in lightweight javascript and html.</li>
<li>A simple but powerful library of extensible of data components - data
grid, graphing, and data connectors - which you can selectively use and build
on.</li>
</ul>
</div>
<div class="span6">
<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
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>
</div>
</div>
<div class="row">
<div class="span4">
<h2 id="features">Main Features</h2>
<ul>
<li>View and edit your data in a clean grid / table interface</li>
<li>Bulk update/clean your data using an easy scripting UI</li>
<li>Easily extensible with new Backends so you can connect to your
database or storage layer</li>
<li>Visualize data</li>
<li>Open-source, pure javascript and designed for integration -- so it is
easy to embed in other sites and applications</li>
<li>Built on the simple but powerful <a
href="http://documentcloud.github.com/backbone/">Backbone</a> giving a
clean and robust design which is easy to extend</li>
<li>Properly designed model with clean separation of data and presentation</li>
<li>Componentized design means you use only what you need</li>
</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>
<p>Recline is two things:</p>
<section id="docs">
<div class="container">
<div class="row">
<div class="span12">
<h2>Data Explorer Documentation</h2>
</div>
</div>
<div class="row">
<div class="span6">
<p>Usage instructions are built into the <a href="app/">Data Explorer</a>
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
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>
</div>
</div>
<div class="row">
<div class="span12">
<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
javascript in: <a href="app/js/app.js">app.js</a>.</p>
</div>
</div>
<div class="row">
<div class="span12">
<h2>Library Documentation</h2>
<h3 id="docs-using">Examples</h3>
<p><strong>Note:</strong> A quick read through of the Concepts section will
likely be useful in understanding the details of the examples.</p>
<p><strong>Note</strong>: for all the following examples you should have
included relevant Recline dependencies.</p>
<h4>Simple in-memory dataset.</h4>
<pre>
// Some data you have
// Your data must be in the form of list of documents / rows
// Each document/row is an Object with keys and values
var data = [
{id: 0, x: 1, y: 2, z: 3, country: 'UK', label: 'first'}
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
];
// Create a Dataset object from local in-memory data
// Dataset object is a Backbone model - more info on attributes in model docs below
var dataset = recline.Backend.createDataset(data);
// Now create the main explorer view (it will create other views as needed)
// DataExplorer is a Backbone View
var explorer = recline.View.DataExplorer({
model: dataset,
// you can specify any element to bind to in the dom
el: $('.data-explorer-here')
});
// Start Backbone routing (if you want routing support)
Backbone.history.start();
</pre>
<h4>Creating a Dataset Explicitly with a Backend</h4>
<pre>
// Connect to ElasticSearch index/type as our data source
// There are many other backends you can use (and you can write your own)
var backend = new recline.Backend.ElasticSearch();
// Dataset is a Backbone model so the first hash become model attributes
var dataset = recline.Model.Dataset({
id: 'my-id',
// url for source of this dataset - will be used by backend
url: 'http://localhost:9200/my-index/my-type',
// any other metadata e.g.
title: 'My Dataset Title'
},
backend
);
</pre>
</div>
</div>
<div class="row">
<div class="span12">
<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
distinction inherent in Backbone.</p>
<h4>Models</h4>
<p>There are two main model objects:</p>
<ul>
<li><a href="docs/model.html#dataset">Dataset</a>: represents the dataset.
Holds dataset info and a pointer to list of data items (Documents in our
terminology) which it can load from the relevant Backend.</li>
<li><a href="docs/model.html#document">Document</a>: an individual data item
(e.g. a row from a relational database or a spreadsheet, a document from from
a document DB like CouchDB or MongoDB).</li>
</ul>
<p>Additional, related models:</p>
<ul>
<li><a href="docs/model.html#field">Field</a>: a field/column on a
dataset.</li>
<li><a href="docs/model.html#query">Query</a>: an object to encapsulate a
query to the backend (useful both for creating queries and for storing and
manipulating query state - e.g. from a query editor).</li>
<li><a href="docs/model.html#facte">Facet</a>: Object to store Facet
information, that is summary information (e.g. values and counts) about a
field obtained by some faceting method on the backend.</li>
</ul>
<p>More detail of how these work can be found in the <a
href="docs/model.html">Model source docs</a>.</p>
</div>
<ul>
<li>A Data Explorer combining a data grid, Google Refine-style data
transforms and visualizations all in lightweight javascript and html.</li>
<li>A simple but powerful library of extensible of data components - data
grid, graphing, and data connectors - which you can selectively use and build
on.</li>
</ul>
<div class="span6">
<h4>Backends</h4>
<p>Backends connect Dataset and Documents to data from a
specific 'Backend' data source. They provide methods for loading and saving
Datasets and individuals Documents as well as for bulk loading via a query API
and doing bulk transforms on the backend.</p>
<p>A template Base class can be found <a href="docs/backend/base.html">in the
Backend base module of the source docs</a>. It documents both the relevant
methods a Backend must have and (optionally) provides a base 'class' for
inheritance. You can also find detailed examples of backend implementations in
the source documentation below.</p>
<h4>Views</h4>
<p>Complementing the model are various Views (you can
also easily write your own). Each view holds a pointer to a Dataset:</p>
<ul>
<li>DataExplorer: the parent view which manages the overall app and sets up
sub views.</li>
<li>Grid: the data grid view.</li>
<li>Graph: a simple graphing view using <a
href="http://code.google.com/p/flot/">Flot</a>.</li>
<li>Map: a map view using <a href="http://leaflet.cloudmade.com/">Leaflet</a>.</li>
</ul>
<p>There are additional views which do not display a whole dataset but which
are useful:</p>
<ul>
<li>QueryEditor: a query editor view</li>
<li>FacetViewer: display facets</li>
</ul>
</div>
</div>
<div class="row">
<div class="span12">
<h3 id="docs-source">Source Docs (via Docco)</h3>
</div>
</div>
<div class="row">
<div class="span6">
<h4>Models and Views (Widgets)</h4>
<ul>
<li><a href="docs/model.html">Models</a></li>
<li><a href="docs/view.html">DataExplorer View (plus common view code)</a></li>
<li><a href="docs/view-grid.html">(Data) Grid View</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>
</ul>
</div>
<div class="span6">
<h4>Backends</h4>
<ul>
<li><a href="docs/backend/base.html">Backend: Base (base class providing a template for backends)</a></li>
<li><a href="docs/backend/memory.html">Backend: Memory (local data)</a></li>
<li><a href="docs/backend/elasticsearch.html">Backend: ElasticSearch</a></li>
<li><a href="docs/backend/dataproxy.html">Backend: DataProxy (CSV and XLS on the Web)</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>
</ul>
</div>
</div>
<div class="row">
<div class="span6">
<h2 id="tests">Tests</h2>
<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>
</div>
</div>
<div class="row">
<div class="span6">
<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.
Meanwhile, Rufus Pollock and the <a href="http://ckan.org/">CKAN team</a> at
the <a href="http://okfn.org/">Open Knowledge Foundation</a> had been working
on a <a href="http://github.com/okfn/dataexplorer">Data Explorer</a> for use in
<a href="http://thedatahub.org">the DataHub</a> and <a
href="http://ckan.org/">CKAN software</a>.</p>
<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
Explorer.</p>
</div>
<div class="span6">
<p>The new project forked off <a
href="https://github.com/maxogden/recline">Max's original recline
codebase</a> combining some portions of the <a
href="http://github.com/okfn/dataexplorer">original Data Explorer</a>.
However, it has been rewritten from the ground up using Backbone.</p>
</div>
</div>
</div>
</section>
<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
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>
<section class="footer">
<div class="container">
<div class="row">
<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>
<h5>Get the Library</h5>
<p class="getit-btn"><a href="recline.js" class="btn primary">Development Version<br />v0.3 (67k)</a></p>
</div>
<div class="span3">
<h5>Dependencies</h5>
<ul class="deps">
<li>JQuery &gt;= 1.6</li>
<li><a href="http://backbonejs.org/">Backbone</a> &gt;= 0.5.1</li>
<li>Underscore &gt;= 1.0</li>
<li>JQuery Mustache</li>
<li><a href="http://code.google.com/p/flot/">JQuery Flot &gt;= 0.7</a>: (Optional) for graphing</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>
</ul>
</div>
<div class="span3">
<h5>Documentation</h5>
<ul>
<li><a href="#docs-using">Using it</a></li>
<li><a href="#docs-concepts">Concepts and Structure</a></li>
<li><a href="#docs-source">Source Docs (Docco)</a></li>
</ul>
</div>
<div class="span3">
<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>
<h2 id="features">Main Features</h2>
<ul>
<li>View and edit your data in a clean grid / table interface</li>
<li>Bulk update/clean your data using an easy scripting UI</li>
<li>Easily extensible with new Backends so you can connect to your
database or storage layer</li>
<li>Visualize data</li>
<li>Open-source, pure javascript and designed for integration -- so it is
easy to embed in other sites and applications</li>
<li>Built on the simple but powerful <a
href="http://documentcloud.github.com/backbone/">Backbone</a> giving a
clean and robust design which is easy to extend</li>
<li>Properly designed model with clean separation of data and presentation</li>
<li>Componentized design means you use only what you need</li>
</ul>
<h2>Screenshots</h2>
<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>
<h2 id="demo">Demo</h2>
<p><a href="app/index.html" class="btn">For demo see the Data Explorer &raquo;</a></p>
<h2 id="docs">Data Explorer Documentation</h2>
<p>Usage instructions are built into the <a href="app/">Data Explorer</a>
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
your web page:</p>
<textarea class="span6">&lt;iframe src="http://okfnlabs.org/recline/app/" width="100%"&gt;&lt;/iframe&gt;</textarea>
<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
javascript in: <a href="app/js/app.js">app.js</a>.</p>
<h2 id="docs">Library Documentation</h2>
<h3 id="docs-using">Examples</h3>
<p><strong>Note:</strong> A quick read through of the Concepts section will
likely be useful in understanding the details of the examples.</p>
<p><strong>Note</strong>: for all the following examples you should have
included relevant Recline dependencies.</p>
<h4>Simple in-memory dataset.</h4>
<pre>
// Some data you have
// Your data must be in the form of list of documents / rows
// Each document/row is an Object with keys and values
var data = [
{id: 0, x: 1, y: 2, z: 3, country: 'UK', label: 'first'}
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
];
// Create a Dataset object from local in-memory data
// Dataset object is a Backbone model - more info on attributes in model docs below
var dataset = recline.Backend.createDataset(data);
// Now create the main explorer view (it will create other views as needed)
// DataExplorer is a Backbone View
var explorer = recline.View.DataExplorer({
model: dataset,
// you can specify any element to bind to in the dom
el: $('.data-explorer-here')
});
// Start Backbone routing (if you want routing support)
Backbone.history.start();
</pre>
<h4>Creating a Dataset Explicitly with a Backend</h4>
<pre>
// Connect to ElasticSearch index/type as our data source
// There are many other backends you can use (and you can write your own)
var backend = new recline.Backend.ElasticSearch();
// Dataset is a Backbone model so the first hash become model attributes
var dataset = recline.Model.Dataset({
id: 'my-id',
// url for source of this dataset - will be used by backend
url: 'http://localhost:9200/my-index/my-type',
// any other metadata e.g.
title: 'My Dataset Title'
},
backend
);
</pre>
<h3 id="docs-concepts">Concepts and Structure</h3>
<p>Recline has a simple structure layered on top of the basic Model/View
distinction inherent in Backbone.</p>
<h4>Models</h4>
<p>There are two main model objects:</p>
<ul>
<li><a href="docs/model.html#dataset">Dataset</a>: represents the dataset.
Holds dataset info and a pointer to list of data items (Documents in our
terminology) which it can load from the relevant Backend.</li>
<li><a href="docs/model.html#document">Document</a>: an individual data item
(e.g. a row from a relational database or a spreadsheet, a document from from
a document DB like CouchDB or MongoDB).</li>
</ul>
<p>Additional, related models:</p>
<ul>
<li><a href="docs/model.html#field">Field</a>: a field/column on a
dataset.</li>
<li><a href="docs/model.html#query">Query</a>: an object to encapsulate a
query to the backend (useful both for creating queries and for storing and
manipulating query state - e.g. from a query editor).</li>
<li><a href="docs/model.html#facte">Facet</a>: Object to store Facet
information, that is summary information (e.g. values and counts) about a
field obtained by some faceting method on the backend.</li>
</ul>
<p>More detail of how these work can be found in the <a
href="docs/model.html">Model source docs</a>.</p>
<h4>Backends</h4>
<p>Backends connect Dataset and Documents to data from a
specific 'Backend' data source. They provide methods for loading and saving
Datasets and individuals Documents as well as for bulk loading via a query API
and doing bulk transforms on the backend.</p>
<p>A template Base class can be found <a href="docs/backend/base.html">in the
Backend base module of the source docs</a>. It documents both the relevant
methods a Backend must have and (optionally) provides a base 'class' for
inheritance. You can also find detailed examples of backend implementations in
the source documentation below.</p>
<h4>Views</h4>
<p>Complementing the model are various Views (you can
also easily write your own). Each view holds a pointer to a Dataset:</p>
<ul>
<li>DataExplorer: the parent view which manages the overall app and sets up
sub views.</li>
<li>Grid: the data grid view.</li>
<li>Graph: a simple graphing view using <a
href="http://code.google.com/p/flot/">Flot</a>.</li>
<li>Map: a map view using <a href="http://leaflet.cloudmade.com/">Leaflet</a>.</li>
</ul>
<p>There are additional views which do not display a whole dataset but which
are useful:</p>
<ul>
<li>QueryEditor: a query editor view</li>
<li>FacetViewer: display facets</li>
</ul>
<h3 id="docs-source">Source Docs (via Docco)</h3>
<h4>Models and Views (Widgets)</h4>
<ul>
<li><a href="docs/model.html">Models</a></li>
<li><a href="docs/view.html">DataExplorer View (plus common view code)</a></li>
<li><a href="docs/view-grid.html">(Data) Grid View</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>
</ul>
<h4>Backends</h4>
<ul>
<li><a href="docs/backend/base.html">Backend: Base (base class providing a template for backends)</a></li>
<li><a href="docs/backend/memory.html">Backend: Memory (local data)</a></li>
<li><a href="docs/backend/elasticsearch.html">Backend: ElasticSearch</a></li>
<li><a href="docs/backend/dataproxy.html">Backend: DataProxy (CSV and XLS on the Web)</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>
</ul>
<h2 id="tests">Tests</h2>
<p><a href="test/index.html">Run the tests online</a>.</p>
<h2 id="history">History</h2>
<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.
Meanwhile, Rufus Pollock and the <a href="http://ckan.org/">CKAN team</a> at
the <a href="http://okfn.org/">Open Knowledge Foundation</a> had been working
on a <a href="http://github.com/okfn/dataexplorer">Data Explorer</a> for use in
<a href="http://thedatahub.org">the DataHub</a> and <a
href="http://ckan.org/">CKAN software</a>.</p>
<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
Explorer.</p>
<p>The new project forked off <a
href="https://github.com/maxogden/recline">Max's original recline
codebase</a> combining some portions of the <a
href="http://github.com/okfn/dataexplorer">original Data Explorer</a>.
However, it has been rewritten from the ground up using Backbone.</p>
</div></div> <!-- /span9 /content -->
<div class="span3 sidebar">
<div class="well sidebar-nav">
<h3 class="nav-header">Use the Explorer</h3>
<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>
<p class="getit-btn"><a href="recline.js" class="btn primary">Development Version<br />v0.3 (67k)</a></p>
<h4>Dependencies</h4>
<ul class="deps">
<li>JQuery &gt;= 1.6</li>
<li><a href="http://backbonejs.org/">Backbone</a> &gt;= 0.5.1</li>
<li>Underscore &gt;= 1.0</li>
<li>JQuery Mustache</li>
<li><a href="http://code.google.com/p/flot/">JQuery Flot &gt;= 0.7</a>: (Optional) for graphing</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>
</ul>
<h3 class="nav-header">Documentation</h3>
<ul class="nav nav-list">
<li><a href="#docs-using">Using it</a></li>
<li><a href="#docs-concepts">Concepts and Structure</a></li>
<li><a href="#docs-source">Source Docs (Docco)</a></li>
</ul>
</div><!--/.well -->
</div><!--/span-->
</div></div> <!-- /row /container -->
</body>
</html>

View File

@ -757,22 +757,13 @@ my.Graph = Backbone.View.extend({
<label>Group Column (x-axis)</label> \
<div class="input editor-group"> \
<select> \
<option value="">Please choose ...</option> \
{{#fields}} \
<option value="{{id}}">{{label}}</option> \
{{/fields}} \
</select> \
</div> \
<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 class="editor-buttons"> \
@ -784,13 +775,34 @@ my.Graph = Backbone.View.extend({
</div> \
</form> \
</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> \
',
events: {
'change form select': 'onEditorSubmit',
'click .editor-add': 'addSeries',
'click .editor-add': '_onAddSeries',
'click .action-remove-series': 'removeSeries',
'click .action-toggle-help': 'toggleHelp'
},
@ -807,7 +819,8 @@ my.Graph = Backbone.View.extend({
this.model.currentDocuments.bind('reset', this.redraw);
var stateData = _.extend({
group: null,
series: [],
// so that at least one series chooser box shows up
series: [""],
graphType: 'lines-and-points'
},
options.state
@ -817,21 +830,45 @@ my.Graph = Backbone.View.extend({
},
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);
// now set a load of stuff up
this.$graph = this.el.find('.panel.graph');
// for use later when adding additional series
// could be simpler just to have a common template!
this.$seriesClone = this.el.find('.editor-series').clone();
this._updateSeries();
// set up editor from state
if (this.state.get('graphType')) {
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;
},
// 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) {
var select = this.el.find('.editor-group select');
$editor = this;
var series = this.$series.map(function () {
var $editor = this;
var $series = this.el.find('.editor-series select');
var series = $series.map(function () {
return $(this).val();
});
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
//
// @param typeId graphType id (lines, lines-and-points etc)
getGraphOptions: function(typeId) {
var self = this;
// 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) {
if (self.model.currentDocuments.models[val]) {
var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);
@ -886,20 +933,25 @@ my.Graph = Backbone.View.extend({
}
return val;
};
// 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 options = {
var xaxis = {};
// check for time series on x-axis
if (this.model.fields.get(this.state.get('group')).get('type') === 'date') {
xaxis.mode = 'time';
xaxis.timeformat = '%y-%b';
}
var optionsPerGraphType = {
lines: {
series: {
lines: { show: true }
}
series: {
lines: { show: true }
},
xaxis: xaxis
},
points: {
series: {
points: { show: true }
},
xaxis: xaxis,
grid: { hoverable: true, clickable: true }
},
'lines-and-points': {
@ -907,6 +959,7 @@ my.Graph = Backbone.View.extend({
points: { show: true },
lines: { show: true }
},
xaxis: xaxis,
grid: { hoverable: true, clickable: true }
},
bars: {
@ -930,7 +983,7 @@ my.Graph = Backbone.View.extend({
}
}
};
return options[typeId];
return optionsPerGraphType[typeId];
},
setupTooltips: function() {
@ -987,8 +1040,15 @@ my.Graph = Backbone.View.extend({
_.each(this.state.attributes.series, function(field) {
var points = [];
_.each(self.model.currentDocuments.models, function(doc, index) {
var x = doc.get(self.state.attributes.group);
var y = doc.get(field);
var xfield = self.model.fields.get(self.state.attributes.group);
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') {
x = index;
}
@ -1006,23 +1066,25 @@ my.Graph = Backbone.View.extend({
// 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
// to be removed.
// @param [int] idx index of this series in the list of series
//
// Returns itself.
addSeries: function (e) {
e.preventDefault();
var element = this.$seriesClone.clone(),
label = element.find('label'),
index = this.$series.length;
addSeries: function (idx) {
var data = _.extend({
seriesIndex: idx,
seriesName: String.fromCharCode(idx + 64 + 1),
}, this.model.toTemplateJSON());
this.el.find('.editor-series-group').append(element);
this._updateSeries();
label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]');
label.find('span').text(String.fromCharCode(this.$series.length + 64));
var htmls = $.mustache(this.templateSeriesEditor, data);
this.el.find('.editor-series-group').append(htmls);
return this;
},
_onAddSeries: function(e) {
e.preventDefault();
this.addSeries(this.state.get('series').length);
},
// Public: Removes a series list item from the editor.
//
// Also updates the labels of the remaining series elements.
@ -1030,26 +1092,12 @@ my.Graph = Backbone.View.extend({
e.preventDefault();
var $el = $(e.target);
$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();
},
toggleHelp: function() {
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);
@ -1199,6 +1247,8 @@ my.Grid = Backbone.View.extend({
var hiddenFields = this.state.get('hiddenFields');
hiddenFields.push(this.tempState.currentColumn);
this.state.set({hiddenFields: hiddenFields});
// change event not being triggered (because it is an array?) so trigger manually
this.state.trigger('change');
this.render();
},
@ -1404,7 +1454,7 @@ this.recline.View = this.recline.View || {};
//
// <pre>
// {
// // geomField if specified will be used in preference to lat/lon
// // geomField if specified will be used in preference to lat/lon
// geomField: {id of field containing geometry in the dataset}
// lonField: {id of field containing longitude in the dataset}
// latField: {id of field containing latitude in the dataset}
@ -1462,6 +1512,11 @@ my.Map = Backbone.View.extend({
<div class="editor-buttons"> \
<button class="btn editor-update-map">Update</button> \
</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" /> \
</div> \
</form> \
@ -1479,7 +1534,8 @@ my.Map = Backbone.View.extend({
// Define here events for UI elements
events: {
'click .editor-update-map': 'onEditorSubmit',
'change .editor-field-type': 'onFieldTypeChange'
'change .editor-field-type': 'onFieldTypeChange',
'change #editor-auto-zoom': 'onAutoZoomChange'
},
initialize: function(options) {
@ -1498,15 +1554,27 @@ my.Map = Backbone.View.extend({
// Listen to changes in the documents
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('reset', function(){self.redraw('reset')});
// If the div was hidden, Leaflet needs to recalculate some sizes
// to display properly
this.bind('view:show',function(){
if (self.map) {
self.map.invalidateSize();
// If the div was hidden, Leaflet needs to recalculate some sizes
// to display properly
if (self.map){
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({
@ -1518,6 +1586,7 @@ my.Map = Backbone.View.extend({
);
this.state = new recline.Model.ObjectState(stateData);
this.autoZoom = true;
this.mapReady = false;
this.render();
},
@ -1583,6 +1652,13 @@ my.Map = Backbone.View.extend({
this.features.clearLayers();
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
//
// For each document passed, a GeoJSON geometry will be extracted and added
@ -1656,7 +1736,9 @@ my.Map = Backbone.View.extend({
// TODO: mustache?
html = ''
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};
@ -1707,19 +1789,22 @@ my.Map = Backbone.View.extend({
_getGeometryFromDocument: function(doc){
if (this.geomReady){
if (this.state.get('geomField')){
// We assume that the contents of the field are a valid GeoJSON object
return doc.attributes[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
return value;
}
} else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields
var lon = doc.get(this.state.get('lonField'));
var lat = doc.get(this.state.get('latField'));
if (lon && lat) {
if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
return {
type: 'Point',
coordinates: [
doc.attributes[this.state.get('lonField')],
doc.attributes[this.state.get('latField')]
]
coordinates: [lon,lat]
};
}
}
@ -1761,6 +1846,18 @@ my.Map = Backbone.View.extend({
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.
//
// 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.setView(new L.LatLng(0, 0), 2);
@ -2411,7 +2526,9 @@ my.DataExplorer = Backbone.View.extend({
pageView.view.state.bind('change', function() {
var update = {};
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,7 +3521,9 @@ this.recline.Backend = this.recline.Backend || {};
var options = options || {};
var trm = options.trim;
var separator = options.separator || ',';
var delimiter = options.delimiter || '"';
var cur = '', // The character we are currently processing.
inQuote = false,
fieldQuoted = false,
@ -3451,8 +3570,8 @@ this.recline.Backend = this.recline.Backend || {};
field = '';
fieldQuoted = false;
} else {
// If it's not a ", add it to the field buffer
if (cur !== '"') {
// If it's not a delimiter, add it to the field buffer
if (cur !== delimiter) {
field += cur;
} else {
if (!inQuote) {
@ -3460,9 +3579,9 @@ this.recline.Backend = this.recline.Backend || {};
inQuote = true;
fieldQuoted = true;
} else {
// Next char is ", this is an escaped "
if (s.charAt(i + 1) === '"') {
field += '"';
// Next char is delimiter, this is an escaped delimiter
if (s.charAt(i + 1) === delimiter) {
field += delimiter;
// Skip the next char
i += 1;
} else {

View File

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

View File

@ -1,153 +1,79 @@
/*jshint multistr:true */
var util = function() {
var templates = {
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> \
'
};
this.recline = this.recline || {};
this.recline.Util = this.recline.Util || {};
$.fn.serializeObject = function() {
var o = {};
var a = this.serializeArray();
$.each(a, function() {
if (o[this.name]) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value || '');
} else {
o[this.name] = this.value || '';
}
});
return o;
};
(function(my) {
// ## Miscellaneous Utilities
function registerEmitter() {
var Emitter = function(obj) {
this.emit = function(obj, channel) {
if (!channel) channel = 'data';
this.trigger(channel, obj);
};
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] || ''
};
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});
// 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;
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]();
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);
return {
registerEmitter: registerEmitter,
listenFor: listenFor,
show: show,
hide: hide,
position: position,
render: render,
observeExit: observeExit
};
}();

View File

@ -48,22 +48,13 @@ my.Graph = Backbone.View.extend({
<label>Group Column (x-axis)</label> \
<div class="input editor-group"> \
<select> \
<option value="">Please choose ...</option> \
{{#fields}} \
<option value="{{id}}">{{label}}</option> \
{{/fields}} \
</select> \
</div> \
<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 class="editor-buttons"> \
@ -75,13 +66,34 @@ my.Graph = Backbone.View.extend({
</div> \
</form> \
</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> \
',
events: {
'change form select': 'onEditorSubmit',
'click .editor-add': 'addSeries',
'click .editor-add': '_onAddSeries',
'click .action-remove-series': 'removeSeries',
'click .action-toggle-help': 'toggleHelp'
},
@ -98,7 +110,8 @@ my.Graph = Backbone.View.extend({
this.model.currentDocuments.bind('reset', this.redraw);
var stateData = _.extend({
group: null,
series: [],
// so that at least one series chooser box shows up
series: [""],
graphType: 'lines-and-points'
},
options.state
@ -108,21 +121,45 @@ my.Graph = Backbone.View.extend({
},
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);
// now set a load of stuff up
this.$graph = this.el.find('.panel.graph');
// for use later when adding additional series
// could be simpler just to have a common template!
this.$seriesClone = this.el.find('.editor-series').clone();
this._updateSeries();
// set up editor from state
if (this.state.get('graphType')) {
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;
},
// 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) {
var select = this.el.find('.editor-group select');
$editor = this;
var series = this.$series.map(function () {
var $editor = this;
var $series = this.el.find('.editor-series select');
var series = $series.map(function () {
return $(this).val();
});
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
//
// @param typeId graphType id (lines, lines-and-points etc)
getGraphOptions: function(typeId) {
var self = this;
// 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) {
if (self.model.currentDocuments.models[val]) {
var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);
@ -177,20 +224,25 @@ my.Graph = Backbone.View.extend({
}
return val;
};
// 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 options = {
var xaxis = {};
// check for time series on x-axis
if (this.model.fields.get(this.state.get('group')).get('type') === 'date') {
xaxis.mode = 'time';
xaxis.timeformat = '%y-%b';
}
var optionsPerGraphType = {
lines: {
series: {
lines: { show: true }
}
series: {
lines: { show: true }
},
xaxis: xaxis
},
points: {
series: {
points: { show: true }
},
xaxis: xaxis,
grid: { hoverable: true, clickable: true }
},
'lines-and-points': {
@ -198,6 +250,7 @@ my.Graph = Backbone.View.extend({
points: { show: true },
lines: { show: true }
},
xaxis: xaxis,
grid: { hoverable: true, clickable: true }
},
bars: {
@ -221,7 +274,7 @@ my.Graph = Backbone.View.extend({
}
}
};
return options[typeId];
return optionsPerGraphType[typeId];
},
setupTooltips: function() {
@ -278,8 +331,15 @@ my.Graph = Backbone.View.extend({
_.each(this.state.attributes.series, function(field) {
var points = [];
_.each(self.model.currentDocuments.models, function(doc, index) {
var x = doc.get(self.state.attributes.group);
var y = doc.get(field);
var xfield = self.model.fields.get(self.state.attributes.group);
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') {
x = index;
}
@ -297,23 +357,25 @@ my.Graph = Backbone.View.extend({
// 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
// to be removed.
// @param [int] idx index of this series in the list of series
//
// Returns itself.
addSeries: function (e) {
e.preventDefault();
var element = this.$seriesClone.clone(),
label = element.find('label'),
index = this.$series.length;
addSeries: function (idx) {
var data = _.extend({
seriesIndex: idx,
seriesName: String.fromCharCode(idx + 64 + 1),
}, this.model.toTemplateJSON());
this.el.find('.editor-series-group').append(element);
this._updateSeries();
label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]');
label.find('span').text(String.fromCharCode(this.$series.length + 64));
var htmls = $.mustache(this.templateSeriesEditor, data);
this.el.find('.editor-series-group').append(htmls);
return this;
},
_onAddSeries: function(e) {
e.preventDefault();
this.addSeries(this.state.get('series').length);
},
// Public: Removes a series list item from the editor.
//
// Also updates the labels of the remaining series elements.
@ -321,26 +383,12 @@ my.Graph = Backbone.View.extend({
e.preventDefault();
var $el = $(e.target);
$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();
},
toggleHelp: function() {
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);

View File

@ -35,18 +35,6 @@ my.Grid = Backbone.View.extend({
'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
@ -81,12 +69,12 @@ my.Grid = Backbone.View.extend({
filter: function() {
self.model.queryState.addTermFilter(self.tempState.currentColumn, '');
},
transform: function() { self.showTransformDialog('transform'); },
sortAsc: function() { self.setColumnSort('asc'); },
sortDesc: function() { self.setColumnSort('desc'); },
hideColumn: function() { self.hideColumn(); },
showColumn: function() { self.showColumn(e); },
deleteRow: function() {
var self = this;
var doc = _.find(self.model.currentDocuments.models, function(doc) {
// important this is == as the currentRow will be string (as comes
// from DOM) while id may be int
@ -94,9 +82,9 @@ my.Grid = Backbone.View.extend({
});
doc.destroy().then(function() {
self.model.currentDocuments.remove(doc);
my.notify("Row deleted successfully");
self.trigger('recline:flash', {message: "Row deleted successfully"});
}).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() {
var $el = $('.dialog-content');
util.show('dialog');
var self = this;
var view = new my.ColumnTransform({
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.render();
$el.empty();
$el.append(view.el);
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' });
this.el.append(view.el);
view.el.modal();
},
setColumnSort: function(order) {
@ -143,6 +116,8 @@ my.Grid = Backbone.View.extend({
var hiddenFields = this.state.get('hiddenFields');
hiddenFields.push(this.tempState.currentColumn);
this.state.set({hiddenFields: hiddenFields});
// change event not being triggered (because it is an array?) so trigger manually
this.state.trigger('change');
this.render();
},
@ -291,6 +266,19 @@ my.GridRow = Backbone.View.extend({
// ===================
// 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) {
var editing = this.el.find('.data-table-cell-editor-editor');
if (editing.length > 0) {
@ -299,10 +287,12 @@ my.GridRow = Backbone.View.extend({
$(e.target).addClass("hidden");
var cell = $(e.target).siblings('.data-table-cell-value');
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) {
var self = this;
var cell = $(e.target);
var rowId = cell.parents('tr').attr('data-id');
var field = cell.parents('td').attr('data-field');
@ -310,12 +300,13 @@ my.GridRow = Backbone.View.extend({
var newData = {};
newData[field] = newValue;
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) {
my.notify("Row updated successfully", {category: 'success'});
this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'});
})
.fail(function() {
my.notify('Error saving row', {
this.trigger('recline:flash', {
message: 'Error saving row',
category: 'error',
persist: true
});

View File

@ -17,7 +17,7 @@ this.recline.View = this.recline.View || {};
//
// <pre>
// {
// // geomField if specified will be used in preference to lat/lon
// // geomField if specified will be used in preference to lat/lon
// geomField: {id of field containing geometry in the dataset}
// lonField: {id of field containing longitude in the dataset}
// latField: {id of field containing latitude in the dataset}
@ -75,6 +75,11 @@ my.Map = Backbone.View.extend({
<div class="editor-buttons"> \
<button class="btn editor-update-map">Update</button> \
</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" /> \
</div> \
</form> \
@ -92,7 +97,8 @@ my.Map = Backbone.View.extend({
// Define here events for UI elements
events: {
'click .editor-update-map': 'onEditorSubmit',
'change .editor-field-type': 'onFieldTypeChange'
'change .editor-field-type': 'onFieldTypeChange',
'change #editor-auto-zoom': 'onAutoZoomChange'
},
initialize: function(options) {
@ -111,15 +117,27 @@ my.Map = Backbone.View.extend({
// Listen to changes in the documents
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('reset', function(){self.redraw('reset')});
// If the div was hidden, Leaflet needs to recalculate some sizes
// to display properly
this.bind('view:show',function(){
if (self.map) {
self.map.invalidateSize();
// If the div was hidden, Leaflet needs to recalculate some sizes
// to display properly
if (self.map){
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({
@ -131,6 +149,7 @@ my.Map = Backbone.View.extend({
);
this.state = new recline.Model.ObjectState(stateData);
this.autoZoom = true;
this.mapReady = false;
this.render();
},
@ -196,6 +215,13 @@ my.Map = Backbone.View.extend({
this.features.clearLayers();
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
//
// 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.
//
_add: function(docs){
var self = this;
if (!(docs instanceof Array)) docs = [docs];
@ -269,7 +298,9 @@ my.Map = Backbone.View.extend({
// TODO: mustache?
html = ''
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};
@ -284,13 +315,13 @@ my.Map = Backbone.View.extend({
var msg = 'Wrong geometry value';
if (except.message) msg += ' (' + except.message + ')';
if (wrongSoFar <= 10) {
my.notify(msg,{category:'error'});
self.trigger('recline:flash', {message: msg, category:'error'});
}
}
} else {
wrongSoFar += 1
if (wrongSoFar <= 10) {
my.notify('Wrong geometry value',{category:'error'});
self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'});
}
}
return true;
@ -320,19 +351,22 @@ my.Map = Backbone.View.extend({
_getGeometryFromDocument: function(doc){
if (this.geomReady){
if (this.state.get('geomField')){
// We assume that the contents of the field are a valid GeoJSON object
return doc.attributes[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
return value;
}
} else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields
var lon = doc.get(this.state.get('lonField'));
var lat = doc.get(this.state.get('latField'));
if (lon && lat) {
if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
return {
type: 'Point',
coordinates: [
doc.attributes[this.state.get('lonField')],
doc.attributes[this.state.get('latField')]
]
coordinates: [lon,lat]
};
}
}
@ -374,6 +408,18 @@ my.Map = Backbone.View.extend({
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.
//
// 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.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
(function($, my) {
// View (Dialog) for doing data transformations on whole dataset.
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);
}
});
// ## ColumnTransform
//
// View (Dialog) for doing data transformations (on columns of data).
my.ColumnTransform = Backbone.View.extend({
className: 'transform-column-view',
className: 'transform-column-view modal fade in',
template: ' \
<div class="dialog-header"> \
Functional transform on column {{name}} \
<div class="modal-header"> \
<a class="close" data-dismiss="modal">×</a> \
<h3>Functional transform on column {{name}}</h3> \
</div> \
<div class="dialog-body"> \
<div class="modal-body"> \
<div class="grid-layout layout-tight layout-full"> \
<table> \
<tbody> \
@ -107,10 +42,10 @@ my.ColumnTransform = Backbone.View.extend({
</tr> \
<tr> \
<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> \
<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 id="expression-preview-tabs-preview"> \
<div class="expression-preview-container"> \
</div> \
</div> \
</div> \
@ -125,7 +60,7 @@ my.ColumnTransform = Backbone.View.extend({
</table> \
</div> \
</div> \
<div class="dialog-footer"> \
<div class="modal-footer"> \
<button class="okButton btn primary">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
<button class="cancelButton btn danger">Cancel</button> \
</div> \
@ -158,11 +93,11 @@ my.ColumnTransform = Backbone.View.extend({
var funcText = this.el.find('.expression-preview-code').val();
var editFunc = costco.evalFunction(funcText);
if (editFunc.errorMessage) {
my.notify("Error with function! " + editFunc.errorMessage);
this.trigger('recline:flash', {message: "Error with function! " + editFunc.errorMessage});
return;
}
util.hide('dialog');
my.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true});
this.el.modal('hide');
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) {
return doc.toJSON();
});
@ -172,7 +107,7 @@ my.ColumnTransform = Backbone.View.extend({
function onCompletedUpdate() {
totalToUpdate += -1;
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!)');
self.remove();
}
@ -183,8 +118,38 @@ my.ColumnTransform = Backbone.View.extend({
realDoc.set(editedDoc);
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) {
var self = this;
// 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();
});
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 {
errors.text(editFunc.errorMessage);
}

View File

@ -168,12 +168,6 @@ my.DataExplorer = Backbone.View.extend({
<div class="clearfix"></div> \
</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> \
',
events: {
@ -215,6 +209,7 @@ my.DataExplorer = Backbone.View.extend({
// these must be called after pageViews are created
this.render();
this._bindStateChanges();
this._bindFlashNotifications();
// now do updates based on state (need to come after render)
if (this.state.get('readOnly')) {
this.setReadOnly();
@ -225,24 +220,16 @@ my.DataExplorer = Backbone.View.extend({
this.updateNav(this.pageViews[0].id);
}
this.router = new Backbone.Router();
this.setupRouting();
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() {
my.clearNotifications();
self.clearNotifications();
self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
my.notify('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);
self.notify({message: 'Data loaded', category: 'success'});
});
this.model.bind('query:fail', function(error) {
my.clearNotifications();
self.clearNotifications();
var msg = '';
if (typeof(error) == 'string') {
msg = error;
@ -256,7 +243,7 @@ my.DataExplorer = Backbone.View.extend({
} else {
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
@ -266,7 +253,7 @@ my.DataExplorer = Backbone.View.extend({
self.model.query(self.state.get('query'));
})
.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);
},
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) {
this.el.find('.navigation li').removeClass('active');
this.el.find('.navigation li a').removeClass('disabled');
@ -357,7 +329,7 @@ my.DataExplorer = Backbone.View.extend({
_setupState: function(initialState) {
var self = this;
// get data from the query string / hash url plus some defaults
var qs = my.parseHashQueryString();
var qs = recline.Util.parseHashQueryString();
var query = qs.reclineQuery;
query = query ? JSON.parse(query) : self.model.queryState.toJSON();
// 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() {
var update = {};
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);

View File

@ -1,7 +1,7 @@
(function ($) {
module("Backend Local CSV");
test("parseCSV", function() {
test("parseCSV", function() {
var csv = '"Jones, Jay",10\n' +
'"Xyz ""ABC"" O\'Brien",11:35\n' +
'"Other, AN",12:35\n';
@ -29,7 +29,7 @@ test("parseCSV", function() {
equal(dataset.currentDocuments.length, 3);
});
test("parseCSVsemicolon", function() {
test("parseCSVsemicolon", function() {
var csv = '"Jones; Jay";10\n' +
'"Xyz ""ABC"" O\'Brien";11:35\n' +
'"Other; AN";12:35\n';
@ -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);

View File

@ -1,6 +1,8 @@
var Fixture = {
getDataset: function() {
var fields = [
{id: 'id'},
{id: 'date', type: 'date'},
{id: 'x'},
{id: 'y'},
{id: 'z'},
@ -10,12 +12,12 @@ var Fixture = {
{id: 'lon'}
];
var 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}
{id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', label: 'first', lat:52.56, lon:13.40},
{id: 1, date: '2011-02-02', x: 2, y: 4, z: 6, country: 'UK', label: 'second', lat:54.97, lon:-1.60},
{id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, country: 'US', label: 'third', lat:40.00, lon:-75.5},
{id: 3, date: '2011-04-04', x: 4, y: 8, z: 12, country: 'UK', label: 'fourth', lat:57.27, lon:-6.20},
{id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, country: 'UK', label: 'fifth', lat:51.58, lon:0},
{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);
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/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>
@ -22,6 +21,7 @@
<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/backend/base.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-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>

View File

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

View File

@ -11,3 +11,43 @@ test('basics', function () {
assertPresent('.editor', view.el);
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);
;