diff options
Diffstat (limited to 'askbot/media/js/utils.js')
-rw-r--r-- | askbot/media/js/utils.js | 2932 |
1 files changed, 2932 insertions, 0 deletions
diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js new file mode 100644 index 00000000..8aa582b2 --- /dev/null +++ b/askbot/media/js/utils.js @@ -0,0 +1,2932 @@ +//var $, scriptUrl, askbotSkin +/** + * attention - this function needs to be retired + * as it cannot accurately give url to the media file + */ +var mediaUrl = function(resource){ + return askbot['settings']['static_url'] + 'default' + '/' + resource; +}; + +var cleanUrl = function(url){ + var re = new RegExp('//', 'g'); + return url.replace(re, '/'); +}; + +var copyAltToTitle = function(sel){ + sel.attr('title', sel.attr('alt')); +}; + +var animateHashes = function(){ + var id_value = window.location.hash; + if (id_value != ""){ + var previous_color = $(id_value).css('background-color'); + $(id_value).css('backgroundColor', '#FFF8C6'); + $(id_value) + .animate({backgroundColor: '#ff7f2a'}, 500) + .animate({backgroundColor: '#FFF8C6'}, 500, function(){ + $(id_value).css('backgroundColor', previous_color); + }); + } +}; + +var getUniqueWords = function(value){ + var words = $.trim(value).split(/\s+/); + var uniques = new Object(); + var out = new Array(); + $.each(words, function(idx, item){ + if (!(item in uniques)){ + uniques[item] = 1; + out.push(item); + }; + }); + return out; +}; + +var showMessage = function(element, msg, where) { + var div = $('<div class="vote-notification"><h3>' + msg + '</h3>(' + + gettext('click to close') + ')</div>'); + + div.click(function(event) { + $(".vote-notification").fadeOut("fast", function() { $(this).remove(); }); + }); + + var where = where || 'parent'; + + if (where == 'parent'){ + element.parent().append(div); + } + else { + element.after(div); + } + + div.fadeIn("fast"); +}; + +//outer html hack - https://github.com/brandonaaron/jquery-outerhtml/ +(function($){ + var div; + $.fn.outerHTML = function() { + var elem = this[0], + tmp; + return !elem ? null + : typeof ( tmp = elem.outerHTML ) === 'string' ? tmp + : ( div = div || $('<div/>') ).html( this.eq(0).clone() ).html(); + }; +})(jQuery); + +var makeKeyHandler = function(key, callback){ + return function(e){ + if ((e.which && e.which == key) || (e.keyCode && e.keyCode == key)){ + if(!e.shiftKey){ + callback(); + return false; + } + } + }; +}; + + +var setupButtonEventHandlers = function(button, callback){ + button.keydown(makeKeyHandler(13, callback)); + button.click(callback); +}; + + +var putCursorAtEnd = function(element){ + var el = element.get()[0]; + if (el.setSelectionRange){ + var len = element.val().length * 2; + el.setSelectionRange(len, len); + } + else{ + element.val(element.val()); + } + element.scrollTop = 999999; +}; + +var setCheckBoxesIn = function(selector, value){ + return $(selector + '> input[type=checkbox]').attr('checked', value); +}; + +/* + * Old style notify handler + */ +var notify = function() { + var visible = false; + return { + show: function(html, autohide) { + if (html) { + $("body").addClass('user-messages'); + var par = $('<p class="notification"></p>'); + par.html(html); + $(".notify").prepend(par); + } + $(".notify").fadeIn("slow"); + visible = true; + if (autohide) { + setTimeout( + function() { + notify.close(false); + notify.clear(); + }, + 3000 + ); + } + }, + clear: function() { + $('.notify').empty(); + }, + close: function(doPostback) { + if (doPostback) { + $.post( + askbot['urls']['mark_read_message'], + { formdata: "required" } + ); + } + $(".notify").fadeOut("fast"); + $('body').removeClass('user-messages'); + visible = false; + }, + isVisible: function() { return visible; } + }; +}(); + +/* **************************************************** */ +// Search query-string manipulation utils +/* **************************************************** */ + +var QSutils = QSutils || {}; // TODO: unit-test me + +QSutils.TAG_SEP = ','; // should match const.TAG_SEP; TODO: maybe prepopulate this in javascript.html ? + +QSutils.get_query_string_selector_value = function (query_string, selector) { + var params = query_string.split('/'); + for(var i=0; i<params.length; i++) { + var param_split = params[i].split(':'); + if(param_split[0] === selector) { + return param_split[1]; + } + } + return undefined; +}; + +QSutils.patch_query_string = function (query_string, patch, remove) { + var params = query_string.split('/'); + var patch_split = patch.split(':'); + + var new_query_string = ''; + var mapping = {}; + + if(!remove) { + mapping[patch_split[0]] = patch_split[1]; // prepopulate the patched selector if it's not meant to be removed + } + + for (var i = 0; i < params.length; i++) { + var param_split = params[i].split(':'); + if(param_split[0] !== patch_split[0] && param_split[1]) { + mapping[param_split[0]] = param_split[1]; + } + } + + var add_selector = function(name) { + if(name in mapping) { + new_query_string += name + ':' + mapping[name] + '/'; + } + }; + + /* The order of selectors should match the Django URL */ + add_selector('scope'); + add_selector('sort'); + add_selector('query'); + add_selector('tags'); + add_selector('author'); + add_selector('page'); + + return new_query_string; +}; + +QSutils.remove_search_tag = function(query_string, tag){ + var tag_string = this.get_query_string_selector_value(query_string, 'tags'); + if(!tag_string) { + return query_string; + } + + var tags = tag_string.split(this.TAG_SEP); + + var pos = $.inArray(encodeURIComponent(tag), tags); + if(pos > -1) { + tags.splice(pos, 1); /* array.splice() works in-place */ + } + + if(tags.length === 0) { + return this.patch_query_string(query_string, 'tags:', true); + } else { + return this.patch_query_string(query_string, 'tags:' + tags.join(this.TAG_SEP)); + } +}; + +QSutils.add_search_tag = function(query_string, tag){ + var tag_string = this.get_query_string_selector_value(query_string, 'tags'); + tag = encodeURIComponent(tag); + if(!tag_string) { + tag_string = tag; + } else { + tag_string = [tag_string, tag].join(this.TAG_SEP); + } + + return this.patch_query_string(query_string, 'tags:' + tag_string); +}; + +/* **************************************************** */ + +/* some google closure-like code for the ui elements */ +var inherits = function(childCtor, parentCtor) { + /** @constructor taken from google closure */ + function tempCtor() {}; + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor(); + childCtor.prototype.constructor = childCtor; +}; + +/** wrapper around jQuery object + * @constructor + * the top level "class" for other elements + * I.e. all other things must inherit this class. + * For an example of the inheritance pattern, + * please see the "TippedInput" below. + */ +var WrappedElement = function(){ + this._element = null; + this._in_document = false; +}; +/* note that we do not call inherits() here + * See TippedInput as an example of a subclass + */ + +/** + * notice that we use ObjCls.prototype.someMethod = function() + * notation - as we use Javascript's prototypal inheritance + * explicitly. The point of this is to be able to eventually + * use the Closure Compiler + */ +WrappedElement.prototype.setElement = function(element){ + this._element = element; +}; + +/** + * this function must be overridden for any object + * what will use "DOM generation" pattern + * + * Inside this function two things can happen: + * 1) dom structure creation + * 2) event handlers attached to the dom structure + */ +WrappedElement.prototype.createDom = function(){ + /* inside at the very least you must assign + * a jQuery object to a parameter called _element + */ + this._element = $('<div></div>'); +}; + +/** + * @param {object} element, a jQuery object wrapping a single + * DOM element. + * + * This function must be overridden in the subclasses + * that are used in the "decoration" pattern + */ +WrappedElement.prototype.decorate = function(element){ + this._element = element; +}; + +/** + * This method should not be overridden + * Normally you call this method to generate the dom + * structure, if applicable, or just obtain the + * jQuery object encapsulating the dom. + * + * @return {object} jQuery + */ +WrappedElement.prototype.getElement = function(){ + if (this._element === null){ + this.createDom(); + } + return this._element; +}; +WrappedElement.prototype.inDocument = function(){ + return this._in_document; +}; +WrappedElement.prototype.enterDocument = function(){ + return this._in_document = true; +}; +WrappedElement.prototype.hasElement = function(){ + return (this._element !== null); +}; +/** + * A utility method, returning a new jQuery object for + * some HTML tag + * + * Example: + * var ageInput = this.makeElement('input'); + */ +WrappedElement.prototype.makeElement = function(html_tag){ + //makes jQuery element with tags + return $('<' + html_tag + '></' + html_tag + '>'); +}; +/** + * Removes object's DOM element from the DOM tree + * should be overridden to remove the event handlers + * and properly destroy the dom structure + * as well as any other included sub-elements + */ +WrappedElement.prototype.dispose = function(){ + this._element.remove(); + this._in_document = false; +}; + +/** + * @constructor + * Widget is a Wrapped element with state + */ +var Widget = function() { + WrappedElement.call(this); + this._state = undefined; +}; +inherits(Widget, WrappedElement); + +Widget.prototype.setState = function(state) { + this._state = state; +}; + +Widget.prototype.getState = function() { + return this._state; +}; + +Widget.prototype.makeButton = function(label, handler) { + var button = this.makeElement('button'); + button.html(label); + setupButtonEventHandlers(button, handler); + return button; +}; + +/** + * Can be used for an input box or textarea. + * The original value will be treated as an instruction. + * When user focuses on the field, the tip will be gone, + * when the user escapes without typing anything besides + * perhaps empty text, the instruction is restored. + * When instruction is shown, class "blank" is present + * in the input/textare element. + * + * For the usage examples - search for "new TippedInput" + * there is at least one example for both - decoration and + * the "dom creation" patterns. + * + * Also - in the FileUploadDialog the TippedInput is used + * as a sub-element - the widget composition use case. + */ +var TippedInput = function(){ + /* the call below is part 1 of the inheritance pattern */ + WrappedElement.call(this); + this._instruction = null; + this._attrs = {}; + //this._is_one_shot = false;//if true on starting typing effect is gone +}; +inherits(TippedInput, WrappedElement); +/* the line above is part 2 of the inheritance pattern + see definition of the function "inherits" for more details +*/ + +/* Below are all the custom methods of the + TippedInput class, as well as some required functions +*/ + +TippedInput.prototype.reset = function(){ + $(this._element).val(this._instruction); + $(this._element).addClass('blank'); +}; + +/*TippedInput.prototype.setIsOneShot = function(boolValue) { + this._is_one_shot = boolValue; +};*/ + +TippedInput.prototype.setInstruction = function(text) { + this._instruction = text; +}; + +TippedInput.prototype.setAttr = function(key, value) { + this._attrs[key] = value; +}; + +TippedInput.prototype.isBlank = function(){ + return this.getVal() === this._instruction; +}; + +TippedInput.prototype.getVal = function(){ + return this._element.val(); +}; + +TippedInput.prototype.setVal = function(value){ + if (value) { + this._element.val(value); + if (this.isBlank()){ + this._element.addClass('blank'); + } else { + this._element.removeClass('blank'); + } + } +}; +/** + * Creates the DOM of tipped input from scratch + * Notice that there is also a "decorate" method. + * At least one - createDom or decorate is required, + * depending on the usage. + */ +TippedInput.prototype.createDom = function() { + this._element = this.makeElement('input'); + var element = this._element; + element.val(this._instruction); + + //here we re-use the decorate call (next method) + //to avoid copy-pasting code + this.decorate(element); +}; + +/** + * Attaches the TippedInput behavior to + * a pre-existing <input> element + * + * decorate() method normally does not create + * new dom elements, but it might add some missing elements, + * if necessary. + * + * for example the decorate might be composing inside + * a more complex widget, in which case other elements + * can be added via a "composition" pattern, or + * just "naked dom elements" added to the current widget's element + * + */ +TippedInput.prototype.decorate = function(element){ + this._element = element;//mandatory line + + //part 1 - initialize some values and create dom + element.attr(this._attrs); + + var instruction_text = this.getVal(); + this._instruction = instruction_text; + this.reset(); + var me = this; + + //part 2 - attach event handlers + $(element).focus(function(){ + if (me.isBlank()){ + $(element) + .val('') + .removeClass('blank'); + } + }); + $(element).blur(function(){ + var val = $(element).val(); + if ($.trim(val) === ''){ + $(element) + .val(instruction_text) + .addClass('blank'); + } + }); + $(element).keydown( + makeKeyHandler(27, function(){ + $(element).blur(); + }) + ); +}; + +/** + * will setup a bootstrap.js alert + * programmatically + */ +var AlertBox = function(){ + WrappedElement.call(this); + this._text = null; +}; +inherits(AlertBox, WrappedElement); + +AlertBox.prototype.setClass = function(classes){ + this._classes = classes; + if (this._element){ + this._element.addClass(classes); + } +}; + +AlertBox.prototype.setError = function(state){ + this._is_error = state; + if (this._element) { + if (state === true) { + this._element.addClass('alert-error'); + } else { + this._element.removeClass('alert-error'); + } + } +}; + +AlertBox.prototype.setText = function(text){ + this._text = text; + if (this._content){ + this._content.html(text); + } +}; + +AlertBox.prototype.getContent = function(){ + if (this._content){ + return this._content; + } else { + this._content = this.makeElement('div'); + return this._content; + } +}; + +AlertBox.prototype.setContent = function(content){ + var container = this.getContent(); + container.empty() + container.append(content); +}; + +AlertBox.prototype.addContent = function(content){ + var container = this.getContent(); + container.append(content); +}; + +AlertBox.prototype.createDom = function(){ + this._element = this.makeElement('div'); + this._element.addClass('alert fade in'); + + if (this._is_error) { + this.setError(this._is_error); + } + + if (this._classes){ + this._element.addClass(this._classes); + } + + this._cancel_button = this.makeElement('button'); + this._cancel_button + .addClass('close') + .attr('data-dismiss', 'alert') + .html('×'); + this._element.append(this._cancel_button); + + this._element.append(this.getContent()); + if (this._text){ + this.setText(this._text); + } + + this._element.alert();//bootstrap.js alert +}; + +/** + * @constructor + * just a span with content + * useful for subclassing + */ +var SimpleContent = function(){ + WrappedElement.call(this); + this._content = ''; +}; +inherits(SimpleContent, WrappedElement); + +SimpleContent.prototype.setContent = function(text) { + this._content = text; + if (this._element) { + this._element.html(text); + } +}; + +SimpleContent.prototype.getContent = function() { + return this._content; +}; + +SimpleContent.prototype.createDom = function() { + this._element = this.makeElement('span'); + this._element.html(this._content); +}; + +var SimpleControl = function(){ + WrappedElement.call(this); + this._handler = null; + this._title = null; +}; +inherits(SimpleControl, WrappedElement); + +SimpleControl.prototype.setHandler = function(handler){ + this._handler = handler; + if (this.hasElement()){ + this.setHandlerInternal(); + } +}; + +SimpleControl.prototype.getHandler = function(){ + return this._handler; +}; + +SimpleControl.prototype.setHandlerInternal = function(){ + //default internal setHandler behavior + setupButtonEventHandlers(this._element, this._handler); +}; + +SimpleControl.prototype.setTitle = function(title){ + this._title = title; +}; + +var EditLink = function(){ + SimpleControl.call(this) +}; +inherits(EditLink, SimpleControl); + +EditLink.prototype.createDom = function(){ + var element = $('<a></a>'); + element.addClass('edit'); + this.decorate(element); +}; + +EditLink.prototype.decorate = function(element){ + this._element = element; + this._element.attr('title', gettext('click to edit this comment')); + this._element.html(gettext('edit')); + this.setHandlerInternal(); +}; + +var CommentConvertLink = function(comment_id){ + WrappedElement.call(this) + this._comment_id = comment_id; +}; +inherits(CommentConvertLink, WrappedElement); + +CommentConvertLink.prototype.createDom = function(){ + var element = this.makeElement('form'); + element.addClass('convert-comment'); + element.attr('method', 'POST'); + element.attr('action', askbot['urls']['convertComment']); + var hidden_input = this.makeElement('input'); + hidden_input.attr('type', 'hidden'); + hidden_input.attr('value', this._comment_id); + hidden_input.attr('name', 'comment_id'); + hidden_input.attr('id', 'id_comment_id'); + element.append(hidden_input); + + var submit = this.makeElement('input'); + submit.attr('type', 'submit'); + submit.attr('value', gettext('convert to answer')); + element.append(submit); + this.decorate(element); +}; + + +CommentConvertLink.prototype.decorate = function(element){ + this._element = element; +}; + +var DeleteIcon = function(title){ + SimpleControl.call(this); + this._title = title; + this._content = null; +}; +inherits(DeleteIcon, SimpleControl); + +DeleteIcon.prototype.decorate = function(element){ + this._element = element; + this._element.attr('class', 'delete-icon'); + this._element.attr('title', this._title); + if (this._handler !== null){ + this.setHandlerInternal(); + } +}; + +DeleteIcon.prototype.setHandlerInternal = function(){ + setupButtonEventHandlers(this._element, this._handler); +}; + +DeleteIcon.prototype.createDom = function(){ + this._element = this.makeElement('span'); + this.decorate(this._element); + if (this._content !== null){ + this.setContent(this._content); + } +}; + +DeleteIcon.prototype.setContent = function(content){ + if (this._element === null){ + this._content = content; + } else { + this._content = content; + this._element.html(content); + } +} + +/** + * @contstructor + * Simple modal dialog with Ok/Cancel buttons by default + */ +var ModalDialog = function() { + WrappedElement.call(this); + this._accept_button_text = gettext('Ok'); + this._reject_button_text = gettext('Cancel'); + this._heading_text = 'Add heading by setHeadingText()'; + this._initial_content = undefined; + this._accept_handler = function(){}; + var me = this; + this._reject_handler = function() { me.hide(); }; + this._content_element = undefined; +}; +inherits(ModalDialog, WrappedElement); + +ModalDialog.prototype.show = function() { + this._element.modal('show'); +}; + +ModalDialog.prototype.hide = function() { + this._element.modal('hide'); +}; + +ModalDialog.prototype.setContent = function(content) { + this._initial_content = content; + if (this._content_element) { + this._content_element.html(content); + } +}; + +ModalDialog.prototype.prependContent = function(content) { + this._content_element.prepend(content); +}; + +ModalDialog.prototype.setHeadingText = function(text) { + this._heading_text = text; +}; + +ModalDialog.prototype.setAcceptButtonText = function(text) { + this._accept_button_text = text; +}; + +ModalDialog.prototype.setRejectButtonText = function(text) { + this._reject_button_text = text; +}; + +ModalDialog.prototype.setAcceptHandler = function(handler) { + this._accept_handler = handler; +}; + +ModalDialog.prototype.setRejectHandler = function(handler) { + this._reject_handler = handler; +}; + +ModalDialog.prototype.clearMessages = function() { + this._element.find('.alert').remove(); +}; + +ModalDialog.prototype.setMessage = function(text, message_type) { + var box = new AlertBox(); + box.setText(text); + if (message_type === 'error') { + box.setError(true); + } + this.prependContent(box.getElement()); +}; + +ModalDialog.prototype.createDom = function() { + this._element = this.makeElement('div') + var element = this._element; + + element.addClass('modal'); + + //1) create header + var header = this.makeElement('div') + header.addClass('modal-header'); + element.append(header); + + var close_link = this.makeElement('div'); + close_link.addClass('close'); + close_link.attr('data-dismiss', 'modal'); + close_link.html('x'); + header.append(close_link); + + var title = this.makeElement('h3'); + title.html(this._heading_text); + header.append(title); + + //2) create content + var body = this.makeElement('div') + body.addClass('modal-body'); + element.append(body); + this._content_element = body; + if (this._initial_content) { + this._content_element.append(this._initial_content); + } + + //3) create footer with accept and reject buttons (ok/cancel). + var footer = this.makeElement('div'); + footer.addClass('modal-footer'); + element.append(footer); + + var accept_btn = this.makeElement('button'); + accept_btn.addClass('btn btn-primary'); + accept_btn.html(this._accept_button_text); + footer.append(accept_btn); + + if (this._reject_button_text) { + var reject_btn = this.makeElement('button'); + reject_btn.addClass('btn cancel'); + reject_btn.html(this._reject_button_text); + footer.append(reject_btn); + } + + //4) attach event handlers to the buttons + setupButtonEventHandlers(accept_btn, this._accept_handler); + if (this._reject_button_text) { + setupButtonEventHandlers(reject_btn, this._reject_handler); + } + setupButtonEventHandlers(close_link, this._reject_handler); + + this.hide(); +}; + +/** + * @constructor + */ +var FileUploadDialog = function() { + ModalDialog.call(this); + self._post_upload_handler = undefined; +}; +inherits(FileUploadDialog, ModalDialog); + +FileUploadDialog.prototype.setPostUploadHandler = function(handler) { + this._post_upload_handler = handler; +}; + +FileUploadDialog.prototype.runPostUploadHandler = function(url, descr) { + this._post_upload_handler(url, descr); +}; + +FileUploadDialog.prototype.setInputId = function(id) { + this._input_id = id; +}; + +FileUploadDialog.prototype.getInputId = function() { + return this._input_id; +}; + +FileUploadDialog.prototype.setUrlInputTooltip = function(text) { + this._url_input_tooltip = text; +}; + +FileUploadDialog.prototype.getUrl = function() { + var url_input = this._url_input; + if (url_input.isBlank() === false) { + return url_input.getVal(); + } + return ''; +}; + +//disable description for now +//FileUploadDialog.prototype.getDescription = function() { +// return this._description_input.getVal(); +//}; + +FileUploadDialog.prototype.resetInputs = function() { + this._url_input.reset(); + //this._description_input.reset(); + this._upload_input.val(''); +}; + +FileUploadDialog.prototype.show = function() { + //hack around the ajaxFileUpload plugin + FileUploadDialog.superClass_.show.call(this); + var upload_input = this._upload_input; + upload_input.unbind('change'); + //todo: fix this - make event handler reinstall work + upload_input.change(this.getStartUploadHandler()); +}; + +FileUploadDialog.prototype.getStartUploadHandler = function(){ + /* startUploadHandler is passed in to re-install the event handler + * which is removed by the ajaxFileUpload jQuery extension + */ + var spinner = this._spinner; + var uploadInputId = this.getInputId(); + var urlInput = this._url_input; + var handler = function() { + var options = { + 'spinner': spinner, + 'uploadInputId': uploadInputId, + 'urlInput': urlInput.getElement(), + 'startUploadHandler': handler//pass in itself + }; + return ajaxFileUpload(options); + }; + return handler; +}; + +FileUploadDialog.prototype.createDom = function() { + + var superClass = FileUploadDialog.superClass_; + + var me = this; + superClass.setAcceptHandler.call(this, function(){ + var url = $.trim(me.getUrl()); + //var description = me.getDescription(); + //@todo: have url cleaning code here + if (url.length > 0) { + me.runPostUploadHandler(url);//, description); + me.resetInputs(); + } + me.hide(); + }); + superClass.setRejectHandler.call(this, function(){ + me.resetInputs(); + me.hide(); + }); + superClass.createDom.call(this); + + var form = this.makeElement('form'); + form.css('margin-bottom', 0); + this.prependContent(form); + + // File upload button + var upload_input = this.makeElement('input'); + upload_input.attr({ + id: this._input_id, + type: 'file', + name: 'file-upload', + //size: 26??? + }); + form.append(upload_input); + this._upload_input = upload_input; + form.append($('<br/>')); + + // The url input text box + var url_input = new TippedInput(); + url_input.setInstruction(this._url_input_tooltip || gettext('Or paste file url here')); + var url_input_element = url_input.getElement(); + url_input_element.css({ + 'width': '200px', + 'display': 'none' + }); + form.append(url_input_element); + //form.append($('<br/>')); + this._url_input = url_input; + + var label = this.makeElement('label'); + label.attr('for', this._input_id); + + var types = askbot['settings']['allowedUploadFileTypes']; + types = types.join(', '); + label.html(gettext('Allowed file types are:') + ' ' + types + '.'); + form.append(label); + form.append($('<br/>')); + + /* //Description input box + var descr_input = new TippedInput(); + descr_input.setInstruction(gettext('Describe the image here')); + this.makeElement('input'); + form.append(descr_input.getElement()); + form.append($('<br/>')); + this._description_input = descr_input; + */ + var spinner = this.makeElement('img'); + spinner.attr('src', mediaUrl('media/images/indicator.gif')); + spinner.css('display', 'none'); + form.append(spinner); + this._spinner = spinner; + + upload_input.change(this.getStartUploadHandler()); +}; + +/** + * attaches a modal menu with a text editor + * to a link. The modal menu is from bootstrap.js + * todo: this should probably be a subclass of ModalDialog, + * triggered by a link click, then a whole bunch of methods + * would be simply inherited from the modal dialog: + * clearMessages, etc. + */ +var TextPropertyEditor = function(){ + WrappedElement.call(this); + this._editor = null; +}; +inherits(TextPropertyEditor, WrappedElement); + +TextPropertyEditor.prototype.getWidgetData = function(){ + var data = this._element.data(); + return { + object_id: data['objectId'], + model_name: data['modelName'], + property_name: data['propertyName'], + url: data['url'], + help_text: data['helpText'], + editor_heading: data['editorHeading'] + }; +}; + +TextPropertyEditor.prototype.makeEditor = function(){ + if (this._editor) { + return this._editor; + } + var editor = new ModalDialog(); + this._editor = editor; + editor.setHeadingText(this.getWidgetData()['editor_heading']); + + //create main content for the editor + var textarea = this.makeElement('textarea'); + textarea.addClass('tipped-input blank'); + textarea.val(this.getWidgetData()['help_text']); + + var tipped_input = new TippedInput(); + tipped_input.decorate(textarea); + this._text_input = tipped_input; + + editor.setContent(textarea); + //body.append(textarea); + + editor.setAcceptButtonText(gettext('Save')); + editor.setRejectButtonText(gettext('Cancel')); + + var me = this; + editor.setAcceptHandler(function(){ + me.saveData(); + }); + + $(document).append(editor.getElement()); + return editor; +}; + +TextPropertyEditor.prototype.openEditor = function(){ + this._editor.show(); +}; + +TextPropertyEditor.prototype.clearMessages = function(){ + this._editor.clearMessages() +}; + +TextPropertyEditor.prototype.showAlert = function(text){ + this._editor.setMessage(text, 'alert'); +}; + +TextPropertyEditor.prototype.showError = function(text){ + this._editor.setMessage(text, 'error'); +}; + +TextPropertyEditor.prototype.setText = function(text){ + this._text_input.setVal(text); +}; + +TextPropertyEditor.prototype.getText = function(){ + return this._text_input.getVal(); +}; + +TextPropertyEditor.prototype.hideDialog = function(){ + this._editor.hide(); +}; + +TextPropertyEditor.prototype.startOpeningEditor = function(){ + var me = this; + $.ajax({ + type: 'GET', + dataType: 'json', + cache: false, + url: me.getWidgetData()['url'], + data: me.getWidgetData(), + success: function(data){ + if (data['success']) { + me.makeEditor(); + me.setText($.trim(data['text'])); + me.openEditor(); + } else { + showMessage(me.getElement(), data['message']); + } + } + }); +}; + +TextPropertyEditor.prototype.saveData = function(){ + var data = this.getWidgetData(); + data['text'] = this.getText(); + var me = this; + $.ajax({ + type: 'POST', + dataType: 'json', + cache: false, + url: me.getWidgetData()['url'], + data: data, + success: function(data) { + if (data['success']) { + me.showAlert(gettext('saved')); + setTimeout(function(){ + me.clearMessages(); + me.hideDialog(); + }, 1000); + } else { + me.showError(data['message']); + } + } + }); +}; + +TextPropertyEditor.prototype.decorate = function(element){ + this._element = element; + var me = this; + setupButtonEventHandlers(element, function(){ me.startOpeningEditor() }); +}; + +/** + * A button on which user can click + * and become added to some group (followers, group members, etc.) + * or toggle some state on/off + * The button has four states on-prompt, off-prompt, on-state and off-state + * on-prompt is activated on mouseover, when user is not part of group + * off-prompt - on mouseover, when user is part of group + * on-state - when user is part of group and mouse is not over the button + * off-state - same as above, but when user is not part of the group + */ +var TwoStateToggle = function(){ + SimpleControl.call(this); + this._state = null; + this._state_messages = {}; + this._states = [ + 'on-state', + 'off-state', + 'on-prompt', + 'off-prompt' + ]; + this._handler = this.getDefaultHandler(); + this._post_data = {}; + this.toggleUrl = '';//public property +}; +inherits(TwoStateToggle, SimpleControl); + +TwoStateToggle.prototype.setPostData = function(data){ + this._post_data = data; +}; + +TwoStateToggle.prototype.getPostData = function(){ + return this._post_data; +}; + +TwoStateToggle.prototype.resetStyles = function(){ + var element = this._element; + var states = this._states; + $.each(states, function(idx, state){ + element.removeClass(state); + }); + this._element.html(''); +}; + +TwoStateToggle.prototype.isOn = function(){ + return this._element.hasClass('on'); +}; + +TwoStateToggle.prototype.getDefaultHandler = function(){ + var me = this; + return function(){ + var data = me.getPostData(); + data['disable'] = me.isOn(); + $.ajax({ + type: 'POST', + dataType: 'json', + cache: false, + url: me.toggleUrl, + data: data, + success: function(data) { + if (data['success']) { + if ( data['is_enabled'] ) { + me.setState('on-state'); + } else { + me.setState('off-state'); + } + } else { + showMessage(me.getElement(), data['message']); + } + } + }); + }; +}; + +TwoStateToggle.prototype.isCheckBox = function(){ + var element = this._element; + return element.attr('type') === 'checkbox'; +}; + +TwoStateToggle.prototype.setState = function(state){ + var element = this._element; + this._state = state; + if (element) { + this.resetStyles(); + element.addClass(state); + if (state === 'on-state') { + element.addClass('on'); + } else if (state === 'off-state') { + element.removeClass('on'); + } + if ( this.isCheckBox() ) { + if (state === 'on-state') { + element.attr('checked', true); + } else if (state === 'off-state') { + element.attr('checked', false); + } + } else { + this._element.html(this._state_messages[state]); + } + } +}; + +TwoStateToggle.prototype.decorate = function(element){ + this._element = element; + //read messages for all states + var messages = {}; + messages['on-state'] = + element.attr('data-on-state-text') || gettext('enabled'); + messages['off-state'] = + element.attr('data-off-state-text') || gettext('disabled'); + messages['on-prompt'] = + element.attr('data-on-prompt-text') || messages['on-state']; + messages['off-prompt'] = + element.attr('data-off-prompt-text') || messages['off-state']; + this._state_messages = messages; + + this.toggleUrl = element.attr('data-toggle-url'); + + //detect state and save it + if (this.isCheckBox()) { + this._state = element.attr('checked') ? 'state-on' : 'state-off'; + } else { + var text = $.trim(element.html()); + for (var i = 0; i < this._states.length; i++){ + var state = this._states[i]; + if (text === messages[state]){ + this._state = state; + break; + } + } + } + + //set mouseover handler + var me = this; + element.mouseover(function(){ + var is_on = me.isOn(); + if (is_on){ + me.setState('off-prompt'); + } else { + me.setState('on-prompt'); + } + //element.css('background-color', 'red'); + return false; + }); + element.mouseout(function(){ + var is_on = me.isOn(); + if (is_on){ + me.setState('on-state'); + } else { + me.setState('off-state'); + } + //element.css('background-color', 'white'); + return false; + }); + + setupButtonEventHandlers(element, this.getHandler()); +}; + +/** + * @contstructor + * a simple dropdown select element + * which saves data to the server on change + */ +var DropdownSelect = function() { + WrappedElement.call(this); +}; +inherits(DropdownSelect, WrappedElement); + +DropdownSelect.prototype.setPostData = function(data) { + this._postData = data; +}; + +/** + * posts value of selection to the url given + * with data-url and parameter called "value" + */ +DropdownSelect.prototype.saveChoice = function() { + var element = this._element; + var url = this._url; + var data = this._postData; + data['value'] = element.val(); + $.ajax({ + type: 'POST', + dataType: 'json', + data: data, + cache: false, + url: url, + success: function(data) { + if (!data['success']) { + showMessage(element, data['message']); + } + } + }); +}; + +DropdownSelect.prototype.decorate = function(element) { + this._element = element; + this._url = $(element).data('url'); + var me = this; + this._element.change(function() { + me.saveChoice(); + }); +}; + +var BoxItemContent = function() { + SimpleContent.call(this); +}; +inherits(BoxItemContent, SimpleContent); + +/** + * @override to allow for more complex content + */ +BoxItemContent.prototype.setName = function(name) { + BoxItemContent.superClass_.setContent.call(this, name); +}; +BoxItemContent.prototype.getName = function() { + return BoxItemContent.superClass_.getContent.call(this); +}; + +/** + * @constructor + * an item used for the select box described below + */ +var SelectBoxItem = function() { + Widget.call(this); + this._id = null; + this._name = null; + this._description = null; + this._content_class = BoxItemContent;//default expects a single text node + //content element - instance of this._content_class + this._content = undefined; + this._selector = undefined;//the selector object +}; +inherits(SelectBoxItem, Widget); + +SelectBoxItem.prototype.setId = function(id) { + this._id = id; + if (this._element) { + this._element.data('itemId', id); + } +}; + +SelectBoxItem.prototype.getId = function() { + return this._id; +}; + +SelectBoxItem.prototype.setName = function(name) { + this._name = name; + if (this._content) { + this._content.setName(name); + } +}; + +SelectBoxItem.prototype.setDescription = function(description) { + this._description = description; + if (this._element) { + this._element.data('originalTitle'); + } +}; + +SelectBoxItem.prototype.getData = function () { + //todo: stuck using old key names, change after merge + //with the user-groups branch + return { + id: this._id, + title: this._name, + details: this._description + }; +}; + +SelectBoxItem.prototype.setSelector = function(sel) { + this._selector = sel; +}; + +SelectBoxItem.prototype.getSelector = function(sel) { + return this._selector; +}; + +SelectBoxItem.prototype.getContent = function() { + return this._content; +}; + +SelectBoxItem.prototype.isSelected = function() { + return this._element.hasClass('selected'); +}; + +SelectBoxItem.prototype.setSelected = function(is_selected) { + if (is_selected) { + this._element.addClass('selected'); + } else { + this._element.removeClass('selected'); + } +} + +SelectBoxItem.prototype.detach = function() { + this._element.detach(); +}; + +SelectBoxItem.prototype.createDom = function() { + var elem = this.makeElement('li'); + this._element = elem; + elem.data('itemId', this._id); + elem.data('itemOriginalTitle', this._description); + var content = new this._content_class(); + content.setName(this._name); + elem.append(content.getElement()); + this._content = content; +} +/** + * this method sets css class to the item's DOM element + */ +SelectBoxItem.prototype.addCssClass = function(css_class) { + this._element.addClass(css_class); +}; + +SelectBoxItem.prototype.decorate = function(element) { + this._element = element; + //set id and description + this._id = element.data('itemId'); + this._description = element.data('originalTitle'); + + //work on setting name + var content_source = element.contents().detach(); + var content = new this._content_class(); + //assume that we want first node only + content.setContent(content_source[0]); + this._content = content; + this._name = content.getName();//allows to abstract from structure + + this._element.append(content.getElement()); +}; + +/** + * A list of items from where one can be selected + */ +var SelectBox = function(){ + Widget.call(this); + this._items = []; + this._select_handler = function(){};//empty default + this._is_editable = false; + this._item_class = SelectBoxItem; +}; +inherits(SelectBox, Widget); + +SelectBox.prototype.setEditable = function(is_editable) { + this._is_editable = is_editable; +}; + +SelectBox.prototype.isEditable = function() { + return this._is_editable; +}; + +SelectBox.prototype.detachAllItems = function() { + var items = this._items; + $.each(items, function(idx, item){ + item.detach(); + }); + this._items = []; +}; + +SelectBox.prototype.getItem = function(id){ + var items = this._items; + for (var i = 0; i < items.length; i++) { + if (items[i].getId() === id) { + return items[i]; + } + } + return undefined; +}; + +SelectBox.prototype.getItemByIndex = function(idx) { + return this._items[idx]; +}; + +//why do we have these two almost identical methods? +SelectBox.prototype.removeItem = function(id){ + var item = this.getItem(id); + item.getElement().fadeOut(); + item.dispose(); + var idx = $.inArray(item, this._items); + if (idx !== -1) { + this._items.splice(idx, 1); + } +}; +SelectBox.prototype.deleteItem = function(id) { + var item = this.getItem(id); + if (item === undefined) { + return; + } + item.dispose(); + var idx = $.inArray(item, this._items); + if (idx !== -1) { + this._items.splice(idx, 1); + } +}; + +SelectBox.prototype.createItem = function() { + return new this._item_class(); +}; + +SelectBox.prototype.getItemIndex = function(item) { + var idx = $.inArray(item, this._items); + if (idx === -1) { + throw "index error"; + } + return idx; +}; + +SelectBox.prototype.addItemObject = function(item) { + this._items.push(item); + this._element.append(item.getElement()); + this.selectItem(item); + item.setSelector(this); + //set event handler + var me = this; + setupButtonEventHandlers( + item.getElement(), + me.getSelectHandler(item) + ); +}; + +/** @todo: rename to setItem?? have a problem when id's are all say 0 */ +SelectBox.prototype.addItem = function(id, name, description){ + + if (this.hasElement() === false) { + return; + } + //delete old item + this.deleteItem(id); + //create new item + var item = this.createItem(); + item.setId(id); + item.setName(name); + item.setDescription(description); + //add item to the SelectBox + this.addItemObject(item); + + return item; +}; + +SelectBox.prototype.getSelectedItem = function() { + for (var i = 0; i < this._items.length; i++) { + var item = this._items[i]; + if (item.isSelected()) { + return item; + } + } + return undefined; +}; + +SelectBox.prototype.getSelectedItemData = function() { + var item = this.getSelectedItem(); + if (item) { + return item.getData() || undefined; + } else { + return undefined; + } +}; + +SelectBox.prototype.selectItem = function(item){ + this.clearSelection(); + item.setSelected(true); +}; + +SelectBox.prototype.clearSelection = function(){ + $.each(this._items, function(idx, item) { + item.setSelected(false); + }); +}; + +SelectBox.prototype.setSelectHandler = function(handler) { + this._select_handler = handler; +}; + +SelectBox.prototype.getSelectHandler = function(item) { + var me = this; + var handler = this._select_handler; + return function(){ + me.selectItem(item); + handler(item.getData()); + }; +}; + +SelectBox.prototype.decorate = function(element){ + this._element = element; + var me = this; + var box_items = this._items; + var item_elements = this._element.find('.select-box-item'); + item_elements.each(function(idx, item_element){ + var item = me.createItem(); + item.decorate($(item_element)); + box_items.push(item); + setupButtonEventHandlers( + item.getElement(), + me.getSelectHandler(item) + ); + }); +}; + +/** + * This is a dropdown list elment + */ + +var GroupDropdown = function(groups){ + WrappedElement.call(this); + this._group_list = groups; + this._input_box = new TippedInput(); + this._input_box.setInstruction('group name'); + this._input_box.createDom(); + this._input_box_element = this._input_box.getElement(); + this._input_box_element.attr('class', 'group-name'); + this._input_box_element.hide(); + this._add_link = this.makeElement('a'); + this._add_link.attr('href', '#'); + this._add_link.attr('class', 'group-name'); + this._add_link.text('add new group'); +}; +inherits(GroupDropdown, WrappedElement); + +GroupDropdown.prototype.createDom = function(){ + this._element = this.makeElement('ul'); + this._element.attr('class', 'dropdown-menu'); + this._element.attr('id', 'groups-dropdown'); + this._element.attr('role', 'menu'); + this._element.attr('aria-labelledby', 'navGroups'); + + for (i=0; i<this._group_list.length; i++){ + li_element = this.makeElement('li'); + a_element = this.makeElement('a'); + a_element.text(this._group_list[i].name); + a_element.attr('href', this._group_list[i].link); + a_element.attr('class', 'group-name'); + li_element.append(a_element); + this._element.append(li_element); + } +}; + +GroupDropdown.prototype.decorate = function(element){ + this._element = element; + this._element.attr('class', 'dropdown-menu'); + this._element.attr('id', 'groups-dropdown'); + this._element.attr('role', 'menu'); + this._element.attr('aria-labelledby', 'navGroups'); + + for (i=0; i<this._group_list.length; i++){ + li_element = this.makeElement('li'); + a_element = this.makeElement('a'); + a_element.text(this._group_list[i].name); + a_element.attr('href', this._group_list[i].link); + a_element.attr('class', 'group-name'); + li_element.append(a_element); + this._element.append(li_element); + } +}; + +GroupDropdown.prototype.insertGroup = function(group_name, url){ + var new_group_li = this.makeElement('li'); + new_group_a = this.makeElement('a'); + new_group_a.attr('href', url); + new_group_a.attr('class', 'group-name'); + new_group_a.text(group_name); + new_group_li.append(new_group_a); + links_array = this._element.find('a') + for (i=1; i < links_array.length; i++){ + var value = links_array[i]; + if (value.text.toLowerCase() < group_name.toLowerCase()){ + if (i == links_array.length - 1){ + new_group_li.insertAfter(this._element.find('li')[i-1]) + break; + } else{ + continue; + } + } else { + new_group_li.insertAfter(this._element.find('li')[i-1]) + break; + } + + } +}; + +GroupDropdown.prototype._add_group_handler = function(group_name){ + var group_name = this._input_box_element.val(); + self = this; + if (!group_name){ + return; + } + + $.ajax({ + type: 'POST', + url: askbot['urls']['add_group'], + data: {group: group_name}, + success: function(data){ + if (data.success){ + self.insertGroup(data.group_name, data.url); + self._input_box_element.hide(); + self._add_link.show(); + return true; + } else{ + return false; + } + }, + error: function(){console.log('error');} + }); +}; + +GroupDropdown.prototype.enableAddGroups = function(){ + var self = this; + this._add_link.click(function(){ + self._add_link.hide(); + self._input_box_element.show(); + self._input_box_element.focus(); + }); + this._input_box_element.keydown(function(event){ + if (event.which == 13 || event.keyCode==13){ + self._add_group_handler(); + self._input_box_element.val(''); + } + }); + + var divider = this.makeElement('li'); + divider.attr('class', 'divider'); + this._element.append(divider); + + var container = this.makeElement('li'); + container.append(this._add_link); + container.append(this._input_box_element); + + this._element.append(container); +}; + +var Tag = function(){ + SimpleControl.call(this); + this._deletable = false; + this._delete_handler = null; + this._delete_icon_title = null; + this._tag_title = null; + this._name = null; + this._url_params = null; + this._inner_html_tag = 'a'; + this._html_tag = 'li'; +} +inherits(Tag, SimpleControl); + +Tag.prototype.setName = function(name){ + this._name = name; +}; + +Tag.prototype.getName = function(){ + return this._name; +}; + +Tag.prototype.setHtmlTag = function(html_tag){ + this._html_tag = html_tag; +}; + +Tag.prototype.setDeletable = function(is_deletable){ + this._deletable = is_deletable; +}; + +Tag.prototype.setLinkable = function(is_linkable){ + if (is_linkable === true){ + this._inner_html_tag = 'a'; + } else { + this._inner_html_tag = 'span'; + } +}; + +Tag.prototype.isLinkable = function(){ + return (this._inner_html_tag === 'a'); +}; + +Tag.prototype.isDeletable = function(){ + return this._deletable; +}; + +Tag.prototype.isWildcard = function(){ + return (this.getName().substr(-1) === '*'); +}; + +Tag.prototype.setUrlParams = function(url_params){ + this._url_params = url_params; +}; + +Tag.prototype.setHandlerInternal = function(){ + setupButtonEventHandlers(this._element.find('.tag'), this._handler); +}; + +/* delete handler will be specific to the task */ +Tag.prototype.setDeleteHandler = function(delete_handler){ + this._delete_handler = delete_handler; + if (this.hasElement() && this.isDeletable()){ + this._delete_icon.setHandler(delete_handler); + } +}; + +Tag.prototype.getDeleteHandler = function(){ + return this._delete_handler; +}; + +Tag.prototype.setDeleteIconTitle = function(title){ + this._delete_icon_title = title; +}; + +Tag.prototype.decorate = function(element){ + this._element = element; + var del = element.find('.delete-icon'); + if (del.length === 1){ + this.setDeletable(true); + this._delete_icon = new DeleteIcon(); + if (this._delete_icon_title != null){ + this._delete_icon.setTitle(this._delete_icon_title); + } + //do not set the delete handler here + this._delete_icon.decorate(del); + } + this._inner_element = this._element.find('.tag'); + this._name = this.decodeTagName( + $.trim(this._inner_element.attr('data-tag-name')) + ); + if (this._title !== null){ + this._inner_element.attr('title', this._title); + } + if (this._handler !== null){ + this.setHandlerInternal(); + } +}; + +Tag.prototype.getDisplayTagName = function(){ + //replaces the trailing * symbol with the unicode asterisk + return this._name.replace(/\*$/, '✽'); +}; + +Tag.prototype.decodeTagName = function(encoded_name){ + return encoded_name.replace('\u273d', '*'); +}; + +Tag.prototype.createDom = function(){ + this._element = this.makeElement(this._html_tag); + //render the outer element + if (this._deletable){ + this._element.addClass('deletable-tag'); + } + this._element.addClass('tag-left'); + + //render the inner element + this._inner_element = this.makeElement(this._inner_html_tag); + if (this.isLinkable()){ + var url = askbot['urls']['questions']; + var flag = false + var author = '' + if (this._url_params){ + url += QSutils.add_search_tag(this._url_params, this.getName()); + } + this._inner_element.attr('href', url); + } + this._inner_element.addClass('tag tag-right'); + this._inner_element.attr('rel', 'tag'); + if (this._title === null){ + this.setTitle( + interpolate(gettext("see questions tagged '%s'"), [this.getName()]) + ); + } + this._inner_element.attr('title', this._title); + this._inner_element.html(this.getDisplayTagName()); + this._inner_element.data('tagName', this.getName()); + + this._element.append(this._inner_element); + + if (!this.isLinkable() && this._handler !== null){ + this.setHandlerInternal(); + } + + if (this._deletable){ + this._delete_icon = new DeleteIcon(); + this._delete_icon.setHandler(this.getDeleteHandler()); + if (this._delete_icon_title !== null){ + this._delete_icon.setTitle(this._delete_icon_title); + } + var del_icon_elem = this._delete_icon.getElement(); + del_icon_elem.text('x'); // HACK by Tomasz + this._element.append(del_icon_elem); + } +}; + +//Search Engine Keyword Highlight with Javascript +//http://scott.yang.id.au/code/se-hilite/ +Hilite={elementid:"content",exact:true,max_nodes:1000,onload:true,style_name:"hilite",style_name_suffix:true,debug_referrer:""};Hilite.search_engines=[["local","q"],["cnprog\\.","q"],["google\\.","q"],["search\\.yahoo\\.","p"],["search\\.msn\\.","q"],["search\\.live\\.","query"],["search\\.aol\\.","userQuery"],["ask\\.com","q"],["altavista\\.","q"],["feedster\\.","q"],["search\\.lycos\\.","q"],["alltheweb\\.","q"],["technorati\\.com/search/([^\\?/]+)",1],["dogpile\\.com/info\\.dogpl/search/web/([^\\?/]+)",1,true]];Hilite.decodeReferrer=function(d){var g=null;var e=new RegExp("");for(var c=0;c<Hilite.search_engines.length;c++){var f=Hilite.search_engines[c];e.compile("^http://(www\\.)?"+f[0],"i");var b=d.match(e);if(b){var a;if(isNaN(f[1])){a=Hilite.decodeReferrerQS(d,f[1])}else{a=b[f[1]+1]}if(a){a=decodeURIComponent(a);if(f.length>2&&f[2]){a=decodeURIComponent(a)}a=a.replace(/\'|"/g,"");a=a.split(/[\s,\+\.]+/);return a}break}}return null};Hilite.decodeReferrerQS=function(f,d){var b=f.indexOf("?");var c;if(b>=0){var a=new String(f.substring(b+1));b=0;c=0;while((b>=0)&&((c=a.indexOf("=",b))>=0)){var e,g;e=a.substring(b,c);b=a.indexOf("&",c)+1;if(e==d){if(b<=0){return a.substring(c+1)}else{return a.substring(c+1,b-1)}}else{if(b<=0){return null}}}}return null};Hilite.hiliteElement=function(f,e){if(!e||f.childNodes.length==0){return}var c=new Array();for(var b=0;b<e.length;b++){e[b]=e[b].toLowerCase();if(Hilite.exact){c.push("\\b"+e[b]+"\\b")}else{c.push(e[b])}}c=new RegExp(c.join("|"),"i");var a={};for(var b=0;b<e.length;b++){if(Hilite.style_name_suffix){a[e[b]]=Hilite.style_name+(b+1)}else{a[e[b]]=Hilite.style_name}}var d=function(m){var j=c.exec(m.data);if(j){var n=j[0];var i="";var h=m.splitText(j.index);var g=h.splitText(n.length);var l=m.ownerDocument.createElement("SPAN");m.parentNode.replaceChild(l,h);l.className=a[n.toLowerCase()];l.appendChild(h);return l}else{return m}};Hilite.walkElements(f.childNodes[0],1,d)};Hilite.hilite=function(){var a=Hilite.debug_referrer?Hilite.debug_referrer:document.referrer;var b=null;a=Hilite.decodeReferrer(a);if(a&&((Hilite.elementid&&(b=document.getElementById(Hilite.elementid)))||(b=document.body))){Hilite.hiliteElement(b,a)}};Hilite.walkElements=function(d,f,e){var a=/^(script|style|textarea)/i;var c=0;while(d&&f>0){c++;if(c>=Hilite.max_nodes){var b=function(){Hilite.walkElements(d,f,e)};setTimeout(b,50);return}if(d.nodeType==1){if(!a.test(d.tagName)&&d.childNodes.length>0){d=d.childNodes[0];f++;continue}}else{if(d.nodeType==3){d=e(d)}}if(d.nextSibling){d=d.nextSibling}else{while(f>0){d=d.parentNode;f--;if(d.nextSibling){d=d.nextSibling;break}}}}};if(Hilite.onload){if(window.attachEvent){window.attachEvent("onload",Hilite.hilite)}else{if(window.addEventListener){window.addEventListener("load",Hilite.hilite,false)}else{var __onload=window.onload;window.onload=function(){Hilite.hilite();__onload()}}}}; + +if(!this.JSON){this.JSON={}}(function(){function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i<length;i+=1){partial[i]=str(i,value)||"null"}v=partial.length===0?"[]":gap?"[\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"]":"["+partial.join(",")+"]";gap=mind;return v}if(rep&&typeof rep==="object"){length=rep.length;for(i=0;i<length;i+=1){k=rep[i];if(typeof k==="string"){v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v)}}}}else{for(k in value){if(Object.hasOwnProperty.call(value,k)){v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v)}}}}v=partial.length===0?"{}":gap?"{\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"}":"{"+partial.join(",")+"}";gap=mind;return v}}if(typeof JSON.stringify!=="function"){JSON.stringify=function(value,replacer,space){var i;gap="";indent="";if(typeof space==="number"){for(i=0;i<space;i+=1){indent+=" "}}else{if(typeof space==="string"){indent=space}}rep=replacer;if(replacer&&typeof replacer!=="function"&&(typeof replacer!=="object"||typeof replacer.length!=="number")){throw new Error("JSON.stringify")}return str("",{"":value})}}if(typeof JSON.parse!=="function"){JSON.parse=function(text,reviver){var j;function walk(holder,key){var k,v,value=holder[key];if(value&&typeof value==="object"){for(k in value){if(Object.hasOwnProperty.call(value,k)){v=walk(value,k);if(v!==undefined){value[k]=v}else{delete value[k]}}}}return reviver.call(holder,key,value)}text=String(text);cx.lastIndex=0;if(cx.test(text)){text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})}if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""))){j=eval("("+text+")");return typeof reviver==="function"?walk({"":j},""):j}throw new SyntaxError("JSON.parse")}}}()); +//jquery fieldselection +(function(){var a={getSelection:function(){var b=this.jquery?this[0]:this;return(("selectionStart" in b&&function(){var c=b.selectionEnd-b.selectionStart;return{start:b.selectionStart,end:b.selectionEnd,length:c,text:b.value.substr(b.selectionStart,c)}})||(document.selection&&function(){b.focus();var d=document.selection.createRange();if(d==null){return{start:0,end:b.value.length,length:0}}var c=b.createTextRange();var e=c.duplicate();c.moveToBookmark(d.getBookmark());e.setEndPoint("EndToStart",c);return{start:e.text.length,end:e.text.length+d.text.length,length:d.text.length,text:d.text}})||function(){return{start:0,end:b.value.length,length:0}})()},replaceSelection:function(){var b=this.jquery?this[0]:this;var c=arguments[0]||"";return(("selectionStart" in b&&function(){b.value=b.value.substr(0,b.selectionStart)+c+b.value.substr(b.selectionEnd,b.value.length);return this})||(document.selection&&function(){b.focus();document.selection.createRange().text=c;return this})||function(){b.value+=c;return this})()}};jQuery.each(a,function(b){jQuery.fn[b]=this})})(); +/** + * AutoCompleter Object, refactored closure style from + * jQuery autocomplete plugin + * @param {Object=} options Settings + * @constructor + */ +var AutoCompleter = function(options) { + + /** + * Default options for autocomplete plugin + */ + var defaults = { + promptText: '', + autocompleteMultiple: true, + multipleSeparator: ' ',//a single character + inputClass: 'acInput', + loadingClass: 'acLoading', + resultsClass: 'acResults', + selectClass: 'acSelect', + queryParamName: 'q', + limitParamName: 'limit', + extraParams: {}, + lineSeparator: '\n', + cellSeparator: '|', + minChars: 2, + maxItemsToShow: 10, + delay: 400, + useCache: true, + maxCacheLength: 10, + matchSubset: true, + matchCase: false, + matchInside: true, + mustMatch: false, + preloadData: false, + selectFirst: false, + stopCharRegex: /\s+/, + selectOnly: false, + formatItem: null, // TBD + onItemSelect: false, + autoFill: false, + filterResults: true, + sortResults: true, + sortFunction: false, + onNoMatch: false + }; + + /** + * Options dictionary + * @type Object + * @private + */ + this.options = $.extend({}, defaults, options); + + /** + * Cached data + * @type Object + * @private + */ + this.cacheData_ = {}; + + /** + * Number of cached data items + * @type number + * @private + */ + this.cacheLength_ = 0; + + /** + * Class name to mark selected item + * @type string + * @private + */ + this.selectClass_ = 'jquery-autocomplete-selected-item'; + + /** + * Handler to activation timeout + * @type ?number + * @private + */ + this.keyTimeout_ = null; + + /** + * Last key pressed in the input field (store for behavior) + * @type ?number + * @private + */ + this.lastKeyPressed_ = null; + + /** + * Last value processed by the autocompleter + * @type ?string + * @private + */ + this.lastProcessedValue_ = null; + + /** + * Last value selected by the user + * @type ?string + * @private + */ + this.lastSelectedValue_ = null; + + /** + * Is this autocompleter active? + * @type boolean + * @private + */ + this.active_ = false; + + /** + * Is it OK to finish on blur? + * @type boolean + * @private + */ + this.finishOnBlur_ = true; + + this.options.minChars = parseInt(this.options.minChars, 10); + if (isNaN(this.options.minChars) || this.options.minChars < 1) { + this.options.minChars = 2; + } + + this.options.maxItemsToShow = parseInt(this.options.maxItemsToShow, 10); + if (isNaN(this.options.maxItemsToShow) || this.options.maxItemsToShow < 1) { + this.options.maxItemsToShow = 10; + } + + this.options.maxCacheLength = parseInt(this.options.maxCacheLength, 10); + if (isNaN(this.options.maxCacheLength) || this.options.maxCacheLength < 1) { + this.options.maxCacheLength = 10; + } + + if (this.options['preloadData'] === true){ + this.fetchRemoteData('', function(){}); + } +}; +inherits(AutoCompleter, WrappedElement); + +AutoCompleter.prototype.decorate = function(element){ + + /** + * Init DOM elements repository + */ + this._element = element; + + /** + * Switch off the native autocomplete + */ + this._element.attr('autocomplete', 'off'); + + /** + * Set prompt text + */ + this.setPrompt(); + + /** + * Create DOM element to hold results + */ + this._results = $('<div></div>').hide(); + if (this.options.resultsClass) { + this._results.addClass(this.options.resultsClass); + } + this._results.css({ + position: 'absolute' + }); + $('body').append(this._results); + + this.setEventHandlers(); +}; + +AutoCompleter.prototype.setPrompt = function() { + this._element.val(this.options['promptText']); + this._element.addClass('prompt'); +}; + +AutoCompleter.prototype.removePrompt = function() { + if (this._element.hasClass('prompt')) { + this._element.removeClass('prompt'); + var val = this._element.val(); + if (val === this.options['promptText']) { + this._element.val(''); + } + } +}; + +AutoCompleter.prototype.setEventHandlers = function(){ + /** + * Shortcut to self + */ + var self = this; + + /** + * Attach keyboard monitoring to $elem + */ + self._element.keydown(function(e) { + + self.removePrompt(); + + self.lastKeyPressed_ = e.keyCode; + switch(self.lastKeyPressed_) { + + case 38: // up + e.preventDefault(); + if (self.active_) { + self.focusPrev(); + } else { + self.activate(); + } + return false; + break; + + case 40: // down + e.preventDefault(); + if (self.active_) { + self.focusNext(); + } else { + self.activate(); + } + return false; + break; + + case 9: // tab + case 13: // return + if (self.active_) { + e.preventDefault(); + self.selectCurrent(); + return false; + } + break; + + case 27: // escape + if ($.trim(self._element.val()) === '') { + self._element.blur(); + return false; + } + if (self.active_) { + e.preventDefault(); + self.finish(); + return false; + } + break; + + default: + self.activate(); + + } + }); + self._element.focus(function() { + self.removePrompt(); + }); + self._element.blur(function() { + if ($.trim(self._element.val()) === '') { + self.setPrompt(); + self._results.hide(); + return true; + } + if (self.finishOnBlur_) { + setTimeout(function() { self.finish(); }, 200); + } + }); +}; + +AutoCompleter.prototype.position = function() { + var offset = this._element.offset(); + this._results.css({ + top: offset.top + this._element.outerHeight(), + left: offset.left + }); +}; + +AutoCompleter.prototype.cacheRead = function(filter) { + var filterLength, searchLength, search, maxPos, pos; + if (this.options.useCache) { + filter = String(filter); + filterLength = filter.length; + if (this.options.matchSubset) { + searchLength = 1; + } else { + searchLength = filterLength; + } + while (searchLength <= filterLength) { + if (this.options.matchInside) { + maxPos = filterLength - searchLength; + } else { + maxPos = 0; + } + pos = 0; + while (pos <= maxPos) { + search = filter.substr(0, searchLength); + if (this.cacheData_[search] !== undefined) { + return this.cacheData_[search]; + } + pos++; + } + searchLength++; + } + } + return false; +}; + +AutoCompleter.prototype.cacheWrite = function(filter, data) { + if (this.options.useCache) { + if (this.cacheLength_ >= this.options.maxCacheLength) { + this.cacheFlush(); + } + filter = String(filter); + if (this.cacheData_[filter] !== undefined) { + this.cacheLength_++; + } + return this.cacheData_[filter] = data; + } + return false; +}; + +AutoCompleter.prototype.cacheFlush = function() { + this.cacheData_ = {}; + this.cacheLength_ = 0; +}; + +AutoCompleter.prototype.callHook = function(hook, data) { + var f = this.options[hook]; + if (f && $.isFunction(f)) { + return f(data, this); + } + return false; +}; + +AutoCompleter.prototype.activate = function() { + var self = this; + var activateNow = function() { + self.activateNow(); + }; + var delay = parseInt(this.options.delay, 10); + if (isNaN(delay) || delay <= 0) { + delay = 250; + } + if (this.keyTimeout_) { + clearTimeout(this.keyTimeout_); + } + this.keyTimeout_ = setTimeout(activateNow, delay); +}; + +AutoCompleter.prototype.activateNow = function() { + var value = this.getValue(); + if (value !== this.lastProcessedValue_ && value !== this.lastSelectedValue_) { + if (value.length >= this.options.minChars) { + this.active_ = true; + this.lastProcessedValue_ = value; + this.fetchData(value); + } + } +}; + +AutoCompleter.prototype.fetchData = function(value) { + if (this.options.data) { + this.filterAndShowResults(this.options.data, value); + } else { + var self = this; + this.fetchRemoteData(value, function(remoteData) { + self.filterAndShowResults(remoteData, value); + }); + } +}; + +AutoCompleter.prototype.fetchRemoteData = function(filter, callback) { + var data = this.cacheRead(filter); + if (data) { + callback(data); + } else { + var self = this; + if (this._element){ + this._element.addClass(this.options.loadingClass); + } + var ajaxCallback = function(data) { + var parsed = false; + if (data !== false) { + parsed = self.parseRemoteData(data); + self.options.data = parsed;//cache data forever - E.F. + self.cacheWrite(filter, parsed); + } + if (self._element){ + self._element.removeClass(self.options.loadingClass); + } + callback(parsed); + }; + $.ajax({ + url: this.makeUrl(filter), + success: ajaxCallback, + error: function() { + ajaxCallback(false); + } + }); + } +}; + +AutoCompleter.prototype.setOption = function(name, value){ + this.options[name] = value; +}; + +AutoCompleter.prototype.setExtraParam = function(name, value) { + var index = $.trim(String(name)); + if (index) { + if (!this.options.extraParams) { + this.options.extraParams = {}; + } + if (this.options.extraParams[index] !== value) { + this.options.extraParams[index] = value; + this.cacheFlush(); + } + } +}; + +AutoCompleter.prototype.makeUrl = function(param) { + var self = this; + var url = this.options.url; + var params = $.extend({}, this.options.extraParams); + // If options.queryParamName === false, append query to url + // instead of using a GET parameter + if (this.options.queryParamName === false) { + url += encodeURIComponent(param); + } else { + params[this.options.queryParamName] = param; + } + + if (this.options.limitParamName && this.options.maxItemsToShow) { + params[this.options.limitParamName] = this.options.maxItemsToShow; + } + + var urlAppend = []; + $.each(params, function(index, value) { + urlAppend.push(self.makeUrlParam(index, value)); + }); + if (urlAppend.length) { + url += url.indexOf('?') == -1 ? '?' : '&'; + url += urlAppend.join('&'); + } + return url; +}; + +AutoCompleter.prototype.makeUrlParam = function(name, value) { + return String(name) + '=' + encodeURIComponent(value); +}; + +/** + * Sanitize CR and LF, then split into lines + */ +AutoCompleter.prototype.splitText = function(text) { + return String(text).replace(/(\r\n|\r|\n)/g, '\n').split(this.options.lineSeparator); +}; + +AutoCompleter.prototype.parseRemoteData = function(remoteData) { + var value, lines, i, j, data; + var results = []; + var lines = this.splitText(remoteData); + for (i = 0; i < lines.length; i++) { + var line = lines[i].split(this.options.cellSeparator); + data = []; + for (j = 0; j < line.length; j++) { + data.push(unescape(line[j])); + } + value = data.shift(); + results.push({ value: unescape(value), data: data }); + } + return results; +}; + +AutoCompleter.prototype.filterAndShowResults = function(results, filter) { + this.showResults(this.filterResults(results, filter), filter); +}; + +AutoCompleter.prototype.filterResults = function(results, filter) { + + var filtered = []; + var value, data, i, result, type, include; + var regex, pattern, testValue; + + for (i = 0; i < results.length; i++) { + result = results[i]; + type = typeof result; + if (type === 'string') { + value = result; + data = {}; + } else if ($.isArray(result)) { + value = result[0]; + data = result.slice(1); + } else if (type === 'object') { + value = result.value; + data = result.data; + } + value = String(value); + if (value > '') { + if (typeof data !== 'object') { + data = {}; + } + if (this.options.filterResults) { + pattern = String(filter); + testValue = String(value); + if (!this.options.matchCase) { + pattern = pattern.toLowerCase(); + testValue = testValue.toLowerCase(); + } + include = testValue.indexOf(pattern); + if (this.options.matchInside) { + include = include > -1; + } else { + include = include === 0; + } + } else { + include = true; + } + if (include) { + filtered.push({ value: value, data: data }); + } + } + } + + if (this.options.sortResults) { + filtered = this.sortResults(filtered, filter); + } + + if (this.options.maxItemsToShow > 0 && this.options.maxItemsToShow < filtered.length) { + filtered.length = this.options.maxItemsToShow; + } + + return filtered; + +}; + +AutoCompleter.prototype.sortResults = function(results, filter) { + var self = this; + var sortFunction = this.options.sortFunction; + if (!$.isFunction(sortFunction)) { + sortFunction = function(a, b, f) { + return self.sortValueAlpha(a, b, f); + }; + } + results.sort(function(a, b) { + return sortFunction(a, b, filter); + }); + return results; +}; + +AutoCompleter.prototype.sortValueAlpha = function(a, b, filter) { + a = String(a.value); + b = String(b.value); + if (!this.options.matchCase) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + return 0; +}; + +AutoCompleter.prototype.showResults = function(results, filter) { + var self = this; + var $ul = $('<ul></ul>'); + var i, result, $li, extraWidth, first = false, $first = false; + var numResults = results.length; + for (i = 0; i < numResults; i++) { + result = results[i]; + $li = $('<li>' + this.showResult(result.value, result.data) + '</li>'); + $li.data('value', result.value); + $li.data('data', result.data); + $li.click(function() { + var $this = $(this); + self.selectItem($this); + }).mousedown(function() { + self.finishOnBlur_ = false; + }).mouseup(function() { + self.finishOnBlur_ = true; + }); + $ul.append($li); + if (first === false) { + first = String(result.value); + $first = $li; + $li.addClass(this.options.firstItemClass); + } + if (i == numResults - 1) { + $li.addClass(this.options.lastItemClass); + } + } + + // Alway recalculate position before showing since window size or + // input element location may have changed. This fixes #14 + this.position(); + + this._results.html($ul).show(); + extraWidth = this._results.outerWidth() - this._results.width(); + this._results.width(this._element.outerWidth() - extraWidth); + $('li', this._results).hover( + function() { self.focusItem(this); }, + function() { /* void */ } + ); + if (this.autoFill(first, filter)) { + this.focusItem($first); + } +}; + +AutoCompleter.prototype.showResult = function(value, data) { + if ($.isFunction(this.options.showResult)) { + return this.options.showResult(value, data); + } else { + return value; + } +}; + +AutoCompleter.prototype.autoFill = function(value, filter) { + var lcValue, lcFilter, valueLength, filterLength; + if (this.options.autoFill && this.lastKeyPressed_ != 8) { + lcValue = String(value).toLowerCase(); + lcFilter = String(filter).toLowerCase(); + valueLength = value.length; + filterLength = filter.length; + if (lcValue.substr(0, filterLength) === lcFilter) { + this._element.val(value); + this.selectRange(filterLength, valueLength); + return true; + } + } + return false; +}; + +AutoCompleter.prototype.focusNext = function() { + this.focusMove(+1); +}; + +AutoCompleter.prototype.focusPrev = function() { + this.focusMove(-1); +}; + +AutoCompleter.prototype.focusMove = function(modifier) { + var i, $items = $('li', this._results); + modifier = parseInt(modifier, 10); + for (var i = 0; i < $items.length; i++) { + if ($($items[i]).hasClass(this.selectClass_)) { + this.focusItem(i + modifier); + return; + } + } + this.focusItem(0); +}; + +AutoCompleter.prototype.focusItem = function(item) { + var $item, $items = $('li', this._results); + if ($items.length) { + $items.removeClass(this.selectClass_).removeClass(this.options.selectClass); + if (typeof item === 'number') { + item = parseInt(item, 10); + if (item < 0) { + item = 0; + } else if (item >= $items.length) { + item = $items.length - 1; + } + $item = $($items[item]); + } else { + $item = $(item); + } + if ($item) { + $item.addClass(this.selectClass_).addClass(this.options.selectClass); + } + } +}; + +AutoCompleter.prototype.selectCurrent = function() { + var $item = $('li.' + this.selectClass_, this._results); + if ($item.length == 1) { + this.selectItem($item); + } else { + this.finish(); + } +}; + +AutoCompleter.prototype.selectItem = function($li) { + var value = $li.data('value'); + var data = $li.data('data'); + var displayValue = this.displayValue(value, data); + this.lastProcessedValue_ = displayValue; + this.lastSelectedValue_ = displayValue; + + this.setValue(displayValue); + + this.setCaret(displayValue.length); + this.callHook('onItemSelect', { value: value, data: data }); + this.finish(); +}; + +/** + * @return {boolean} true if the symbol matches something that is + * considered content and false otherwise + * @param {string} symbol - a single char string + */ +AutoCompleter.prototype.isContentChar = function(symbol){ + if (symbol.match(this.options['stopCharRegex'])){ + return false; + } else if (symbol === this.options['multipleSeparator']){ + return false; + } else { + return true; + } +}; + +/** + * takes value from the input box + * and saves _selection_start and _selection_end coordinates + * respects settings autocompleteMultiple and + * multipleSeparator + * @return {string} the current word in the + * autocompletable word + */ +AutoCompleter.prototype.getValue = function(){ + var sel = this._element.getSelection(); + var text = this._element.val(); + var pos = sel.start;//estimated start + //find real start + var start = pos; + for (cpos = pos; cpos >= 0; cpos = cpos - 1){ + if (cpos === text.length){ + continue; + } + var symbol = text.charAt(cpos); + if (!this.isContentChar(symbol)){ + break; + } + start = cpos; + } + //find real end + var end = pos; + for (cpos = pos; cpos < text.length; cpos = cpos + 1){ + if (cpos === 0){ + continue; + } + var symbol = text.charAt(cpos); + if (!this.isContentChar(symbol)){ + break; + } + end = cpos; + } + this._selection_start = start; + this._selection_end = end; + return text.substring(start, end); +} + +/** + * sets value of the input box + * by replacing the previous selection + * with the value from the autocompleter + */ +AutoCompleter.prototype.setValue = function(val){ + var prefix = this._element.val().substring(0, this._selection_start); + var postfix = this._element.val().substring(this._selection_end + 1); + this._element.val(prefix + val + postfix); +}; + +AutoCompleter.prototype.displayValue = function(value, data) { + if ($.isFunction(this.options.displayValue)) { + return this.options.displayValue(value, data); + } else { + return value; + } +}; + +AutoCompleter.prototype.finish = function() { + if (this.keyTimeout_) { + clearTimeout(this.keyTimeout_); + } + if (this._element.val() !== this.lastSelectedValue_) { + if (this.options.mustMatch) { + this._element.val(''); + } + this.callHook('onNoMatch'); + } + this._results.hide(); + this.lastKeyPressed_ = null; + this.lastProcessedValue_ = null; + if (this.active_) { + this.callHook('onFinish'); + } + this.active_ = false; +}; + +AutoCompleter.prototype.selectRange = function(start, end) { + var input = this._element.get(0); + if (input.setSelectionRange) { + input.focus(); + input.setSelectionRange(start, end); + } else if (this.createTextRange) { + var range = this.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', start); + range.select(); + } +}; + +AutoCompleter.prototype.setCaret = function(pos) { + this.selectRange(pos, pos); +}; + +(function($){function isRGBACapable(){var $script=$("script:first"),color=$script.css("color"),result=false;if(/^rgba/.test(color)){result=true}else{try{result=(color!=$script.css("color","rgba(0, 0, 0, 0.5)").css("color"));$script.css("color",color)}catch(e){}}return result}$.extend(true,$,{support:{rgba:isRGBACapable()}});var properties=["color","backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","outlineColor"];$.each(properties,function(i,property){$.fx.step[property]=function(fx){if(!fx.init){fx.begin=parseColor($(fx.elem).css(property));fx.end=parseColor(fx.end);fx.init=true}fx.elem.style[property]=calculateColor(fx.begin,fx.end,fx.pos)}});$.fx.step.borderColor=function(fx){if(!fx.init){fx.end=parseColor(fx.end)}var borders=properties.slice(2,6);$.each(borders,function(i,property){if(!fx.init){fx[property]={begin:parseColor($(fx.elem).css(property))}}fx.elem.style[property]=calculateColor(fx[property].begin,fx.end,fx.pos)});fx.init=true};function calculateColor(begin,end,pos){var color="rgb"+($.support.rgba?"a":"")+"("+parseInt((begin[0]+pos*(end[0]-begin[0])),10)+","+parseInt((begin[1]+pos*(end[1]-begin[1])),10)+","+parseInt((begin[2]+pos*(end[2]-begin[2])),10);if($.support.rgba){color+=","+(begin&&end?parseFloat(begin[3]+pos*(end[3]-begin[3])):1)}color+=")";return color}function parseColor(color){var match,triplet;if(match=/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/.exec(color)){triplet=[parseInt(match[1],16),parseInt(match[2],16),parseInt(match[3],16),1]}else{if(match=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/.exec(color)){triplet=[parseInt(match[1],16)*17,parseInt(match[2],16)*17,parseInt(match[3],16)*17,1]}else{if(match=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)){triplet=[parseInt(match[1]),parseInt(match[2]),parseInt(match[3]),1]}else{if(match=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9\.]*)\s*\)/.exec(color)){triplet=[parseInt(match[1],10),parseInt(match[2],10),parseInt(match[3],10),parseFloat(match[4])]}else{if(color=="transparent"){triplet=[0,0,0,0]}}}}}return triplet}})(jQuery); + +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 0.11.1 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2011, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org) + */ +(function($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowFuture: false, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: gettext("ago"), + suffixFromNow: gettext("from now"), + seconds: gettext("just now"), + minute: gettext("about a minute"), + minutes: gettext("%d minutes"), + hour: gettext("about an hour"), + hours: gettext("%d hours"), + day: gettext("yesterday"), + days: gettext("%d days"), + month: gettext("about a month"), + months: gettext("%d months"), + year: gettext("about a year"), + years: gettext("%d years"), + wordSeparator: " ", + numbers: [] + } + }, + inWords: function(distanceMillis) { + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator === undefined ? " " : $l.wordSeparator; + return $.trim([prefix, words, suffix].join(separator)); + }, + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d\d\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + return new Date(s); + }, + datetime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + var isTime = $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + var iso8601 = isTime ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + } + }); + + $.fn.timeago = function() { + var self = this; + self.each(refresh); + + var $s = $t.settings; + if ($s.refreshMillis > 0) { + setInterval(function() { self.each(refresh); }, $s.refreshMillis); + } + return self; + }; + + function refresh() { + var data = prepareData(this); + if (!isNaN(data.datetime)) { + $(this).text(inWords(data.datetime)); + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if (text.length > 0) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + var distanceMillis = distance(date); + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + var months = [ + gettext('Jan'), + gettext('Feb'), + gettext('Mar'), + gettext('Apr'), + gettext('May'), + gettext('Jun'), + gettext('Jul'), + gettext('Aug'), + gettext('Sep'), + gettext('Oct'), + gettext('Nov'), + gettext('Dec') + ]; + //todo: rewrite this in javascript + if (days > 2){ + var month_date = months[date.getMonth()] + ' ' + date.getDate() + if (years == 0){ + //how to do this in js??? + return month_date; + } else { + return month_date + ' ' + "'" + date.getYear() % 20; + } + } else if (days == 2) { + return gettext('2 days ago') + } else if (days == 1) { + return gettext('yesterday') + } else if (minutes >= 60) { + return interpolate( + ngettext( + '%s hour ago', + '%s hours ago', + hours + ), + [Math.floor(hours),] + ) + } else if (seconds > 90){ + return interpolate( + ngettext( + '%s min ago', + '%s mins ago', + minutes + ), + [Math.floor(minutes),] + ) + } else { + return gettext('just now') + } + } + + function distance(date) { + return (new Date() - date); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +}(jQuery)); |