diff --git a/grails-app/assets/javascripts/forms-knockout-bindings.js b/grails-app/assets/javascripts/forms-knockout-bindings.js
index 20715f3..abba7af 100644
--- a/grails-app/assets/javascripts/forms-knockout-bindings.js
+++ b/grails-app/assets/javascripts/forms-knockout-bindings.js
@@ -1,1238 +1,1226 @@
/**
* Custom knockout bindings used by the forms library
*/
-(function () {
-
- /**
- * Exposes extra context to child bindings via the binding context.
- * Used as a mechanism to allow clients to pass configuration to
- * components rendered by this plugin.
- */
- ko.bindingHandlers.withContext = {
- init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
- // Make a modified binding context, with a extra properties, and apply it to descendant elements
- var innerBindingContext = bindingContext.extend(valueAccessor);
- ko.applyBindingsToDescendants(innerBindingContext, element);
-
- // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
- return {controlsDescendantBindings: true};
+(function() {
+
+ /**
+ * Exposes extra context to child bindings via the binding context.
+ * Used as a mechanism to allow clients to pass configuration to
+ * components rendered by this plugin.
+ */
+ ko.bindingHandlers.withContext = {
+ init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+ // Make a modified binding context, with a extra properties, and apply it to descendant elements
+ var innerBindingContext = bindingContext.extend(valueAccessor);
+ ko.applyBindingsToDescendants(innerBindingContext, element);
+
+ // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
+ return { controlsDescendantBindings: true };
+ }
+ };
+
+ var image = function(props) {
+
+ var imageObj = {
+ id:props.id,
+ name:props.name,
+ size:props.size,
+ url: props.url,
+ thumbnail_url: props.thumbnail_url,
+ viewImage : function() {
+ window['showImageInViewer'](this.id, this.url, this.name);
}
};
-
- var image = function (props) {
-
- var imageObj = {
- id: props.id,
- name: props.name,
- size: props.size,
- url: props.url,
- thumbnail_url: props.thumbnail_url,
- viewImage: function () {
- window['showImageInViewer'](this.id, this.url, this.name);
- }
+ return imageObj;
+ };
+
+ ko.bindingHandlers.photoPointUpload = {
+ init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+
+ var defaultConfig = {
+ maxWidth: 300,
+ minWidth:150,
+ minHeight:150,
+ maxHeight: 300,
+ previewSelector: '.preview'
};
- return imageObj;
- };
-
- ko.bindingHandlers.photoPointUpload = {
- init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
+ var size = ko.observable();
+ var progress = ko.observable();
+ var error = ko.observable();
+ var complete = ko.observable(true);
- var defaultConfig = {
- maxWidth: 300,
- minWidth: 150,
- minHeight: 150,
- maxHeight: 300,
- previewSelector: '.preview'
- };
- var size = ko.observable();
- var progress = ko.observable();
- var error = ko.observable();
- var complete = ko.observable(true);
-
- var uploadProperties = {
+ var uploadProperties = {
- size: size,
- progress: progress,
- error: error,
- complete: complete
+ size: size,
+ progress: progress,
+ error:error,
+ complete:complete
- };
- var innerContext = bindingContext.createChildContext(bindingContext);
- ko.utils.extend(innerContext, uploadProperties);
-
- var config = valueAccessor();
- config = $.extend({}, config, defaultConfig);
-
- var target = config.target; // Expected to be a ko.observableArray
- $(element).fileupload({
- url: config.url,
- autoUpload: true,
- dataType: 'json'
- }).on('fileuploadadd', function (e, data) {
- complete(false);
- progress(1);
- }).on('fileuploadprocessalways', function (e, data) {
- if (data.files[0].preview) {
- if (config.previewSelector !== undefined) {
- var previewElem = $(element).parent().find(config.previewSelector);
- previewElem.append(data.files[0].preview);
- }
+ };
+ var innerContext = bindingContext.createChildContext(bindingContext);
+ ko.utils.extend(innerContext, uploadProperties);
+
+ var config = valueAccessor();
+ config = $.extend({}, config, defaultConfig);
+
+ var target = config.target; // Expected to be a ko.observableArray
+ $(element).fileupload({
+ url:config.url,
+ autoUpload:true,
+ dataType:'json'
+ }).on('fileuploadadd', function(e, data) {
+ complete(false);
+ progress(1);
+ }).on('fileuploadprocessalways', function(e, data) {
+ if (data.files[0].preview) {
+ if (config.previewSelector !== undefined) {
+ var previewElem = $(element).parent().find(config.previewSelector);
+ previewElem.append(data.files[0].preview);
}
- }).on('fileuploadprogressall', function (e, data) {
- progress(Math.floor(data.loaded / data.total * 100));
- size(data.total);
- }).on('fileuploaddone', function (e, data) {
+ }
+ }).on('fileuploadprogressall', function(e, data) {
+ progress(Math.floor(data.loaded / data.total * 100));
+ size(data.total);
+ }).on('fileuploaddone', function(e, data) {
// var resultText = $('pre', data.result).text();
// var result = $.parseJSON(resultText);
- var result = data.result;
- if (!result) {
- result = {};
- error('No response from server');
- }
-
- if (result.files[0]) {
- target.push(result.files[0]);
- complete(true);
- } else {
- error(result.error);
- }
-
- }).on('fileuploadfail', function (e, data) {
- error(data.errorThrown);
- });
-
- ko.applyBindingsToDescendants(innerContext, element);
-
- return {controlsDescendantBindings: true};
- }
- };
-
- ko.bindingHandlers.imageUpload = {
- init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
- var defaultConfig = {
- maxWidth: 300,
- minWidth: 150,
- minHeight: 150,
- maxHeight: 300,
- previewSelector: '.preview',
- viewModel: viewModel
- };
- var size = ko.observable();
- var progress = ko.observable();
- var error = ko.observable();
- var complete = ko.observable(true);
-
- var config = valueAccessor();
- config = $.extend({}, config, defaultConfig);
-
- var target = config.target,
- dropZone = $(element).find('.dropzone');
-
- var context = config.context;
- var uploadProperties = {
- size: size,
- progress: progress,
- error: error,
- complete: complete
- };
-
- var innerContext = bindingContext.createChildContext(bindingContext);
- ko.utils.extend(innerContext, uploadProperties);
- var previewElem = $(element).parent().find(config.previewSelector);
-
- // For a reason I can't determine, when forms are loaded via ajax the
- // fileupload widget gets a blank widgetEventPrefix. (normally it would be 'fileupload').
- // This checks for this condition and registers the correct event listeners.
- var eventPrefix = 'fileupload';
- if ($.blueimp && $.blueimp.fileupload) {
- eventPrefix = $.blueimp.fileupload.prototype.widgetEventPrefix;
+ var result = data.result;
+ if (!result) {
+ result = {};
+ error('No response from server');
}
- $(element).fileupload({
- url: config.url,
- autoUpload: true,
- dropZone: dropZone,
- pasteZone: null,
- dataType: 'json'
- }).on(eventPrefix + 'add', function (e, data) {
- previewElem.html('');
- complete(false);
- progress(1);
- }).on(eventPrefix + 'processalways', function (e, data) {
- if (data.files[0].preview) {
- if (config.previewSelector !== undefined) {
- previewElem.append(data.files[0].preview);
- }
- }
- }).on(eventPrefix + 'progressall', function (e, data) {
- progress(Math.floor(data.loaded / data.total * 100));
- size(data.total);
- }).on(eventPrefix + 'done', function (e, data) {
- var result = data.result;
- var $doc = $(document);
- if (!result) {
- result = {};
- error('No response from server');
- }
-
- if (result.files[0]) {
- result.files.forEach(function (f) {
- // flag to indicate the image is in biocollect and needs to be save to ecodata as a document
- var data = {
- thumbnailUrl: f.thumbnail_url,
- url: f.url,
- contentType: f.contentType,
- filename: f.name,
- name: f.name,
- filesize: f.size,
- dateTaken: f.isoDate,
- staged: true,
- attribution: f.attribution,
- licence: f.licence
- };
-
- target.push(new ImageViewModel(data, true, context));
-
- if (f.decimalLongitude && f.decimalLatitude) {
- $doc.trigger('imagelocation', {
- decimalLongitude: f.decimalLongitude,
- decimalLatitude: f.decimalLatitude
- });
- }
-
- if (f.isoDate) {
- $doc.trigger('imagedatetime', {
- date: f.isoDate
- });
- }
+ if (result.files[0]) {
+ target.push(result.files[0]);
+ complete(true);
+ }
+ else {
+ error(result.error);
+ }
- });
+ }).on('fileuploadfail', function(e, data) {
+ error(data.errorThrown);
+ });
- complete(true);
- } else {
- error(result.error);
- }
+ ko.applyBindingsToDescendants(innerContext, element);
- }).on(eventPrefix + 'fail', function (e, data) {
- error(data.errorThrown);
- });
+ return { controlsDescendantBindings: true };
+ }
+ };
+
+ ko.bindingHandlers.imageUpload = {
+ init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+ var defaultConfig = {
+ maxWidth: 300,
+ minWidth:150,
+ minHeight:150,
+ maxHeight: 300,
+ previewSelector: '.preview',
+ viewModel: viewModel
+ };
+ var size = ko.observable();
+ var progress = ko.observable();
+ var error = ko.observable();
+ var complete = ko.observable(true);
+
+ var config = valueAccessor();
+ config = $.extend({}, config, defaultConfig);
+
+ var target = config.target,
+ dropZone = $(element).find('.dropzone');
+
+ var context = config.context;
+ var uploadProperties = {
+ size: size,
+ progress: progress,
+ error:error,
+ complete:complete
+ };
- ko.applyBindingsToDescendants(innerContext, element);
+ var innerContext = bindingContext.createChildContext(bindingContext);
+ ko.utils.extend(innerContext, uploadProperties);
+ var previewElem = $(element).parent().find(config.previewSelector);
- return {controlsDescendantBindings: true};
+ // For a reason I can't determine, when forms are loaded via ajax the
+ // fileupload widget gets a blank widgetEventPrefix. (normally it would be 'fileupload').
+ // This checks for this condition and registers the correct event listeners.
+ var eventPrefix = 'fileupload';
+ if ($.blueimp && $.blueimp.fileupload) {
+ eventPrefix = $.blueimp.fileupload.prototype.widgetEventPrefix;
}
- };
- ko.bindingHandlers.editDocument = {
- init: function (element, valueAccessor) {
- if (ko.isObservable(valueAccessor())) {
- var document = ko.utils.unwrapObservable(valueAccessor());
- if (typeof document.status == 'function') {
- document.status.subscribe(function (status) {
- if (status == 'deleted') {
- valueAccessor()(null);
- }
- });
+ $(element).fileupload({
+ url:config.url,
+ autoUpload:true,
+ dropZone: dropZone,
+ pasteZone: null,
+ dataType:'json'
+ }).on(eventPrefix+'add', function(e, data) {
+ previewElem.html('');
+ complete(false);
+ progress(1);
+ }).on(eventPrefix+'processalways', function(e, data) {
+ if (data.files[0].preview) {
+ if (config.previewSelector !== undefined) {
+ previewElem.append(data.files[0].preview);
}
}
- var options = {
- name: 'documentEditTemplate',
- data: valueAccessor()
- };
- return ko.bindingHandlers['template'].init(element, function () {
- return options;
- });
- },
- update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
- var options = {
- name: 'documentEditTemplate',
- data: valueAccessor()
- };
- ko.bindingHandlers['template'].update(element, function () {
- return options;
- }, allBindings, viewModel, bindingContext);
- }
- };
-
- ko.bindingHandlers.expression = {
-
- update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
-
- var expressionString = ko.utils.unwrapObservable(valueAccessor());
- var result = ecodata.forms.expressionEvaluator.evaluate(expressionString, bindingContext);
-
- $(element).text(result);
- }
-
- };
-
-
- /*
- * Fused Autocomplete supports two versions of autocomplete (original autocomplete implementation by Jorn Zaefferer and jquery_ui)
- * Expects three parameters source, name and guid.
- * Ajax response lists needs name attribute.
- * Doco url: http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
- * Note: Autocomplete implementation by Jorn Zaefferer is now been deprecated and its been migrated to jquery_ui.
- *
- */
+ }).on(eventPrefix+'progressall', function(e, data) {
+ progress(Math.floor(data.loaded / data.total * 100));
+ size(data.total);
+ }).on(eventPrefix+'done', function(e, data) {
+ var result = data.result;
+ var $doc = $(document);
+ if (!result) {
+ result = {};
+ error('No response from server');
+ }
- ko.bindingHandlers.fusedAutocomplete = {
+ if (result.files[0]) {
+ result.files.forEach(function( f ){
+ // flag to indicate the image is in biocollect and needs to be save to ecodata as a document
+ var data = {
+ thumbnailUrl: f.thumbnail_url,
+ url: f.url,
+ contentType: f.contentType,
+ filename: f.name,
+ name: f.name,
+ filesize: f.size,
+ dateTaken: f.isoDate,
+ staged: true,
+ attribution: f.attribution,
+ licence: f.licence
+ };
+
+ target.push(new ImageViewModel(data, true, context));
+
+ if(f.decimalLongitude && f.decimalLatitude){
+ $doc.trigger('imagelocation', {
+ decimalLongitude: f.decimalLongitude,
+ decimalLatitude: f.decimalLatitude
+ });
+ }
- init: function (element, params) {
- var params = params();
- var options = {};
- var url = ko.utils.unwrapObservable(params.source);
- options.source = function (request, response) {
- $(element).addClass("ac_loading");
- $.ajax({
- url: url,
- dataType: 'json',
- data: {q: request.term},
- success: function (data) {
- var items = $.map(data.autoCompleteList, function (item) {
- return {
- label: item.name,
- value: item.name,
- source: item
- }
+ if(f.isoDate){
+ $doc.trigger('imagedatetime', {
+ date: f.isoDate
});
- response(items);
-
- },
- error: function () {
- items = [{
- label: "Error during species lookup",
- value: request.term,
- source: {listId: 'error-unmatched', name: request.term}
- }];
- response(items);
- },
- complete: function () {
- $(element).removeClass("ac_loading");
}
+
});
- };
- options.select = function (event, ui) {
- var selectedItem = ui.item;
- params.name(selectedItem.source.name);
- params.guid(selectedItem.source.guid);
- };
- if (!$(element).autocomplete(options).data("ui-autocomplete")) {
- // Fall back mechanism to handle deprecated version of autocomplete.
- var options = {};
- options.source = url;
- options.matchSubset = false;
- options.formatItem = function (row, i, n) {
- return row.name;
- };
- options.highlight = false;
- options.parse = function (data) {
- var rows = new Array();
- data = data.autoCompleteList;
- for (var i = 0; i < data.length; i++) {
- rows[i] = {
- data: data[i],
- value: data[i],
- result: data[i].name
- };
- }
- return rows;
- };
+ complete(true);
+ }
+ else {
+ error(result.error);
+ }
- $(element).autocomplete(options.source, options).result(function (event, data, formatted) {
- if (data) {
- params.name(data.name);
- params.guid(data.guid);
+ }).on(eventPrefix+'fail', function(e, data) {
+ error(data.errorThrown);
+ });
+
+ ko.applyBindingsToDescendants(innerContext, element);
+
+ return { controlsDescendantBindings: true };
+ }
+ };
+
+ ko.bindingHandlers.editDocument = {
+ init:function(element, valueAccessor) {
+ if (ko.isObservable(valueAccessor())) {
+ var document = ko.utils.unwrapObservable(valueAccessor());
+ if (typeof document.status == 'function') {
+ document.status.subscribe(function(status) {
+ if (status == 'deleted') {
+ valueAccessor()(null);
}
});
}
}
- };
+ var options = {
+ name:'documentEditTemplate',
+ data:valueAccessor()
+ };
+ return ko.bindingHandlers['template'].init(element, function() {return options;});
+ },
+ update:function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+ var options = {
+ name:'documentEditTemplate',
+ data:valueAccessor()
+ };
+ ko.bindingHandlers['template'].update(element, function() {return options;}, allBindings, viewModel, bindingContext);
+ }
+ };
- ko.bindingHandlers.speciesAutocomplete = {
- init: function (element, params, allBindings, viewModel, bindingContext) {
- var param = params();
- var url = ko.utils.unwrapObservable(param.url);
- var list = ko.utils.unwrapObservable(param.listId);
- var valueCallback = ko.utils.unwrapObservable(param.valueChangeCallback)
- var options = {};
+ ko.bindingHandlers.expression = {
- var lastHeader;
+ update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- function rowTitle(listId) {
- if (listId == 'unmatched' || listId == 'error-unmatched') {
- return '';
- }
- if (!listId) {
- return 'Atlas of Living Australia';
- }
- return 'Species List';
- }
+ var expressionString = ko.utils.unwrapObservable(valueAccessor());
+ var result = ecodata.forms.expressionEvaluator.evaluate(expressionString, bindingContext);
- var renderItem = function (row) {
+ $(element).text(result);
+ }
- var result = '';
- var title = rowTitle(row.listId);
- if (title && lastHeader !== title) {
- result += '
' + title + '
';
- }
- // We are keeping track of list headers so we only render each one once.
- lastHeader = title;
- result += '';
- if (row.listId && row.listId === 'unmatched') {
- result += 'Unlisted or unknown species';
- } else if (row.listId && row.listId === 'error-unmatched') {
- result += 'OfflineSpecies:' + row.name + '
';
- } else {
+ };
+
+
+ /*
+ * Fused Autocomplete supports two versions of autocomplete (original autocomplete implementation by Jorn Zaefferer and jquery_ui)
+ * Expects three parameters source, name and guid.
+ * Ajax response lists needs name attribute.
+ * Doco url: http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
+ * Note: Autocomplete implementation by Jorn Zaefferer is now been deprecated and its been migrated to jquery_ui.
+ *
+ */
+
+ ko.bindingHandlers.fusedAutocomplete = {
+
+ init: function (element, params) {
+ var params = params();
+ var options = {};
+ var url = ko.utils.unwrapObservable(params.source);
+ options.source = function(request, response) {
+ $(element).addClass("ac_loading");
+ $.ajax({
+ url: url,
+ dataType:'json',
+ data: {q:request.term},
+ success: function(data) {
+ var items = $.map(data.autoCompleteList, function(item) {
+ return {
+ label:item.name,
+ value: item.name,
+ source: item
+ }
+ });
+ response(items);
- var commonNameMatches = row.commonNameMatches !== undefined ? row.commonNameMatches : "";
+ },
+ error: function() {
+ items = [{label:"Error during species lookup", value:request.term, source: {listId:'error-unmatched', name: request.term}}];
+ response(items);
+ },
+ complete: function() {
+ $(element).removeClass("ac_loading");
+ }
+ });
+ };
+ options.select = function(event, ui) {
+ var selectedItem = ui.item;
+ params.name(selectedItem.source.name);
+ params.guid(selectedItem.source.guid);
+ };
- result += (row.scientificNameMatches && row.scientificNameMatches.length > 0) ? row.scientificNameMatches[0] : commonNameMatches;
- if (row.name != result && row.rankString) {
- result = result + "" + row.rankString + ": " + row.name + "
";
- } else if (row.rankString) {
- result = result + "" + row.rankString + "
";
- } else {
- result = result + "" + row.name + "
";
- }
+ if(!$(element).autocomplete(options).data("ui-autocomplete")){
+ // Fall back mechanism to handle deprecated version of autocomplete.
+ var options = {};
+ options.source = url;
+ options.matchSubset = false;
+ options.formatItem = function(row, i, n) {
+ return row.name;
+ };
+ options.highlight = false;
+ options.parse = function(data) {
+ var rows = new Array();
+ data = data.autoCompleteList;
+ for(var i=0; i < data.length; i++) {
+ rows[i] = {
+ data: data[i],
+ value: data[i],
+ result: data[i].name
+ };
}
- result += '';
- return result;
+ return rows;
};
- options.source = function (request, response) {
- $(element).addClass("ac_loading");
-
- if (valueCallback !== undefined) {
- valueCallback(request.term);
- }
- var data = {q: request.term};
- if (list) {
- $.extend(data, {listId: list});
+ $(element).autocomplete(options.source, options).result(function(event, data, formatted) {
+ if (data) {
+ params.name(data.name);
+ params.guid(data.guid);
}
- $.ajax({
- url: url,
- dataType: 'json',
- data: data,
- success: function (data) {
- var items = $.map(data.autoCompleteList, function (item) {
- return {
- label: item.name,
- value: item.name,
- source: item
- }
- });
- items = [{
- label: "Missing or unidentified species",
- value: request.term,
- source: {listId: 'unmatched', name: request.term}
- }].concat(items);
- response(items);
-
- },
- error: function () {
- items = [{
- label: "Error during species lookup",
- value: request.term,
- source: {listId: 'error-unmatched', name: request.term}
- }];
- response(items);
- },
- complete: function () {
- $(element).removeClass("ac_loading");
- }
- });
- };
- options.select = function (event, ui) {
- ko.utils.unwrapObservable(param.result)(event, ui.item.source);
- };
+ });
+ }
+ }
+ };
- if ($(element).autocomplete(options).data("ui-autocomplete")) {
+ ko.bindingHandlers.speciesAutocomplete = {
+ init: function (element, params, allBindings, viewModel, bindingContext) {
+ var param = params();
+ var url = ko.utils.unwrapObservable(param.url);
+ var list = ko.utils.unwrapObservable(param.listId);
+ var valueCallback = ko.utils.unwrapObservable(param.valueChangeCallback)
+ var options = {};
- $(element).autocomplete(options).data("ui-autocomplete")._renderItem = function (ul, item) {
- var result = $('').html(renderItem(item.source));
- return result.appendTo(ul);
+ var lastHeader;
- };
- } else {
- $(element).autocomplete(options);
+ function rowTitle(listId) {
+ if (listId == 'unmatched' || listId == 'error-unmatched') {
+ return '';
+ }
+ if (!listId) {
+ return 'Atlas of Living Australia';
}
+ return 'Species List';
}
- };
+ var renderItem = function(row) {
- function forceSelect2ToRespectPercentageTableWidths(element, percentageWidth) {
- var $parentColumn = $(element).parent('td');
- var $parentTable = $parentColumn.closest('table');
- var resizeHandler = null;
- if ($parentColumn.length) {
- var select2 = $parentColumn.find('.select2-container');
-
- function calculateWidth() {
- var parentWidth = $parentTable.width();
-
- // If the table has overflowed due to long selections then we need to try and find a parent div
- // as the div won't have overflowed.
- var windowWidth = window.innerWidth;
- if (parentWidth > windowWidth) {
- var parent = $parentTable.parent('div');
- if (parent.length) {
- parentWidth = parent.width();
- } else {
- parentWidth = windowWidth;
- }
- }
- var columnWidth = parentWidth * percentageWidth / 100;
+ var result = '';
+ var title = rowTitle(row.listId);
+ if (title && lastHeader !== title) {
+ result+=' '+title+'
';
+ }
+ // We are keeping track of list headers so we only render each one once.
+ lastHeader = title;
+ result+='';
+ if (row.listId && row.listId === 'unmatched') {
+ result += 'Unlisted or unknown species';
+ }
+ else if (row.listId && row.listId === 'error-unmatched') {
+ result += 'OfflineSpecies:'+row.name+'
';
+ }
+ else {
+
+ var commonNameMatches = row.commonNameMatches !== undefined ? row.commonNameMatches : "";
- if (columnWidth > 10) {
- select2.css('max-width', columnWidth + 'px');
- $(element).validationEngine('updatePromptsPosition');
+ result += (row.scientificNameMatches && row.scientificNameMatches.length>0) ? row.scientificNameMatches[0] : commonNameMatches ;
+ if (row.name != result && row.rankString) {
+ result = result + "" + row.rankString + ": " + row.name + "
";
+ } else if (row.rankString) {
+ result = result + "" + row.rankString + "
";
} else {
- // The table is not visible yet, so wait a bit and try again.
- setTimeout(calculateWidth, 200);
+ result = result + "" + row.name + "
";
}
}
+ result += '';
+ return result;
+ };
- resizeHandler = function () {
- clearTimeout(calculateWidth);
- setTimeout(calculateWidth, 300);
- };
- $(window).on('resize', resizeHandler);
+ options.source = function(request, response) {
+ $(element).addClass("ac_loading");
+
+ if (valueCallback !== undefined) {
+ valueCallback(request.term);
+ }
+ var data = {q:request.term};
+ if (list) {
+ $.extend(data, {listId: list});
+ }
+ $.ajax({
+ url: url,
+ dataType:'json',
+ data: data,
+ success: function(data) {
+ var items = $.map(data.autoCompleteList, function(item) {
+ return {
+ label:item.name,
+ value: item.name,
+ source: item
+ }
+ });
+ items = [{label:"Missing or unidentified species", value:request.term, source: {listId:'unmatched', name: request.term}}].concat(items);
+ response(items);
- ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
- $(window).off('resize', resizeHandler);
+ },
+ error: function() {
+ items = [{label:"Error during species lookup", value:request.term, source: {listId:'error-unmatched', name: request.term}}];
+ response(items);
+ },
+ complete: function() {
+ $(element).removeClass("ac_loading");
+ }
});
- calculateWidth();
- }
+ };
+ options.select = function(event, ui) {
+ ko.utils.unwrapObservable(param.result)(event, ui.item.source);
+ };
- }
+ if ($(element).autocomplete(options).data("ui-autocomplete")) {
- function applySelect2ValidationCompatibility(element) {
- var $element = $(element);
- var select2 = $element.next('.select2-container');
- $element.on('select2:close', function (e) {
- $element.validationEngine('validate');
- }).attr("data-prompt-position", "topRight:" + select2.width());
+ $(element).autocomplete(options).data("ui-autocomplete")._renderItem = function(ul, item) {
+ var result = $('').html(renderItem(item.source));
+ return result.appendTo(ul);
+
+ };
+ }
+ else {
+ $(element).autocomplete(options);
+ }
}
+ };
+
+ function forceSelect2ToRespectPercentageTableWidths(element, percentageWidth) {
+ var $parentColumn = $(element).parent('td');
+ var $parentTable = $parentColumn.closest('table');
+ var resizeHandler = null;
+ if ($parentColumn.length) {
+ var select2 = $parentColumn.find('.select2-container');
+ function calculateWidth() {
+ var parentWidth = $parentTable.width();
+
+ // If the table has overflowed due to long selections then we need to try and find a parent div
+ // as the div won't have overflowed.
+ var windowWidth = window.innerWidth;
+ if (parentWidth > windowWidth) {
+ var parent = $parentTable.parent('div');
+ if (parent.length) {
+ parentWidth = parent.width();
+ }
+ else {
+ parentWidth = windowWidth;
+ }
+ }
+ var columnWidth = parentWidth*percentageWidth/100;
- ko.bindingHandlers.speciesSelect2 = {
- select2AwareFormatter: function (data, container, delegate) {
- if (data.text) {
- return data.text;
+ if (columnWidth > 10) {
+ select2.css('max-width', columnWidth+'px');
+ $(element).validationEngine('updatePromptsPosition');
+ }
+ else {
+ // The table is not visible yet, so wait a bit and try again.
+ setTimeout(calculateWidth, 200);
}
- return delegate(data);
- },
- init: function (element, valueAccessor) {
-
- var self = ko.bindingHandlers.speciesSelect2;
- var model = valueAccessor();
-
- $.fn.select2.amd.require(['select2/species'], function (SpeciesAdapter) {
- $(element).select2({
- dataAdapter: SpeciesAdapter,
- placeholder: {id: -1, text: 'Start typing species name to search...'},
- templateResult: function (data, container) {
- return self.select2AwareFormatter(data, container, model.formatSearchResult);
- },
- templateSelection: function (data, container) {
- return self.select2AwareFormatter(data, container, model.formatSelectedSpecies);
- },
- dropdownAutoWidth: true,
- model: model,
- escapeMarkup: function (markup) {
- return markup; // We want to apply our own formatting so manually escape the user input.
- },
- ajax: {} // We want infinite scroll and this is how to get it.
- });
- applySelect2ValidationCompatibility(element);
- })
- },
- update: function (element, valueAccessor) {
}
- };
+ resizeHandler = function() {
+ clearTimeout(calculateWidth);
+ setTimeout(calculateWidth, 300);
+ };
+ $(window).on('resize', resizeHandler);
- /**
- * Supports custom rendering of results in a Select2 dropdown.
- */
- function constraintIconRenderer(config) {
- return function (item) {
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+ $(window).off('resize', resizeHandler);
+ });
+ calculateWidth();
+ }
- var constraint = item.id;
- if (config[constraint]) {
- var icon = config[constraint];
+ }
+ function applySelect2ValidationCompatibility(element) {
+ var $element = $(element);
+ var select2 = $element.next('.select2-container');
+ $element.on('select2:close', function(e) {
+ $element.validationEngine('validate');
+ }).attr("data-prompt-position", "topRight:"+select2.width());
+ }
- var iconElement;
- if (icon.url) {
- iconElement = $("").addClass('constraint-image').css("src", icon.url);
- } else {
- iconElement = $("").addClass('constraint-icon');
- if (icon.class) {
- if (_.isArray(icon.class)) {
- _.each(icon.class, function (val) {
- iconElement.addClass(val);
- });
- } else {
- _.each(icon.class.split(" "), function (val) {
- iconElement.addClass(icon.class);
- });
- }
+ ko.bindingHandlers.speciesSelect2 = {
+ select2AwareFormatter: function(data, container, delegate) {
+ if (data.text) {
+ return data.text;
+ }
+ return delegate(data);
+ },
+ init: function (element, valueAccessor) {
+
+ var self = ko.bindingHandlers.speciesSelect2;
+ var model = valueAccessor();
+
+ $.fn.select2.amd.require(['select2/species'], function(SpeciesAdapter) {
+ $(element).select2({
+ dataAdapter: SpeciesAdapter,
+ placeholder:{id:-1, text:'Start typing species name to search...'},
+ templateResult: function(data, container) { return self.select2AwareFormatter(data, container, model.formatSearchResult); },
+ templateSelection: function(data, container) { return self.select2AwareFormatter(data, container, model.formatSelectedSpecies); },
+ dropdownAutoWidth: true,
+ model:model,
+ escapeMarkup: function(markup) {
+ return markup; // We want to apply our own formatting so manually escape the user input.
+ },
+ ajax:{} // We want infinite scroll and this is how to get it.
+ });
+ applySelect2ValidationCompatibility(element);
+ })
+ },
+ update: function (element, valueAccessor) {}
+ };
+
+ /**
+ * Supports custom rendering of results in a Select2 dropdown.
+ */
+ function constraintIconRenderer(config) {
+ return function(item) {
+
+ var constraint = item.id;
+ if (config[constraint]) {
+ var icon = config[constraint];
+
+ var iconElement;
+ if (icon.url) {
+ iconElement = $("").addClass('constraint-image').css("src", icon.url);
+ }
+ else {
+ iconElement = $("").addClass('constraint-icon');
+ if (icon.class) {
+ if (_.isArray(icon.class)) {
+ _.each(icon.class, function(val) {
+ iconElement.addClass(val);
+ });
}
- if (icon.style) {
- _.each(icon.style, function (value, key) {
- iconElement.css(key, value);
+ else {
+ _.each(icon.class.split(" "), function (val) {
+ iconElement.addClass(icon.class);
});
}
}
- return $("").append(iconElement).append($("").addClass('constraint-text').text(constraint));
+ if (icon.style) {
+ _.each(icon.style, function(value, key) {
+ iconElement.css(key, value);
+ });
+ }
}
+ return $("").append(iconElement).append($("").addClass('constraint-text').text(constraint));
+ }
- return item.text;
- };
+ return item.text;
};
+ };
+
+ /**
+ * Provides support for applying https://select2.org for options selection.
+ * The value supplied to this binding will be passed through as options to the select2
+ * widget. It is expected this binding will be used in conjunction with the value binding
+ * so that updates to the view model will be reflected in the select 2 component.
+ * @type {{init: ko.bindingHandlers.select2.init}}
+ */
+ ko.bindingHandlers.select2 = {
+ init: function(element, valueAccessor, allBindings) {
+ var defaults = {
+ placeholder:'Please select...',
+ dropdownAutoWidth:true,
+ allowClear:true
+ };
+ var options = _.defaults(valueAccessor() || {}, defaults);
+ if (options.constraintIcons) {
+ var renderer = constraintIconRenderer(options.constraintIcons);
+ options.templateResult = renderer;
+ options.templateSelection = renderer;
- /**
- * Provides support for applying https://select2.org for options selection.
- * The value supplied to this binding will be passed through as options to the select2
- * widget. It is expected this binding will be used in conjunction with the value binding
- * so that updates to the view model will be reflected in the select 2 component.
- * @type {{init: ko.bindingHandlers.select2.init}}
- */
- ko.bindingHandlers.select2 = {
- init: function (element, valueAccessor, allBindings) {
- var defaults = {
- placeholder: 'Please select...',
- dropdownAutoWidth: true,
- allowClear: true
- };
- var options = _.defaults(valueAccessor() || {}, defaults);
- if (options.constraintIcons) {
- var renderer = constraintIconRenderer(options.constraintIcons);
- options.templateResult = renderer;
- options.templateSelection = renderer;
-
- }
- var $element = $(element);
- $element.select2(options);
+ }
+ var $element = $(element);
+ $element.select2(options);
+ applySelect2ValidationCompatibility(element);
+
+ // Listen for changes to the view model and ensure the select2 component is
+ // updated to reflect the change.
+ var valueBinding = allBindings.get('value');
+ if (ko.isObservable(valueBinding)) {
+ valueBinding.subscribe(function(newValue) {
+ // Depending on the order the bindings are declared (value before select2
+ // or vice versa), they can interfere with each other.
+ var currentValue = $element.val();
+ if (currentValue != newValue) {
+ // If the value is out of sync with the model, update the value.
+ $element.val(newValue);
+ }
+ // Make sure the select2 library is aware of the change.
+ $element.trigger('change');
+ });
+ }
+ if (options.preserveColumnWidth) {
+ forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth);
+ }
+ else {
applySelect2ValidationCompatibility(element);
-
- // Listen for changes to the view model and ensure the select2 component is
- // updated to reflect the change.
- var valueBinding = allBindings.get('value');
- if (ko.isObservable(valueBinding)) {
- valueBinding.subscribe(function (newValue) {
- // Depending on the order the bindings are declared (value before select2
- // or vice versa), they can interfere with each other.
- var currentValue = $element.val();
- if (currentValue != newValue) {
- // If the value is out of sync with the model, update the value.
- $element.val(newValue);
- }
- // Make sure the select2 library is aware of the change.
- $element.trigger('change');
- });
- }
- if (options.preserveColumnWidth) {
- forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth);
- } else {
- applySelect2ValidationCompatibility(element);
- }
}
- };
-
- ko.bindingHandlers.multiSelect2 = {
- init: function (element, valueAccessor, allBindings) {
- var defaults = {
- placeholder: 'Select all that apply...',
- dropdownAutoWidth: true,
- allowClear: false,
- tags: true
- };
- var options = valueAccessor();
- var model = options.value;
+ }
+ };
+
+ ko.bindingHandlers.multiSelect2 = {
+ init: function(element, valueAccessor, allBindings) {
+ var defaults = {
+ placeholder:'Select all that apply...',
+ dropdownAutoWidth:true,
+ allowClear:false,
+ tags:true
+ };
+ var options = valueAccessor();
+ var model = options.value;
- if (!ko.isObservable(model, ko.observableArray)) {
- throw "The options require a key with name 'value' with a value of type ko.observableArray";
- }
+ if (!ko.isObservable(model, ko.observableArray)) {
+ throw "The options require a key with name 'value' with a value of type ko.observableArray";
+ }
- var constraints;
- if (model.hasOwnProperty('constraints')) {
- constraints = model.constraints;
- } else {
- // Attempt to use the options binding to see if we can observe changes to the constraints
- constraints = allBindings.get('options');
- }
+ var constraints;
+ if (model.hasOwnProperty('constraints')) {
+ constraints = model.constraints;
+ }
+ else {
+ // Attempt to use the options binding to see if we can observe changes to the constraints
+ constraints = allBindings.get('options');
+ }
- // Because constraints can be initialised by an AJAX call, constraints can be added after initialisation
- // which can result in duplicate OPTIONS tags for pre-selected values, which confuses select2.
- // Here we watch for changes to the model constraints and make sure any duplicates are removed.
- if (constraints && ko.isObservable(constraints)) {
-
- constraints.subscribe(function (val) {
- var existing = {};
- var duplicates = [];
- var currentOptions = $(element).find("option").each(function () {
- var val = $(this).val();
- if (existing[val]) {
- duplicates.push(this);
- } else {
- existing[val] = true;
- }
- });
- // Remove any duplicates
- for (var i = 0; i < duplicates.length; i++) {
- element.removeChild(duplicates[i]);
+ // Because constraints can be initialised by an AJAX call, constraints can be added after initialisation
+ // which can result in duplicate OPTIONS tags for pre-selected values, which confuses select2.
+ // Here we watch for changes to the model constraints and make sure any duplicates are removed.
+ if (constraints && ko.isObservable(constraints)) {
+
+ constraints.subscribe(function(val) {
+ var existing = {};
+ var duplicates = [];
+ var currentOptions = $(element).find("option").each(function() {
+ var val = $(this).val();
+ if (existing[val]) {
+ duplicates.push(this);
+ }
+ else {
+ existing[val] = true;
}
});
-
- }
- delete options.value;
- var options = _.defaults(valueAccessor() || {}, defaults);
-
- $(element).select2(options).change(function (e) {
- model($(element).val());
+ // Remove any duplicates
+ for (var i=0; i").val(extraOptions[i]).text(extraOptions[i]));
- }
- var elementValue = $element.val();
- if (!_.isEqual(elementValue, data)) {
- $element.val(valueAccessor().value()).trigger('change');
- }
+ $(element).select2(options).change(function(e) {
+ model($(element).val());
+ });
+ if (options.preserveColumnWidth) {
+ forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth);
}
- };
- var popoverWarningOptions = {
- placement: 'top',
- trigger: 'manual',
- template: ''
- };
+ applySelect2ValidationCompatibility(element);
+ },
+ update: function(element, valueAccessor) {
+ var $element = $(element);
+ var data = valueAccessor().value();
+ var currentOptions = $element.find("option").map(function() {return $(this).val();}).get();
+ var extraOptions = _.difference(data, currentOptions);
+ for (var i=0; i").val(extraOptions[i]).text(extraOptions[i]));
+ }
+ var elementValue = $element.val();
+ if (!_.isEqual(elementValue, data)) {
+ $element.val(valueAccessor().value()).trigger('change');
+ }
+ }
+ };
+
+ var popoverWarningOptions = {
+ placement:'top',
+ trigger:'manual',
+ template: ''
+ };
+
+
+ /**
+ * This binding requires that the observable has used the metadata extender. It is meant to work with the
+ * form rendering code so isn't very useful as a stand alone binding.
+ *
+ * @type {{init: ko.bindingHandlers.warning.init, update: ko.bindingHandlers.warning.update}}
+ */
+ ko.bindingHandlers.warning = {
+ init: function(element, valueAccessor) {
+ var target = valueAccessor();
+ if (typeof target.checkWarnings !== 'function') {
+ throw "This binding requires the target observable to have used the \"metadata\" extender"
+ }
- /**
- * This binding requires that the observable has used the metadata extender. It is meant to work with the
- * form rendering code so isn't very useful as a stand alone binding.
- *
- * @type {{init: ko.bindingHandlers.warning.init, update: ko.bindingHandlers.warning.update}}
- */
- ko.bindingHandlers.warning = {
- init: function (element, valueAccessor) {
- var target = valueAccessor();
- if (typeof target.checkWarnings !== 'function') {
- throw "This binding requires the target observable to have used the \"metadata\" extender"
+ var $element = $(element);
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+ if (target.popoverInitialised) {
+ $element.popover("destroy");
}
+ });
- var $element = $(element);
- ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
- if (target.popoverInitialised) {
- $element.popover("destroy");
- }
- });
+ // We are implementing the validation routine by adding a subscriber to avoid triggering the validation
+ // on initialisation.
+ target.subscribe(function() {
+ var valid = $element.validationEngine('validate');
- // We are implementing the validation routine by adding a subscriber to avoid triggering the validation
- // on initialisation.
- target.subscribe(function () {
- var valid = $element.validationEngine('validate');
-
- // Only check warnings if the validation passes to avoid showing two sets of popups.
- if (valid) {
- var result = target.checkWarnings();
-
- if (result) {
- if (!target.popoverInitialised) {
- $element.popover(_.extend({content: result.val[0]}, popoverWarningOptions));
- var popover = $element.data('bs.popover').getTipElement();
- $(popover).click(function () {
- $element.popover('hide');
- });
- target.popoverInitialised = true;
- }
- $element.popover('show');
- } else {
- if (target.popoverInitialised) {
+ // Only check warnings if the validation passes to avoid showing two sets of popups.
+ if (valid) {
+ var result = target.checkWarnings();
+
+ if (result) {
+ if (!target.popoverInitialised) {
+ $element.popover(_.extend({content:result.val[0]}, popoverWarningOptions));
+ var popover = $element.data('bs.popover').getTipElement();
+ $(popover).click(function() {
$element.popover('hide');
- }
+ });
+ target.popoverInitialised = true;
}
- } else {
+ $element.popover('show');
+ }
+ else {
if (target.popoverInitialised) {
$element.popover('hide');
}
}
- });
-
- },
- update: function () {
- }
- };
-
- ko.bindingHandlers.conditionalValidation = {
- init: function (element, valueAccessor) {
- var target = valueAccessor();
- if (typeof target.evaluateBehaviour !== 'function') {
- throw "This binding requires the target observable to have used the \"metadata\" extender"
}
- var defaults = {
- validate: target.get('validate'),
- message: null
- };
- var validationAttributes = ko.computed(function () {
- return target.evaluateBehaviour("conditional_validation", defaults);
- });
- validationAttributes.subscribe(function (value) {
- updateJQueryValidationEngineAttributes(element, value.validate, value.message);
- });
- },
- update: function () {
- }
- };
-
- /**
- * Creates a validation string compatible with the jQueryValidationEngine plugin from data item validation
- * configuration.
- *
- * @param config an array containing an object describing each validation rule e.g
- * [
- * {
- * rule:"min",
- * params: [
- * {
- * "type":"computed",
- * "expression":"item2*0.01"
- * }
- * ]
- * }
- * ]
- * @param expressionContext the context which any expressions should be evaluated against (normally the view model
- * or binding context)
- * @returns {string}
- */
- function createValidationString(config, expressionContext) {
- var validationString = '';
- _.each(config || [], function (ruleConfig) {
- if (validationString) {
- validationString += ',';
- }
- validationString += ruleConfig.rule;
- if (ruleConfig.param) {
- var paramString = ecodata.forms.evaluate(ruleConfig.param, expressionContext);
- validationString += '[' + paramString + ']';
+ else {
+ if (target.popoverInitialised) {
+ $element.popover('hide');
+ }
}
});
- return validationString;
- };
+ },
+ update: function() {}
+ };
- /**
- * Adds or removes the jqueryValidationEngine validation attributes 'data-validation-engine' and 'data-errormessage'
- * to/from the supplied element.
- * @param element the HTML element to modify.
- * @param validationString the validation string to use (minus the validate[])
- * @param messageString a string to use for data-errormessage
- */
- function updateJQueryValidationEngineAttributes(element, validationString, messageString) {
- var $element = $(element);
+ ko.bindingHandlers.conditionalValidation = {
+ init: function(element, valueAccessor) {
+ var target = valueAccessor();
+ if (typeof target.evaluateBehaviour !== 'function') {
+ throw "This binding requires the target observable to have used the \"metadata\" extender"
+ }
+ var defaults = {
+ validate:target.get('validate'),
+ message:null
+ };
+ var validationAttributes = ko.computed(function() {
+ return target.evaluateBehaviour("conditional_validation", defaults);
+ });
+ validationAttributes.subscribe(function(value) {
+ updateJQueryValidationEngineAttributes(element, value.validate, value.message);
+ });
+ },
+ update: function() {}
+ };
+
+ /**
+ * Creates a validation string compatible with the jQueryValidationEngine plugin from data item validation
+ * configuration.
+ *
+ * @param config an array containing an object describing each validation rule e.g
+ * [
+ * {
+ * rule:"min",
+ * params: [
+ * {
+ * "type":"computed",
+ * "expression":"item2*0.01"
+ * }
+ * ]
+ * }
+ * ]
+ * @param expressionContext the context which any expressions should be evaluated against (normally the view model
+ * or binding context)
+ * @returns {string}
+ */
+ function createValidationString(config, expressionContext) {
+ var validationString = '';
+ _.each(config || [], function(ruleConfig) {
if (validationString) {
- $element.attr('data-validation-engine', 'validate[' + validationString + ']');
- } else {
- $element.removeAttr('data-validation-engine');
+ validationString += ',';
}
-
- if (messageString) {
- $element.attr('data-errormessage', messageString)
- } else {
- $element.removeAttr('data-errormessage');
+ validationString += ruleConfig.rule;
+ if (ruleConfig.param) {
+ var paramString = ecodata.forms.evaluate(ruleConfig.param, expressionContext);
+ validationString += '['+paramString+']';
}
-
- // Trigger the validation after the knockout processing is complete - this prevents the validation
- // from firing before the page has been initialised on load.
- setTimeout(function () {
- if (messageString) {
- $element.validationEngine('validate');
- } else {
- $element.validationEngine('hide');
- }
- }, 100);
+ });
+
+ return validationString;
+ };
+
+ /**
+ * Adds or removes the jqueryValidationEngine validation attributes 'data-validation-engine' and 'data-errormessage'
+ * to/from the supplied element.
+ * @param element the HTML element to modify.
+ * @param validationString the validation string to use (minus the validate[])
+ * @param messageString a string to use for data-errormessage
+ */
+ function updateJQueryValidationEngineAttributes(element, validationString, messageString) {
+ var $element = $(element);
+ if (validationString) {
+ $element.attr('data-validation-engine', 'validate['+validationString+']');
+ }
+ else {
+ $element.removeAttr('data-validation-engine');
}
- /**
- * Evaluates a validation configuration and populates the bound element with attributes used by the
- * jQueryValidationEngine.
- * @see createValidationString for the format of the configuration.
- * @type {{init: ko.bindingHandlers.computedValidation.init, update: ko.bindingHandlers.computedValidation.update}}
- */
- ko.bindingHandlers.computedValidation = {
- init: function (element, valueAccessor, allBindings, viewModel) {
- var modelItem = valueAccessor();
-
- var validationAttributes = ko.pureComputed(function () {
- return createValidationString(modelItem, viewModel);
- });
- validationAttributes.subscribe(function (value) {
- updateJQueryValidationEngineAttributes(element, value);
- });
- updateJQueryValidationEngineAttributes(element, validationAttributes());
+ if (messageString) {
+ $element.attr('data-errormessage', messageString)
+ }
+ else {
+ $element.removeAttr('data-errormessage');
+ }
- },
- update: function () {
+ // Trigger the validation after the knockout processing is complete - this prevents the validation
+ // from firing before the page has been initialised on load.
+ setTimeout(function() {
+ if (messageString) {
+ $element.validationEngine('validate');
}
- };
+ else {
+ $element.validationEngine('hide');
+ }
+ }, 100);
+ }
- /**
- * custom handler for fancybox plugin.
- * @type {{init: Function}}
- * config to fancybox plugin can be passed to custom binding using knockout syntax.
- * eg:
- *
- *
- * or
- *
- *
- */
- ko.bindingHandlers.fancybox = {
- init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
- var config = valueAccessor(),
- $elem = $(element);
- // suppress auto scroll on clicking image to view in fancybox
- config = $.extend({
- width: 700,
- height: 500,
- // fix for bringing the modal dialog to focus to make it accessible via keyboard.
- afterShow: function () {
- $('.fancybox-wrap').focus();
+ /**
+ * Evaluates a validation configuration and populates the bound element with attributes used by the
+ * jQueryValidationEngine.
+ * @see createValidationString for the format of the configuration.
+ * @type {{init: ko.bindingHandlers.computedValidation.init, update: ko.bindingHandlers.computedValidation.update}}
+ */
+ ko.bindingHandlers.computedValidation = {
+ init: function(element, valueAccessor, allBindings, viewModel) {
+ var modelItem = valueAccessor();
+
+ var validationAttributes = ko.pureComputed(function() {
+ return createValidationString(modelItem, viewModel);
+ });
+ validationAttributes.subscribe(function(value) {
+ updateJQueryValidationEngineAttributes(element, value);
+ });
+ updateJQueryValidationEngineAttributes(element, validationAttributes());
+
+ },
+ update: function() {}
+ };
+
+ /**
+ * custom handler for fancybox plugin.
+ * @type {{init: Function}}
+ * config to fancybox plugin can be passed to custom binding using knockout syntax.
+ * eg:
+ *
+ *
+ * or
+ *
+ *
+ */
+ ko.bindingHandlers.fancybox = {
+ init: function(element, valueAccessor, allBindings, viewModel, bindingContext){
+ var config = valueAccessor(),
+ $elem = $(element);
+ // suppress auto scroll on clicking image to view in fancybox
+ config = $.extend({
+ width: 700,
+ height: 500,
+ // fix for bringing the modal dialog to focus to make it accessible via keyboard.
+ afterShow: function(){
+ $('.fancybox-wrap').focus();
+ },
+ helpers: {
+ title: {
+ type : 'inside',
+ position : 'bottom'
},
- helpers: {
- title: {
- type: 'inside',
- position: 'bottom'
- },
- overlay: {
- locked: false
- }
+ overlay: {
+ locked: false
}
- }, config);
-
- if ($elem.attr('target') == 'fancybox') {
- $elem.fancybox(config);
- } else {
- $elem.find('a[target=fancybox]').fancybox(config);
}
+ }, config);
+
+ if($elem.attr('target') == 'fancybox'){
+ $elem.fancybox(config);
+ }else{
+ $elem.find('a[target=fancybox]').fancybox(config);
}
- };
+ }
+ };
+
+ /**
+ * A very simple binding to allow an element to toggle the visibility of another element.
+ * Created for the featureMap because using bootstrap collapse was causing side effects with the modal.
+ *
+ * @type {{init: ko.bindingHandlers.toggleVisibility.init}}
+ */
+ ko.bindingHandlers.toggleVisibility = {
+ init: function (element, valueAccessor) {
+ var unwrapped = ko.utils.unwrapObservable(valueAccessor());
+ var visibleClass = 'fa-angle-down';
+ var hiddenClass = 'fa-angle-up';
- /**
- * A very simple binding to allow an element to toggle the visibility of another element.
- * Created for the featureMap because using bootstrap collapse was causing side effects with the modal.
- *
- * @type {{init: ko.bindingHandlers.toggleVisibility.init}}
- */
- ko.bindingHandlers.toggleVisibility = {
- init: function (element, valueAccessor) {
- var unwrapped = ko.utils.unwrapObservable(valueAccessor());
- var visibleClass = 'fa-angle-down';
- var hiddenClass = 'fa-angle-up';
+ var $element = $(element);
+ var $i = $('').addClass('fa').addClass(visibleClass);
+ if (unwrapped.collapsedByDefault != undefined && !unwrapped.collapsedByDefault) {
+ $i = $('').addClass('fa').addClass(hiddenClass);
+ }
+ $element.append($i);
- var $element = $(element);
- var $i = $('').addClass('fa').addClass(visibleClass);
- if (unwrapped.collapsedByDefault != undefined && !unwrapped.collapsedByDefault) {
- $i = $('').addClass('fa').addClass(hiddenClass);
+ $element.click(function() {
+ var selector = '';
+ if (unwrapped.collapsedByDefault != undefined && unwrapped.blockId) {
+ selector = unwrapped.blockId;
+ } else {
+ selector = unwrapped;
}
- $element.append($i);
- $element.click(function () {
- var selector = '';
- if (unwrapped.collapsedByDefault != undefined && unwrapped.blockId) {
- selector = unwrapped.blockId;
- } else {
- selector = unwrapped;
- }
+ var $section = $(selector);
+ if ($section.is(':visible')) {
+ $section.hide();
+ $i.removeClass(visibleClass);
+ $i.addClass(hiddenClass);
+ }
+ else {
+ $section.show();
+ $i.removeClass(hiddenClass);
+ $i.addClass(visibleClass);
+ }
+ return false;
+ });
- var $section = $(selector);
- if ($section.is(':visible')) {
- $section.hide();
- $i.removeClass(visibleClass);
- $i.addClass(hiddenClass);
- } else {
- $section.show();
- $i.removeClass(hiddenClass);
- $i.addClass(visibleClass);
- }
- return false;
- });
+ }
+ };
+
+ /**
+ * This binding will listen for the start of a validation event,
+ * and expand a collapsed section so data in that section can be
+ * validated.
+ */
+ ko.bindingHandlers.expandOnValidate = {
+ init: function (element, valueAccessor) {
+ var selector = valueAccessor() || ".validationEngineContainer";
+ var event = "jqv.form.validating";
+ var $section = $(element);
+ var validationListener = function() {
+ $section.show();
+ };
+ $section.closest(selector).on(event, validationListener);
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+ $section.closest(selector).off(event, validationListener);
+ });
+ }
+ };
+
+ /**
+ * Behaves as per the knockoutjs enable binding, but additionally clears the observable associated with the
+ * value binding if it is also applied to the same element.
+ * @type {{update: ko.bindingHandlers.enableAndClear.update}}
+ */
+ ko.bindingHandlers['enableAndClear'] = {
+ 'update': function (element, valueAccessor, allBindings) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ if (value && element.disabled)
+ element.removeAttribute("disabled");
+ else if ((!value) && (!element.disabled)) {
+ element.disabled = true;
+ var value = allBindings.get('value');
+ if (ko.isObservable(value)) {
+ value(undefined);
+ }
}
- };
-
- /**
- * This binding will listen for the start of a validation event,
- * and expand a collapsed section so data in that section can be
- * validated.
- */
- ko.bindingHandlers.expandOnValidate = {
- init: function (element, valueAccessor) {
- var selector = valueAccessor() || ".validationEngineContainer";
- var event = "jqv.form.validating";
- var $section = $(element);
- var validationListener = function () {
- $section.show();
- };
- $section.closest(selector).on(event, validationListener);
- ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
- $section.closest(selector).off(event, validationListener);
- });
+ }
+ };
+
+ /**
+ * Because the jQueryValidationEngine triggers validation on blur, fields that don't accept focus
+ * (in particular computed fields with validation rules attached) can use this binding to trigger validation
+ * based on model value changes.
+ * @type {{init: ko.bindingHandlers.validateOnChange.init}}
+ */
+ ko.bindingHandlers['validateOnChange'] = {
+ 'init': function (element, valueAccessor) {
+
+ if (ko.isObservable(valueAccessor())) {
+ var $element = $(element);
+ valueAccessor().subscribe(function() {
+ setTimeout(function() {
+ $element.validationEngine('validate');
+ });
+ })
}
- };
+ }
+ };
+
+ /**
+ * Passes the result of evaluating an expression to another binding. This allows for the reuse of
+ * standard bindings which evaluate expressions against the view model rather than binding directly
+ * against the view model.
+ * @param delegatee the binding to delegate to.
+ * @returns {{init: (function(*=, *, *=, *=, *=): *)}}
+ */
+ function delegatingExpressionBinding(delegatee) {
+ var result = {};
+
+ // This handles a quirk of the output data model that stores the main data we bind against in a "data"
+ // attribute. Nested data structures inside the model do not use the data prefix.
+ var modelTransformer = function(viewModel) {
+ if (viewModel && _.isObject(viewModel.data)) {
+ return viewModel.data;
+ }
+ return viewModel;
+ }
- /**
- * Behaves as per the knockoutjs enable binding, but additionally clears the observable associated with the
- * value binding if it is also applied to the same element.
- * @type {{update: ko.bindingHandlers.enableAndClear.update}}
- */
- ko.bindingHandlers['enableAndClear'] = {
- 'update': function (element, valueAccessor, allBindings) {
- var value = ko.utils.unwrapObservable(valueAccessor());
- if (value && element.disabled)
- element.removeAttribute("disabled");
- else if ((!value) && (!element.disabled)) {
- element.disabled = true;
- var value = allBindings.get('value');
- if (ko.isObservable(value)) {
- value(undefined);
- }
- }
+ var valueTransformer = function(valueAccessor, viewModel) {
+ return function() {
+ var result = ecodata.forms.expressionEvaluator.evaluateBoolean(valueAccessor(), modelTransformer(viewModel));
+ return result;
+ };
+ }
+ if (_.isFunction(delegatee.init)) {
+ result['init'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) {
+ return delegatee.init(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext);
}
- };
-
- /**
- * Because the jQueryValidationEngine triggers validation on blur, fields that don't accept focus
- * (in particular computed fields with validation rules attached) can use this binding to trigger validation
- * based on model value changes.
- * @type {{init: ko.bindingHandlers.validateOnChange.init}}
- */
- ko.bindingHandlers['validateOnChange'] = {
- 'init': function (element, valueAccessor) {
-
- if (ko.isObservable(valueAccessor())) {
- var $element = $(element);
- valueAccessor().subscribe(function () {
- setTimeout(function () {
- $element.validationEngine('validate');
- });
- })
- }
+ }
+ if (_.isFunction(delegatee.update)) {
+ result['update'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) {
+ return delegatee.update(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext);
}
- };
+ }
+ return result;
+ }
- /**
- * Passes the result of evaluating an expression to another binding. This allows for the reuse of
- * standard bindings which evaluate expressions against the view model rather than binding directly
- * against the view model.
- * @param delegatee the binding to delegate to.
- * @returns {{init: (function(*=, *, *=, *=, *=): *)}}
- */
- function delegatingExpressionBinding(delegatee) {
- var result = {};
-
- // This handles a quirk of the output data model that stores the main data we bind against in a "data"
- // attribute. Nested data structures inside the model do not use the data prefix.
- var modelTransformer = function (viewModel) {
- if (viewModel && _.isObject(viewModel.data)) {
- return viewModel.data;
- }
- return viewModel;
+ ko.bindingHandlers['ifexpression'] = delegatingExpressionBinding(ko.bindingHandlers['if']);
+ ko.virtualElements.allowedBindings.ifexpression = true;
+ ko.bindingHandlers['visibleexpression'] = delegatingExpressionBinding(ko.bindingHandlers['visible']);
+ ko.virtualElements.allowedBindings.visibleexpression = true;
+ ko.bindingHandlers['enableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['enable']);
+ ko.bindingHandlers['disableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['disable']);
+ ko.bindingHandlers['enableAndClearExpression'] = delegatingExpressionBinding(ko.bindingHandlers['enableAndClear']);
+
+
+ /**
+ * Extends the target as a ecodata.forms.DataModelItem. This is required to support many of the
+ * dynamic behaviour features, including warnings and conditional validation rules.
+ * @param target the observable to extend.
+ * @param context the dataModel metadata as defined for the field in dataModel.json
+ */
+ ko.extenders.metadata = function(target, options) {
+ ecodata.forms.DataModelItem.apply(target, [options.metadata, options.context, options.config]);
+ return target;
+ };
+
+ ko.extenders.list = function(target, options) {
+ ecodata.forms.OutputListSupport.apply(target, [options.metadata, options.constructorFunction, options.context, options.userAddedRows, options.config]);
+ };
+
+ /**
+ * This is kind of a hack to make the closure config object available to the any components that use the model.
+ */
+ ko.extenders.configurationContainer = function(target, config) {
+ target.globalConfig = config;
+ };
+
+ /**
+ * The writableComputed extender will continuously update the value of an observable from a supplied expression
+ * until such time as the value is explicitly set (for example by the user typing something into the field).
+ * @param target
+ * @param options {expression: , context:} expression is the expression to be evaluated, context is the context
+ * in which the expression will be evaluated. (normally the parent model object of the target).
+ * @returns {*}
+ */
+ ko.extenders.writableComputed = function(target, options) {
+
+ var value = ko.observable();
+ var ev = ecodata.forms.expressionEvaluator;
+ var valueHolder = ko.pureComputed({
+ read: function() {
+ var val = value();
+ return val ? val : ev.evaluate(options.expression, options.context, options.decimalPlaces);
+ },
+ write:function(newValue) {
+ value(newValue);
}
-
- var valueTransformer = function (valueAccessor, viewModel) {
- return function () {
- var result = ecodata.forms.expressionEvaluator.evaluateBoolean(valueAccessor(), modelTransformer(viewModel));
- return result;
- };
+ });
+ return valueHolder;
+ };
+
+ /**
+ * Identifies that this field can contribute to reporting targets by attaching a class and
+ * tooltip to the field.
+ * This binding expects the bound value to be an array of scores (objects with a label property).
+ */
+ ko.bindingHandlers['score'] = {
+ init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
+ var scores = valueAccessor();
+
+ if (!scores || !_.isArray(scores)) {
+ console.log("Warning: scores binding applied but supplied value is not an array");
+ return;
}
- if (_.isFunction(delegatee.init)) {
- result['init'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) {
- return delegatee.init(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext);
- }
- }
- if (_.isFunction(delegatee.update)) {
- result['update'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) {
- return delegatee.update(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext);
- }
+ $(element).addClass("score");
+
+ var message = 'This field can contribute to: ';
+ for (var i=0; i'+target+'';
}
- return result;
- }
+ message += '
';
- ko.bindingHandlers['ifexpression'] = delegatingExpressionBinding(ko.bindingHandlers['if']);
- ko.virtualElements.allowedBindings.ifexpression = true;
- ko.bindingHandlers['visibleexpression'] = delegatingExpressionBinding(ko.bindingHandlers['visible']);
- ko.virtualElements.allowedBindings.visibleexpression = true;
- ko.bindingHandlers['enableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['enable']);
- ko.bindingHandlers['disableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['disable']);
- ko.bindingHandlers['enableAndClearExpression'] = delegatingExpressionBinding(ko.bindingHandlers['enableAndClear']);
-
-
- /**
- * Extends the target as a ecodata.forms.DataModelItem. This is required to support many of the
- * dynamic behaviour features, including warnings and conditional validation rules.
- * @param target the observable to extend.
- * @param context the dataModel metadata as defined for the field in dataModel.json
- */
- ko.extenders.metadata = function (target, options) {
- ecodata.forms.DataModelItem.apply(target, [options.metadata, options.context, options.config]);
- return target;
- };
+ var options = {
+ trigger:'hover',
+ placement:'top',
+ content: message,
+ html: true
+ }
+ $(element).popover(options);
- ko.extenders.list = function (target, options) {
- ecodata.forms.OutputListSupport.apply(target, [options.metadata, options.constructorFunction, options.context, options.userAddedRows, options.config]);
- };
+ }
+ };
- /**
- * This is kind of a hack to make the closure config object available to the any components that use the model.
- */
- ko.extenders.configurationContainer = function (target, config) {
- target.globalConfig = config;
- };
+ ko.extenders.dataLoader = function(target, options) {
- /**
- * The writableComputed extender will continuously update the value of an observable from a supplied expression
- * until such time as the value is explicitly set (for example by the user typing something into the field).
- * @param target
- * @param options {expression: , context:} expression is the expression to be evaluated, context is the context
- * in which the expression will be evaluated. (normally the parent model object of the target).
- * @returns {*}
- */
- ko.extenders.writableComputed = function (target, options) {
-
- var value = ko.observable();
- var ev = ecodata.forms.expressionEvaluator;
- var valueHolder = ko.pureComputed({
- read: function () {
- var val = value();
- return val ? val : ev.evaluate(options.expression, options.context, options.decimalPlaces);
- },
- write: function (newValue) {
- value(newValue);
- }
+ var dataLoader = new ecodata.forms.dataLoader(target.context, target.config);
+ var dataLoaderConfig = target.get('computed');
+ if (!dataLoaderConfig) {
+ throw "This extender can only be used with the metadata extender and expects a computed property to be defined";
+ }
+ var dependencyTracker = ko.computed(function () {
+ return dataLoader.prepop(dataLoaderConfig).done( function(data) {
+ target(data);
});
- return valueHolder;
- };
+ }); // This is a computed rather than a pureComputed as it has a side effect.
+ return target;
+ };
- /**
- * Identifies that this field can contribute to reporting targets by attaching a class and
- * tooltip to the field.
- * This binding expects the bound value to be an array of scores (objects with a label property).
- */
- ko.bindingHandlers['score'] = {
- init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
- var scores = valueAccessor();
-
- if (!scores || !_.isArray(scores)) {
- console.log("Warning: scores binding applied but supplied value is not an array");
- return;
- }
+ ko.bindingHandlers['triggerPrePopulate'] = {
+ 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
- $(element).addClass("score");
- var message = 'This field can contribute to: ';
- for (var i = 0; i < scores.length; i++) {
- var target = scores[i].label;
- message += '- ' + target + '
';
- }
- message += '
';
+ var dataModelItem = valueAccessor();
+ var behaviours = dataModelItem.get('behaviour');
+ for (var i = 0; i < behaviours.length; i++) {
+ var behaviour = behaviours[i];
- var options = {
- trigger: 'hover',
- placement: 'top',
- content: message,
- html: true
- }
- $(element).popover(options);
+ if (behaviour.type == 'pre_populate') {
+ var config = behaviour.config;
+ var dataLoaderContext = dataModelItem.context;
- }
- };
+ var dataLoader = new ecodata.forms.dataLoader(dataLoaderContext, dataModelItem.config);
- ko.extenders.dataLoader = function (target, options) {
+ var dependencyTracker = ko.computed(function () {
+ dataModelItem(); // register dependency on the observable.
+ dataLoader.prepop(config).done(function (data) {
+ data = data || {};
+ var target = config.target;
+ if (!target) {
+ target = viewModel;
+ }
+ else {
+ target = dataModelItem.findNearestByName(target, bindingContext);
+ }
+ if (!target) {
+ throw "Unable to locate target for pre-population: "+target;
+ }
+ if (_.isFunction(target.loadData)) {
+ target.loadData(data);
+ } else if (_.isFunction(target.load)) {
+ target.load(data);
+ } else if (ko.isObservable(target)) {
+ target(data);
+ } else {
+ console.log("Warning: target for pre-populate is invalid");
+ }
- var dataLoader = new ecodata.forms.dataLoader(target.context, target.config);
- var dataLoaderConfig = target.get('computed');
- if (!dataLoaderConfig) {
- throw "This extender can only be used with the metadata extender and expects a computed property to be defined";
+ }); // This is a computed rather than a pureComputed as it has a side effect.
+ });
+ }
}
- var dependencyTracker = ko.computed(function () {
- return dataLoader.prepop(dataLoaderConfig).done(function (data) {
- target(data);
- });
- }); // This is a computed rather than a pureComputed as it has a side effect.
- return target;
- };
- ko.bindingHandlers['triggerPrePopulate'] = {
- 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
-
-
- var dataModelItem = valueAccessor();
- var behaviours = dataModelItem.get('behaviour');
- for (var i = 0; i < behaviours.length; i++) {
- var behaviour = behaviours[i];
-
- if (behaviour.type == 'pre_populate') {
- var config = behaviour.config;
- var dataLoaderContext = dataModelItem.context;
-
- var dataLoader = new ecodata.forms.dataLoader(dataLoaderContext, dataModelItem.config);
-
- var dependencyTracker = ko.computed(function () {
- dataModelItem(); // register dependency on the observable.
- dataLoader.prepop(config).done(function (data) {
- data = data || {};
- var target = config.target;
- if (!target) {
- target = viewModel;
- }
- else {
- target = dataModelItem.findNearestByName(target, bindingContext);
- }
- if (!target) {
- throw "Unable to locate target for pre-population: "+target;
- }
- if (_.isFunction(target.loadData)) {
- target.loadData(data);
- } else if (_.isFunction(target.load)) {
- target.load(data);
- } else if (ko.isObservable(target)) {
- target(data);
- } else {
- console.log("Warning: target for pre-populate is invalid");
- }
-
- }); // This is a computed rather than a pureComputed as it has a side effect.
- });
- }
- }
+ }
+ };
- }
- };
- }
-)();
+})();