Browse Source

HUE-1335 [search] In place editing of facets and sorting fields

Added the Knockout editable binding
Enrico Berti 12 years ago
parent
commit
5ad4ec2

+ 59 - 15
apps/search/src/search/templates/admin_collection_facets.mako

@@ -32,6 +32,11 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
     ${ layout.sidebar(hue_collection, 'facets') }
   </%def>
   <%def name="content()">
+
+    <link href="/static/ext/css/bootstrap-editable.css" rel="stylesheet">
+    <script src="/static/ext/js/bootstrap-editable.min.js"></script>
+    <script src="/static/ext/js/knockout.x-editable.js"></script>
+
     <form method="POST" class="form-horizontal" data-bind="submit: submit">
 
       <div class="section">
@@ -87,7 +92,7 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
           </div>
           <div data-bind="foreach: fieldFacets">
             <div class="bubble">
-              <strong><span data-bind="text: label"></span></strong>
+              <strong><span data-bind="editable: label"></span></strong>
               <span style="color:#666;font-size: 12px">(<span data-bind="text: field"></span>)</span>
               <a class="btn btn-small" data-bind="click: $root.removeFieldFacet"><i class="icon-trash"></i></a>
             </div>
@@ -111,10 +116,10 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
         </div>
         <div data-bind="foreach: rangeFacets">
           <div class="bubble">
-            <strong><span data-bind="text: label"></span></strong>
+            <strong><span data-bind="editable: label"></span></strong>
             <span style="color:#666;font-size: 12px">
-              (<span data-bind="text: field"></span>, <span data-bind="text: start"></span> <i class="icon-double-angle-right"></i> <span data-bind="text: end"></span>,
-              <i class="icon-resize-horizontal"></i> <span data-bind="text: gap"></span>)
+              (<span data-bind="text: field"></span>, <span data-bind="editable: start"></span> <i class="icon-double-angle-right"></i> <span data-bind="editable: end"></span>,
+              <i class="icon-resize-horizontal"></i> <span data-bind="editable: gap"></span>)
             </span>
             <a class="btn btn-small" data-bind="click: $root.removeRangeFacet"><i class="icon-trash"></i></a>
           </div>
@@ -145,10 +150,10 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
         </div>
         <div data-bind="foreach: dateFacets">
           <div class="bubble">
-            <strong><span data-bind="text: label"></span></strong>
+            <strong><span data-bind="editable: label"></span></strong>
             <span style="color:#666;font-size: 12px">
-              (<span data-bind="text: field"></span>, <span data-bind="text: start"></span> <i class="icon-double-angle-right"></i> <span data-bind="text: end"></span>,
-              <i class="icon-resize-horizontal"></i> <span data-bind="text: gap"></span><span data-bind="visible: format != ''">, <i class="icon-calendar"></i> <span data-bind="text: format"></span></span>)
+              (<span data-bind="text: field"></span>, <span data-bind="editable: start"></span> <i class="icon-double-angle-right"></i> <span data-bind="editable: end"></span>,
+              <i class="icon-resize-horizontal"></i> <span data-bind="editable: gap"></span>, <i class="icon-calendar"></i> <span data-bind="editable: format"></span>)
             </span>
             <a class="btn btn-small" data-bind="click: $root.removeDateFacet"><i class="icon-trash"></i></a>
           </div>
@@ -503,18 +508,57 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
   }
 
   var Facet = function (args) {
-    return {
+    var _facet = {
       uuid: (typeof args['uuid'] !== 'undefined' && args['uuid'] != null) ? args['uuid'] : UUID(),
       type: args['type'],
       field: args['field'],
-      label: args['label'],
-      format: args['format'],
-      start: args['start'],
-      end: args['end'],
-      gap: args['gap'],
+      label: ko.observable(args['label']),
+      format: ko.observable(args['format']),
+      start: ko.observable(args['start']),
+      end: ko.observable(args['end']),
+      gap: ko.observable(args['gap']),
       isVerbatim: false,
       verbatim: ""
     }
+    _facet.label.subscribe(function (newValue) {
+      if ($.trim(newValue) == ""){
+        _facet.label(_facet.field);
+      }
+      viewModel.isSaveBtnVisible(true);
+    });
+    _facet.format.subscribe(function (newValue) {
+      viewModel.isSaveBtnVisible(true);
+    });
+    _facet.start.subscribe(function (newValue) {
+      if (_facet.type == "date" && typeof newValue == "string"){
+        valueParseHelper(newValue, _facet.start, 10);
+      }
+      viewModel.isSaveBtnVisible(true);
+    });
+    _facet.end.subscribe(function (newValue) {
+      if (_facet.type == "date" && typeof newValue == "string"){
+        valueParseHelper(newValue, _facet.end, 0);
+      }
+      viewModel.isSaveBtnVisible(true);
+    });
+    _facet.gap.subscribe(function (newValue) {
+      if (_facet.type == "date" && typeof newValue == "string"){
+        valueParseHelper(newValue, _facet.gap, 1);
+      }
+      viewModel.isSaveBtnVisible(true);
+    });
+    return _facet;
+  }
+
+  function valueParseHelper(value, field, defaultFrequency){
+    // try to parse the user free input
+    try {
+      field(new DateMath({frequency: parseFloat($.trim(value).split(" ")[0]), unit: $.trim(value).split(" ")[1]}));
+    }
+    catch (exception){
+      $.jHueNotify.error("${ _('There was an error parsing your input') }");
+      field(new DateMath({frequency: defaultFrequency, unit: 'DAYS'}));
+    }
   }
 
   var FieldFacet = function (obj) {
@@ -748,8 +792,8 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
       $.ajax("${ url('search:admin_collection_facets', collection_id=hue_collection.id) }", {
         data: {
           'properties': ko.toJSON(self.properties),
-          'fields': ko.utils.stringifyJson(self.fieldFacets),
-          'ranges': ko.utils.stringifyJson(self.rangeFacets),
+          'fields': ko.toJSON(self.fieldFacets),
+          'ranges': ko.toJSON(self.rangeFacets),
           'dates': ko.toJSON(self.dateFacets),
           'order': ko.toJSON(ko.utils.arrayMap(self.sortableFacets(), function (obj) {
                       return obj.uuid;

+ 28 - 8
apps/search/src/search/templates/admin_collection_sorting.mako

@@ -34,6 +34,11 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
   </%def>
 
   <%def name="content()">
+
+    <link href="/static/ext/css/bootstrap-editable.css" rel="stylesheet">
+    <script src="/static/ext/js/bootstrap-editable.min.js"></script>
+    <script src="/static/ext/js/knockout.x-editable.js"></script>
+
     <form method="POST" class="form-horizontal" data-bind="submit: submit">
       <div class="section">
         <div class="alert alert-info">
@@ -55,9 +60,9 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
         </div>
         <div data-bind="foreach: sortingFields">
           <div class="bubble">
-            <strong><span data-bind="text: label"></span></strong>
+            <strong><span data-bind="editable: label"></span></strong>
             <span style="color:#666;font-size: 12px">
-              (<span data-bind="text: field"></span> <i class="icon-arrow-up" data-bind="visible: asc == true"></i><i class="icon-arrow-down" data-bind="visible: asc == false"></i> )
+              (<span data-bind="text: field"></span> <i class="icon-arrow-up" data-bind="visible: asc() == true"></i><i class="icon-arrow-down" data-bind="visible: asc() == false"></i> <span data-bind="editable: order"></span> )
             </span>
             <a class="btn btn-small" data-bind="click: $root.removeSortingField"><i class="icon-trash"></i></a>
           </div>
@@ -114,11 +119,26 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
 <script type="text/javascript">
 
   var SortingField = function (field, label, asc) {
-    return {
+    var _field = {
       field: field,
-      label: label,
-      asc: asc
-    }
+      label: ko.observable(label),
+      asc: ko.observable(asc),
+      order: ko.observable(asc ? "ASC" : "DESC")
+    };
+    _field.label.subscribe(function (newValue) {
+      if ($().trim(newValue) == "") {
+        _field.label(f.field);
+      }
+    });
+    _field.order.subscribe(function (newValue) {
+      if ($.trim(newValue).toUpperCase() == "DESC") {
+        _field.asc(false);
+      }
+      else {
+        _field.asc(true);
+      }
+    });
+    return _field;
   }
 
   function ViewModel() {
@@ -162,8 +182,8 @@ ${ commonheader(_('Search'), "search", user, "40px") | n,unicode }
     self.submit = function () {
       $.ajax("${ url('search:admin_collection_sorting', collection_id=hue_collection.id) }", {
         data: {
-          'properties': ko.utils.stringifyJson({'is_enabled': self.isEnabled()}),
-          'fields': ko.utils.stringifyJson(self.sortingFields)
+          'properties': ko.toJSON({'is_enabled': self.isEnabled()}),
+          'fields': ko.toJSON(self.sortingFields)
         },
         contentType: 'application/json',
         type: 'POST',

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

@@ -92,3 +92,7 @@ h1.emptyMessage {
 .stepDetails {
   padding: 5px;
 }
+
+.editable {
+  cursor: pointer;
+}

+ 116 - 0
desktop/core/static/ext/js/knockout.x-editable.js

@@ -0,0 +1,116 @@
+(function(factory) {
+    if (typeof define === "function" && define.amd) {
+        // AMD anonymous module
+        define(["knockout", "jquery"], factory);
+    } else {
+        // No module loader (plain <script> tag) - put directly in global namespace
+        factory(window.ko, window.jQuery);
+    }
+})(function(ko, $) {
+	ko.bindingHandlers.editable = {
+		init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+			var $element = $(element),
+				value = valueAccessor(),
+				allBindings = allBindingsAccessor(),
+				editableOptions = allBindings.editableOptions || {};
+
+			editableOptions.value = ko.utils.unwrapObservable(value);
+
+			if (!editableOptions.name) {
+				$.each(bindingContext.$data, function (k, v) {
+					if (v == value) {
+						editableOptions.name = k;
+						return false;
+					}
+				});
+			}
+			
+			//wrap calls to knockout.validation
+			if (!editableOptions.validate && value.isValid) {
+				editableOptions.validate = function (testValue) {
+					//have to set to new value, then call validate, then reset to original value
+					//not pretty, but works
+					var initalValue = value();
+					value(testValue);
+					var res = value.isValid() ? null : ko.utils.unwrapObservable(value.error);
+					value(initalValue);
+					return res;
+				}
+			}
+
+			if ((editableOptions.type === 'select' || editableOptions.type === 'checklist'|| editableOptions.type === 'typeahead') && !editableOptions.source && editableOptions.options) {
+				if (editableOptions.optionsCaption)
+					editableOptions.prepend = editableOptions.optionsCaption;
+
+				//taken directly from ko.bindingHandlers['options']
+				function applyToObject(object, predicate, defaultValue) {
+					var predicateType = typeof predicate;
+					if (predicateType == "function")    // Given a function; run it against the data value
+						return predicate(object);
+					else if (predicateType == "string") // Given a string; treat it as a property name on the data value
+						return object[predicate];
+					else                                // Given no optionsText arg; use the data value itself
+						return defaultValue;
+				}
+
+				editableOptions.source = function() {
+					return ko.utils.arrayMap(editableOptions.options(), function (item) {
+						var optionValue = applyToObject(item, editableOptions.optionsValue, item);
+						var optionText = applyToObject(item, editableOptions.optionsText, optionText);
+
+						return {
+							value: ko.utils.unwrapObservable(optionValue),
+							text: ko.utils.unwrapObservable(optionText)
+						};
+					});
+				}
+			}
+
+			if (editableOptions.visible && ko.isObservable(editableOptions.visible)) {
+				editableOptions.toggle = 'manual';
+			}
+			
+			//create editable
+			var $editable = $element.editable(editableOptions);
+
+			//update observable on save
+			if (ko.isObservable(value)) {
+				$editable.on('save.ko', function (e, params) {
+					value(params.newValue);
+				})
+			};
+
+			if (editableOptions.save) {
+				$editable.on('save', editableOptions.save);
+			}
+			
+			//setup observable to fire only when editable changes, not when options change
+			//http://www.knockmeout.net/2012/06/knockoutjs-performance-gotcha-3-all-bindings.html
+			ko.computed({
+				read: function () {
+					var val = ko.utils.unwrapObservable(valueAccessor());
+					if (val === null) val = '';
+					$editable.editable('setValue', val, true)
+				},
+				owner: this,
+				disposeWhenNodeIsRemoved: element
+			});
+	
+			if (editableOptions.visible && ko.isObservable(editableOptions.visible)) {
+				ko.computed({
+					read: function () {
+						var val = ko.utils.unwrapObservable(editableOptions.visible());
+						if (val)
+							$editable.editable('show');
+					},
+					owner: this,
+					disposeWhenNodeIsRemoved: element
+				});
+
+				$editable.on('hidden.ko', function (e, params) {
+					editableOptions.visible(false);
+				});
+			}
+		}
+	};
+});