Browse Source

[search] Admin UI

First push with template editing
Enrico Berti 12 years ago
parent
commit
81cd0ac
26 changed files with 2094 additions and 408 deletions
  1. 1 1
      apps/search/src/search/models.py
  2. 80 10
      apps/search/src/search/templates/admin.mako
  3. 0 49
      apps/search/src/search/templates/admin_core.mako
  4. 73 65
      apps/search/src/search/templates/admin_core_facets.mako
  5. 148 0
      apps/search/src/search/templates/admin_core_properties.mako
  6. 0 44
      apps/search/src/search/templates/admin_core_result.mako
  7. 64 0
      apps/search/src/search/templates/admin_core_schema.mako
  8. 120 15
      apps/search/src/search/templates/admin_core_sorting.mako
  9. 173 0
      apps/search/src/search/templates/admin_core_template.mako
  10. 0 36
      apps/search/src/search/templates/admin_settings.mako
  11. 79 0
      apps/search/src/search/templates/layout.mako
  12. 0 53
      apps/search/src/search/templates/navigation_bar_admin.mako
  13. 3 3
      apps/search/src/search/urls.py
  14. 47 34
      apps/search/src/search/views.py
  15. 17 0
      apps/search/static/css/admin.css
  16. 110 98
      apps/search/static/css/search.css
  17. 11 0
      desktop/core/static/ext/css/bootstrap-select.css
  18. 13 0
      desktop/core/static/ext/css/freshereditor.css
  19. 51 0
      desktop/core/static/ext/farbtastic/farbtastic.css
  20. 345 0
      desktop/core/static/ext/farbtastic/farbtastic.js
  21. BIN
      desktop/core/static/ext/farbtastic/marker.png
  22. BIN
      desktop/core/static/ext/farbtastic/mask.png
  23. BIN
      desktop/core/static/ext/farbtastic/wheel.png
  24. 43 0
      desktop/core/static/ext/js/bootstrap-select.js
  25. 490 0
      desktop/core/static/ext/js/freshereditor.min.js
  26. 226 0
      desktop/core/static/ext/js/shortcut.js

+ 1 - 1
apps/search/src/search/models.py

@@ -127,7 +127,7 @@ class Core(models.Model):
     return self.facets.get_query_params()
 
   def get_absolute_url(self):
-    return reverse('search:admin_core', kwargs={'core': self.name})
+    return reverse('search:admin_core_properties', kwargs={'core': self.name})
 
   @property
   def fields(self):

+ 80 - 10
apps/search/src/search/templates/admin.mako

@@ -15,27 +15,97 @@
 ## limitations under the License.
 
 <%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
 %>
 
 <%namespace name="macros" file="macros.mako" />
-<%namespace name="navigation" file="navigation_bar_admin.mako" />
+<%namespace name="actionbar" file="actionbar.mako" />
 
 ${ commonheader(_('Search'), "search", user) | n,unicode }
 
+<link rel="stylesheet" href="/search/static/css/admin.css">
 
 <div class="container-fluid">
-  ${ navigation.menubar('cores') }
+  <h1>${_('Search Admin - Cores')}</h1>
+  <%actionbar:render>
+    <%def name="search()">
+      <input type="text" placeholder="${_('Filter cores by name...')}" class="input-xxlarge search-query" id="filterInput">
+    </%def>
+  </%actionbar:render>
+  <div class="row-fluid">
+    <div class="span12">
+      <ul id="cores">
+      % for core in hue_cores:
+        <li style="cursor: move" data-core="${ core.name }">
+          <a href="${ core.get_absolute_url() }" class="pull-right" style="margin-top: 10px;margin-right: 10px"><i class="icon-edit"></i> ${_('Edit')}</a>
+          <h4><i class="icon-list"></i> ${ core.name }</h4>
+        </li>
+      % endfor
+      </ul>
+    </div>
+  </div>
+</div>
 
-  % for core in hue_cores:
-    <p><a href="${ core.get_absolute_url() }">${ core.name }</a></p>
-  % endfor
+<style type="text/css">
+  #cores {
+    list-style-type: none;
+    margin: 0;
+    padding: 0;
+    width: 100%;
+  }
 
+  #cores li {
+    margin-bottom: 10px;
+    padding: 10px;
+    border: 1px solid #E3E3E3;
+    height: 40px;
+  }
 
-  ${ cores }
-  ${ hue_cores }
+  .placeholder {
+    height: 40px;
+    background-color: #F5F5F5;
+    border: 1px solid #E3E3E3;
+  }
+</style>
 
-</div>
+<script src="/static/ext/js/jquery/plugins/jquery-ui-draggable-droppable-sortable-1.8.23.min.js"></script>
+
+<script type="text/javascript">
+  $(document).ready(function () {
+    var orderedCores;
+    serializeList();
+    $("#cores").sortable({
+      placeholder: "placeholder",
+      update: function (event, ui) {
+        serializeList();
+        ##TODO: serialize via ajax the order of cores
+        ## the array is: orderedCores
+        ## console.log(orderedCores)
+      }
+    });
+    $("#cores").disableSelection();
+
+    function serializeList() {
+      orderedCores = [];
+      $("#cores li").each(function () {
+        orderedCores.push($(this).data("core"));
+      });
+    }
+
+    var filter = -1;
+    $("#filterInput").on("keyup", function () {
+      clearTimeout(filter);
+      filter = window.setTimeout(function () {
+        $("#cores li").removeClass("hide");
+        $("#cores li").each(function () {
+          if ($(this).data("core").toLowerCase().indexOf($("#filterInput").val().toLowerCase()) == -1) {
+            $(this).addClass("hide");
+          }
+        });
+      }, 300);
+    });
+  });
+</script>
 
 ${ commonfooter(messages) | n,unicode }

+ 0 - 49
apps/search/src/search/templates/admin_core.mako

@@ -1,49 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. 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.
-
-<%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="macros" file="macros.mako" />
-<%namespace name="navigation" file="navigation_bar_admin.mako" />
-
-${ commonheader(_('Search'), "search", user) | n,unicode }
-
-
-<div class="container-fluid">
-  ${ navigation.menubar('cores') }
-  ${ navigation.sub_menubar(hue_core.name, 'properties') }
-
-  Solr
-  <p>
-  ${ solr_core }
-  </p>
-
-  Hue
-  <p>
-  ${ hue_core }
-  </p>
-
-  Schema
-  <p>
-  ${ solr_schema.decode('utf-8') }
-  </p>
-
-</div>
-
-${ commonfooter(messages) | n,unicode }

+ 73 - 65
apps/search/src/search/templates/admin_core_facets.mako

@@ -15,86 +15,94 @@
 ## limitations under the License.
 
 <%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
 %>
 
+<%namespace name="layout" file="layout.mako" />
 <%namespace name="macros" file="macros.mako" />
-<%namespace name="navigation" file="navigation_bar_admin.mako" />
 
 ${ commonheader(_('Search'), "search", user) | n,unicode }
 
-
-<div class="container-fluid">
-  ${ navigation.menubar('cores') }
-  ${ navigation.sub_menubar(hue_core.name, 'facets') }
-
-  Solr
-  <p>
-  ${ solr_core }
-  </p>
-
-  Hue
-  <p>
-  ${ hue_core }
-  </p>
-
-  <!--
-  <div class="btn-group" style="display: inline">
-    <a href="#" data-toggle="dropdown" class="btn dropdown-toggle">
-      <i class="icon-plus-sign"></i> New
-      <span class="caret"></span>
-    </a>
-    <ul class="dropdown-menu" style="top: auto">
-      <li><a href="#" class="create-file-link" title="${ _('Field facet') }"><i class="icon-bookmark"></i> ${ _('Field') }</a></li>
-      <li><a href="#" class="create-directory-link" title="${ _('Range') }"><i class="icon-resize-full"></i> ${ _('Range') }</a></li>
-      <li><a href="#" class="create-directory-link" title="Directory"><i class="icon-calendar"></i> ${ _('Date') }</a></li>
-    </ul>
-  </div>
-  -->
-
-  <form method="POST" id="facets" data-bind="submit: submit">
-    <ul data-bind="foreach: field_facets">
-      <li>
-        <span data-bind="text: $data"></span>
-        <button data-bind="click: $parent.removeFieldFacets">${ _('Remove') }</button>
-      </li>
-    </ul>
-
-    <button type="submit">${ _('Save') }</button>
-  </form>
-
-
-  <select data-bind="options: fields, selectedOptions: field_facets" size="5" multiple="true"></select>
-
-  ${ hue_core.fields }
-
-</div>
+<%layout:skeleton>
+  <%def name="title()">
+    <h1>${_('Search Admin - ')}${hue_core.name}</h1>
+  </%def>
+  <%def name="navigation()">
+    ${ layout.sidebar(hue_core.name, 'facets') }
+  </%def>
+  <%def name="content()">
+    <form method="POST" id="facets" data-bind="submit: submit">
+
+    <div class="well"><h3>${_('Field Facets')}</h3></div>
+
+    <table class="table table-striped">
+      <thead data-bind="visible: fieldsFacets().length == 0">
+      <tr>
+        <td colspan="3">
+          <div class="alert">
+          ${_('There are currently no field Facets defined. Please add at least one from the bottom.')}
+          </div>
+        </td>
+      </tr>
+      </thead>
+      <tbody id="fields" data-bind="foreach: fieldsFacets">
+      <tr data-bind="attr: {'data-field': $data}">
+        <td><span data-bind="text: $data"></span></td>
+        <td width="30"><a class="btn btn-small" data-bind="click: $root.removeFieldsFacets"><i class="icon-trash"></i></a></td>
+      </tr>
+      </tbody>
+      <tfoot>
+      <tr style="padding-top: 20px">
+        <td><select data-bind="options: fields, value: selectedField" style="width:100%"></select></td>
+        <td width="30"><a class="btn btn-small" data-bind="click: $root.addFieldsFacets"><i class="icon-plus"></i></a></td>
+      </tr>
+      </tfoot>
+    </table>
+
+    <div class="form-actions">
+      <button type="submit" class="btn btn-primary btn-large" id="save-facets">${_('Save Facets')}</button>
+    </div>
+    </form>
+  </%def>
+</%layout:skeleton>
 
 <script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 
 <script type="text/javascript">
-  $(document).ready(function(){
-     function ViewModel() {
-       var self = this;
-       self.fields = ko.observableArray(${ hue_core.fields | n,unicode });
-       self.field_facets = ko.observableArray(${ hue_core.facets.data | n,unicode }.fields);
-
-       self.removeFieldFacets = function(facet) {
-         self.field_facets.remove(facet);
-       };
-
-      self.submit = function() {
-	    $.ajax("${ url('search:admin_core_facets', core=hue_core.name) }", {
-		    data : {'fields': ko.utils.stringifyJson(self.field_facets)},
-		    contentType : 'application/json',
-		    type : 'POST'
-	    }); // notif + refresh?
+  $(document).ready(function () {
+    function ViewModel() {
+      var self = this;
+      self.fields = ko.observableArray(${ hue_core.fields | n,unicode });
+      self.fieldsFacets = ko.observableArray(${ hue_core.facets.data | n,unicode }.fields
+    )
+      ;
+      self.selectedField = ko.observable();
+
+      self.removeFieldsFacets = function (facet) {
+        self.fieldsFacets.remove(facet);
+      };
+
+      self.addFieldsFacets = function () {
+        self.fieldsFacets.push(self.selectedField());
+      };
+
+      self.submit = function () {
+        $.ajax("${ url('search:admin_core_facets', core=hue_core.name) }", {
+          data: {'fields': ko.utils.stringifyJson(self.fieldsFacets)},
+          contentType: 'application/json',
+          type: 'POST',
+          complete: function (data) {
+            location.reload();
+          }
+        });
       };
     };
 
     ko.applyBindings(new ViewModel());
+
   });
 </script>
 
 ${ commonfooter(messages) | n,unicode }
+

+ 148 - 0
apps/search/src/search/templates/admin_core_properties.mako

@@ -0,0 +1,148 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. 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.
+
+<%!
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="layout" file="layout.mako" />
+<%namespace name="macros" file="macros.mako" />
+
+${ commonheader(_('Search'), "search", user) | n,unicode }
+
+<%def name="indexProperty(key)">
+  %if key in solr_core["status"][hue_core.name]["index"]:
+      ${ solr_core["status"][hue_core.name]["index"][key] }
+    %endif
+</%def>
+<%def name="coreProperty(key)">
+  %if key in solr_core["status"][hue_core.name]:
+      ${ solr_core["status"][hue_core.name][key] }
+    %endif
+</%def>
+
+<%layout:skeleton>
+  <%def name="title()">
+    <h1>${_('Search Admin - ')}${hue_core.name}</h1>
+  </%def>
+  <%def name="navigation()">
+    ${ layout.sidebar(hue_core.name, 'properties') }
+  </%def>
+  <%def name="content()">
+    <h3>${_('Index properties')}</h3>
+    <table class="table">
+      <thead>
+      <tr>
+        <th width="20%">${_('Property')}</th>
+        <th>${_('Value')}</th>
+      </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td>sizeInBytes</td>
+          <td>${ indexProperty('sizeInBytes') }</td>
+        </tr>
+        <tr>
+          <td>segmentCount</td>
+          <td>${ indexProperty('segmentCount') }</td>
+        </tr>
+        <tr>
+          <td>maxDoc</td>
+          <td>${ indexProperty('maxDoc') }</td>
+        </tr>
+        <tr>
+          <td>lastModified</td>
+          <td>${ indexProperty('lastModified') }</td>
+        </tr>
+        <tr>
+          <td>current</td>
+          <td>${ indexProperty('current') }</td>
+        </tr>
+        <tr>
+          <td>version</td>
+          <td>${ indexProperty('version') }</td>
+        </tr>
+        <tr>
+          <td>directory</td>
+          <td>${ indexProperty('directory') }</td>
+        </tr>
+        <tr>
+          <td>numDocs</td>
+          <td>${ indexProperty('numDocs') }</td>
+        </tr>
+        <tr>
+          <td>hasDeletions</td>
+          <td>${ indexProperty('hasDeletions') }</td>
+        </tr>
+        <tr>
+          <td>size</td>
+          <td>${ indexProperty('size') }</td>
+        </tr>
+      </tbody>
+    </table>
+
+    <h3>${_('Core properties')}</h3>
+    <table class="table">
+      <thead>
+      <tr>
+        <th width="20%">${_('Property')}</th>
+        <th>${_('Value')}</th>
+      </tr>
+      </thead>
+      <tbody>
+      <tr>
+        <td>uptime</td>
+        <td>${ coreProperty('uptime') }</td>
+      </tr>
+      <tr>
+        <td>name</td>
+        <td>${ coreProperty('name') }</td>
+      </tr>
+      <tr>
+        <td>isDefaultCore</td>
+        <td>${ coreProperty('isDefaultCore') }</td>
+      </tr>
+      <tr>
+        <td>dataDir</td>
+        <td>${ coreProperty('dataDir') }</td>
+      </tr>
+      <tr>
+        <td>instanceDir</td>
+        <td>${ coreProperty('instanceDir') }</td>
+      </tr>
+      <tr>
+        <td>startTime</td>
+        <td>${ coreProperty('startTime') }</td>
+      </tr>
+      <tr>
+        <td>config</td>
+        <td>${ coreProperty('config') }</td>
+      </tr>
+      <tr>
+        <td>schema</td>
+        <td>${ coreProperty('schema') }</td>
+      </tr>
+      </tbody>
+    </table>
+
+
+  </%def>
+</%layout:skeleton>
+
+${ commonfooter(messages) | n,unicode }
+
+

+ 0 - 44
apps/search/src/search/templates/admin_core_result.mako

@@ -1,44 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. 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.
-
-<%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="macros" file="macros.mako" />
-<%namespace name="navigation" file="navigation_bar_admin.mako" />
-
-${ commonheader(_('Search'), "search", user) | n,unicode }
-
-
-<div class="container-fluid">
-  ${ navigation.menubar('cores') }
-  ${ navigation.sub_menubar(hue_core.name, 'result') }
-
-  Solr
-  <p>
-  ${ solr_core }
-  </p>
-
-  Hue
-  <p>
-  ${ hue_core }
-  </p>
-
-</div>
-
-${ commonfooter(messages) | n,unicode }

+ 64 - 0
apps/search/src/search/templates/admin_core_schema.mako

@@ -0,0 +1,64 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. 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.
+
+<%!
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="layout" file="layout.mako" />
+<%namespace name="macros" file="macros.mako" />
+
+${ commonheader(_('Search'), "search", user) | n,unicode }
+
+<%layout:skeleton>
+  <%def name="title()">
+    <h1>${_('Search Admin - ')}${hue_core.name}</h1>
+  </%def>
+  <%def name="navigation()">
+    ${ layout.sidebar(hue_core.name, 'schema') }
+  </%def>
+  <%def name="content()">
+    <textarea id="schema">
+    ${ solr_schema.decode('utf-8') }
+    </textarea>
+  </%def>
+</%layout:skeleton>
+
+
+<script src="/static/ext/js/codemirror-3.0.js"></script>
+<link rel="stylesheet" href="/static/ext/css/codemirror.css">
+<script src="/static/ext/js/codemirror-xml.js"></script>
+
+
+<script type="text/javascript">
+  $(document).ready(function () {
+    var schemaViewer = $("#schema")[0];
+
+    var codeMirror = CodeMirror(function (elt) {
+      schemaViewer.parentNode.replaceChild(elt, schemaViewer);
+    }, {
+      value: schemaViewer.value,
+      readOnly: true,
+      lineNumbers: true
+    });
+
+    codeMirror.setSize("100%", $(document).height() - 150);
+
+  });
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 120 - 15
apps/search/src/search/templates/admin_core_sorting.mako

@@ -15,30 +15,135 @@
 ## limitations under the License.
 
 <%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
 %>
 
+<%namespace name="layout" file="layout.mako" />
 <%namespace name="macros" file="macros.mako" />
-<%namespace name="navigation" file="navigation_bar_admin.mako" />
 
 ${ commonheader(_('Search'), "search", user) | n,unicode }
 
+<%layout:skeleton>
+  <%def name="title()">
+    <h1>${_('Search Admin - ')}${hue_core.name}</h1>
+  </%def>
+  <%def name="navigation()">
+    ${ layout.sidebar(hue_core.name, 'sorting') }
+  </%def>
+  <%def name="content()">
+    <div class="well"><h3>${_('Fields')}</h3></div>
 
-<div class="container-fluid">
-  ${ navigation.menubar('cores') }
-  ${ navigation.sub_menubar(hue_core.name, 'sorting') }
+    <table class="table table-striped">
+      <thead data-bind="visible: fieldsSorting().length == 0">
+        <tr>
+          <td colspan="3">
+            <div class="alert">
+            ${_('There are currently no Sorting fields defined. Please add at least one from the bottom.')}
+            </div>
+          </td>
+        </tr>
+      </thead>
+      <tbody id="fields" data-bind="foreach: fieldsSorting">
+        <tr data-bind="attr: {'data-field': $data}">
+          <td width="30"><i class="icon-list"></i></td>
+          <td><span data-bind="text: $data"></span></td>
+          <td width="30"><a class="btn btn-small" data-bind="click: $root.removeFieldSorting"><i class="icon-trash"></i></a></td>
+        </tr>
+      </tbody>
+      <tfoot>
+        <tr style="padding-top: 20px">
+          <td colspan="2"><select data-bind="options: fields, value: selectedField" style="width:100%"></select></td>
+          <td width="30"><a class="btn btn-small" data-bind="click: $root.addFieldSorting"><i class="icon-plus"></i></a></td>
+        </tr>
+      </tfoot>
+    </table>
 
-  Solr
-  <p>
-  ${ solr_core }
-  </p>
+    <div class="form-actions">
+      <a class="btn btn-primary btn-large" id="save-sorting">${_('Save Sorting')}</a>
+    </div>
+  </%def>
+</%layout:skeleton>
 
-  Hue
-  <p>
-  ${ hue_core }
-  </p>
+<style type="text/css">
+  #fields {
+    list-style-type: none;
+    margin: 0;
+    padding: 0;
+    width: 100%;
+  }
+
+  #fields li {
+    margin-bottom: 10px;
+    padding: 10px;
+    border: 1px solid #E3E3E3;
+    height: 30px;
+  }
+
+  .placeholder {
+    height: 30px;
+    background-color: #F5F5F5;
+    border: 1px solid #E3E3E3;
+  }
+</style>
+
+
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/jquery/plugins/jquery-ui-draggable-droppable-sortable-1.8.23.min.js"></script>
+
+<script type="text/javascript">
+  $(document).ready(function () {
+    function ViewModel() {
+      var self = this;
+      self.fields = ko.observableArray(${ hue_core.fields | n,unicode });
+      self.fieldsSorting = ko.observableArray();
+
+      self.selectedField = ko.observable();
+
+      self.removeFieldSorting = function (sort) {
+        self.fieldsSorting.remove(sort);
+      };
+
+      self.addFieldSorting = function () {
+        self.fieldsSorting.push(self.selectedField());
+      };
+
+      self.submit = function () {
+        $.ajax("${ url('search:admin_core_sorting', core=hue_core.name) }", {
+          data: {'fields': ko.utils.stringifyJson(self.fieldsSorting)},
+          contentType: 'application/json',
+          type: 'POST',
+          complete: function (data) {
+            location.reload();
+          }
+        });
+      };
+    };
+
+    var viewModel = new ViewModel();
+    ko.applyBindings(viewModel);
+
+    $("#fields").sortable({
+      placeholder: "placeholder",
+      update: function (event, ui) {
+        var reorderedFields = [];
+        $("#fields tr").each(function () {
+          reorderedFields.push($(this).data("field"));
+        });
+        viewModel.fieldsSorting([]);
+        viewModel.fieldsSorting(reorderedFields);
+      }
+    });
+    $("#fields").disableSelection();
+
+    $("#save-sorting").click(function () {
+      ##TODO: save sorting
+      ## you can access to the fields like this: viewModel.fieldsSorting()
+      ## console.log(viewModel.fieldsSorting());
+    });
+
+  });
+</script>
 
-</div>
 
 ${ commonfooter(messages) | n,unicode }

+ 173 - 0
apps/search/src/search/templates/admin_core_template.mako

@@ -0,0 +1,173 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. 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.
+
+<%!
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="layout" file="layout.mako" />
+<%namespace name="macros" file="macros.mako" />
+
+${ commonheader(_('Search'), "search", user) | n,unicode }
+
+<style type="text/css">
+  .available-fields {
+    padding: 0;
+    padding-left:10px;
+  }
+  .preview-row:nth-child(odd) {
+    background-color:#f9f9f9;
+  }
+</style>
+
+<%layout:skeleton>
+  <%def name="title()">
+    <h1>${_('Search Admin - ')}${hue_core.name}</h1>
+  </%def>
+  <%def name="navigation()">
+    ${ layout.sidebar(hue_core.name, 'template') }
+  </%def>
+  <%def name="content()">
+
+    <ul class="nav nav-tabs">
+      <li class="active"><a href="#visual" data-toggle="tab">${_('Visual editor')}</a></li>
+      <li><a href="#source" data-toggle="tab">${_('Source')}</a></li>
+      <li><a href="#preview" data-toggle="tab">${_('Preview')}</a></li>
+    </ul>
+    <div class="tab-content">
+      <div class="tab-pane active" id="visual">
+
+        <div class="row-fluid">
+          <div class="span9">
+            <div id="toolbar"></div>
+            <div id="content-editor" class="clear">${_('Add your content here...')}</div>
+          </div>
+
+          <div class="span3">
+            <div class="well available-fields">
+              <h4>${_('Available Fields')}</h4>
+              <ul data-bind="foreach: fields">
+                <li data-bind="text: $data, click: $root.addField"></li>
+              </ul>
+            </div>
+          </div>
+        </div>
+
+
+      </div>
+      <div class="tab-pane" id="source">
+        <div class="row-fluid">
+          <div class="span9">
+            <textarea id="template-source"></textarea>
+          </div>
+
+          <div class="span3">
+            <div class="well available-fields">
+              <h4>${_('Available Fields')}</h4>
+              <ul data-bind="foreach: fields">
+                <li data-bind="text: $data, click: $root.addField"></li>
+              </ul>
+            </div>
+          </div>
+        </div>
+
+      </div>
+
+      <div class="tab-pane" id="preview">
+        <div id="preview-container"></div>
+      </div>
+    </div>
+
+    <div class="form-actions">
+      <a class="btn btn-primary btn-large" id="save-template">${_('Save Template')}</a>
+    </div>
+
+  </%def>
+</%layout:skeleton>
+
+<link rel="stylesheet" href="/static/ext/farbtastic/farbtastic.css">
+<link rel="stylesheet" href="/static/ext/css/freshereditor.css">
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/farbtastic/farbtastic.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/shortcut.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/freshereditor.min.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/codemirror-3.0.js"></script>
+<link rel="stylesheet" href="/static/ext/css/codemirror.css">
+<script src="/static/ext/js/codemirror-xml.js"></script>
+<script src="/static/ext/js/mustache.js"></script>
+
+
+<script type="text/javascript">
+  $(document).ready(function () {
+    function ViewModel() {
+      var self = this;
+      self.fields = ko.observableArray(${ hue_core.fields | n,unicode });
+      self.addField = function (field) {
+        $("#content-editor").focus();
+        $("#content-editor").html($("#content-editor").html() + "{{" + field + "}}");
+      };
+    };
+
+    ko.applyBindings(new ViewModel());
+
+    var samples = ${ sample_data | n,unicode };
+
+    var templateEditor = $("#template-source")[0];
+
+    var codeMirror = CodeMirror(function (elt) {
+      templateEditor.parentNode.replaceChild(elt, templateEditor);
+    }, {
+      value: templateEditor.value,
+      readOnly: false,
+      lineNumbers: true
+    });
+
+    $("#content-editor").freshereditor({toolbar_selector: "#toolbar", excludes: ['strikethrough', 'removeFormat', 'backcolor', 'insertorderedlist', 'insertheading1', 'insertheading2', 'superscript', 'subscript']});
+    $("#content-editor").freshereditor("edit", true);
+
+    // force refresh on tab change
+    $("a[data-toggle='tab']").on("shown", function (e) {
+      if ($(e.target).attr("href") == "#source") {
+        codeMirror.setValue($("#content-editor").html());
+        codeMirror.refresh();
+      }
+      if ($(e.target).attr("href") == "#preview") {
+        $("#preview-container").empty();
+        $(samples).each(function (cnt, item) {
+          $("<div>").addClass("preview-row").html(Mustache.render($("#content-editor").html(), item)).appendTo($("#preview-container"));
+        });
+      }
+    });
+
+    var delay = -1;
+    codeMirror.on("change", function () {
+      clearTimeout(delay);
+      delay = setTimeout(function () {
+        $("#content-editor").html(codeMirror.getValue());
+      }, 300);
+    });
+
+    $("#save-template").click(function () {
+      ##TODO: save template
+      ## you can access it with: $("#content-editor").html()
+      ## console.log($("#content-editor").html());
+    });
+
+  });
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 0 - 36
apps/search/src/search/templates/admin_settings.mako

@@ -1,36 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. 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.
-
-<%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="macros" file="macros.mako" />
-<%namespace name="navigation" file="navigation_bar_admin.mako" />
-
-${ commonheader(_('Search'), "search", user) | n,unicode }
-
-
-<div class="container-fluid">
-  ${ navigation.menubar('settings') }
-
-  ${ cores }
-  ${ hue_cores }
-
-</div>
-
-${ commonfooter(messages) | n,unicode }

+ 79 - 0
apps/search/src/search/templates/layout.mako

@@ -0,0 +1,79 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. 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.
+
+<%!
+  from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="utils" file="utils.inc.mako" />
+
+<%def name="skeleton()">
+  <link rel="stylesheet" href="/search/static/css/admin.css">
+
+  <div class="container-fluid">
+    <div class="pull-right">
+      <a class="btn" href="${ url('search:admin') }"><i class="icon-list"></i> ${ _('Core list') }</a>
+    </div>
+    %if hasattr(caller, "title"):
+      ${caller.title()}
+    %else:
+        <h1>${_('Search Admin')}</h1>
+    %endif
+    <div class="row-fluid">
+      %if hasattr(caller, "navigation"):
+          <div class="span2">
+            ${caller.navigation()}
+          </div>
+
+          <div class="span10">
+            %if hasattr(caller, "content"):
+              ${caller.content()}
+            %endif
+          </div>
+      %else:
+        <div class="span12">
+          %if hasattr(caller, "content"):
+              ${caller.content()}
+          %endif
+        </div>
+      %endif
+    </div>
+  </div>
+</%def>
+
+<%def name="sidebar(core, section='')">
+  <div class="well sidebar-nav" style="min-height: 250px">
+    <ul class="nav nav-list">
+      <li class="nav-header">${_('Core')}</li>
+      <li class="${ utils.is_selected(section, 'properties') }">
+        <a href="${ url('search:admin_core_properties', core=core) }">${_('Properties')}</a>
+      </li>
+      <li class="${ utils.is_selected(section, 'schema') }">
+        <a href="${ url('search:admin_core_schema', core=core) }">${_('Schema')}</a>
+      </li>
+      <li class="nav-header">${_('Results')}</li>
+      <li class="${ utils.is_selected(section, 'template') }">
+        <a href="${ url('search:admin_core_template', core=core) }">${_('Template')}</a>
+      </li>
+      <li class="${ utils.is_selected(section, 'facets') }">
+        <a href="${ url('search:admin_core_facets', core=core) }">${_('Facets')}</a>
+      </li>
+      <li class="${ utils.is_selected(section, 'sorting') }">
+        <a href="${ url('search:admin_core_sorting', core=core) }">${_('Sorting')}</a>
+      </li>
+    </ul>
+  </div>
+</%def>

+ 0 - 53
apps/search/src/search/templates/navigation_bar_admin.mako

@@ -1,53 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. 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.
-
-<%!
-  from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="utils" file="utils.inc.mako" />
-
-
-<%def name="menubar(section='')">
-  <ul class="nav nav-tabs">
-    <li class="${ utils.is_selected(section, 'search') }">
-      <a href="${ url('search:index') }">${ _('Search') }</a>
-    </li>
-    <li class="${ utils.is_selected(section, 'cores') }">
-      <a href="${ url('search:admin_cores') }">${ _('Cores') }</a>
-    </li>
-    <li class="${ utils.is_selected(section, 'settings') }">
-      <a href="${ url('search:admin_settings') }">${ _('Settings') }</a>
-    </li>
-  </ul>
-</%def>
-
-<%def name="sub_menubar(core, section='')">
-  <ul class="nav nav-pills">
-    <li class="${ utils.is_selected(section, 'properties') }">
-      <a href="${ url('search:admin_core', core=core) }">${ _('Properties') }</a>
-    </li>
-    <li class="${ utils.is_selected(section, 'result') }">
-      <a href="${ url('search:admin_core_result', core=core) }">${ _('Result') }</a>
-    </li>
-    <li class="${ utils.is_selected(section, 'facets') }">
-      <a href="${ url('search:admin_core_facets', core=core) }">${ _('Facets') }</a>
-    </li>
-    <li class="${ utils.is_selected(section, 'sorting') }">
-      <a href="${ url('search:admin_core_sorting', core=core) }">${ _('Sorting') }</a>
-    </li>
-  </ul>
-</%def>

+ 3 - 3
apps/search/src/search/urls.py

@@ -22,9 +22,9 @@ urlpatterns = patterns('search.views',
   url(r'^query$', 'index', name='query'),
   url(r'^admin$', 'admin', name='admin'),
   url(r'^admin/cores$', 'admin', name='admin_cores'),
-  url(r'^admin/settings$', 'admin_settings', name='admin_settings'),
-  url(r'^admin/core/(?P<core>\w+)/settings$', 'admin_core', name='admin_core'),
-  url(r'^admin/core/(?P<core>\w+)/result$', 'admin_core_result', name='admin_core_result'),
+  url(r'^admin/core/(?P<core>\w+)/properties$', 'admin_core_properties', name='admin_core_properties'),
+  url(r'^admin/core/(?P<core>\w+)/schema$', 'admin_core_schema', name='admin_core_schema'),
+  url(r'^admin/core/(?P<core>\w+)/template$', 'admin_core_template', name='admin_core_template'),
   url(r'^admin/core/(?P<core>\w+)/facets$', 'admin_core_facets', name='admin_core_facets'),
   url(r'^admin/core/(?P<core>\w+)/sorting$', 'admin_core_sorting', name='admin_core_sorting'),
 

+ 47 - 34
apps/search/src/search/views.py

@@ -58,12 +58,12 @@ def index(request):
     response = SolrApi(SOLR_URL.get()).query(solr_query, hue_core)
 
   return render('index.mako', request, {
-                    'search_form': search_form,
-                    'response': response,
-                    'solr_query': solr_query,
-                    'hue_core': hue_core,
-                    'rr': json.dumps(response),
-                })
+    'search_form': search_form,
+    'response': response,
+    'solr_query': solr_query,
+    'hue_core': hue_core,
+    'rr': json.dumps(response),
+  })
 
 
 @allow_admin_only
@@ -73,41 +73,53 @@ def admin(request):
   hue_cores = Core.objects.all()
 
   return render('admin.mako', request, {
-                    'cores': cores,
-                    'hue_cores': hue_cores,
-                })
+    'cores': cores,
+    'hue_cores': hue_cores,
+  })
+
 
 @allow_admin_only
-def admin_settings(request):
-  cores = SolrApi(SOLR_URL.get()).cores()
-  hue_cores = Core.objects.all()
+def admin_core_properties(request, core):
+  solr_core = SolrApi(SOLR_URL.get()).core(core)
+  hue_core = Core.objects.get(name=core)
+
+  return render('admin_core_properties.mako', request, {
+    'solr_core': solr_core,
+    'hue_core': hue_core,
+  })
 
-  return render('admin_settings.mako', request, {
-                    'cores': cores,
-                    'hue_cores': hue_cores,
-                })
 
 @allow_admin_only
-def admin_core(request, core):
-  solr_core = SolrApi(SOLR_URL.get()).core(core)
+def admin_core_schema(request, core):
   solr_schema = SolrApi(SOLR_URL.get()).schema(core)
   hue_core = Core.objects.get(name=core)
+  return render('admin_core_schema.mako', request, {
+    'solr_schema': solr_schema,
+    'hue_core': hue_core,
+  })
 
-  return render('admin_core.mako', request, {
-                    'solr_core': solr_core,
-                    'solr_schema': solr_schema,
-                    'hue_core': hue_core,
-                })
 
 @allow_admin_only
-def admin_core_result(request, core):
+def admin_core_template(request, core):
   solr_core = SolrApi(SOLR_URL.get()).core(core)
   hue_core = Core.objects.get(name=core)
 
-  return render('admin_core_result.mako', request, {
-                    'solr_core': solr_core,
-                    'hue_core': hue_core,
-                })
+  solr_query = {}
+  solr_query['core'] = core
+  solr_query['q'] = ''
+  solr_query['fq'] = ''
+  solr_query['rows'] = 10
+  solr_query['start'] = 0
+  solr_query['facets'] = 0
+
+  response = SolrApi(SOLR_URL.get()).query(solr_query, hue_core)
+
+  return render('admin_core_template.mako', request, {
+    'solr_core': solr_core,
+    'hue_core': hue_core,
+    'sample_data': json.dumps(response["response"]["docs"]),
+  })
+
 
 @allow_admin_only
 def admin_core_facets(request, core):
@@ -121,9 +133,10 @@ def admin_core_facets(request, core):
     request.info(_('Facets updated'))
 
   return render('admin_core_facets.mako', request, {
-                    'solr_core': solr_core,
-                    'hue_core': hue_core,
-                })
+    'solr_core': solr_core,
+    'hue_core': hue_core,
+  })
+
 
 @allow_admin_only
 def admin_core_sorting(request, core):
@@ -131,6 +144,6 @@ def admin_core_sorting(request, core):
   hue_core = Core.objects.get(name=core)
 
   return render('admin_core_sorting.mako', request, {
-                    'solr_core': solr_core,
-                    'hue_core': hue_core,
-                })
+    'solr_core': solr_core,
+    'hue_core': hue_core,
+  })

+ 17 - 0
apps/search/static/css/admin.css

@@ -0,0 +1,17 @@
+.sidebar-nav {
+  padding: 9px 0;
+}
+
+#content-editor {
+  border: 1px dotted #DDD;
+}
+
+.availableFields ul {
+  list-style: none outside none;
+  padding: 0;
+  margin: 0;
+}
+
+.availableFields ul li {
+  cursor: pointer;
+}

+ 110 - 98
apps/search/static/css/search.css

@@ -1,98 +1,110 @@
-  .content {
-    margin-left: 58px;
-  }
-
-  .action {
-    margin-right: 5px;
-  }
-
-  .action a, .time a, .account-group, .retweeted {
-    color: #999999;
-  }
-
-  .account-group a {
-    text-decoration: none;
-    font-weight: normal;
-  }
-
-  .username {
-    font-size: 12px;
-  }
-
-  .time {
-    color: #BBBBBB;
-    float: right;
-    margin-top: 1px;
-    position: relative;
-  }
-
-  .avatar {
-    position: absolute;
-    margin-left: -56px!important;
-    margin-top: 4px!important;
-    -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
-    -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
-    box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
-    -webkit-border-radius: 5px;
-    -moz-border-radius: 5px;
-    border-radius: 5px;
-  }
-
-  .text {
-    margin-bottom: 4px;
-    cursor: pointer;
-  }
-
-  .tweet-actions li {
-    display: inline;
-  }
-
-  .stream-item-footer {
-    font-size: 12px;
-    color: #999999;
-  }
-
-  ul.tweet-actions {
-    list-style: none outside none;
-  }
-
-  ul.tweet-actions {
-    margin: 0;
-    padding: 0;
-  }
-
-  .fullname {
-    color: #333333;
-    font-weight: bold;
-  }
-
-  .stream-item-footer, .retweeted {
-    font-size: 12px;
-    padding-top: 1px;
-  }
-
-  .icon {
-    background-position: 0 0;
-    background-repeat: no-repeat;
-    display: inline-block;
-    vertical-align: text-top;
-    height: 13px;
-    width: 14px;
-    margin-top: 0;
-    margin-left: -2px;
-  }
-  .icon-reply {
-    background-image: url("/search/static/art/reply.png");
-  }
-  .icon-retweet {
-    background-image: url("/search/static/art/retweet.png");
-  }
-  .twitter-logo {
-    background-image: url("/search/static/art/bird_gray_32.png");
-    width: 32px;
-    height: 32px;
-    background-repeat: no-repeat;
-    display: inline-block;
-    vertical-align: top;
-    margin-top: 2px;
-  }
+.content {
+  margin-left: 58px;
+}
+
+.action {
+  margin-right: 5px;
+}
+
+.action a, .time a, .account-group, .retweeted {
+  color: #999999;
+}
+
+.account-group a {
+  text-decoration: none;
+  font-weight: normal;
+}
+
+.username {
+  font-size: 12px;
+}
+
+.time {
+  color: #BBBBBB;
+  float: right;
+  margin-top: 1px;
+  position: relative;
+}
+
+.avatar {
+  position: absolute;
+  margin-left: -56px !important;
+  margin-top: 4px !important;
+  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
+  -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
+  box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+
+.text {
+  margin-bottom: 4px;
+  cursor: pointer;
+}
+
+.tweet-actions li {
+  display: inline;
+}
+
+.stream-item-footer {
+  font-size: 12px;
+  color: #999999;
+}
+
+ul.tweet-actions {
+  list-style: none outside none;
+}
+
+ul.tweet-actions {
+  margin: 0;
+  padding: 0;
+}
+
+.fullname {
+  color: #333333;
+  font-weight: bold;
+}
+
+.stream-item-footer, .retweeted {
+  font-size: 12px;
+  padding-top: 1px;
+}
+
+.icon {
+  background-position: 0 0;
+  background-repeat: no-repeat;
+  display: inline-block;
+  vertical-align: text-top;
+  height: 13px;
+  width: 14px;
+  margin-top: 0;
+  margin-left: -2px;
+}
+
+.icon-reply {
+  background-image: url("/search/static/art/reply.png");
+}
+
+.icon-retweet {
+  background-image: url("/search/static/art/retweet.png");
+}
+
+.twitter-logo {
+  background-image: url("/search/static/art/bird_gray_32.png");
+  width: 32px;
+  height: 32px;
+  background-repeat: no-repeat;
+  display: inline-block;
+  vertical-align: top;
+  margin-top: 2px;
+}
+
+.dropdown-menu li {
+  font-size: 14px;
+}
+
+.dropdown a.dropdown-toggle {
+  padding-left: 10px;
+  padding-right: 10px;
+}

+ 11 - 0
desktop/core/static/ext/css/bootstrap-select.css

@@ -0,0 +1,11 @@
+a.dropdown-toggle, a.dropdown-toggle:hover {
+  text-decoration: none;
+}
+
+.dropdown a.dropdown-toggle {
+  color: gray;
+}
+
+.dropdown:hover a.dropdown-toggle, .open a.dropdown-toggle {
+  color: black;
+}

+ 13 - 0
desktop/core/static/ext/css/freshereditor.css

@@ -0,0 +1,13 @@
+.fresheditor-toolbar 
+{
+	font-family:"Georgia", Serif;
+	line-height:1.67em;
+	font-size:1em;
+}
+.toolbar-btn {
+	height: 20px;
+}
+.colorpanel {
+	background-color: #333;
+}
+

+ 51 - 0
desktop/core/static/ext/farbtastic/farbtastic.css

@@ -0,0 +1,51 @@
+/**
+ * Farbtastic Color Picker 1.2
+ * © 2008 Steven Wittens
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+.farbtastic {
+  position: relative;
+}
+.farbtastic * {
+  position: absolute;
+  cursor: crosshair;
+}
+.farbtastic, .farbtastic .wheel {
+  width: 195px;
+  height: 195px;
+}
+.farbtastic .color, .farbtastic .overlay {
+  top: 47px;
+  left: 47px;
+  width: 101px;
+  height: 101px;
+}
+.farbtastic .wheel {
+  background: url(wheel.png) no-repeat;
+  width: 195px;
+  height: 195px;
+}
+.farbtastic .overlay {
+  background: url(mask.png) no-repeat;
+}
+.farbtastic .marker {
+  width: 17px;
+  height: 17px;
+  margin: -8px 0 0 -8px;
+  overflow: hidden; 
+  background: url(marker.png) no-repeat;
+}
+

+ 345 - 0
desktop/core/static/ext/farbtastic/farbtastic.js

@@ -0,0 +1,345 @@
+/**
+ * Farbtastic Color Picker 1.2
+ * © 2008 Steven Wittens
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+jQuery.fn.farbtastic = function (callback) {
+  $.farbtastic(this, callback);
+  return this;
+};
+
+jQuery.farbtastic = function (container, callback) {
+  var container = $(container).get(0);
+  return container.farbtastic || (container.farbtastic = new jQuery._farbtastic(container, callback));
+}
+
+jQuery._farbtastic = function (container, callback) {
+  // Store farbtastic object
+  var fb = this;
+
+  // Insert markup
+  $(container).html('<div class="farbtastic"><div class="color"></div><div class="wheel"></div><div class="overlay"></div><div class="h-marker marker"></div><div class="sl-marker marker"></div></div>');
+  var e = $('.farbtastic', container);
+  fb.wheel = $('.wheel', container).get(0);
+  // Dimensions
+  fb.radius = 84;
+  fb.square = 100;
+  fb.width = 194;
+
+  // Fix background PNGs in IE6
+  if (navigator.appVersion.match(/MSIE [0-6]\./)) {
+    $('*', e).each(function () {
+      if (this.currentStyle.backgroundImage != 'none') {
+        var image = this.currentStyle.backgroundImage;
+        image = this.currentStyle.backgroundImage.substring(5, image.length - 2);
+        $(this).css({
+          'backgroundImage': 'none',
+          'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
+        });
+      }
+    });
+  }
+
+  /**
+   * Link to the given element(s) or callback.
+   */
+  fb.linkTo = function (callback) {
+    // Unbind previous nodes
+    if (typeof fb.callback == 'object') {
+      $(fb.callback).unbind('keyup', fb.updateValue);
+    }
+
+    // Reset color
+    fb.color = null;
+
+    // Bind callback or elements
+    if (typeof callback == 'function') {
+      fb.callback = callback;
+    }
+    else if (typeof callback == 'object' || typeof callback == 'string') {
+      fb.callback = $(callback);
+      fb.callback.bind('keyup', fb.updateValue);
+      if (fb.callback.get(0).value) {
+        fb.setColor(fb.callback.get(0).value);
+      }
+    }
+    return this;
+  }
+  fb.updateValue = function (event) {
+    if (this.value && this.value != fb.color) {
+      fb.setColor(this.value);
+    }
+  }
+
+  /**
+   * Change color with HTML syntax #123456
+   */
+  fb.setColor = function (color) {
+    var unpack = fb.unpack(color);
+    if (fb.color != color && unpack) {
+      fb.color = color;
+      fb.rgb = unpack;
+      fb.hsl = fb.RGBToHSL(fb.rgb);
+      fb.updateDisplay();
+    }
+    return this;
+  }
+
+  /**
+   * Change color with HSL triplet [0..1, 0..1, 0..1]
+   */
+  fb.setHSL = function (hsl) {
+    fb.hsl = hsl;
+    fb.rgb = fb.HSLToRGB(hsl);
+    fb.color = fb.pack(fb.rgb);
+    fb.updateDisplay();
+    return this;
+  }
+
+  /////////////////////////////////////////////////////
+
+  /**
+   * Retrieve the coordinates of the given event relative to the center
+   * of the widget.
+   */
+  fb.widgetCoords = function (event) {
+    var x, y;
+    var el = event.target || event.srcElement;
+    var reference = fb.wheel;
+
+    if (typeof event.offsetX != 'undefined') {
+      // Use offset coordinates and find common offsetParent
+      var pos = { x: event.offsetX, y: event.offsetY };
+
+      // Send the coordinates upwards through the offsetParent chain.
+      var e = el;
+      while (e) {
+        e.mouseX = pos.x;
+        e.mouseY = pos.y;
+        pos.x += e.offsetLeft;
+        pos.y += e.offsetTop;
+        e = e.offsetParent;
+      }
+
+      // Look for the coordinates starting from the wheel widget.
+      var e = reference;
+      var offset = { x: 0, y: 0 }
+      while (e) {
+        if (typeof e.mouseX != 'undefined') {
+          x = e.mouseX - offset.x;
+          y = e.mouseY - offset.y;
+          break;
+        }
+        offset.x += e.offsetLeft;
+        offset.y += e.offsetTop;
+        e = e.offsetParent;
+      }
+
+      // Reset stored coordinates
+      e = el;
+      while (e) {
+        e.mouseX = undefined;
+        e.mouseY = undefined;
+        e = e.offsetParent;
+      }
+    }
+    else {
+      // Use absolute coordinates
+      var pos = fb.absolutePosition(reference);
+      x = (event.pageX || 0*(event.clientX + $('html').get(0).scrollLeft)) - pos.x;
+      y = (event.pageY || 0*(event.clientY + $('html').get(0).scrollTop)) - pos.y;
+    }
+    // Subtract distance to middle
+    return { x: x - fb.width / 2, y: y - fb.width / 2 };
+  }
+
+  /**
+   * Mousedown handler
+   */
+  fb.mousedown = function (event) {
+    // Capture mouse
+    if (!document.dragging) {
+      $(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
+      document.dragging = true;
+    }
+
+    // Check which area is being dragged
+    var pos = fb.widgetCoords(event);
+    fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square;
+
+    // Process
+    fb.mousemove(event);
+    return false;
+  }
+
+  /**
+   * Mousemove handler
+   */
+  fb.mousemove = function (event) {
+    // Get coordinates relative to color picker center
+    var pos = fb.widgetCoords(event);
+
+    // Set new HSL parameters
+    if (fb.circleDrag) {
+      var hue = Math.atan2(pos.x, -pos.y) / 6.28;
+      if (hue < 0) hue += 1;
+      fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]);
+    }
+    else {
+      var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5));
+      var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5));
+      fb.setHSL([fb.hsl[0], sat, lum]);
+    }
+    return false;
+  }
+
+  /**
+   * Mouseup handler
+   */
+  fb.mouseup = function () {
+    // Uncapture mouse
+    $(document).unbind('mousemove', fb.mousemove);
+    $(document).unbind('mouseup', fb.mouseup);
+    document.dragging = false;
+  }
+
+  /**
+   * Update the markers and styles
+   */
+  fb.updateDisplay = function () {
+    // Markers
+    var angle = fb.hsl[0] * 6.28;
+    $('.h-marker', e).css({
+      left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px',
+      top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px'
+    });
+
+    $('.sl-marker', e).css({
+      left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px',
+      top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px'
+    });
+
+    // Saturation/Luminance gradient
+    $('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));
+
+    // Linked elements or callback
+    if (typeof fb.callback == 'object') {
+      // Set background/foreground color
+      $(fb.callback).css({
+        backgroundColor: fb.color,
+        color: fb.hsl[2] > 0.5 ? '#000' : '#fff'
+      });
+
+      // Change linked value
+      $(fb.callback).each(function() {
+        if (this.value && this.value != fb.color) {
+          this.value = fb.color;
+        }
+      });
+    }
+    else if (typeof fb.callback == 'function') {
+      fb.callback.call(fb, fb.color);
+    }
+  }
+
+  /**
+   * Get absolute position of element
+   */
+  fb.absolutePosition = function (el) {
+    var r = { x: el.offsetLeft, y: el.offsetTop };
+    // Resolve relative to offsetParent
+    if (el.offsetParent) {
+      var tmp = fb.absolutePosition(el.offsetParent);
+      r.x += tmp.x;
+      r.y += tmp.y;
+    }
+    return r;
+  };
+
+  /* Various color utility functions */
+  fb.pack = function (rgb) {
+    var r = Math.round(rgb[0] * 255);
+    var g = Math.round(rgb[1] * 255);
+    var b = Math.round(rgb[2] * 255);
+    return '#' + (r < 16 ? '0' : '') + r.toString(16) +
+           (g < 16 ? '0' : '') + g.toString(16) +
+           (b < 16 ? '0' : '') + b.toString(16);
+  }
+
+  fb.unpack = function (color) {
+    if (color.length == 7) {
+      return [parseInt('0x' + color.substring(1, 3)) / 255,
+        parseInt('0x' + color.substring(3, 5)) / 255,
+        parseInt('0x' + color.substring(5, 7)) / 255];
+    }
+    else if (color.length == 4) {
+      return [parseInt('0x' + color.substring(1, 2)) / 15,
+        parseInt('0x' + color.substring(2, 3)) / 15,
+        parseInt('0x' + color.substring(3, 4)) / 15];
+    }
+  }
+
+  fb.HSLToRGB = function (hsl) {
+    var m1, m2, r, g, b;
+    var h = hsl[0], s = hsl[1], l = hsl[2];
+    m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s;
+    m1 = l * 2 - m2;
+    return [this.hueToRGB(m1, m2, h+0.33333),
+        this.hueToRGB(m1, m2, h),
+        this.hueToRGB(m1, m2, h-0.33333)];
+  }
+
+  fb.hueToRGB = function (m1, m2, h) {
+    h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
+    if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
+    if (h * 2 < 1) return m2;
+    if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
+    return m1;
+  }
+
+  fb.RGBToHSL = function (rgb) {
+    var min, max, delta, h, s, l;
+    var r = rgb[0], g = rgb[1], b = rgb[2];
+    min = Math.min(r, Math.min(g, b));
+    max = Math.max(r, Math.max(g, b));
+    delta = max - min;
+    l = (min + max) / 2;
+    s = 0;
+    if (l > 0 && l < 1) {
+      s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
+    }
+    h = 0;
+    if (delta > 0) {
+      if (max == r && max != g) h += (g - b) / delta;
+      if (max == g && max != b) h += (2 + (b - r) / delta);
+      if (max == b && max != r) h += (4 + (r - g) / delta);
+      h /= 6;
+    }
+    return [h, s, l];
+  }
+
+  // Install mousedown handler (the others are set on the document on-demand)
+  $('*', e).mousedown(fb.mousedown);
+
+    // Init color
+  fb.setColor('#000000');
+
+  // Set linked elements/callback
+  if (callback) {
+    fb.linkTo(callback);
+  }
+}

BIN
desktop/core/static/ext/farbtastic/marker.png


BIN
desktop/core/static/ext/farbtastic/mask.png


BIN
desktop/core/static/ext/farbtastic/wheel.png


+ 43 - 0
desktop/core/static/ext/js/bootstrap-select.js

@@ -0,0 +1,43 @@
+/*
+ * bootstrap-select.js
+ */
+
+(function($) {
+
+  'use strict'; // jshint ;_;
+
+  $.fn.dropSelect = function(option) {
+    return this.each(function() {
+
+      var $this = $(this);
+      var display = $this.find('.dropdown-display');        // display span
+      var field = $this.find('input.dropdown-field');       // hidden input
+      var options = $this.find('ul.dropdown-menu > li > a');// select options
+
+      // when the hidden field is updated, update dropdown-toggle
+      var onFieldChange = function(event) {
+        var val = $(this).val();
+        var displayText = options.filter("[data-value='" + val + "']").html();
+        display.html(displayText);
+      };
+
+      // when an option is clicked, update the hidden field
+      var onOptionClick = function(event) {
+        // stop click from causing page refresh
+        event.preventDefault();
+        field.val($(this).attr('data-value'));
+        field.change();
+      };
+
+      field.change(onFieldChange);
+      options.click(onOptionClick);
+
+    });
+  };
+
+  // invoke on every div element with 'data-select=true'
+  $(function() {
+    $('div[data-select=true]').dropSelect();
+  });
+
+})(window.jQuery);

+ 490 - 0
desktop/core/static/ext/js/freshereditor.min.js

@@ -0,0 +1,490 @@
+(function() {
+  (function($) {
+    var methods;
+    methods = {
+      edit: function(isEditing) {
+        return this.each(function() {
+          return $(this).attr("contentEditable", isEditing || false);
+        });
+      },
+      save: function(callback) {
+        return this.each(function() {
+          return callback($(this).attr('id'), $(this).html());
+        });
+      },
+      createlink: function() {
+        var urlPrompt;
+        urlPrompt = prompt("Enter URL:", "http://");
+        return document.execCommand("createlink", false, urlPrompt);
+      },
+      insertimage: function() {
+        var urlPrompt;
+        urlPrompt = prompt("Enter Image URL:", "http://");
+        return document.execCommand("insertimage", false, urlPrompt);
+      },
+      formatblock: function(block) {
+        return document.execCommand("formatblock", null, block);
+      },
+      init: function(opts) {
+        var $toolbar, button, command, commands, excludes, font, font_list, fontnames, fontsize, fontsizes, group, groups, options, shortcuts, size_list, _i, _j, _k, _l, _len, _len2, _len3, _len4;
+        options = opts || {};
+        groups = [
+          [
+            {
+              name: 'bold',
+              label: "<span style='font-weight:bold;'>B</span>",
+              title: 'Bold (Ctrl+B)',
+              classname: 'toolbar_bold'
+            }, {
+            name: 'italic',
+            label: "<span style='font-style:italic;'>I</span>",
+            title: 'Italic (Ctrl+I)',
+            classname: 'toolbar_italic'
+          }, {
+            name: 'underline',
+            label: "<span style='text-decoration:underline!important;'>U</span>",
+            title: 'Underline (Ctrl+U)',
+            classname: 'toolbar_underline'
+          }, {
+            name: 'strikethrough',
+            label: "<span style='text-shadow:none;text-decoration:line-through;'>ABC</span>",
+            title: 'Strikethrough',
+            classname: 'toolbar_strikethrough'
+          }, {
+            name: 'removeFormat',
+            label: "<i class='icon-minus'></i>",
+            title: 'Remove Formating (Ctrl+M)',
+            classname: 'toolbar_remove'
+          }
+          ], [
+            {
+              name: 'fontname',
+              label: "F <span class='caret'></span>",
+              title: 'Select font name',
+              classname: 'toolbar_fontname dropdown-toggle',
+              dropdown: true
+            }
+          ], [
+            {
+              name: 'FontSize',
+              label: "<span style='font:bold 16px;'>A</span><span style='font-size:8px;'>A</span> <span class='caret'></span>",
+              title: 'Select font size',
+              classname: 'toolbar_fontsize dropdown-toggle',
+              dropdown: true
+            }
+          ], [
+            {
+              name: 'forecolor',
+              label: "<div style='color:#ff0000;'>A <span class='caret'></span></div>",
+              title: 'Select font color',
+              classname: 'toolbar_forecolor dropdown-toggle',
+              dropdown: true
+            }
+          ], [
+            {
+              name: 'backcolor',
+              label: "<div style='display:inline-block;margin:3px;width:15px;height:12px;background-color:#0000ff;'></div> <span class='caret'></span>",
+              title: 'Select background color',
+              classname: 'toolbar_bgcolor dropdown-toggle',
+              dropdown: true
+            }
+          ], [
+            {
+              name: 'justifyleft',
+              label: "<i class='icon-align-left' style='margin-top:2px;'></i>",
+              title: 'Left justify',
+              classname: 'toolbar_justifyleft'
+            }, {
+              name: 'justifycenter',
+              label: "<i class='icon-align-center' style='margin-top:2px;'></i>",
+              title: 'Center justify',
+              classname: 'toolbar_justifycenter'
+            }, {
+              name: 'justifyright',
+              label: "<i class='icon-align-right' style='margin-top:2px;'></i>",
+              title: 'Right justify',
+              classname: 'toolbar_justifyright'
+            }, {
+              name: 'justifyfull',
+              label: "<i class='icon-align-justify' style='margin-top:2px;'></i>",
+              title: 'Full justify',
+              classname: 'toolbar_justifyfull'
+            }
+          ], [
+            {
+              name: 'createlink',
+              label: '@',
+              title: 'Link to a web page (Ctrl+L)',
+              userinput: "yes",
+              classname: 'toolbar_link'
+            }, {
+              name: 'insertimage',
+              label: "<i style='margin-top:2px;' class='icon-picture'></i>",
+              title: 'Insert an image (Ctrl+G)',
+              userinput: "yes",
+              classname: 'toolbar_image'
+            }, {
+              name: 'insertorderedlist',
+              label: "<i class='icon-list-alt' style='margin-top:2px;'></i>",
+              title: 'Insert ordered list',
+              classname: 'toolbar_ol'
+            }, {
+              name: 'insertunorderedlist',
+              label: "<i class='icon-list' style='margin-top:2px;'></i>",
+              title: 'Insert unordered list',
+              classname: 'toolbar_ul'
+            }
+          ], [
+            {
+              name: 'insertparagraph',
+              label: 'P',
+              title: 'Insert a paragraph (Ctrl+Alt+0)',
+              classname: 'toolbar_p',
+              block: 'p'
+            }, {
+              name: 'insertheading1',
+              label: 'H1',
+              title: "Heading 1 (Ctrl+Alt+1)",
+              classname: 'toolbar_h1',
+              block: 'h1'
+            }, {
+              name: 'insertheading2',
+              label: 'H2',
+              title: "Heading 2 (Ctrl+Alt+2)",
+              classname: 'toolbar_h2',
+              block: 'h2'
+            }, {
+              name: 'insertheading3',
+              label: 'H3',
+              title: "Heading 3 (Ctrl+Alt+3)",
+              classname: 'toolbar_h3',
+              block: 'h3'
+            }, {
+              name: 'insertheading4',
+              label: 'H4',
+              title: "Heading 4 (Ctrl+Alt+4)",
+              classname: 'toolbar_h4',
+              block: 'h4'
+            }
+          ], [
+            {
+              name: 'blockquote',
+              label: "<i style='margin-top:2px;' class='icon-comment'></i>",
+              title: 'Blockquote (Ctrl+Q)',
+              classname: 'toolbar_blockquote',
+              block: 'blockquote'
+            }, {
+              name: 'code',
+              label: '{&nbsp;}',
+              title: 'Code (Ctrl+Alt+K)',
+              classname: 'toolbar_code',
+              block: 'pre'
+            }, {
+              name: 'superscript',
+              label: 'x<sup>2</sup>',
+              title: 'Superscript',
+              classname: 'toolbar_superscript'
+            }, {
+              name: 'subscript',
+              label: 'x<sub>2</sub>',
+              title: 'Subscript',
+              classname: 'toolbar_subscript'
+            }
+          ]
+        ];
+        if (options.toolbar_selector != null) {
+          $toolbar = $(options.toolbar_selector);
+        } else {
+          $(this).before("<div id='editor-toolbar'></div>");
+          $toolbar = $('#editor-toolbar');
+        }
+        $toolbar.addClass('fresheditor-toolbar');
+        $toolbar.append("<div class='btn-toolbar'></div>");
+        excludes = options.excludes || [];
+        for (_i = 0, _len = groups.length; _i < _len; _i++) {
+          commands = groups[_i];
+          group = '';
+          for (_j = 0, _len2 = commands.length; _j < _len2; _j++) {
+            command = commands[_j];
+            if (jQuery.inArray(command.name, excludes) < 0) {
+              button = "<a href='#' class='btn toolbar-btn toolbar-cmd " + command.classname + "' title='" + command.title + "' command='" + command.name + "'";
+              if (command.userinput != null) {
+                button += " userinput='" + command.userinput + "'";
+              }
+              if (command.block != null) {
+                button += " block='" + command.block + "'";
+              }
+              if (command.dropdown) {
+                button += " data-toggle='dropdown'";
+              }
+              button += ">" + command.label + "</a>";
+              group += button;
+            }
+          }
+          $('.btn-toolbar', $toolbar).append("<div class='btn-group'>" + group + "</div>");
+        }
+        $("[data-toggle='dropdown']").removeClass('toolbar-cmd');
+        if (jQuery.inArray('fontname', excludes) < 0) {
+          fontnames = ["Arial", "Arial Black", "Comic Sans MS", "Courier New", "Georgia", "Helvetica", "Sans Serif", "Tahoma", "Times New Roman", "Trebuchet MS", "Verdana"];
+          font_list = '';
+          for (_k = 0, _len3 = fontnames.length; _k < _len3; _k++) {
+            font = fontnames[_k];
+            font_list += "<li><a href='#' class='fontname-option' style='font-family:" + font + ";'>" + font + "</a></li>";
+          }
+          $('.toolbar_fontname').after("<ul class='dropdown-menu'>" + font_list + "</ul>");
+          $('.fontname-option').on('click', function() {
+            document.execCommand("fontname", false, $(this).text());
+            $(this).closest('.btn-group').removeClass('open');
+            return false;
+          });
+        }
+        if (jQuery.inArray('FontSize', excludes) < 0) {
+          fontsizes = [
+            {
+              size: 1,
+              point: 8
+            }, {
+              size: 2,
+              point: 10
+            }, {
+              size: 3,
+              point: 12
+            }, {
+              size: 4,
+              point: 14
+            }, {
+              size: 5,
+              point: 18
+            }, {
+              size: 6,
+              point: 24
+            }, {
+              size: 7,
+              point: 36
+            }
+          ];
+          size_list = '';
+          for (_l = 0, _len4 = fontsizes.length; _l < _len4; _l++) {
+            fontsize = fontsizes[_l];
+            size_list += "<li><a href='#' class='font-option fontsize-option' style='font-size:" + fontsize.point + "px;' fontsize='" + fontsize.size + "'>" + fontsize.size + "(" + fontsize.point + "pt)</a></li>";
+          }
+          $('.toolbar_fontsize').after("<ul class='dropdown-menu'>" + size_list + "</ul>");
+          $('a.fontsize-option').on('click', function() {
+            console.log($(this).attr('fontsize'));
+            document.execCommand("FontSize", false, $(this).attr('fontsize'));
+            $(this).closest('.btn-group').removeClass('open');
+            return false;
+          });
+        }
+        if (jQuery.inArray('forecolor', excludes) < 0) {
+          $('a.toolbar_forecolor').after("<ul class='dropdown-menu colorpanel'><input type='text' id='forecolor-input' value='#000000' /><div id='forecolor-picker'></div></ul>");
+          $('#forecolor-picker').farbtastic(function(color) {
+            $('#forecolor-input').val(color);
+            document.execCommand("forecolor", false, color);
+            $(this).closest('.btn-group').removeClass('open');
+            $('.toolbar_forecolor div').css({
+              "color": color
+            });
+            return false;
+          });
+        }
+        if (jQuery.inArray('backcolor', excludes) < 0) {
+          $('a.toolbar_bgcolor').after("<ul class='dropdown-menu colorpanel'><input type='text' id='bgcolor-input' value='#000000' /><div id='bgcolor-picker'></div></ul>");
+          $('#bgcolor-picker').farbtastic(function(color) {
+            $('#bgcolor-input').val(color);
+            document.execCommand("backcolor", false, color);
+            $(this).closest('.btn-group').removeClass('open');
+            $('.toolbar_bgcolor div').css({
+              "background-color": color
+            });
+            return false;
+          });
+        }
+        $(this).on('focus', function() {
+          var $this;
+          $this = $(this);
+          $this.data('before', $this.html());
+          return $this;
+        }).on('blur keyup paste', function() {
+              var $this;
+              $this = $(this);
+              if ($this.data('before') !== $this.html()) {
+                $this.data('before', $this.html());
+                $this.trigger('change');
+              }
+              return $this;
+            });
+        $("a.toolbar-cmd").on('click', function() {
+          var ceNode, cmd, dummy, range;
+          cmd = $(this).attr('command');
+          if ($(this).attr('userinput') === 'yes') {
+            methods[cmd].apply(this);
+          } else if ($(this).attr('block')) {
+            methods['formatblock'].apply(this, ["<" + ($(this).attr('block')) + ">"]);
+          } else {
+            if ((cmd === 'justifyright') || (cmd === 'justifyleft') || (cmd === 'justifycenter') || (cmd === 'justifyfull')) {
+              try {
+                document.execCommand(cmd, false, null);
+              } catch (e) {
+                if (e && e.result === 2147500037) {
+                  range = window.getSelection().getRangeAt(0);
+                  dummy = document.createElement('br');
+                  ceNode = range.startContainer.parentNode;
+                  while ((ceNode != null) && ceNode.contentEditable !== 'true') {
+                    ceNode = ceNode.parentNode;
+                  }
+                  if (!ceNode) {
+                    throw 'Selected node is not editable!';
+                  }
+                  ceNode.insertBefore(dummy, ceNode.childNodes[0]);
+                  document.execCommand(cmd, false, null);
+                  dummy.parentNode.removeChild(dummy);
+                } else if (console && console.log) {
+                  console.log(e);
+                }
+              }
+            } else {
+              document.execCommand(cmd, false, null);
+            }
+          }
+          return false;
+        });
+        shortcuts = [
+          {
+            keys: 'Ctrl+l',
+            method: function() {
+              return methods.createlink.apply(this);
+            }
+          }, {
+            keys: 'Ctrl+g',
+            method: function() {
+              return methods.insertimage.apply(this);
+            }
+          }, {
+            keys: 'Ctrl+Alt+U',
+            method: function() {
+              return document.execCommand('insertunorderedlist', false, null);
+            }
+          }, {
+            keys: 'Ctrl+Alt+O',
+            method: function() {
+              return document.execCommand('insertorderedlist', false, null);
+            }
+          }, {
+            keys: 'Ctrl+q',
+            method: function() {
+              return methods.formatblock.apply(this, ["<blockquote>"]);
+            }
+          }, {
+            keys: 'Ctrl+Alt+k',
+            method: function() {
+              return methods.formatblock.apply(this, ["<pre>"]);
+            }
+          }, {
+            keys: 'Ctrl+.',
+            method: function() {
+              return document.execCommand('superscript', false, null);
+            }
+          }, {
+            keys: 'Ctrl+Shift+.',
+            method: function() {
+              return document.execCommand('subscript', false, null);
+            }
+          }, {
+            keys: 'Ctrl+Alt+0',
+            method: function() {
+              return methods.formatblock.apply(this, ["p"]);
+            }
+          }, {
+            keys: 'Ctrl+b',
+            method: function() {
+              return document.execCommand('bold', false, null);
+            }
+          }, {
+            keys: 'Ctrl+i',
+            method: function() {
+              return document.execCommand('italic', false, null);
+            }
+          }, {
+            keys: 'Ctrl+Alt+1',
+            method: function() {
+              return methods.formatblock.apply(this, ["H1"]);
+            }
+          }, {
+            keys: 'Ctrl+Alt+2',
+            method: function() {
+              return methods.formatblock.apply(this, ["H2"]);
+            }
+          }, {
+            keys: 'Ctrl+Alt+3',
+            method: function() {
+              return methods.formatblock.apply(this, ["H3"]);
+            }
+          }, {
+            keys: 'Ctrl+Alt+4',
+            method: function() {
+              return methods.formatblock.apply(this, ["H4"]);
+            }
+          }, {
+            keys: 'Ctrl+m',
+            method: function() {
+              return document.execCommand("removeFormat", false, null);
+            }
+          }, {
+            keys: 'Ctrl+u',
+            method: function() {
+              return document.execCommand('underline', false, null);
+            }
+          }, {
+            keys: 'tab',
+            method: function() {
+              return document.execCommand('indent', false, null);
+            }
+          }, {
+            keys: 'Ctrl+tab',
+            method: function() {
+              return document.execCommand('indent', false, null);
+            }
+          }, {
+            keys: 'Shift+tab',
+            method: function() {
+              return document.execCommand('outdent', false, null);
+            }
+          }
+        ];
+        $.each(shortcuts, function(index, elem) {
+          return shortcut.add(elem.keys, function() {
+            elem.method();
+            return false;
+          }, {
+            'type': 'keydown',
+            'propagate': false
+          });
+        });
+        return this.each(function() {
+          var $this, data, tooltip;
+          $this = $(this);
+          data = $this.data('fresheditor');
+          tooltip = $('<div/>', {
+            text: $this.attr('title')
+          });
+          if (!data) {
+            return $(this).data('fresheditor', {
+              target: $this,
+              tooltip: tooltip
+            });
+          }
+        });
+      }
+    };
+    return $.fn.freshereditor = function(method) {
+      if (methods[method]) {
+        methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+      } else if (typeof method === 'object' || !method) {
+        methods.init.apply(this, arguments);
+      } else {
+        $.error('Method ' + method + ' does not exist on jQuery.contentEditable');
+      }
+    };
+  })(jQuery);
+}).call(this);

+ 226 - 0
desktop/core/static/ext/js/shortcut.js

@@ -0,0 +1,226 @@
+/**
+* http://www.openjs.com/scripts/events/keyboard_shortcuts/
+* Version : 2.01.B
+* By Binny V A
+* License : BSD
+*/
+var shortcut = {
+	'all_shortcuts': {}, //All the shortcuts are stored in this array
+	'add': function (shortcut_combination, callback, opt) {
+		//Provide a set of default options
+		var default_options = {
+			'type': 'keydown',
+			'propagate': false,
+			'disable_in_input': false,
+			'target': document,
+			'keycode': false
+		};
+		if (!opt) {
+			opt = default_options;
+		}
+		else {
+			var dfo;
+			for (dfo in default_options) {
+				if (typeof opt[dfo] == 'undefined') opt[dfo] = default_options[dfo];
+			}
+		}
+
+		var ele = opt.target;
+		if (typeof opt.target == 'string') ele = document.getElementById(opt.target);
+		var ths = this;
+		shortcut_combination = shortcut_combination.toLowerCase();
+
+		//The function to be called at keypress
+		var func = function (e) {
+			e = e || window.event;
+
+			if (opt['disable_in_input']) { //Don't enable shortcut keys in Input, Textarea fields
+				var element;
+				if (e.target) element = e.target;
+				else if (e.srcElement) element = e.srcElement;
+				if (element.nodeType == 3) element = element.parentNode;
+
+				if (element.tagName == 'INPUT' || element.tagName == 'TEXTAREA') return;
+			}
+
+			//Find Which key is pressed
+			if (e.keyCode) code = e.keyCode;
+			else if (e.which) code = e.which;
+			var character = String.fromCharCode(code).toLowerCase();
+
+			if (code == 188) character = ","; //If the user presses , when the type is onkeydown
+			if (code == 190) character = "."; //If the user presses , when the type is onkeydown
+
+			var keys = shortcut_combination.split("+");
+			//Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
+			var kp = 0;
+
+			//Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
+			var shift_nums = {
+				"`": "~",
+				"1": "!",
+				"2": "@",
+				"3": "#",
+				"4": "$",
+				"5": "%",
+				"6": "^",
+				"7": "&",
+				"8": "*",
+				"9": "(",
+				"0": ")",
+				"-": "_",
+				"=": "+",
+				";": ":",
+				"'": "\"",
+				",": "<",
+				".": ">",
+				"/": "?",
+				"\\": "|"
+			}
+			//Special Keys - and their codes
+			var special_keys = {
+				'esc': 27,
+				'escape': 27,
+				'tab': 9,
+				'space': 32,
+				'return': 13,
+				'enter': 13,
+				'backspace': 8,
+
+				'scrolllock': 145,
+				'scroll_lock': 145,
+				'scroll': 145,
+				'capslock': 20,
+				'caps_lock': 20,
+				'caps': 20,
+				'numlock': 144,
+				'num_lock': 144,
+				'num': 144,
+
+				'pause': 19,
+				'break': 19,
+
+				'insert': 45,
+				'home': 36,
+				'delete': 46,
+				'end': 35,
+
+				'pageup': 33,
+				'page_up': 33,
+				'pu': 33,
+
+				'pagedown': 34,
+				'page_down': 34,
+				'pd': 34,
+
+				'left': 37,
+				'up': 38,
+				'right': 39,
+				'down': 40,
+
+				'f1': 112,
+				'f2': 113,
+				'f3': 114,
+				'f4': 115,
+				'f5': 116,
+				'f6': 117,
+				'f7': 118,
+				'f8': 119,
+				'f9': 120,
+				'f10': 121,
+				'f11': 122,
+				'f12': 123
+			}
+
+			var modifiers = {
+				shift: { wanted: false, pressed: false },
+				ctrl: { wanted: false, pressed: false },
+				alt: { wanted: false, pressed: false },
+				meta: { wanted: false, pressed: false}	//Meta is Mac specific
+			};
+
+			if (e.ctrlKey) modifiers.ctrl.pressed = true;
+			if (e.shiftKey) modifiers.shift.pressed = true;
+			if (e.altKey) modifiers.alt.pressed = true;
+			if (e.metaKey) modifiers.meta.pressed = true;
+
+			for (var i = 0; k = keys[i], i < keys.length; i++) {
+				//Modifiers
+				if (k == 'ctrl' || k == 'control') {
+					kp++;
+					modifiers.ctrl.wanted = true;
+
+				} else if (k == 'shift') {
+					kp++;
+					modifiers.shift.wanted = true;
+
+				} else if (k == 'alt') {
+					kp++;
+					modifiers.alt.wanted = true;
+				} else if (k == 'meta') {
+					kp++;
+					modifiers.meta.wanted = true;
+				} else if (k.length > 1) { //If it is a special key
+					if (special_keys[k] == code) kp++;
+
+				} else if (opt['keycode']) {
+					if (opt['keycode'] == code) kp++;
+
+				} else { //The special keys did not match
+					if (character == k) kp++;
+					else {
+						if (shift_nums[character] && e.shiftKey) { //Stupid Shift key bug created by using lowercase
+							character = shift_nums[character];
+							if (character == k) kp++;
+						}
+					}
+				}
+			}
+
+			if (kp == keys.length &&
+						modifiers.ctrl.pressed == modifiers.ctrl.wanted &&
+						modifiers.shift.pressed == modifiers.shift.wanted &&
+						modifiers.alt.pressed == modifiers.alt.wanted &&
+						modifiers.meta.pressed == modifiers.meta.wanted) {
+				callback(e);
+
+				if (!opt['propagate']) { //Stop the event
+					//e.cancelBubble is supported by IE - this will kill the bubbling process.
+					e.cancelBubble = true;
+					e.returnValue = false;
+
+					//e.stopPropagation works in Firefox.
+					if (e.stopPropagation) {
+						e.stopPropagation();
+						e.preventDefault();
+					}
+					return false;
+				}
+			}
+		}
+		this.all_shortcuts[shortcut_combination] = {
+			'callback': func,
+			'target': ele,
+			'event': opt['type']
+		};
+		//Attach the function with the event
+		if (ele.addEventListener) ele.addEventListener(opt['type'], func, false);
+		else if (ele.attachEvent) ele.attachEvent('on' + opt['type'], func);
+		else ele['on' + opt['type']] = func;
+	},
+
+	//Remove the shortcut - just specify the shortcut and I will remove the binding
+	'remove': function (shortcut_combination) {
+		shortcut_combination = shortcut_combination.toLowerCase();
+		var binding = this.all_shortcuts[shortcut_combination];
+		delete (this.all_shortcuts[shortcut_combination])
+		if (!binding) return;
+		var type = binding['event'];
+		var ele = binding['target'];
+		var callback = binding['callback'];
+
+		if (ele.detachEvent) ele.detachEvent('on' + type, callback);
+		else if (ele.removeEventListener) ele.removeEventListener(type, callback, false);
+		else ele['on' + type] = false;
+	}
+}