AMBARI-10064. Storm Ambari view (Sriharsha Chintalapani via srimanth)

Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/cd6398a1
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/cd6398a1
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/cd6398a1

Branch: refs/heads/trunk
Commit: cd6398a1dad3ea9503d408d98623088ecb76ef3b
Parents: 11ef5ca
Author: Srimanth Gunturi <[email protected]>
Authored: Fri May 22 12:40:51 2015 -0700
Committer: Srimanth Gunturi <[email protected]>
Committed: Fri May 22 12:41:02 2015 -0700

----------------------------------------------------------------------
 contrib/views/storm/pom.xml                     |  105 +
 .../org/apache/ambari/storm/ProxyServlet.java   |  100 +
 contrib/views/storm/src/main/resources/404.html |  169 +
 .../storm/src/main/resources/WEB-INF/web.xml    |   37 +
 .../views/storm/src/main/resources/index.html   |   56 +
 .../bower/backbone-forms/js/backbone-forms.js   | 2446 +++++
 .../libs/bower/backbone-forms/js/list.js        |  650 ++
 .../js/backbone.babysitter.js                   |  190 +
 .../js/backbone.marionette.js                   | 3128 ++++++
 .../bower/backbone.wreqr/js/backbone.wreqr.js   |  435 +
 .../libs/bower/backbone/js/backbone.js          | 1608 +++
 .../libs/bower/backgrid/css/backgrid.min.css    |    1 +
 .../libs/bower/backgrid/js/backgrid.js          | 2887 ++++++
 .../resources/libs/bower/bootbox/js/bootbox.js  |  894 ++
 .../libs/bower/bootstrap/css/bootstrap.min.css  |    5 +
 .../fonts/glyphicons-halflings-regular.svg      |  288 +
 .../bootstrap/js/bootstrap-filestyle.min.js     |    1 +
 .../libs/bower/bootstrap/js/bootstrap-notify.js |   97 +
 .../libs/bower/bootstrap/js/bootstrap.js        | 2306 +++++
 .../bower/font-awesome/css/font-awesome.min.css |    4 +
 .../font-awesome/fonts/fontawesome-webfont.svg  |  565 ++
 .../libs/bower/globalize/js/globalize.js        | 1586 +++
 .../jquery-fileupload/js/jquery.fileupload.js   | 1201 +++
 .../libs/bower/jquery-ui/css/jquery-ui.min.css  |    5 +
 .../jquery-ui/js/jquery-ui-1.10.3.custom.js     |   12 +
 .../bower/jquery-ui/js/jquery.ui.widget.min.js  |    5 +
 .../resources/libs/bower/jquery/js/jquery.js    | 9205 ++++++++++++++++++
 .../libs/bower/jquery/js/jquery.min.js          |    5 +
 .../libs/bower/jquery/js/jquery.min.map         |    1 +
 .../require-handlebars-plugin/js/handlebars.js  | 2220 +++++
 .../bower/require-handlebars-plugin/js/hbs.js   |  492 +
 .../js/i18nprecompile.js                        |   57 +
 .../bower/require-handlebars-plugin/js/json2.js |  365 +
 .../libs/bower/requirejs-text/js/text.js        |  390 +
 .../libs/bower/requirejs/js/require.js          | 2083 ++++
 .../libs/bower/underscore/js/underscore.js      | 1415 +++
 .../main/resources/libs/other/arbor-graphics.js |   51 +
 .../main/resources/libs/other/arbor-tween.js    |   81 +
 .../src/main/resources/libs/other/arbor.js      |   67 +
 .../storm/src/main/resources/scripts/App.js     |   41 +
 .../scripts/collection/BaseCollection.js        |   61 +
 .../scripts/collection/VTopologyList.js         |   42 +
 .../resources/scripts/globalize/message/en.js   |  180 +
 .../storm/src/main/resources/scripts/main.js    |  151 +
 .../main/resources/scripts/models/BaseModel.js  |   71 +
 .../main/resources/scripts/models/Cluster.js    |   42 +
 .../src/main/resources/scripts/models/VBolt.js  |   42 +
 .../src/main/resources/scripts/models/VError.js |   42 +
 .../main/resources/scripts/models/VExecutor.js  |   42 +
 .../src/main/resources/scripts/models/VModel.js |   42 +
 .../main/resources/scripts/models/VNimbus.js    |   42 +
 .../resources/scripts/models/VNimbusConfig.js   |   42 +
 .../resources/scripts/models/VOutputStat.js     |   42 +
 .../src/main/resources/scripts/models/VSpout.js |   44 +
 .../resources/scripts/models/VSupervisor.js     |   42 +
 .../main/resources/scripts/models/VTopology.js  |   56 +
 .../resources/scripts/models/VTopologyConfig.js |   42 +
 .../main/resources/scripts/modules/Helpers.js   |  157 +
 .../src/main/resources/scripts/modules/Vent.js  |   22 +
 .../src/main/resources/scripts/router/Router.js |   98 +
 .../src/main/resources/scripts/utils/Globals.js |   30 +
 .../main/resources/scripts/utils/LangSupport.js |  116 +
 .../main/resources/scripts/utils/Overrides.js   |  218 +
 .../main/resources/scripts/utils/TableLayout.js |  106 +
 .../src/main/resources/scripts/utils/Utils.js   |  133 +
 .../scripts/views/Cluster/ClusterSummary.js     |  381 +
 .../scripts/views/Spout/SpoutCollectionView.js  |   53 +
 .../scripts/views/Spout/SpoutItemView.js        |  355 +
 .../scripts/views/Topology/RebalanceForm.js     |   84 +
 .../scripts/views/Topology/TopologyDetail.js    |  734 ++
 .../scripts/views/Topology/TopologyForm.js      |  102 +
 .../scripts/views/Topology/TopologyGraphView.js |  423 +
 .../scripts/views/Topology/TopologySummary.js   |  300 +
 .../main/resources/scripts/views/site/Header.js |   79 +
 .../storm/src/main/resources/styles/default.css |  298 +
 .../templates/cluster/clusterSummary.html       |   83 +
 .../main/resources/templates/site/header.html   |   23 +
 .../templates/spout/spoutItemView.html          |   46 +
 .../templates/topology/topologyDetail.html      |  108 +
 .../templates/topology/topologyForm.html        |    8 +
 .../templates/topology/topologySummary.html     |   23 +
 contrib/views/storm/src/main/resources/view.xml |   22 +
 pom.xml                                         |    3 +
 83 files changed, 40284 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/cd6398a1/contrib/views/storm/pom.xml
----------------------------------------------------------------------
diff --git a/contrib/views/storm/pom.xml b/contrib/views/storm/pom.xml
new file mode 100644
index 0000000..8ad78e5
--- /dev/null
+++ b/contrib/views/storm/pom.xml
@@ -0,0 +1,105 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ -->
+ <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+  <parent>
+    <groupId>org.apache.ambari.contrib.views</groupId>
+    <artifactId>ambari-contrib-views</artifactId>
+    <version>2.0.0-SNAPSHOT</version>
+  </parent>
+  <properties>
+    <ambari.dir>${project.parent.parent.parent.basedir}</ambari.dir>
+  </properties>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.ambari.contrib.views</groupId>
+  <artifactId>storm-view</artifactId>
+  <name>Storm_Monitoring</name>
+  <version>0.1.0</version>
+  <description>Storm Monitoring View</description>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.0</version>
+        <configuration>
+          <source>1.7</source>
+          <target>1.7</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>rpm-maven-plugin</artifactId>
+        <version>2.0.1</version>
+        <executions>
+          <execution>
+            <phase>none</phase>
+            <goals>
+              <goal>rpm</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <configuration>
+          <excludes>
+            <exclude>src/main/resources/libs/**</exclude>
+            <exclude>src/main/resources/styles/**</exclude>
+           <exclude>src/main/resources/templates/**</exclude>
+          </excludes>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>test</phase>
+            <goals>
+              <goal>check</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>com.sun.jersey</groupId>
+      <artifactId>jersey-server</artifactId>
+      <version>1.8</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.ambari</groupId>
+      <artifactId>ambari-views</artifactId>
+      <version>${ambari.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <version>3.0.1</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>
+
+  </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/ambari/blob/cd6398a1/contrib/views/storm/src/main/java/org/apache/ambari/storm/ProxyServlet.java
----------------------------------------------------------------------
diff --git 
a/contrib/views/storm/src/main/java/org/apache/ambari/storm/ProxyServlet.java 
b/contrib/views/storm/src/main/java/org/apache/ambari/storm/ProxyServlet.java
new file mode 100644
index 0000000..6e6bea2
--- /dev/null
+++ 
b/contrib/views/storm/src/main/java/org/apache/ambari/storm/ProxyServlet.java
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.view.storm;
+
+import org.apache.ambari.view.ViewContext;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URLDecoder;
+import java.util.*;
+import java.io.*;
+
+/**
+ * Simple servlet for proxying requests with doAs impersonation.
+ */
+public class ProxyServlet extends HttpServlet {
+
+  private ViewContext viewContext;
+
+  @Override
+  public void init(ServletConfig config) throws ServletException {
+    super.init(config);
+
+    ServletContext context = config.getServletContext();
+    viewContext = (ViewContext) 
context.getAttribute(ViewContext.CONTEXT_ATTRIBUTE);
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse 
response) throws IOException {
+    InputStream body = null;
+    String urlToRead = URLDecoder.decode(request.getParameter("url"));
+    HashMap<String,String> headersMap = this.getHeaders(request);
+    InputStream resultStream = 
viewContext.getURLStreamProvider().readAsCurrent(urlToRead, "GET", body, 
headersMap);
+    this.setResponse(request, response, resultStream);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest request, HttpServletResponse 
response) throws IOException {
+    InputStream stream = request.getInputStream();
+    String urlToRead = URLDecoder.decode(request.getParameter("url"));
+    HashMap<String,String> headersMap = this.getHeaders(request);
+    InputStream resultStream = 
viewContext.getURLStreamProvider().readAsCurrent(urlToRead, "POST", stream, 
headersMap);
+    this.setResponse(request, response, resultStream);
+  }
+
+  /**
+   * Get headers from request
+   * @param  request HTTPServletRequest
+   * @return HashMap map containing headers
+   */
+  public HashMap<String,String> getHeaders(HttpServletRequest request){
+    Enumeration headerNames = request.getHeaderNames();
+    HashMap<String,String> map = new HashMap<String, String>();
+    while (headerNames.hasMoreElements()) {
+      String key = (String) headerNames.nextElement();
+      String value = request.getHeader(key);
+      map.put(key, value);
+    }
+    map.put("Content-Type",request.getContentType());
+    return map;
+  }
+
+  /**
+   * Set response to the get/post request
+   * @param request      HttpServletRequest
+   * @param response     HttpServletResponse
+   * @param resultStream InputStream
+   */
+  public void setResponse(HttpServletRequest request, HttpServletResponse 
response, InputStream resultStream) throws IOException{
+    Scanner scanner = new Scanner(resultStream).useDelimiter("\\A");
+    String result = scanner.hasNext() ? scanner.next() : "";
+    Boolean notFound = result == "" || 
result.indexOf("\"exception\":\"NotFoundException\"") != -1;
+    response.setContentType(request.getContentType());
+    response.setStatus(notFound ? HttpServletResponse.SC_NOT_FOUND : 
HttpServletResponse.SC_OK);
+    PrintWriter writer = response.getWriter();
+    writer.print(result);
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/cd6398a1/contrib/views/storm/src/main/resources/404.html
----------------------------------------------------------------------
diff --git a/contrib/views/storm/src/main/resources/404.html 
b/contrib/views/storm/src/main/resources/404.html
new file mode 100644
index 0000000..4f16be5
--- /dev/null
+++ b/contrib/views/storm/src/main/resources/404.html
@@ -0,0 +1,169 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. Kerberos, LDAP, Custom. Binary/Htt
+-->
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <title>Page Not Found :(</title>
+        <style>
+            ::-moz-selection {
+                background: #b3d4fc;
+                text-shadow: none;
+            }
+
+            ::selection {
+                background: #b3d4fc;
+                text-shadow: none;
+            }
+
+            html {
+                padding: 30px 10px;
+                font-size: 20px;
+                line-height: 1.4;
+                color: #737373;
+                background: #f0f0f0;
+                -webkit-text-size-adjust: 100%;
+                -ms-text-size-adjust: 100%;
+            }
+
+            html,
+            input {
+                font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+            }
+
+            body {
+                max-width: 500px;
+                _width: 500px;
+                padding: 30px 20px 50px;
+                border: 1px solid #b3b3b3;
+                border-radius: 4px;
+                margin: 0 auto;
+                box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
+                background: #fcfcfc;
+            }
+
+            h1 {
+                margin: 0 10px;
+                font-size: 50px;
+                text-align: center;
+            }
+
+            h1 span {
+                color: #bbb;
+            }
+
+            h3 {
+                margin: 1.5em 0 0.5em;
+            }
+
+            p {
+                margin: 1em 0;
+            }
+
+            ul {
+                padding: 0 0 0 40px;
+                margin: 1em 0;
+            }
+
+            .container {
+                max-width: 380px;
+                _width: 380px;
+                margin: 0 auto;
+            }
+
+            /* google search */
+
+            #goog-fixurl ul {
+                list-style: none;
+                padding: 0;
+                margin: 0;
+            }
+
+            #goog-fixurl form {
+                margin: 0;
+            }
+
+            #goog-wm-qt,
+            #goog-wm-sb {
+                border: 1px solid #bbb;
+                font-size: 16px;
+                line-height: normal;
+                vertical-align: top;
+                color: #444;
+                border-radius: 2px;
+            }
+
+            #goog-wm-qt {
+                width: 220px;
+                height: 20px;
+                padding: 5px;
+                margin: 5px 10px 0 0;
+                box-shadow: inset 0 1px 1px #ccc;
+            }
+
+            #goog-wm-sb {
+                display: inline-block;
+                height: 32px;
+                padding: 0 10px;
+                margin: 5px 0 0;
+                white-space: nowrap;
+                cursor: pointer;
+                background-color: #f5f5f5;
+                background-image: -webkit-linear-gradient(rgba(255,255,255,0), 
#f1f1f1);
+                background-image: -moz-linear-gradient(rgba(255,255,255,0), 
#f1f1f1);
+                background-image: -ms-linear-gradient(rgba(255,255,255,0), 
#f1f1f1);
+                background-image: -o-linear-gradient(rgba(255,255,255,0), 
#f1f1f1);
+                -webkit-appearance: none;
+                -moz-appearance: none;
+                appearance: none;
+                *overflow: visible;
+                *display: inline;
+                *zoom: 1;
+            }
+
+            #goog-wm-sb:hover,
+            #goog-wm-sb:focus {
+                border-color: #aaa;
+                box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+                background-color: #f8f8f8;
+            }
+
+            #goog-wm-qt:hover,
+            #goog-wm-qt:focus {
+                border-color: #105cb6;
+                outline: 0;
+                color: #222;
+            }
+
+            input::-moz-focus-inner {
+                padding: 0;
+                border: 0;
+            }
+        </style>
+    </head>
+    <body>
+        <div class="container">
+            <h1>Not found <span>:(</span></h1>
+            <p>Sorry, but the page you were trying to view does not exist.</p>
+            <p>It looks like this was the result of either:</p>
+            <ul>
+                <li>a mistyped address</li>
+                <li>an out-of-date link</li>
+            </ul>
+        </div>
+    </body>
+</html>

http://git-wip-us.apache.org/repos/asf/ambari/blob/cd6398a1/contrib/views/storm/src/main/resources/WEB-INF/web.xml
----------------------------------------------------------------------
diff --git a/contrib/views/storm/src/main/resources/WEB-INF/web.xml 
b/contrib/views/storm/src/main/resources/WEB-INF/web.xml
new file mode 100644
index 0000000..e406de1
--- /dev/null
+++ b/contrib/views/storm/src/main/resources/WEB-INF/web.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. Kerberos, LDAP, Custom. Binary/Htt
+-->
+
+<web-app xmlns="http://java.sun.com/xml/ns/j2ee";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd";
+         version="2.4">
+
+  <display-name>Proxy Application</display-name>
+  <description>
+    This is the proxy application.
+  </description>
+  <servlet>
+    <servlet-name>ProxyServlet</servlet-name>
+    <servlet-class>org.apache.ambari.view.storm.ProxyServlet</servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ProxyServlet</servlet-name>
+    <url-pattern>/proxy</url-pattern>
+  </servlet-mapping>
+</web-app>

http://git-wip-us.apache.org/repos/asf/ambari/blob/cd6398a1/contrib/views/storm/src/main/resources/index.html
----------------------------------------------------------------------
diff --git a/contrib/views/storm/src/main/resources/index.html 
b/contrib/views/storm/src/main/resources/index.html
new file mode 100644
index 0000000..0f59153
--- /dev/null
+++ b/contrib/views/storm/src/main/resources/index.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. Kerberos, LDAP, Custom. Binary/Htt
+-->
+<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
+<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
+<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
+<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
+    <head>
+        <meta charset="utf-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+        <title>Storm Monitoring</title>
+        <meta name="description" content="">
+        <meta name="viewport" content="width=device-width">
+        <!-- Place favicon.ico and apple-touch-icon.png in the root directory 
-->
+        <link rel="stylesheet" href="libs/bower/backgrid/css/backgrid.min.css">
+        <link rel="stylesheet" 
href="libs/bower/bootstrap/css/bootstrap.min.css">
+        <link rel="stylesheet" 
href="libs/bower/font-awesome/css/font-awesome.min.css">
+        <link rel="stylesheet" 
href="libs/bower/jquery-ui/css/jquery-ui.min.css">
+        <link rel="stylesheet" href="styles/default.css">
+    </head>
+
+    <body>
+    <div class="loading"></div>
+        <div class='container-fluid'>
+            <div class="row">
+                <div class="col-md-12">
+                 <section id="header"></section>
+                 <section id="content">
+                    <div class="notifications top-right"></div>
+                    <div class="tab-content">
+                        <div role="sv_tabpanel" class="tab-pane active" 
id="topology"></div>
+                        <div role="sv_tabpanel" class="tab-pane" 
id="cluster"></div>
+                    </div>
+                 </section>
+                </div>
+            </div>
+        </div>
+        <!-- build:js scripts/main.js -->
+        <script data-main="scripts/main" 
src="libs/bower/requirejs/js/require.js"></script>
+        <!-- endbuild -->
+    </body>
+</html>

http://git-wip-us.apache.org/repos/asf/ambari/blob/cd6398a1/contrib/views/storm/src/main/resources/libs/bower/backbone-forms/js/backbone-forms.js
----------------------------------------------------------------------
diff --git 
a/contrib/views/storm/src/main/resources/libs/bower/backbone-forms/js/backbone-forms.js
 
b/contrib/views/storm/src/main/resources/libs/bower/backbone-forms/js/backbone-forms.js
new file mode 100644
index 0000000..9e02ebc
--- /dev/null
+++ 
b/contrib/views/storm/src/main/resources/libs/bower/backbone-forms/js/backbone-forms.js
@@ -0,0 +1,2446 @@
+/**
+ * Backbone Forms v0.13.0
+ *
+ * Copyright (c) 2013 Charles Davison, Pow Media Ltd
+ *
+ * License and more information at:
+ * http://github.com/powmedia/backbone-forms
+ */
+;(function(root) {
+
+  //DEPENDENCIES
+  //CommonJS
+  if (typeof exports !== 'undefined' && typeof require !== 'undefined') {
+    var $ = root.jQuery || root.Zepto || root.ender || require('jquery'),
+        _ = root._ || require('underscore'),
+        Backbone = root.Backbone || require('backbone');
+  }
+
+  //Browser
+  else {
+    var $ = root.jQuery,
+        _ = root._,
+        Backbone = root.Backbone;
+  }
+
+
+  //SOURCE
+  
//==================================================================================================
+//FORM
+//==================================================================================================
+
+var Form = Backbone.View.extend({
+
+  /**
+   * Constructor
+   *
+   * @param {Object} [options.schema]
+   * @param {Backbone.Model} [options.model]
+   * @param {Object} [options.data]
+   * @param {String[]|Object[]} [options.fieldsets]
+   * @param {String[]} [options.fields]
+   * @param {String} [options.idPrefix]
+   * @param {Form.Field} [options.Field]
+   * @param {Form.Fieldset} [options.Fieldset]
+   * @param {Function} [options.template]
+   */
+  initialize: function(options) {
+    var self = this;
+
+    options = options || {};
+
+    //Find the schema to use
+    var schema = this.schema = (function() {
+      //Prefer schema from options
+      if (options.schema) return _.result(options, 'schema');
+
+      //Then schema on model
+      var model = options.model;
+      if (model && model.schema) {
+        return (_.isFunction(model.schema)) ? model.schema() : model.schema;
+      }
+
+      //Then built-in schema
+      if (self.schema) {
+        return (_.isFunction(self.schema)) ? self.schema() : self.schema;
+      }
+
+      //Fallback to empty schema
+      return {};
+    })();
+
+    //Store important data
+    _.extend(this, _.pick(options, 'model', 'data', 'idPrefix', 
'templateData'));
+
+    //Override defaults
+    var constructor = this.constructor;
+    this.template = options.template || this.template || constructor.template;
+    this.Fieldset = options.Fieldset || this.Fieldset || constructor.Fieldset;
+    this.Field = options.Field || this.Field || constructor.Field;
+    this.NestedField = options.NestedField || this.NestedField || 
constructor.NestedField;
+
+    //Check which fields will be included (defaults to all)
+    var selectedFields = this.selectedFields = options.fields || 
_.keys(schema);
+
+    //Create fields
+    var fields = this.fields = {};
+
+    _.each(selectedFields, function(key) {
+      var fieldSchema = schema[key];
+      fields[key] = this.createField(key, fieldSchema);
+    }, this);
+
+    //Create fieldsets
+    var fieldsetSchema = options.fieldsets || [selectedFields],
+        fieldsets = this.fieldsets = [];
+
+    _.each(fieldsetSchema, function(itemSchema) {
+      this.fieldsets.push(this.createFieldset(itemSchema));
+    }, this);
+  },
+
+  /**
+   * Creates a Fieldset instance
+   *
+   * @param {String[]|Object[]} schema       Fieldset schema
+   *
+   * @return {Form.Fieldset}
+   */
+  createFieldset: function(schema) {
+    var options = {
+      schema: schema,
+      fields: this.fields
+    };
+
+    return new this.Fieldset(options);
+  },
+
+  /**
+   * Creates a Field instance
+   *
+   * @param {String} key
+   * @param {Object} schema       Field schema
+   *
+   * @return {Form.Field}
+   */
+  createField: function(key, schema) {
+    var options = {
+      form: this,
+      key: key,
+      schema: schema,
+      idPrefix: this.idPrefix
+    };
+
+    if (this.model) {
+      options.model = this.model;
+    } else if (this.data) {
+      options.value = this.data[key];
+    } else {
+      options.value = null;
+    }
+
+    var field = new this.Field(options);
+
+    this.listenTo(field.editor, 'all', this.handleEditorEvent);
+
+    return field;
+  },
+
+  /**
+   * Callback for when an editor event is fired.
+   * Re-triggers events on the form as key:event and triggers additional 
form-level events
+   *
+   * @param {String} event
+   * @param {Editor} editor
+   */
+  handleEditorEvent: function(event, editor) {
+    //Re-trigger editor events on the form
+    var formEvent = editor.key+':'+event;
+
+    this.trigger.call(this, formEvent, this, editor, 
Array.prototype.slice.call(arguments, 2));
+
+    //Trigger additional events
+    switch (event) {
+      case 'change':
+        this.trigger('change', this);
+        break;
+
+      case 'focus':
+        if (!this.hasFocus) this.trigger('focus', this);
+        break;
+
+      case 'blur':
+        if (this.hasFocus) {
+          //TODO: Is the timeout etc needed?
+          var self = this;
+          setTimeout(function() {
+            var focusedField = _.find(self.fields, function(field) {
+              return field.editor.hasFocus;
+            });
+
+            if (!focusedField) self.trigger('blur', self);
+          }, 0);
+        }
+        break;
+    }
+  },
+
+  render: function() {
+    var self = this,
+        fields = this.fields;
+
+    //Render form
+    var $form = $($.trim(this.template(_.result(this, 'templateData'))));
+
+    //Render standalone editors
+    $form.find('[data-editors]').add($form).each(function(i, el) {
+      var $container = $(el),
+          selection = $container.attr('data-editors');
+
+      if (_.isUndefined(selection)) return;
+
+      //Work out which fields to include
+      var keys = (selection == '*')
+        ? self.selectedFields || _.keys(fields)
+        : selection.split(',');
+
+      //Add them
+      _.each(keys, function(key) {
+        var field = fields[key];
+
+        $container.append(field.editor.render().el);
+      });
+    });
+
+    //Render standalone fields
+    $form.find('[data-fields]').add($form).each(function(i, el) {
+      var $container = $(el),
+          selection = $container.attr('data-fields');
+
+      if (_.isUndefined(selection)) return;
+
+      //Work out which fields to include
+      var keys = (selection == '*')
+        ? self.selectedFields || _.keys(fields)
+        : selection.split(',');
+
+      //Add them
+      _.each(keys, function(key) {
+        var field = fields[key];
+
+        $container.append(field.render().el);
+      });
+    });
+
+    //Render fieldsets
+    $form.find('[data-fieldsets]').add($form).each(function(i, el) {
+      var $container = $(el),
+          selection = $container.attr('data-fieldsets');
+
+      if (_.isUndefined(selection)) return;
+
+      _.each(self.fieldsets, function(fieldset) {
+        $container.append(fieldset.render().el);
+      });
+    });
+
+    //Set the main element
+    this.setElement($form);
+
+    //Set class
+    $form.addClass(this.className);
+
+    return this;
+  },
+
+  /**
+   * Validate the data
+   *
+   * @return {Object}       Validation errors
+   */
+  validate: function(options) {
+    var self = this,
+        fields = this.fields,
+        model = this.model,
+        errors = {};
+
+    options = options || {};
+
+    //Collect errors from schema validation
+    _.each(fields, function(field) {
+      var error = field.validate();
+      if (error) {
+        errors[field.key] = error;
+      }
+    });
+
+    //Get errors from default Backbone model validator
+    if (!options.skipModelValidate && model && model.validate) {
+      var modelErrors = model.validate(this.getValue());
+
+      if (modelErrors) {
+        var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
+
+        //If errors are not in object form then just store on the error object
+        if (!isDictionary) {
+          errors._others = errors._others || [];
+          errors._others.push(modelErrors);
+        }
+
+        //Merge programmatic errors (requires model.validate() to return an 
object e.g. { fieldKey: 'error' })
+        if (isDictionary) {
+          _.each(modelErrors, function(val, key) {
+            //Set error on field if there isn't one already
+            if (fields[key] && !errors[key]) {
+              fields[key].setError(val);
+              errors[key] = val;
+            }
+
+            else {
+              //Otherwise add to '_others' key
+              errors._others = errors._others || [];
+              var tmpErr = {};
+              tmpErr[key] = val;
+              errors._others.push(tmpErr);
+            }
+          });
+        }
+      }
+    }
+
+    return _.isEmpty(errors) ? null : errors;
+  },
+
+  /**
+   * Update the model with all latest values.
+   *
+   * @param {Object} [options]  Options to pass to Model#set (e.g. { silent: 
true })
+   *
+   * @return {Object}  Validation errors
+   */
+  commit: function(options) {
+    //Validate
+    options = options || {};
+
+    var validateOptions = {
+        skipModelValidate: !options.validate
+    };
+
+    var errors = this.validate(validateOptions);
+    if (errors) return errors;
+
+    //Commit
+    var modelError;
+
+    var setOptions = _.extend({
+      error: function(model, e) {
+        modelError = e;
+      }
+    }, options);
+
+    this.model.set(this.getValue(), setOptions);
+
+    if (modelError) return modelError;
+  },
+
+  /**
+   * Get all the field values as an object.
+   * Use this method when passing data instead of objects
+   *
+   * @param {String} [key]    Specific field value to get
+   */
+  getValue: function(key) {
+    //Return only given key if specified
+    if (key) return this.fields[key].getValue();
+
+    //Otherwise return entire form
+    var values = {};
+    _.each(this.fields, function(field) {
+      values[field.key] = field.getValue();
+    });
+
+    return values;
+  },
+
+  /**
+   * Update field values, referenced by key
+   *
+   * @param {Object|String} key     New values to set, or property to set
+   * @param val                     Value to set
+   */
+  setValue: function(prop, val) {
+    var data = {};
+    if (typeof prop === 'string') {
+      data[prop] = val;
+    } else {
+      data = prop;
+    }
+
+    var key;
+    for (key in this.schema) {
+      if (data[key] !== undefined) {
+        this.fields[key].setValue(data[key]);
+      }
+    }
+  },
+
+  /**
+   * Returns the editor for a given field key
+   *
+   * @param {String} key
+   *
+   * @return {Editor}
+   */
+  getEditor: function(key) {
+    var field = this.fields[key];
+    if (!field) throw new Error('Field not found: '+key);
+
+    return field.editor;
+  },
+
+  /**
+   * Gives the first editor in the form focus
+   */
+  focus: function() {
+    if (this.hasFocus) return;
+
+    //Get the first field
+    var fieldset = this.fieldsets[0],
+        field = fieldset.getFieldAt(0);
+
+    if (!field) return;
+
+    //Set focus
+    field.editor.focus();
+  },
+
+  /**
+   * Removes focus from the currently focused editor
+   */
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    var focusedField = _.find(this.fields, function(field) {
+      return field.editor.hasFocus;
+    });
+
+    if (focusedField) focusedField.editor.blur();
+  },
+
+  /**
+   * Manages the hasFocus property
+   *
+   * @param {String} event
+   */
+  trigger: function(event) {
+    if (event === 'focus') {
+      this.hasFocus = true;
+    }
+    else if (event === 'blur') {
+      this.hasFocus = false;
+    }
+
+    return Backbone.View.prototype.trigger.apply(this, arguments);
+  },
+
+  /**
+   * Override default remove function in order to remove embedded views
+   *
+   * TODO: If editors are included directly with data-editors="x", they need 
to be removed
+   * May be best to use XView to manage adding/removing views
+   */
+  remove: function() {
+    _.each(this.fieldsets, function(fieldset) {
+      fieldset.remove();
+    });
+
+    _.each(this.fields, function(field) {
+      field.remove();
+    });
+
+    return Backbone.View.prototype.remove.apply(this, arguments);
+  }
+
+}, {
+
+  //STATICS
+  template: _.template('\
+    <form data-fieldsets></form>\
+  ', null, this.templateSettings),
+
+  templateSettings: {
+    evaluate: /<%([\s\S]+?)%>/g,
+    interpolate: /<%=([\s\S]+?)%>/g,
+    escape: /<%-([\s\S]+?)%>/g
+  },
+
+  editors: {}
+
+});
+
+
+//==================================================================================================
+//VALIDATORS
+//==================================================================================================
+
+Form.validators = (function() {
+
+  var validators = {};
+
+  validators.errMessages = {
+    required: 'This field is required',
+    regexp: 'Invalid',
+    email: 'Invalid email address',
+    url: 'Invalid URL',
+    match: _.template('Must match field "<%= field %>"', null, 
Form.templateSettings)
+  };
+
+  validators.required = function(options) {
+    options = _.extend({
+      type: 'required',
+      message: this.errMessages.required
+    }, options);
+
+    return function required(value) {
+      options.value = value;
+
+      var err = {
+        type: options.type,
+        message: _.isFunction(options.message) ? options.message(options) : 
options.message
+      };
+
+      if (value === null || value === undefined || value === false || value 
=== '') return err;
+    };
+  };
+
+  validators.regexp = function(options) {
+    if (!options.regexp) throw new Error('Missing required "regexp" option for 
"regexp" validator');
+
+    options = _.extend({
+      type: 'regexp',
+      message: this.errMessages.regexp
+    }, options);
+
+    return function regexp(value) {
+      options.value = value;
+
+      var err = {
+        type: options.type,
+        message: _.isFunction(options.message) ? options.message(options) : 
options.message
+      };
+
+      //Don't check empty values (add a 'required' validator for this)
+      if (value === null || value === undefined || value === '') return;
+
+      if (!options.regexp.test(value)) return err;
+    };
+  };
+
+  validators.email = function(options) {
+    options = _.extend({
+      type: 'email',
+      message: this.errMessages.email,
+      regexp: 
/^[\w\-]{1,}([\w\-\+.]{1,1}[\w\-]{1,}){0,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/
+    }, options);
+
+    return validators.regexp(options);
+  };
+
+  validators.url = function(options) {
+    options = _.extend({
+      type: 'url',
+      message: this.errMessages.url,
+      regexp: 
/^(http|https):\/\/(([A-Z0-9][A-Z0-9_\-]*)(\.[A-Z0-9][A-Z0-9_\-]*)+)(:(\d+))?\/?/i
+    }, options);
+
+    return validators.regexp(options);
+  };
+
+  validators.match = function(options) {
+    if (!options.field) throw new Error('Missing required "field" options for 
"match" validator');
+
+    options = _.extend({
+      type: 'match',
+      message: this.errMessages.match
+    }, options);
+
+    return function match(value, attrs) {
+      options.value = value;
+
+      var err = {
+        type: options.type,
+        message: _.isFunction(options.message) ? options.message(options) : 
options.message
+      };
+
+      //Don't check empty values (add a 'required' validator for this)
+      if (value === null || value === undefined || value === '') return;
+
+      if (value !== attrs[options.field]) return err;
+    };
+  };
+
+
+  return validators;
+
+})();
+
+
+//==================================================================================================
+//FIELDSET
+//==================================================================================================
+
+Form.Fieldset = Backbone.View.extend({
+
+  /**
+   * Constructor
+   *
+   * Valid fieldset schemas:
+   *   ['field1', 'field2']
+   *   { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
+   *
+   * @param {String[]|Object[]} options.schema      Fieldset schema
+   * @param {Object} options.fields           Form fields
+   */
+  initialize: function(options) {
+    options = options || {};
+
+    //Create the full fieldset schema, merging defaults etc.
+    var schema = this.schema = this.createSchema(options.schema);
+
+    //Store the fields for this fieldset
+    this.fields = _.pick(options.fields, schema.fields);
+
+    //Override defaults
+    this.template = options.template || this.constructor.template;
+  },
+
+  /**
+   * Creates the full fieldset schema, normalising, merging defaults etc.
+   *
+   * @param {String[]|Object[]} schema
+   *
+   * @return {Object}
+   */
+  createSchema: function(schema) {
+    //Normalise to object
+    if (_.isArray(schema)) {
+      schema = { fields: schema };
+    }
+
+    //Add null legend to prevent template error
+    schema.legend = schema.legend || null;
+
+    return schema;
+  },
+
+  /**
+   * Returns the field for a given index
+   *
+   * @param {Number} index
+   *
+   * @return {Field}
+   */
+  getFieldAt: function(index) {
+    var key = this.schema.fields[index];
+
+    return this.fields[key];
+  },
+
+  /**
+   * Returns data to pass to template
+   *
+   * @return {Object}
+   */
+  templateData: function() {
+    return this.schema;
+  },
+
+  /**
+   * Renders the fieldset and fields
+   *
+   * @return {Fieldset} this
+   */
+  render: function() {
+    var schema = this.schema,
+        fields = this.fields;
+
+    //Render fieldset
+    var $fieldset = $($.trim(this.template(_.result(this, 'templateData'))));
+
+    //Render fields
+    $fieldset.find('[data-fields]').add($fieldset).each(function(i, el) {
+      var $container = $(el),
+          selection = $container.attr('data-fields');
+
+      if (_.isUndefined(selection)) return;
+
+      _.each(fields, function(field) {
+        $container.append(field.render().el);
+      });
+    });
+
+    this.setElement($fieldset);
+
+    return this;
+  },
+
+  /**
+   * Remove embedded views then self
+   */
+  remove: function() {
+    _.each(this.fields, function(field) {
+      field.remove();
+    });
+
+    Backbone.View.prototype.remove.call(this);
+  }
+
+}, {
+  //STATICS
+
+  template: _.template('\
+    <fieldset class="form-horizontal" data-fields>\
+      <% if (legend) { %>\
+        <legend><%= legend %></legend>\
+      <% } %>\
+    </fieldset>\
+  ', null, Form.templateSettings)
+
+});
+
+
+//==================================================================================================
+//FIELD
+//==================================================================================================
+
+Form.Field = Backbone.View.extend({
+
+  /**
+   * Constructor
+   *
+   * @param {Object} options.key
+   * @param {Object} options.form
+   * @param {Object} [options.schema]
+   * @param {Function} [options.schema.template]
+   * @param {Backbone.Model} [options.model]
+   * @param {Object} [options.value]
+   * @param {String} [options.idPrefix]
+   * @param {Function} [options.template]
+   * @param {Function} [options.errorClassName]
+   */
+  initialize: function(options) {
+    options = options || {};
+
+    //Store important data
+    _.extend(this, _.pick(options, 'form', 'key', 'model', 'value', 
'idPrefix'));
+
+    //Create the full field schema, merging defaults etc.
+    var schema = this.schema = this.createSchema(options.schema);
+
+    //Override defaults
+    this.template = options.template || schema.template || 
this.constructor.template;
+    this.errorClassName = options.errorClassName || 
this.constructor.errorClassName;
+
+    //Create editor
+    this.editor = this.createEditor();
+  },
+
+  /**
+   * Creates the full field schema, merging defaults etc.
+   *
+   * @param {Object|String} schema
+   *
+   * @return {Object}
+   */
+  createSchema: function(schema) {
+    if (_.isString(schema)) schema = { type: schema };
+
+    //Set defaults
+    schema = _.extend({
+      type: 'Text',
+      title: this.createTitle()
+    }, schema);
+
+    //Get the real constructor function i.e. if type is a string such as 'Text'
+    schema.type = (_.isString(schema.type)) ? Form.editors[schema.type] : 
schema.type;
+
+    return schema;
+  },
+
+  /**
+   * Creates the editor specified in the schema; either an editor string name 
or
+   * a constructor function
+   *
+   * @return {View}
+   */
+  createEditor: function() {
+    var options = _.extend(
+      _.pick(this, 'schema', 'form', 'key', 'model', 'value'),
+      { id: this.createEditorId() }
+    );
+
+    var constructorFn = this.schema.type;
+
+    return new constructorFn(options);
+  },
+
+  /**
+   * Creates the ID that will be assigned to the editor
+   *
+   * @return {String}
+   */
+  createEditorId: function() {
+    var prefix = this.idPrefix,
+        id = this.key;
+
+    //Replace periods with underscores (e.g. for when using paths)
+    id = id.replace(/\./g, '_');
+
+    //If a specific ID prefix is set, use it
+    if (_.isString(prefix) || _.isNumber(prefix)) return prefix + id;
+    if (_.isNull(prefix)) return id;
+
+    //Otherwise, if there is a model use it's CID to avoid conflicts when 
multiple forms are on the page
+    if (this.model) return this.model.cid + '_' + id;
+
+    return id;
+  },
+
+  /**
+   * Create the default field title (label text) from the key name.
+   * (Converts 'camelCase' to 'Camel Case')
+   *
+   * @return {String}
+   */
+  createTitle: function() {
+    var str = this.key;
+
+    //Add spaces
+    str = str.replace(/([A-Z])/g, ' $1');
+
+    //Uppercase first character
+    str = str.replace(/^./, function(str) { return str.toUpperCase(); });
+
+    return str;
+  },
+
+  /**
+   * Returns the data to be passed to the template
+   *
+   * @return {Object}
+   */
+  templateData: function() {
+    var schema = this.schema;
+
+    return {
+      help: schema.help || '',
+      title: schema.title,
+      fieldAttrs: schema.fieldAttrs,
+      editorAttrs: schema.editorAttrs,
+      key: this.key,
+      editorId: this.editor.id
+    };
+  },
+
+  /**
+   * Render the field and editor
+   *
+   * @return {Field} self
+   */
+  render: function() {
+    var schema = this.schema,
+        editor = this.editor;
+
+    //Only render the editor if Hidden
+    if (schema.type == Form.editors.Hidden) {
+      return this.setElement(editor.render().el);
+    }
+
+    //Render field
+    var $field = $($.trim(this.template(_.result(this, 'templateData'))));
+
+    if (schema.fieldClass) $field.addClass(schema.fieldClass);
+    if (schema.fieldAttrs) $field.attr(schema.fieldAttrs);
+
+    //Render editor
+    $field.find('[data-editor]').add($field).each(function(i, el) {
+      var $container = $(el),
+          selection = $container.attr('data-editor');
+
+      if (_.isUndefined(selection)) return;
+
+      $container.append(editor.render().el);
+    });
+
+    this.setElement($field);
+
+    return this;
+  },
+
+  /**
+   * Check the validity of the field
+   *
+   * @return {String}
+   */
+  validate: function() {
+    var error = this.editor.validate();
+
+    if (error) {
+      this.setError(error.message);
+    } else {
+      this.clearError();
+    }
+
+    return error;
+  },
+
+  /**
+   * Set the field into an error state, adding the error class and setting the 
error message
+   *
+   * @param {String} msg     Error message
+   */
+  setError: function(msg) {
+    //Nested form editors (e.g. Object) set their errors internally
+    if (this.editor.hasNestedForm) return;
+
+    //Add error CSS class
+    this.$el.addClass(this.errorClassName);
+
+    //Set error message
+    this.$('[data-error]').html(msg);
+  },
+
+  /**
+   * Clear the error state and reset the help message
+   */
+  clearError: function() {
+    //Remove error CSS class
+    this.$el.removeClass(this.errorClassName);
+
+    //Clear error message
+    this.$('[data-error]').empty();
+  },
+
+  /**
+   * Update the model with the new value from the editor
+   *
+   * @return {Mixed}
+   */
+  commit: function() {
+    return this.editor.commit();
+  },
+
+  /**
+   * Get the value from the editor
+   *
+   * @return {Mixed}
+   */
+  getValue: function() {
+    return this.editor.getValue();
+  },
+
+  /**
+   * Set/change the value of the editor
+   *
+   * @param {Mixed} value
+   */
+  setValue: function(value) {
+    this.editor.setValue(value);
+  },
+
+  /**
+   * Give the editor focus
+   */
+  focus: function() {
+    this.editor.focus();
+  },
+
+  /**
+   * Remove focus from the editor
+   */
+  blur: function() {
+    this.editor.blur();
+  },
+
+  /**
+   * Remove the field and editor views
+   */
+  remove: function() {
+    this.editor.remove();
+
+    Backbone.View.prototype.remove.call(this);
+  }
+
+}, {
+  //STATICS
+
+  template: _.template('\
+    <div class="form-group">\
+      <label class="col-sm-3 control-label" align="right" for="<%= editorId 
%>"><%= title %></label>\
+      <div class="col-sm-9">\
+        <span data-editor></span>\
+        <div data-error></div>\
+        <div><%= help %></div>\
+      </div>\
+    </div>\
+  ', null, Form.templateSettings),
+
+  /**
+   * CSS class name added to the field when there is a validation error
+   */
+  errorClassName: 'error'
+
+});
+
+
+//==================================================================================================
+//NESTEDFIELD
+//==================================================================================================
+
+Form.NestedField = Form.Field.extend({
+
+  template: _.template($.trim('\
+    <div>\
+      <span data-editor></span>\
+      <% if (help) { %>\
+        <div><%= help %></div>\
+      <% } %>\
+      <div data-error></div>\
+    </div>\
+  '), null, Form.templateSettings)
+
+});
+
+/**
+ * Base editor (interface). To be extended, not used directly
+ *
+ * @param {Object} options
+ * @param {String} [options.id]         Editor ID
+ * @param {Model} [options.model]       Use instead of value, and use commit()
+ * @param {String} [options.key]        The model attribute key. Required when 
using 'model'
+ * @param {Mixed} [options.value]       When not using a model. If neither 
provided, defaultValue will be used
+ * @param {Object} [options.schema]     Field schema; may be required by some 
editors
+ * @param {Object} [options.validators] Validators; falls back to those stored 
on schema
+ * @param {Object} [options.form]       The form
+ */
+Form.Editor = Form.editors.Base = Backbone.View.extend({
+
+  defaultValue: null,
+
+  hasFocus: false,
+
+  initialize: function(options) {
+    var options = options || {};
+
+    //Set initial value
+    if (options.model) {
+      if (!options.key) throw new Error("Missing option: 'key'");
+
+      this.model = options.model;
+
+      this.value = this.model.get(options.key);
+    }
+    else if (options.value !== undefined) {
+      this.value = options.value;
+    }
+
+    if (this.value === undefined) this.value = this.defaultValue;
+
+    //Store important data
+    _.extend(this, _.pick(options, 'key', 'form'));
+
+    var schema = this.schema = options.schema || {};
+
+    this.validators = options.validators || schema.validators;
+
+    //Main attributes
+    this.$el.attr('id', this.id);
+    this.$el.attr('name', this.getName());
+    if (schema.editorClass) this.$el.addClass(schema.editorClass);
+    if (schema.editorAttrs) this.$el.attr(schema.editorAttrs);
+  },
+
+  /**
+   * Get the value for the form input 'name' attribute
+   *
+   * @return {String}
+   *
+   * @api private
+   */
+  getName: function() {
+    var key = this.key || '';
+
+    //Replace periods with underscores (e.g. for when using paths)
+    return key.replace(/\./g, '_');
+  },
+
+  /**
+   * Get editor value
+   * Extend and override this method to reflect changes in the DOM
+   *
+   * @return {Mixed}
+   */
+  getValue: function() {
+    return this.value;
+  },
+
+  /**
+   * Set editor value
+   * Extend and override this method to reflect changes in the DOM
+   *
+   * @param {Mixed} value
+   */
+  setValue: function(value) {
+    this.value = value;
+  },
+
+  /**
+   * Give the editor focus
+   * Extend and override this method
+   */
+  focus: function() {
+    throw new Error('Not implemented');
+  },
+
+  /**
+   * Remove focus from the editor
+   * Extend and override this method
+   */
+  blur: function() {
+    throw new Error('Not implemented');
+  },
+
+  /**
+   * Update the model with the current value
+   *
+   * @param {Object} [options]              Options to pass to model.set()
+   * @param {Boolean} [options.validate]    Set to true to trigger built-in 
model validation
+   *
+   * @return {Mixed} error
+   */
+  commit: function(options) {
+    var error = this.validate();
+    if (error) return error;
+
+    this.listenTo(this.model, 'invalid', function(model, e) {
+      error = e;
+    });
+    this.model.set(this.key, this.getValue(), options);
+
+    if (error) return error;
+  },
+
+  /**
+   * Check validity
+   *
+   * @return {Object|Undefined}
+   */
+  validate: function() {
+    var $el = this.$el,
+        error = null,
+        value = this.getValue(),
+        formValues = this.form ? this.form.getValue() : {},
+        validators = this.validators,
+        getValidator = this.getValidator;
+
+    if (validators) {
+      //Run through validators until an error is found
+      _.every(validators, function(validator) {
+        error = getValidator(validator)(value, formValues);
+
+        return error ? false : true;
+      });
+    }
+
+    return error;
+  },
+
+  /**
+   * Set this.hasFocus, or call parent trigger()
+   *
+   * @param {String} event
+   */
+  trigger: function(event) {
+    if (event === 'focus') {
+      this.hasFocus = true;
+    }
+    else if (event === 'blur') {
+      this.hasFocus = false;
+    }
+
+    return Backbone.View.prototype.trigger.apply(this, arguments);
+  },
+
+  /**
+   * Returns a validation function based on the type defined in the schema
+   *
+   * @param {RegExp|String|Function} validator
+   * @return {Function}
+   */
+  getValidator: function(validator) {
+    var validators = Form.validators;
+
+    //Convert regular expressions to validators
+    if (_.isRegExp(validator)) {
+      return validators.regexp({ regexp: validator });
+    }
+
+    //Use a built-in validator if given a string
+    if (_.isString(validator)) {
+      if (!validators[validator]) throw new Error('Validator "'+validator+'" 
not found');
+
+      return validators[validator]();
+    }
+
+    //Functions can be used directly
+    if (_.isFunction(validator)) return validator;
+
+    //Use a customised built-in validator if given an object
+    if (_.isObject(validator) && validator.type) {
+      var config = validator;
+
+      return validators[config.type](config);
+    }
+
+    //Unkown validator type
+    throw new Error('Invalid validator: ' + validator);
+  }
+});
+
+/**
+ * Text
+ *
+ * Text input with focus, blur and change events
+ */
+Form.editors.Text = Form.Editor.extend({
+
+  tagName: 'input',
+
+  defaultValue: '',
+
+  previousValue: '',
+
+  events: {
+    'keyup':    'determineChange',
+    'keypress': function(event) {
+      var self = this;
+      setTimeout(function() {
+        self.determineChange();
+      }, 0);
+    },
+    'select':   function(event) {
+      this.trigger('select', this);
+    },
+    'focus':    function(event) {
+      this.trigger('focus', this);
+    },
+    'blur':     function(event) {
+      this.trigger('blur', this);
+    }
+  },
+
+  initialize: function(options) {
+    Form.editors.Base.prototype.initialize.call(this, options);
+
+    var schema = this.schema;
+
+    //Allow customising text type (email, phone etc.) for HTML5 browsers
+    var type = 'text';
+
+    if (schema && schema.editorAttrs && schema.editorAttrs.type) type = 
schema.editorAttrs.type;
+    if (schema && schema.dataType) type = schema.dataType;
+
+    this.$el.attr('type', type);
+  },
+
+  /**
+   * Adds the editor to the DOM
+   */
+  render: function() {
+    this.setValue(this.value);
+
+    return this;
+  },
+
+  determineChange: function(event) {
+    var currentValue = this.$el.val();
+    var changed = (currentValue !== this.previousValue);
+
+    if (changed) {
+      this.previousValue = currentValue;
+
+      this.trigger('change', this);
+    }
+  },
+
+  /**
+   * Returns the current editor value
+   * @return {String}
+   */
+  getValue: function() {
+    return this.$el.val();
+  },
+
+  /**
+   * Sets the value of the form element
+   * @param {String}
+   */
+  setValue: function(value) {
+    this.$el.val(value);
+  },
+
+  focus: function() {
+    if (this.hasFocus) return;
+
+    this.$el.focus();
+  },
+
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    this.$el.blur();
+  },
+
+  select: function() {
+    this.$el.select();
+  }
+
+});
+
+/**
+ * TextArea editor
+ */
+Form.editors.TextArea = Form.editors.Text.extend({
+
+  tagName: 'textarea',
+
+  /**
+   * Override Text constructor so type property isn't set (issue #261)
+   */
+  initialize: function(options) {
+    Form.editors.Base.prototype.initialize.call(this, options);
+  }
+
+});
+
+/**
+ * Password editor
+ */
+Form.editors.Password = Form.editors.Text.extend({
+
+  initialize: function(options) {
+    Form.editors.Text.prototype.initialize.call(this, options);
+
+    this.$el.attr('type', 'password');
+  }
+
+});
+
+/**
+ * NUMBER
+ *
+ * Normal text input that only allows a number. Letters etc. are not entered.
+ */
+Form.editors.Number = Form.editors.Text.extend({
+
+  defaultValue: 0,
+
+  events: _.extend({}, Form.editors.Text.prototype.events, {
+    'keypress': 'onKeyPress',
+    'change': 'onKeyPress'
+  }),
+
+  initialize: function(options) {
+    Form.editors.Text.prototype.initialize.call(this, options);
+
+    var schema = this.schema;
+
+    this.$el.attr('type', 'number');
+
+    if (!schema || !schema.editorAttrs || !schema.editorAttrs.step) {
+      // provide a default for `step` attr,
+      // but don't overwrite if already specified
+      this.$el.attr('step', 'any');
+    }
+    this.$el.attr('class','form-control');
+    this.$el.attr('min','0');
+  },
+
+  /**
+   * Check value is numeric
+   */
+  onKeyPress: function(event) {
+    var self = this,
+        delayedDetermineChange = function() {
+          setTimeout(function() {
+            self.determineChange();
+          }, 0);
+        };
+
+    //Allow backspace
+    if (event.charCode === 0) {
+      delayedDetermineChange();
+      return;
+    }
+
+    //Get the whole new value so that we can prevent things like double 
decimals points etc.
+    var newVal = this.$el.val()
+    if( event.charCode != undefined ) {
+      newVal = newVal + String.fromCharCode(event.charCode);
+    }
+
+    var numeric = /^[0-9]*\.?[0-9]*?$/.test(newVal);
+
+    if (numeric) {
+      delayedDetermineChange();
+    }
+    else {
+      event.preventDefault();
+    }
+  },
+
+  getValue: function() {
+    var value = this.$el.val();
+
+    return value === "" ? null : parseFloat(value, 10);
+  },
+
+  setValue: function(value) {
+    value = (function() {
+      if (_.isNumber(value)) return value;
+
+      if (_.isString(value) && value !== '') return parseFloat(value, 10);
+
+      return null;
+    })();
+
+    if (_.isNaN(value)) value = null;
+
+    Form.editors.Text.prototype.setValue.call(this, value);
+  }
+
+});
+
+/**
+ * Hidden editor
+ */
+Form.editors.Hidden = Form.editors.Text.extend({
+
+  defaultValue: '',
+
+  initialize: function(options) {
+    Form.editors.Text.prototype.initialize.call(this, options);
+
+    this.$el.attr('type', 'hidden');
+  },
+
+  focus: function() {
+
+  },
+
+  blur: function() {
+
+  }
+
+});
+
+/**
+ * Checkbox editor
+ *
+ * Creates a single checkbox, i.e. boolean value
+ */
+Form.editors.Checkbox = Form.editors.Base.extend({
+
+  defaultValue: false,
+
+  tagName: 'input',
+
+  events: {
+    'click':  function(event) {
+      this.trigger('change', this);
+    },
+    'focus':  function(event) {
+      this.trigger('focus', this);
+    },
+    'blur':   function(event) {
+      this.trigger('blur', this);
+    }
+  },
+
+  initialize: function(options) {
+    Form.editors.Base.prototype.initialize.call(this, options);
+
+    this.$el.attr('type', 'checkbox');
+  },
+
+  /**
+   * Adds the editor to the DOM
+   */
+  render: function() {
+    this.setValue(this.value);
+
+    return this;
+  },
+
+  getValue: function() {
+    return this.$el.prop('checked');
+  },
+
+  setValue: function(value) {
+    if (value) {
+      this.$el.prop('checked', true);
+    }else{
+      this.$el.prop('checked', false);
+    }
+  },
+
+  focus: function() {
+    if (this.hasFocus) return;
+
+    this.$el.focus();
+  },
+
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    this.$el.blur();
+  }
+
+});
+
+/**
+ * Select editor
+ *
+ * Renders a <select> with given options
+ *
+ * Requires an 'options' value on the schema.
+ *  Can be an array of options, a function that calls back with the array of 
options, a string of HTML
+ *  or a Backbone collection. If a collection, the models must implement a 
toString() method
+ */
+Form.editors.Select = Form.editors.Base.extend({
+
+  tagName: 'select',
+
+  events: {
+    'change': function(event) {
+      this.trigger('change', this);
+    },
+    'focus':  function(event) {
+      this.trigger('focus', this);
+    },
+    'blur':   function(event) {
+      this.trigger('blur', this);
+    }
+  },
+
+  initialize: function(options) {
+    Form.editors.Base.prototype.initialize.call(this, options);
+
+    if (!this.schema || !this.schema.options) throw new Error("Missing 
required 'schema.options'");
+  },
+
+  render: function() {
+    this.setOptions(this.schema.options);
+
+    return this;
+  },
+
+  /**
+   * Sets the options that populate the <select>
+   *
+   * @param {Mixed} options
+   */
+  setOptions: function(options) {
+    var self = this;
+
+    //If a collection was passed, check if it needs fetching
+    if (options instanceof Backbone.Collection) {
+      var collection = options;
+
+      //Don't do the fetch if it's already populated
+      if (collection.length > 0) {
+        this.renderOptions(options);
+      } else {
+        collection.fetch({
+          success: function(collection) {
+            self.renderOptions(options);
+          }
+        });
+      }
+    }
+
+    //If a function was passed, run it to get the options
+    else if (_.isFunction(options)) {
+      options(function(result) {
+        self.renderOptions(result);
+      }, self);
+    }
+
+    //Otherwise, ready to go straight to renderOptions
+    else {
+      this.renderOptions(options);
+    }
+  },
+
+  /**
+   * Adds the <option> html to the DOM
+   * @param {Mixed}   Options as a simple array e.g. ['option1', 'option2']
+   *                      or as an array of objects e.g. [{val: 543, label: 
'Title for object 543'}]
+   *                      or as a string of <option> HTML to insert into the 
<select>
+   *                      or any object
+   */
+  renderOptions: function(options) {
+    var $select = this.$el,
+        html;
+
+    html = this._getOptionsHtml(options);
+
+    //Insert options
+    $select.html(html);
+
+    //Select correct option
+    this.setValue(this.value);
+  },
+
+  _getOptionsHtml: function(options) {
+    var html;
+    //Accept string of HTML
+    if (_.isString(options)) {
+      html = options;
+    }
+
+    //Or array
+    else if (_.isArray(options)) {
+      html = this._arrayToHtml(options);
+    }
+
+    //Or Backbone collection
+    else if (options instanceof Backbone.Collection) {
+      html = this._collectionToHtml(options);
+    }
+
+    else if (_.isFunction(options)) {
+      var newOptions;
+
+      options(function(opts) {
+        newOptions = opts;
+      }, this);
+
+      html = this._getOptionsHtml(newOptions);
+    //Or any object
+    }else{
+      html=this._objectToHtml(options);
+    }
+
+    return html;
+  },
+
+
+  getValue: function() {
+    return this.$el.val();
+  },
+
+  setValue: function(value) {
+    this.$el.val(value);
+  },
+
+  focus: function() {
+    if (this.hasFocus) return;
+
+    this.$el.focus();
+  },
+
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    this.$el.blur();
+  },
+
+  /**
+   * Transforms a collection into HTML ready to use in the renderOptions method
+   * @param {Backbone.Collection}
+   * @return {String}
+   */
+  _collectionToHtml: function(collection) {
+    //Convert collection to array first
+    var array = [];
+    collection.each(function(model) {
+      array.push({ val: model.id, label: model.toString() });
+    });
+
+    //Now convert to HTML
+    var html = this._arrayToHtml(array);
+
+    return html;
+  },
+  /**
+   * Transforms an object into HTML ready to use in the renderOptions method
+   * @param {Object}
+   * @return {String}
+   */
+  _objectToHtml: function(obj) {
+    //Convert object to array first
+    var array = [];
+    for(var key in obj){
+      if( obj.hasOwnProperty( key ) ) {
+        array.push({ val: key, label: obj[key] });
+      }
+    }
+
+    //Now convert to HTML
+    var html = this._arrayToHtml(array);
+
+    return html;
+  },
+
+
+
+  /**
+   * Create the <option> HTML
+   * @param {Array}   Options as a simple array e.g. ['option1', 'option2']
+   *                      or as an array of objects e.g. [{val: 543, label: 
'Title for object 543'}]
+   * @return {String} HTML
+   */
+  _arrayToHtml: function(array) {
+    var html = [];
+
+    //Generate HTML
+    _.each(array, function(option) {
+      if (_.isObject(option)) {
+        if (option.group) {
+          html.push('<optgroup label="'+option.group+'">');
+          html.push(this._getOptionsHtml(option.options))
+          html.push('</optgroup>');
+        } else {
+          var val = (option.val || option.val === 0) ? option.val : '';
+          html.push('<option value="'+val+'">'+option.label+'</option>');
+        }
+      }
+      else {
+        html.push('<option>'+option+'</option>');
+      }
+    }, this);
+
+    return html.join('');
+  }
+
+});
+
+/**
+ * Radio editor
+ *
+ * Renders a <ul> with given options represented as <li> objects containing 
radio buttons
+ *
+ * Requires an 'options' value on the schema.
+ *  Can be an array of options, a function that calls back with the array of 
options, a string of HTML
+ *  or a Backbone collection. If a collection, the models must implement a 
toString() method
+ */
+Form.editors.Radio = Form.editors.Select.extend({
+
+  tagName: 'ul',
+
+  events: {
+    'change input[type=radio]': function() {
+      this.trigger('change', this);
+    },
+    'focus input[type=radio]': function() {
+      if (this.hasFocus) return;
+      this.trigger('focus', this);
+    },
+    'blur input[type=radio]': function() {
+      if (!this.hasFocus) return;
+      var self = this;
+      setTimeout(function() {
+        if (self.$('input[type=radio]:focus')[0]) return;
+        self.trigger('blur', self);
+      }, 0);
+    }
+  },
+
+  getValue: function() {
+    return this.$('input[type=radio]:checked').val();
+  },
+
+  setValue: function(value) {
+    this.$('input[type=radio]').val([value]);
+  },
+
+  focus: function() {
+    if (this.hasFocus) return;
+
+    var checked = this.$('input[type=radio]:checked');
+    if (checked[0]) {
+      checked.focus();
+      return;
+    }
+
+    this.$('input[type=radio]').first().focus();
+  },
+
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    this.$('input[type=radio]:focus').blur();
+  },
+
+  /**
+   * Create the radio list HTML
+   * @param {Array}   Options as a simple array e.g. ['option1', 'option2']
+   *                      or as an array of objects e.g. [{val: 543, label: 
'Title for object 543'}]
+   * @return {String} HTML
+   */
+  _arrayToHtml: function (array) {
+    var html = [];
+    var self = this;
+
+    _.each(array, function(option, index) {
+      var itemHtml = '<li>';
+      if (_.isObject(option)) {
+        var val = (option.val || option.val === 0) ? option.val : '';
+        itemHtml += ('<input type="radio" name="'+self.getName()+'" 
value="'+val+'" id="'+self.id+'-'+index+'" />');
+        itemHtml += ('<label 
for="'+self.id+'-'+index+'">'+option.label+'</label>');
+      }
+      else {
+        itemHtml += ('<input type="radio" name="'+self.getName()+'" 
value="'+option+'" id="'+self.id+'-'+index+'" />');
+        itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
+      }
+      itemHtml += '</li>';
+      html.push(itemHtml);
+    });
+
+    return html.join('');
+  }
+
+});
+
+/**
+ * Checkboxes editor
+ *
+ * Renders a <ul> with given options represented as <li> objects containing 
checkboxes
+ *
+ * Requires an 'options' value on the schema.
+ *  Can be an array of options, a function that calls back with the array of 
options, a string of HTML
+ *  or a Backbone collection. If a collection, the models must implement a 
toString() method
+ */
+Form.editors.Checkboxes = Form.editors.Select.extend({
+
+  tagName: 'ul',
+
+  groupNumber: 0,
+
+  events: {
+    'click input[type=checkbox]': function() {
+      this.trigger('change', this);
+    },
+    'focus input[type=checkbox]': function() {
+      if (this.hasFocus) return;
+      this.trigger('focus', this);
+    },
+    'blur input[type=checkbox]':  function() {
+      if (!this.hasFocus) return;
+      var self = this;
+      setTimeout(function() {
+        if (self.$('input[type=checkbox]:focus')[0]) return;
+        self.trigger('blur', self);
+      }, 0);
+    }
+  },
+
+  getValue: function() {
+    var values = [];
+    this.$('input[type=checkbox]:checked').each(function() {
+      values.push($(this).val());
+    });
+    return values;
+  },
+
+  setValue: function(values) {
+    if (!_.isArray(values)) values = [values];
+    this.$('input[type=checkbox]').val(values);
+  },
+
+  focus: function() {
+    if (this.hasFocus) return;
+
+    this.$('input[type=checkbox]').first().focus();
+  },
+
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    this.$('input[type=checkbox]:focus').blur();
+  },
+
+  /**
+   * Create the checkbox list HTML
+   * @param {Array}   Options as a simple array e.g. ['option1', 'option2']
+   *                      or as an array of objects e.g. [{val: 543, label: 
'Title for object 543'}]
+   * @return {String} HTML
+   */
+  _arrayToHtml: function (array) {
+    var html = [];
+    var self = this;
+
+    _.each(array, function(option, index) {
+      var itemHtml = '<li>';
+                       var close = true;
+      if (_.isObject(option)) {
+        if (option.group) {
+          var originalId = self.id;
+          self.id += "-" + self.groupNumber++;
+          itemHtml = ('<fieldset class="group"> 
<legend>'+option.group+'</legend>');
+          itemHtml += (self._arrayToHtml(option.options));
+          itemHtml += ('</fieldset>');
+          self.id = originalId;
+                                       close = false;
+        }else{
+          var val = (option.val || option.val === 0) ? option.val : '';
+          itemHtml += ('<input type="checkbox" name="'+self.getName()+'" 
value="'+val+'" id="'+self.id+'-'+index+'" />');
+          itemHtml += ('<label 
for="'+self.id+'-'+index+'">'+option.label+'</label>');
+        }
+      }
+      else {
+        itemHtml += ('<input type="checkbox" name="'+self.getName()+'" 
value="'+option+'" id="'+self.id+'-'+index+'" />');
+        itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
+      }
+                       if(close){
+                               itemHtml += '</li>';
+                       }
+      html.push(itemHtml);
+    });
+
+    return html.join('');
+  }
+
+});
+
+/**
+ * Object editor
+ *
+ * Creates a child form. For editing Javascript objects
+ *
+ * @param {Object} options
+ * @param {Form} options.form                 The form this editor belongs to; 
used to determine the constructor for the nested form
+ * @param {Object} options.schema             The schema for the object
+ * @param {Object} options.schema.subSchema   The schema for the nested form
+ */
+Form.editors.Object = Form.editors.Base.extend({
+  //Prevent error classes being set on the main control; they are internally 
on the individual fields
+  hasNestedForm: true,
+
+  initialize: function(options) {
+    //Set default value for the instance so it's not a shared object
+    this.value = {};
+
+    //Init
+    Form.editors.Base.prototype.initialize.call(this, options);
+
+    //Check required options
+    if (!this.form) throw new Error('Missing required option "form"');
+    if (!this.schema.subSchema) throw new Error("Missing required 
'schema.subSchema' option for Object editor");
+  },
+
+  render: function() {
+    //Get the constructor for creating the nested form; i.e. the same 
constructor as used by the parent form
+    var NestedForm = this.form.constructor;
+
+    //Create the nested form
+    this.nestedForm = new NestedForm({
+      schema: this.schema.subSchema,
+      data: this.value,
+      idPrefix: this.id + '_',
+      Field: NestedForm.NestedField
+    });
+
+    this._observeFormEvents();
+
+    this.$el.html(this.nestedForm.render().el);
+
+    if (this.hasFocus) this.trigger('blur', this);
+
+    return this;
+  },
+
+  getValue: function() {
+    if (this.nestedForm) return this.nestedForm.getValue();
+
+    return this.value;
+  },
+
+  setValue: function(value) {
+    this.value = value;
+
+    this.render();
+  },
+
+  focus: function() {
+    if (this.hasFocus) return;
+
+    this.nestedForm.focus();
+  },
+
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    this.nestedForm.blur();
+  },
+
+  remove: function() {
+    this.nestedForm.remove();
+
+    Backbone.View.prototype.remove.call(this);
+  },
+
+  validate: function() {
+    return this.nestedForm.validate();
+  },
+
+  _observeFormEvents: function() {
+    if (!this.nestedForm) return;
+
+    this.nestedForm.on('all', function() {
+      // args = ["key:change", form, fieldEditor]
+      var args = _.toArray(arguments);
+      args[1] = this;
+      // args = ["key:change", this=objectEditor, fieldEditor]
+
+      this.trigger.apply(this, args);
+    }, this);
+  }
+
+});
+
+/**
+ * NestedModel editor
+ *
+ * Creates a child form. For editing nested Backbone models
+ *
+ * Special options:
+ *   schema.model:   Embedded model constructor
+ */
+Form.editors.NestedModel = Form.editors.Object.extend({
+  initialize: function(options) {
+    Form.editors.Base.prototype.initialize.call(this, options);
+
+    if (!this.form) throw new Error('Missing required option "form"');
+    if (!options.schema.model) throw new Error('Missing required 
"schema.model" option for NestedModel editor');
+  },
+
+  render: function() {
+    //Get the constructor for creating the nested form; i.e. the same 
constructor as used by the parent form
+    var NestedForm = this.form.constructor;
+
+    var data = this.value || {},
+        key = this.key,
+        nestedModel = this.schema.model;
+
+    //Wrap the data in a model if it isn't already a model instance
+    var modelInstance = (data.constructor === nestedModel) ? data : new 
nestedModel(data);
+
+    this.nestedForm = new NestedForm({
+      model: modelInstance,
+      idPrefix: this.id + '_',
+      fieldTemplate: 'nestedField'
+    });
+
+    this._observeFormEvents();
+
+    //Render form
+    this.$el.html(this.nestedForm.render().el);
+
+    if (this.hasFocus) this.trigger('blur', this);
+
+    return this;
+  },
+
+  /**
+   * Update the embedded model, checking for nested validation errors and pass 
them up
+   * Then update the main model if all OK
+   *
+   * @return {Error|null} Validation error or null
+   */
+  commit: function() {
+    var error = this.nestedForm.commit();
+    if (error) {
+      this.$el.addClass('error');
+      return error;
+    }
+
+    return Form.editors.Object.prototype.commit.call(this);
+  }
+
+});
+
+/**
+ * Date editor
+ *
+ * Schema options
+ * @param {Number|String} [options.schema.yearStart]  First year in list. 
Default: 100 years ago
+ * @param {Number|String} [options.schema.yearEnd]    Last year in list. 
Default: current year
+ *
+ * Config options (if not set, defaults to options stored on the main Date 
class)
+ * @param {Boolean} [options.showMonthNames]  Use month names instead of 
numbers. Default: true
+ * @param {String[]} [options.monthNames]     Month names. Default: Full 
English names
+ */
+Form.editors.Date = Form.editors.Base.extend({
+
+  events: {
+    'change select':  function() {
+      this.updateHidden();
+      this.trigger('change', this);
+    },
+    'focus select':   function() {
+      if (this.hasFocus) return;
+      this.trigger('focus', this);
+    },
+    'blur select':    function() {
+      if (!this.hasFocus) return;
+      var self = this;
+      setTimeout(function() {
+        if (self.$('select:focus')[0]) return;
+        self.trigger('blur', self);
+      }, 0);
+    }
+  },
+
+  initialize: function(options) {
+    options = options || {};
+
+    Form.editors.Base.prototype.initialize.call(this, options);
+
+    var Self = Form.editors.Date,
+        today = new Date();
+
+    //Option defaults
+    this.options = _.extend({
+      monthNames: Self.monthNames,
+      showMonthNames: Self.showMonthNames
+    }, options);
+
+    //Schema defaults
+    this.schema = _.extend({
+      yearStart: today.getFullYear() - 100,
+      yearEnd: today.getFullYear()
+    }, options.schema || {});
+
+    //Cast to Date
+    if (this.value && !_.isDate(this.value)) {
+      this.value = new Date(this.value);
+    }
+
+    //Set default date
+    if (!this.value) {
+      var date = new Date();
+      date.setSeconds(0);
+      date.setMilliseconds(0);
+
+      this.value = date;
+    }
+
+    //Template
+    this.template = options.template || this.constructor.template;
+  },
+
+  render: function() {
+    var options = this.options,
+        schema = this.schema;
+
+    var datesOptions = _.map(_.range(1, 32), function(date) {
+      return '<option value="'+date+'">' + date + '</option>';
+    });
+
+    var monthsOptions = _.map(_.range(0, 12), function(month) {
+      var value = (options.showMonthNames)
+          ? options.monthNames[month]
+          : (month + 1);
+
+      return '<option value="'+month+'">' + value + '</option>';
+    });
+
+    var yearRange = (schema.yearStart < schema.yearEnd)
+      ? _.range(schema.yearStart, schema.yearEnd + 1)
+      : _.range(schema.yearStart, schema.yearEnd - 1, -1);
+
+    var yearsOptions = _.map(yearRange, function(year) {
+      return '<option value="'+year+'">' + year + '</option>';
+    });
+
+    //Render the selects
+    var $el = $($.trim(this.template({
+      dates: datesOptions.join(''),
+      months: monthsOptions.join(''),
+      years: yearsOptions.join('')
+    })));
+
+    //Store references to selects
+    this.$date = $el.find('[data-type="date"]');
+    this.$month = $el.find('[data-type="month"]');
+    this.$year = $el.find('[data-type="year"]');
+
+    //Create the hidden field to store values in case POSTed to server
+    this.$hidden = $('<input type="hidden" name="'+this.key+'" />');
+    $el.append(this.$hidden);
+
+    //Set value on this and hidden field
+    this.setValue(this.value);
+
+    //Remove the wrapper tag
+    this.setElement($el);
+    this.$el.attr('id', this.id);
+    this.$el.attr('name', this.getName());
+
+    if (this.hasFocus) this.trigger('blur', this);
+
+    return this;
+  },
+
+  /**
+   * @return {Date}   Selected date
+   */
+  getValue: function() {
+    var year = this.$year.val(),
+        month = this.$month.val(),
+        date = this.$date.val();
+
+    if (!year || !month || !date) return null;
+
+    return new Date(year, month, date);
+  },
+
+  /**
+   * @param {Date} date
+   */
+  setValue: function(date) {
+    this.$date.val(date.getDate());
+    this.$month.val(date.getMonth());
+    this.$year.val(date.getFullYear());
+
+    this.updateHidden();
+  },
+
+  focus: function() {
+    if (this.hasFocus) return;
+
+    this.$('select').first().focus();
+  },
+
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    this.$('select:focus').blur();
+  },
+
+  /**
+   * Update the hidden input which is maintained for when submitting a form
+   * via a normal browser POST
+   */
+  updateHidden: function() {
+    var val = this.getValue();
+
+    if (_.isDate(val)) val = val.toISOString();
+
+    this.$hidden.val(val);
+  }
+
+}, {
+  //STATICS
+  template: _.template('\
+    <div>\
+      <select data-type="date"><%= dates %></select>\
+      <select data-type="month"><%= months %></select>\
+      <select data-type="year"><%= years %></select>\
+    </div>\
+  ', null, Form.templateSettings),
+
+  //Whether to show month names instead of numbers
+  showMonthNames: true,
+
+  //Month names to use if showMonthNames is true
+  //Replace for localisation, e.g. Form.editors.Date.monthNames = ['Janvier', 
'Fevrier'...]
+  monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 
'August', 'September', 'October', 'November', 'December']
+});
+
+/**
+ * DateTime editor
+ *
+ * @param {Editor} [options.DateEditor]           Date editor view to use (not 
definition)
+ * @param {Number} [options.schema.minsInterval]  Interval between minutes. 
Default: 15
+ */
+Form.editors.DateTime = Form.editors.Base.extend({
+
+  events: {
+    'change select':  function() {
+      this.updateHidden();
+      this.trigger('change', this);
+    },
+    'focus select':   function() {
+      if (this.hasFocus) return;
+      this.trigger('focus', this);
+    },
+    'blur select':    function() {
+      if (!this.hasFocus) return;
+      var self = this;
+      setTimeout(function() {
+        if (self.$('select:focus')[0]) return;
+        self.trigger('blur', self);
+      }, 0);
+    }
+  },
+
+  initialize: function(options) {
+    options = options || {};
+
+    Form.editors.Base.prototype.initialize.call(this, options);
+
+    //Option defaults
+    this.options = _.extend({
+      DateEditor: Form.editors.DateTime.DateEditor
+    }, options);
+
+    //Schema defaults
+    this.schema = _.extend({
+      minsInterval: 15
+    }, options.schema || {});
+
+    //Create embedded date editor
+    this.dateEditor = new this.options.DateEditor(options);
+
+    this.value = this.dateEditor.value;
+
+    //Template
+    this.template = options.template || this.constructor.template;
+  },
+
+  render: function() {
+    function pad(n) {
+      return n < 10 ? '0' + n : n;
+    }
+
+    var schema = this.schema;
+
+    //Create options
+    var hoursOptions = _.map(_.range(0, 24), function(hour) {
+      return '<option value="'+hour+'">' + pad(hour) + '</option>';
+    });
+
+    var minsOptions = _.map(_.range(0, 60, schema.minsInterval), function(min) 
{
+      return '<option value="'+min+'">' + pad(min) + '</option>';
+    });
+
+    //Render time selects
+    var $el = $($.trim(this.template({
+      hours: hoursOptions.join(),
+      mins: minsOptions.join()
+    })));
+
+    //Include the date editor
+    $el.find('[data-date]').append(this.dateEditor.render().el);
+
+    //Store references to selects
+    this.$hour = $el.find('select[data-type="hour"]');
+    this.$min = $el.find('select[data-type="min"]');
+
+    //Get the hidden date field to store values in case POSTed to server
+    this.$hidden = $el.find('input[type="hidden"]');
+
+    //Set time
+    this.setValue(this.value);
+
+    this.setElement($el);
+    this.$el.attr('id', this.id);
+    this.$el.attr('name', this.getName());
+
+    if (this.hasFocus) this.trigger('blur', this);
+
+    return this;
+  },
+
+  /**
+   * @return {Date}   Selected datetime
+   */
+  getValue: function() {
+    var date = this.dateEditor.getValue();
+
+    var hour = this.$hour.val(),
+        min = this.$min.val();
+
+    if (!date || !hour || !min) return null;
+
+    date.setHours(hour);
+    date.setMinutes(min);
+
+    return date;
+  },
+
+  /**
+   * @param {Date}
+   */
+  setValue: function(date) {
+    if (!_.isDate(date)) date = new Date(date);
+
+    this.dateEditor.setValue(date);
+
+    this.$hour.val(date.getHours());
+    this.$min.val(date.getMinutes());
+
+    this.updateHidden();
+  },
+
+  focus: function() {
+    if (this.hasFocus) return;
+
+    this.$('select').first().focus();
+  },
+
+  blur: function() {
+    if (!this.hasFocus) return;
+
+    this.$('select:focus').blur();
+  },
+
+  /**
+   * Update the hidden input which is maintained for when submitting a form
+   * via a normal browser POST
+   */
+  updateHidden: function() {
+    var val = this.getValue();
+    if (_.isDate(val)) val = val.toISOString();
+
+    this.$hidden.val(val);
+  },
+
+  /**
+   * Remove the Date editor before removing self
+   */
+  remove: function() {
+    this.dateEditor.remove();
+
+    Form.editors.Base.prototype.remove.call(this);
+  }
+
+}, {
+  //STATICS
+  template: _.template('\
+    <div class="bbf-datetime">\
+      <div class="bbf-date-container" data-date></div>\
+      <select data-type="hour"><%= hours %></select>\
+      :\
+      <select data-type="min"><%= mins %></select>\
+    </div>\
+  ', null, Form.templateSettings),
+
+  //The date editor to use (constructor function, not instance)
+  DateEditor: Form.editors.Date
+});
+
+
+
+  //Metadata
+  Form.VERSION = '0.13.0';
+
+
+  //Exports
+  Backbone.Form = Form;
+  if (typeof exports !== 'undefined') exports = Form;
+
+})(window || global || this);

Reply via email to