diff options
Diffstat (limited to 'extensions/BayotBase/web/js/bayot.util.js')
-rw-r--r-- | extensions/BayotBase/web/js/bayot.util.js | 1290 |
1 files changed, 1290 insertions, 0 deletions
diff --git a/extensions/BayotBase/web/js/bayot.util.js b/extensions/BayotBase/web/js/bayot.util.js new file mode 100644 index 0000000..64e801d --- /dev/null +++ b/extensions/BayotBase/web/js/bayot.util.js @@ -0,0 +1,1290 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (C) 2012 Jolla Ltd. + * Contact: Pami Ketolainen <pami.ketolainen@jollamobile.com> + * + * The Initial Developer of the Original Code is "Nokia Corporation" + * Portions created by the Initial Developer are Copyright (C) 2011 the + * Initial Developer. All Rights Reserved. + * + * Contributor(s): + * David Wilson <ext-david.3.wilson@nokia.com> + */ + + +/** + * Run a function, logging any exception thrown to the console. Used for + * debugging XMLHTTPRequest event handlers, whose exceptions are silently + * discarded. + */ +function absorb(fn) +{ + try { + return fn(); + } catch(e) { + if(typeof console !== 'undefined') { + console.error('absorb(): %o', e); + } + throw e; + } +} + + +/** + * RPC object. Wraps the parameters of a Bugzilla RPC up along with callbacks + * indicating completion state. + * + * All methods return the Rpc object itself, so that the calls can be chained. + * + * new Rpc('Foo', 'bar', {baz:1}) + * .done(onBarSuccess) + * .fail(onBarFail) + * .complete(onBarComplete); + * + */ +var Rpc = Base.extend({ + /** + * Create an instance. + * @param {String} namespace + * RPC namespace. + * @param {String} method + * RPC method name. + * @param {Object} params + * RPC method parameters. + * @param {Boolean} immediate + * Optional; if false, don't immediately start the RPC (e.g. if it is + * going to be added to a queue). Defaults to true. + */ + constructor: function(namespace, method, params, immediate) + { + this.namespace = namespace + this.method = method; + this.params = params; + this.response = null; + this.error = null; + + this._startedCb = jQuery.Callbacks(); + this._doneCb = jQuery.Callbacks(); + this._failCb = jQuery.Callbacks(); + this._completeCb = jQuery.Callbacks() + + if(immediate !== false) { + this.start(); + } + }, + + /** + * Add callback to be called when the RPC is started. + * @param {Function} cb + * @return {Rpc} + * + * Function cb gets the Rpc object as first parameter. + */ + started: function(cb) + { + this._startedCb.add(cb); + return this; + }, + + /** + * Add function to be called when the RPC succeeds. + * @param {Function} cb + * @return {Rpc} + * + * Function cb gets RPC result as first parameter. + */ + done: function(cb) + { + this._doneCb.add(cb); + return this; + }, + + /** + * Add function to be called when the RPC fails. + * @param {Function} cb + * @return {Rpc} + * + * Function cb gets RPC error object as first parameter. + */ + fail: function(cb) + { + this._failCb.add(cb); + return this; + }, + + /** + * Add function to be called when the RPC completes (success or failure). + * @param {Function} cb + * @return {Rpc} + * + * Function cb gets the Rpc object as first parameter. + */ + complete: function(cb) + { + this._completeCb.add(cb); + return this; + }, + + /** + * Start the RPC. + * @return {Rpc} + * + * Should be used when Rpc object was constructed with immediate == false + */ + start: function() + { + $.jsonRPC.setup({ + endPoint: 'jsonrpc.cgi', + namespace: this.namespace + }) + + $.jsonRPC.request(this.method, { + params: [this.params || {}], + success: $.proxy(this, "_onSuccess"), + error: $.proxy(this, "_onError") + }); + + this._startedCb.fire(this); + return this; + }, + + /** + * Fired on success; records the RPC result and fires any callbacks. + * @private + */ + _onSuccess: function(response) + { + this.response = response.result; + var that = this; + absorb(function() + { + that._doneCb.fire(response.result); + that._completeCb.fire(that); + }); + }, + + /** + * Fired on failure; records the error and fires any callbacks. + * @private + */ + _onError: function(response) + { + if ($.isPlainObject(response.error)){ + this.error = response.error; + } else { + /* + * jquery.jsonrpc response in case of network or other unknown + * errors is { error: "Internal Server Error", version "2.0" } + * Not sure if that is correct, or what would be the correct way to + * handle that, so fixing it here + */ + this.error = { + message: "Network error or other unexpected problem", + code: -32603 + }; + } + if(typeof console !== 'undefined') { + console.log('jsonRPC error: %o', this.error); + } + var that = this; + absorb(function() + { + that._failCb.fire(that.error); + that._completeCb.fire(that); + }); + } +}); + + +/** + * Display a small progress indicator at the top of the document while any + * jQuery XMLHttpRequest is in progress. + */ +var RpcProgressView = { + _CSS_PROPS: { + background: '#7f0000', + color: 'white', + padding: '0.5ex', + position: 'fixed', + top: 0, + right: 0, + 'z-index': 9999999, + 'text-decoration': 'blink' + }, + + init: function() + { + if(this._progress) { + return; + } + + this._active = 0; + this._progress = $('<div>Working..</div>'); + this._progress.css(this._CSS_PROPS); + this._progress.hide(); + this._progress.appendTo('body'); + $(document).ajaxSend($.proxy(this, "_onAjaxSend")); + $(document).ajaxComplete($.proxy(this, "_onAjaxComplete")); + }, + + /** + * Handle request start by incrementing the active count. + */ + _onAjaxSend: function() + { + this._active++; + this._progress.show(); + }, + + /** + * Handle request completion by decrementing the active count, and hiding + * the progress indicator if there are no more active requests. + */ + _onAjaxComplete: function() + { + this._active--; + if(! this._active) { + this._progress.hide(); + } + } +}; + +// TODO: this should be moved to somewhere sensible. +$(document).ready($.proxy(RpcProgressView, "init")); + +/** + * Bug class. + * Stores single bug and handles calling create and update RPC methods. + */ +var Bug = Base.extend({ + + constructor: function(bug, noAlerts) + { + // Holders for deffed objects + this._fetching = null; + this._alert = !noAlerts; + + // Fires when bug field has been updated in DB + this._updateCb = jQuery.Callbacks(); + this.updated = $.proxy(this._updateCb, "add"); + // Fires when bug field is changed via set/add/remove + // Callback params (bug_object, field_name, new_field_value) + this._changedCb = jQuery.Callbacks(); + this.changed = $.proxy(this._changedCb, "add"); + // Fires when choices for field change, i.e when the field it depends + // on changes + // Callback params (bug_object, changed_field, dependent_field, new_choices) + this._choicesCb = jQuery.Callbacks(); + this.choicesUpdated = $.proxy(this._choicesCb, "add"); + // Fires when visibility of a field change, i.e when the field it + // depends on changes + // Callback params (bug_object, changed_field, controlled_field, is_visible) + this._visibilityCb = jQuery.Callbacks(); + this.visibilityUpdated = $.proxy(this._visibilityCb, "add"); + + if (bug.id) { + // TODO: Might need a better check of bug data completeness + this.id = bug.id; + this._data = $.extend(true, {}, bug); + this._modified = {}; + } else { + this.id = null; + this._modified = $.extend(true, {}, bug); + this._data = {}; + for (var name in BB_FIELDS) { + if (bug[name] != undefined) continue; + var def = this.defaultValue(name); + if (def != undefined) this.set(name, def); + } + } + }, + isModified: function() + { + return !$.isEmptyObject(this._modified); + }, + + /** + * Save changes or new bug + */ + save: function() + { + if (!this.isModified()) { + var def = $.Deferred() + def.resolve(this); + return def; + } + this._saving = $.Deferred(); + if (this.id) { + var rpc = new Rpc("Bug", "update", + this._getUpdateParams()); + } else { + var rpc = new Rpc("Bug", "create", + this._getCreateParams()); + } + rpc.done($.proxy(this, "_saveDone")) + .fail($.proxy(this, "_saveFail")); + return this._saving.promise() + }, + + _getUpdateParams: function() + { + var params = {ids: [this.id]}; + for (var name in this._modified) { + if (name == 'comment') { + params[name] = {body: this._modified[name]}; + // TODO private comment support? + } else if (this.field(name).multivalue) { + var add = []; + var remove = this._data[name].map(String); + this._modified[name].forEach(function(value) { + var index = remove.indexOf(String(value)); + if(index == -1) { + add.push(value); + } else { + remove.splice(index, 1); + } + }); + params[name] = {add: add, remove: remove}; + } else { + params[name] = this._modified[name]; + } + } + return params; + }, + + _getCreateParams: function() + { + var params = {}; + for (var name in this._modified) { + var field = this.field(name); + var value = this._modified[name]; + if (!field.is_on_bug_entry) continue; + if (!value) continue; + if (field.multivalue) { + if (typeof(value) == "string") { + value = value.split(/\s?,\s?/); + } else if (typeof(value) == "number") { + value = [value]; + } + } + params[name] = value; + } + return params; + }, + + _saveDone: function(result) + { + this._modified = {}; + if (result.id) { + // Newly created bug, update + this.id = result.id; + this.update(); + } else { + // Existing bug updated + var changes = result.bugs[0].changes; + for (var name in changes) { + var field = this.field(name); + var change = changes[name]; + if (field.multivalue) { + if (!$.isArray(this._data[name])) this._data[name] = []; + var added = change.added ? change.added.split(/\s*,\s*/) : []; + var removed = change.removed ? change.removed.split(/\s*,\s*/) : []; + if (field.type == Bug.FieldType.BUGID) { + added = added.map(Number); + removed = removed.map(Number); + } + for (var i=0; i < added.length; i++) { + this._data[name].push(added[i]); + } + for (var i=0; i < removed.length; i++) { + var index = this._data[name].indexOf(removed[i]); + if (index != -1) this._data[name].splice(index,1); + } + } else if (name == 'work_time') { + // Special handling for work_time / actual_time + name = 'actual_time'; + this._data['actual_time'] += Number(change.added); + } else if (change.added) { + this._data[name] = change.added; + } else if (change.removed) { + this._data[name] = ""; + } + + this._updateCb.fire(this, name, this._data[name]); + } + if (this._saving) { + this._saving.resolve(this); + this._saving = null; + } + } + }, + _saveFail: function(error) + { + if (this._alert) alert("Saving bug failed: " + error.message); + if (this._saving) { + this._saving.reject(this, error); + this._saving = null; + } + }, + + /** + * Update bug data from database + */ + update: function() { + if (this._fetching) return this._fetching.promise(); + if (!this.id) throw "Can't update unsaved bug"; + this._fetching = $.Deferred(); + new Rpc("Bug", "get", {ids:[this.id]}) + .done($.proxy(this, "_getDone")) + .fail($.proxy(this, "_getFail")); + return this._fetching.promise(); + }, + + _getDone: function(result) { + for (var name in result.bugs[0]) { + try { + var field = this.field(name); + this.set(field, result.bugs[0][name]); + } catch(e) { + // We just skip unknown fields + continue; + } + } + for (var name in this._modified) { + this._data[name] = this._modified[name]; + delete this._modified[name]; + this._updateCb.fire(this, name, this._data[name]); + + } + if (this._saving) { + this._saving.resolve(this); + this._saving = null; + } + if (this._fetching) { + this._fetching.resolve(this); + this._fetching = null; + } + }, + + _getFail: function(error) { + if (this._alert) alert("Loading bug failed: " + error.message); + if (this._saving) { + this._saving.reject(this); + this._saving = null; + } + if (this._fetching) { + this._fetching.reject(this); + this._fetching = null; + } + }, + + value: function(field) + { + field = this.field(field); + return this._modified[field.name] || this._data[field.name]; + }, + + defaultValue: function(field) + { + field = this.field(field); + for (var i=0; i < field.values.length; i++) { + if (field.values[i].is_default) return field.values[i].name; + } + if (this.isMandatory(field)) { + var choices = this.choices(field); + if (choices.length == 1) return choices[0]; + } + }, + + choices: function(field) + { + field = this.field(field); + var current = this._data[field.name]; + var choices = []; + var visibleFor = field.value_field ? this.value(field.value_field) : null; + var allowUnconfrimed = field.name == 'status' ? + this._allowUnconfirmed() : true; + field.values.forEach(function(value) { + if (visibleFor && value.visibility_values.indexOf(visibleFor) == -1) + return; + if (value.name == 'UNCONFIRMED' && !allowUnconfrimed) + return; + choices.push(value); + }); + choices.sort(function(a,b) { + var result = a.sort_key - b.sork_key; + if(result == 0) { + if (a.name < b.name) result = -1; + if (a.name > b.name) result = 1; + } + return result; + }); + choices = choices.map(function(value) {return value.name}); + return choices; + }, + + _allowUnconfirmed: function() { + var field = this.field('product'); + var product = this.value(field); + for (var i=0; i < field.values.length; i++) { + if (field.values[i].name == product) { + return field.values[i].allows_unconfirmed; + } + } + return true; + }, + + isMandatory: function(field) + { + field = this.field(field); + return field.is_mandatory && this.choices(field).length > 1 + && this.isVisible(field); + }, + + /** + * Get field descriptors for fields required in Bug.create() RPC. + */ + requiredFields: function() { + var required = []; + for (var name in BB_FIELDS) { + var field = BB_FIELDS[name]; + if (this.isMandatory(field)) { + required.push(field); + } + } + return required; + }, + + /** + * Set bug field values + * + * set({ field_name: value, ...}) - to set multiple values + * or + * set(field_name, value) - to set single value + */ + set: function(field, value) { + if(arguments.length == 1) { + for (var key in field) { + this.set(key, name[key]); + } + return; + } + field = this.field(field); + if (field.immutable) + return; + var diff = false; + if (field.multivalue) { + if (!value) { + value = []; + } else if ( !$.isArray(value) ){ + value = value.split(/\s*,\s*/); + } + if (this._data[field.name] == undefined) this._data[field.name] = []; + diff = value.sort().join() != this._data[field.name].sort().join(); + } else { + diff = value != this._data[field.name]; + } + if (diff){ + if (field.type == Bug.FieldType.BUGID) { + if(field.multivalue) { + value = value.map(Number); + } else { + value = Number(value); + } + } + this._modified[field.name] = value; + this._changedCb.fire(this, field.name, value); + this._checkDependencies(field.name); + this._checkVisibilities(field.name); + } else { + delete this._modified[field.name]; + this._checkDependencies(field.name); + this._checkVisibilities(field.name); + } + }, + add: function(field, value) { + field = this.field(field); + if (field.type == Bug.FieldType.BUGID) value = Number(value); + if (!field.multivalue) { + this.set(field, value); + } else { + var new_value = this.value(field).slice(); + if (new_value.indexOf(value) == -1) { + new_value.push(value); + this.set(field, new_value); + } + } + }, + remove: function(field, value) { + field = this.field(field); + if (field.type == Bug.FieldType.BUGID) value = Number(value); + if (!field.multivalue) { + if (value == this.value(field)) this.set(field, ''); + } else { + var new_value = this.value(field).slice(); + var index = new_value.indexOf(value); + if (index != -1) { + new_value.splice(index, 1); + this.set(field, new_value); + } + } + }, + _checkDependencies: function(name) + { + if (!Bug._depends[name]) return; + for (var i=0; i < Bug._depends[name].length; i++) { + var dname = Bug._depends[name][i]; + var choices = this.choices(dname); + if (choices.indexOf(this.value(dname)) == -1) { + this.set(dname, choices[0]); + } + this._choicesCb.fire(this, name, dname, choices); + } + }, + _checkVisibilities: function(name) + { + if (!Bug._visibility[name]) return; + var values = this.value(name); + if (!$.isArray(values)) values = [values]; + for (var i=0; i < Bug._visibility[name].length; i++) { + var dname = Bug._visibility[name][i]; + var visibleOn = this.field(dname).visibility_values; + for (var j=0; j < values.length; j++) { + if(visibleOn.indexOf(values[j]) == -1) { + this._visibilityCb.fire(this, name, dname, false); + } else { + this._visibilityCb.fire(this, name, dname, true); + } + } + } + }, + + /** + * Check if field is visible + * @param {String} name Field name + * @return {Boolean} True if field is visible + */ + isVisible: function(field) + { + field = this.field(field); + if (!field.visibility_field) return true; + var visibilityValue = this.value(field.visibility_field); + if (field.visibility_values.indexOf(visibilityValue) != -1) return true; + return false; + }, + + /** + * Creates input element for given field + * @param {String} field Name of field descriptor + * @param {Boolean} hidden If true, then field is created as type=hidden + * @param {Boolean} connect If true, then the change events are connected + * @return {Object} jQuery element + */ + createInput: function(field, hidden, connect) { + field = this.field(field); + if (hidden) { + var element = $('<input type="hidden"></input>'); + } else if (field.type == Bug.FieldType.SELECT || + field.type == Bug.FieldType.MULTI) { + var element = $("<select></select>"); + } else if (field.type == Bug.FieldType.TEXT) { + var element = $("<textarea></textarea>"); + } else { + var element = $("<input></input>"); + element.addClass('text_input field_value'); + } + element.attr("name", field.name); + + if (element.is('select')) { + if (field.type == Bug.FieldType.MULTI || field.multivalue) { + element.attr('multiple', 'multiple'); + } + this._setSelectOptions(element); + this.set(field, element.val()); + } else { + var value = this.value(field.name); + if (value == undefined) value = this.defaultValue(field); + element.val(value); + } + + if (field.type == Bug.FieldType.USER) { + element.userautocomplete({multiple: field.name == 'cc'}); + } + if (field.type == Bug.FieldType.KEYWORDS) { + element.keywordautocomplete(); + } + if (connect) { + element.change($.proxy(this, "_inputChanged")); + if(field.value_field) { + var that = this; + this.choicesUpdated(function(bug, changed, field, choices) { + if (element.attr('name') != field) return; + if (element.attr('type') == 'hidden') { + element.val(bug.value(field)); + } else if(element.is('select')) { + that._setSelectOptions(element); + } + }); + } + if(field.visibility_field) { + var that = this; + this.visibilityUpdated( + function(bug, changed, field, is_visible){ + if (element.attr('name') != field) return; + if(is_visible) { + element.show(); + // reset value when field is shown + that.set(field, that._data[field]); + } else { + element.hide(); + // if field is hidden it should not have value + that.set(field, undefined); + } + }); + } + } + if (!this.isVisible(field.name)) { + element.hide(); + } + return element; + }, + /** + * Input change handler + */ + _inputChanged: function(ev) + { + var target = $(ev.target); + var name = target.attr('name'); + var value = target.val(); + this.set(name, value); + }, + + /** + * Set options for select field + */ + _setSelectOptions: function(element) + { + if(!element.is('select')) return; + element.empty(); + var name = element.attr('name'); + var currentValue = this.value(name); + var defaultValue = this.defaultValue(name); + if (!$.isArray(currentValue)) currentValue = [currentValue]; + this.choices(name).forEach(function(value) { + var option = $('<option>' + value + '</option>') + .attr('value', value); + if ( (currentValue.length == 0 && value == defaultValue) || + (currentValue.indexOf(value) != -1) ) + { + option.attr('selected', 'selected'); + element.prepend(option); + } else { + element.append(option); + } + }); + }, + + /** + * Create lable element for the field input + * @param {String} field Name or descriptor + * @return {Object} jQuery element + */ + createLabel: function(field) + { + field = this.field(field); + var element = $("<label>") + .attr('for', field.name) + .text(field.display_name); + if (!this.isVisible(field.name)) { + element.hide(); + } + this.visibilityUpdated( + function(bug, changed, field, is_visible){ + if (element.attr('for') != field) return; + if(is_visible) { + element.show(); + } else { + element.hide(); + } + }); + return element; + }, + + field: function(field) + { + if (!$.isPlainObject(field)) { + var fdesc = BB_FIELDS[field] || BB_FIELDS[Bug._internal[field]]; + if(!fdesc) throw "Unknown field: " + field; + return fdesc; + } + return field; + } + +}, { + get: function(ids, callback) + { + var multiple = true; + if (!$.isArray(ids)) { + ids = [ids]; + multiple = false; + } + new Rpc("Bug", "get", {ids: ids}) + .done(function(result) { + var bugs = []; + for(var i=0; i < result.bugs.length; i++) { + bugs.push(new Bug(result.bugs[i])); + } + if (!multiple) { + bugs = bugs[0]; + } + callback(bugs); + }).fail(function(error) { + callback([], error.message); + }); + }, + + /** + * Field type numbers + */ + FieldType: { + UNKNOWN: 0, + STRING: 1, + SELECT: 2, + MULTI: 3, + TEXT: 4, + DATE: 5, + BUGID: 6, + URL: 7, + KEYWORDS: 8, + USER: 11, + BOOLEAN: 12 + }, + + _initFields: function() { + // Field dependency map + Bug._depends = {}; + // Field visibility map + Bug._visibility = {}; + // Field "internal" name map + Bug._internal = {}; + for (var name in BB_FIELDS) { + var fdesc = BB_FIELDS[name]; + if (fdesc.value_field) { + if (Bug._depends[fdesc.value_field] == undefined) + Bug._depends[fdesc.value_field] = []; + Bug._depends[fdesc.value_field].push(fdesc.name); + } + if (fdesc.visibility_field) { + if (Bug._visibility[fdesc.visibility_field] == undefined) + Bug._visibility[fdesc.visibility_field] = []; + Bug._visibility[fdesc.visibility_field].push(fdesc.name); + } + if (fdesc.name != fdesc.internal_name) { + Bug._internal[fdesc.internal_name] = fdesc.name; + } + } + } +}); + +Bug._initFields(); + +/** + * User input field autocomplete widget + */ +$.widget("bb.userautocomplete", { + // Default options + options: { + multiple: false + }, + /** + * Initialize the widget + */ + _create: function() + { + // Initialize autocomplete on the element + this.element.autocomplete({ + delay: 500, + search: $.proxy(this, "_search"), + source: $.proxy(this, "_source"), + focus: $.proxy(this, "_onItemFocus"), + select: $.proxy(this, "_onItemSelect") + }) + .data("autocomplete")._renderItem = function(ul, item) { + // Custom rendering for the suggestion list items + return $("<li></li>").data("item.autocomplete", item) + .append("<a>" + item.real_name + "</a>") + .appendTo(ul); + }; + // Add spinner + this.spinner = $("<div/>").addClass("bb-spinner") + .css("position", "absolute") + .hide(); + this.element.after(this.spinner) + + this._respCallback = null; + }, + + /** + * Destroy the widget + */ + destroy: function() + { + this.element.autocomplete("destroy"); + this.spinner.remove(); + $.Widge.prototype.destroy.apply(this); + }, + + /** + * jQuery UI autocomplete item focus handler + */ + _onItemFocus: function(event, ui) { + + if (!this.options.multiple) { + this.element.val(ui.item.name); + } + return false; + }, + + /** + * jQuery UI autocomplete item select handler + */ + _onItemSelect: function(event, ui) { + var pos = this.element.scrollLeft() + var value = ui.item.name + if (this.options.multiple) { + // remove current input + terms = this.element.val().split(/,\s*/); + terms.pop(); + // add new value and placeholder for , + terms.push(value); + terms.push(''); + value = terms.join(', '); + } + this.element.val(value); + this.element.scrollLeft(pos + 1000); + this.element.change(); + return false; + }, + + /** + * jQuery UI autocomplete search term check + */ + _search: function(event, ui) { + var value = this.element.val(); + if (this.options.multiple) { + // for multivalue check only last input + value = value.split(/,\s*/).pop(); + } + if (value.length < 3 ) return false; + }, + + /** + * jQuery UI autocomplete data source function + */ + _source: function(request, responce) { + this._respCallback = responce; + var value = request.term.toLowerCase(); + if (this.options.multiple) { + value = value.split(/,\s*/).pop(); + } + var terms = this._splitTerms(value); + + new Rpc("User", "get", {match:terms}) + .done($.proxy(this, "_userGetDone")) + .complete($.proxy(function(){ + this.spinner.hide(); + }, this)); + + this.spinner.css("top", this.element.position().top) + .css("left", this.element.position().left + this.element.width()) + .show(); + }, + + /** + * Helper to split user input into separate terms + */ + _splitTerms: function(term) { + var result = []; + var tmp = term.split(' '); + for (var i=0; i < tmp.length; i++) { + if (tmp[i].length > 0) result.push(tmp[i]); + } + return result; + }, + + /** + * Handler for User.get() rpc + */ + _userGetDone: function(result) { + if (this._respCallback) { + this._respCallback(result.users); + } + this._respCallback = null; + } +}); + +/** + * Keyword input field autocomplete widget + */ +$.widget("bb.keywordautocomplete", { + /** + * Initialize the widget + */ + _create: function() + { + // Initialize autocomplete on the element + this.element.autocomplete({ + delay: 500, + focus: function() { return false }, + select: $.proxy(this, "_onItemSelect"), + source: $.proxy(this, "_source") + }) + // Add spinner + this.spinner = $("<div/>").addClass("bb-spinner") + .css("position", "absolute") + .hide(); + this.element.after(this.spinner) + this.keywords = BB_FIELDS.keywords.values.map( + function(value){return value.name}) + }, + + /** + * Destroy the widget + */ + destroy: function() + { + this.element.autocomplete("destroy"); + this.spinner.remove(); + $.Widge.prototype.destroy.apply(this); + }, + + /** + * jQuery UI autocomplete item select handler + */ + _onItemSelect: function(event, ui) { + var pos = this.element.scrollLeft() + var value = ui.item.value + // remove current input + terms = this.element.val().split(/,\s*/); + terms.pop(); + // add new value and placeholder for , + terms.push(value); + terms.push(''); + value = terms.join(', '); + this.element.val(value); + this.element.scrollLeft(pos + 1000); + this.element.change(); + return false; + }, + + /** + * jQuery UI autocomplete data source + */ + _source: function(request, response) { + var term = request.term.split(/,\s*/).pop(); + response( $.ui.autocomplete.filter( + this.keywords, term ) ); + } +}); + +/** + * Bug entry widget + */ +$.widget("bb.bugentry", { + /** + * Default options + * + * mode: 'create' or 'edit' + * fields: Fields to display in the form + * title: Title of the dialog + * defaults: Default values to populate the form with + * bug: Bug object to edit or to use when cloning fields to new bug + * clone: Fields to clone from existing bug when creating new bug + */ + options: { + mode: 'create', + fields: null, + title: '', + defaults: {}, + bug: null, + clone: [] + }, + + /** + * Initialize the widget + */ + _create: function() + { + // Set click handler + this.element.on("click", $.proxy(this, "_openDialog")); + this._form = null; + if (this.options.fields == null) { + this.options.fields = BB_CONFIG.defaults.bugentry_fields; + } + if (this.options.clone.length && this.options.bug == null) { + this.options.clone = []; + } + }, + + /** + * Destroy the widget + */ + destroy: function() + { + this.element.off("click", $.proxy(this, "_openDialog")); + this._destroyDialog(); + }, + + /** + * Opens the bug entry dialog when element is clicked. + */ + _openDialog: function() { + if (this.options.mode == 'create') { + var initial = {}; + var that = this; + this.options.clone.forEach(function(field) { + initial[field] = that.options.bug.value(field); + }); + $.extend(initial, this.options.defaults); + this._bug = new Bug(initial); + } else { + this._bug = this.options.bug; + } + if (this._form == null) { + this._createForm(); + this._form.dialog({ + width: 800, + title: this.options.title, + position: ['center', 'top'], + autoOpen: false, + modal: true, + buttons: { + "Save": $.proxy(this, '_saveBug'), + "Cancel": function (){$(this).dialog("close");} + }, + close: $.proxy(this, '_destroyDialog') + }); + } + this._form.dialog("open"); + this._form.find("input:first").focus(); + }, + + /** + * Creates the bug entry form + */ + _createForm: function() { + if (this._form) return; + this._form = $('<form class="bugentry">'); + var list = $('<ul>'); + this._form.append(list); + + // Create fields + for (var i = 0; i < this.options.fields.length; i++) { + if (this.options.fields[i] == '-') { + list.append('<li class="separator"></li>'); + continue; + } + var field = this._bug.field(this.options.fields[i]); + var input = this._bug.createInput(field, false, true); + var item = $('<li class="'+field.name+'">') + .append(this._bug.createLabel(field)) + .append(input); + list.append(item); + } + // Add required but not shown fields + var required = this._bug.requiredFields(); + for (var i=0; i < required.length; i++) { + var field = required[i]; + if(this.options.fields.indexOf(field.name) != -1) continue; + var input = this._bug.createInput(field, false, true); + this._form.append(input); + } + }, + + /** + * Destroys the dialog + */ + _destroyDialog: function() { + if (this._form == null) return; + this._bug = null; + this._form.dialog("destroy"); + this._form.remove(); + this._form = null; + }, + + /** + * Bug entry dialog save button handler + */ + _saveBug: function() { + var saving = this._bug.save() + if (saving) { + saving.done($.proxy(this, "_saveDone")); + } else { + this._destroyDialog(); + } + }, + + /** + * Bug.save() done-callback handler + */ + _saveDone: function(bug) { + this._destroyDialog(); + this._trigger("success", null, { bug: bug, bug_id: bug.id }); + } +}); + +/** + * Utility function to covert query string to parameter object + * + * @param query + * String in format "key=value&key=othervalue&foo=bar" + * + * @returns Object containing the paramters + * {key: ["value", "othervalue"], foo: "bar"} + * Values will be URI decoded + */ +function getQueryParams(query) +{ + var params = {}; + var regex = /([^=&\?]*)=([^&]*)/g; + var match = null; + while ((match = regex.exec(query)) != null) { + var name = match[1]; + var value = decodeURIComponent(match[2]); + if (params.hasOwnProperty(name)) { + if (! $.isArray(params[name])) { + params[name] = [params[name]]; + } + params[name].push(value); + } else { + params[name] = value; + } + } + return params; +} +/** + * Utility function to convert parameter object ro query string + * + * @param params + * Object containing teh params + * { key: ["value", "othervalue"], foo: "bar" } + * + * @returns Query string + * "?key=value&key=othervalue&foo=bar" + * Values will be URI encoded + */ +function getQueryString(params) +{ + var query = "?" + for (name in params) { + var values = params[name]; + if (! $.isArray(values)) values = [values]; + for (var i = 0; i < values.length; i++) { + query += "&" + name + "=" + encodeURIComponent(values[i]); + } + } + return query; +} |