var SearchDropMenu = function() { WrappedElement.call(this); this._data = undefined; this._selectedItemIndex = 0; this._askButtonEnabled = true; } inherits(SearchDropMenu, WrappedElement); SearchDropMenu.prototype.setData = function(data) { this._data = data; }; SearchDropMenu.prototype.setAskHandler = function(handler) { this._askHandler = handler; }; SearchDropMenu.prototype.setAskButtonEnabled = function(isEnabled) { this._askButtonEnabled = isEnabled; }; /** * assumes that data is already set */ SearchDropMenu.prototype.render = function() { var list = this._resultsList; list.empty(); var me = this; $.each(this._data, function(idx, item) { var listItem = me.makeElement('li'); var link = me.makeElement('a'); link.attr('href', item['url']); link.html(item['title']); listItem.append(link); list.append(listItem); }); if (this._data.length === 0) { list.addClass('empty'); this._element.addClass('empty'); } else { list.removeClass('empty'); this._element.removeClass('empty'); } }; /** * @param {number} idx position of item starting from 1 for the topmost * Selects item inentified by position. * Scrolls the list to make top of the item visible. */ SearchDropMenu.prototype.selectItem = function(idx) { //idx is 1-based index this._selectedItemIndex = idx; var list = this._resultsList; list.find('li').removeClass('selected'); var item = this.getItem(idx); if (item && idx > 0) { item.addClass('selected'); var itemTopY = item.position().top;//relative to visible area var curScrollTop = list.scrollTop(); /* if item is clipped on top, scroll down */ if (itemTopY < 0) { list.scrollTop(curScrollTop + itemTopY); return; } var listHeight = list.outerHeight(); /* pixels above the lower border of the list */ var itemPeepHeight = listHeight - itemTopY; /* pixels below the lower border */ var itemSinkHeight = item.outerHeight() - itemPeepHeight; if (itemSinkHeight > 0) { list.scrollTop(curScrollTop + itemSinkHeight); } } }; SearchDropMenu.prototype.getItem = function(idx) { return $(this._resultsList.find('li')[idx - 1]); }; SearchDropMenu.prototype.getItemCount = function() { return this._resultsList.find('li').length; }; SearchDropMenu.prototype.getSelectedItemIndex = function() { return this._selectedItemIndex; }; SearchDropMenu.prototype.navigateToItem = function(idx) { var item = this.getItem(idx); if (item) { window.location.href = item.find('a').attr('href'); } }; SearchDropMenu.prototype.makeKeyHandler = function() { var me = this; return function(e) { var keyCode = getKeyCode(e); if (keyCode === 27) {//escape me.hide(); return false; } if (keyCode !== 38 && keyCode !== 40 && keyCode !== 13) { return; } var itemCount = me.getItemCount(); if (itemCount > 0) { //count is 0 with no title matches, curItem is 0 when none is selected var curItem = me.getSelectedItemIndex(); if (keyCode === 38) {//upArrow if (curItem > 0) { curItem = curItem - 1; } } else if (keyCode === 40) {//downArrow if (curItem < itemCount) { curItem = curItem + 1; } } else if (keyCode === 13) {//enter if (curItem === 0) { return true; } else { me.navigateToItem(curItem); return false; } } me.selectItem(curItem); return false } }; }; /** todo: change this to state management as >1 thing happens */ SearchDropMenu.prototype.showWaitIcon = function() { if (this._askButtonEnabled) { this._waitIcon.show(); this._footer.hide(); this._element.addClass('empty'); } }; SearchDropMenu.prototype.hideWaitIcon = function() { if (this._askButtonEnabled) { this._waitIcon.hide(); this._footer.show(); this._element.removeClass('empty'); } }; SearchDropMenu.prototype.hideHeader = function() { if (this._header) { this._header.hide(); } }; SearchDropMenu.prototype.showHeader = function() { if (this._header) { this._header.show(); } }; SearchDropMenu.prototype.createDom = function() { this._element = this.makeElement('div'); this._element.addClass('search-drop-menu'); this._element.hide(); if (askbot['data']['languageCode'] === 'ja') { var warning = this.makeElement('p'); this._header = warning; warning.addClass('header'); warning.html(gettext('To see search results, 2 or more characters may be required')); this._element.append(warning); } this._resultsList = this.makeElement('ul'); this._element.append(this._resultsList); this._element.addClass('empty'); var waitIcon = new WaitIcon(); waitIcon.hide(); this._element.append(waitIcon.getElement()); this._waitIcon = waitIcon; //add ask button, @todo: make into separate class? var footer = this.makeElement('div'); this._element.append(footer); this._footer = footer; if (this._askButtonEnabled) { footer.addClass('footer'); var button = this.makeElement('button'); button.addClass('submit'); button.html(gettext('Ask Your Question')) footer.append(button); var handler = this._askHandler; setupButtonEventHandlers(button, handler); } $(document).keydown(this.makeKeyHandler()); }; SearchDropMenu.prototype.isOpen = function() { return this._element.is(':visible'); }; SearchDropMenu.prototype.show = function() { var searchBar = this._element.prev(); var searchBarHeight = searchBar.outerHeight(); var topOffset = searchBar.offset().top + searchBarHeight; this._element.show();//show so that size calcs work var footerHeight = this._footer.outerHeight(); var windowHeight = $(window).height(); this._resultsList.css( 'max-height', windowHeight - topOffset - footerHeight - 40 //what is this number? ); }; SearchDropMenu.prototype.hide = function() { this._element.hide(); }; SearchDropMenu.prototype.reset = function() { this._data = undefined; this._resultsList.empty(); this._selectedItemIndex = 0; this._element.hide(); }; var TagWarningBox = function(){ WrappedElement.call(this); this._tags = []; }; inherits(TagWarningBox, WrappedElement); TagWarningBox.prototype.createDom = function(){ this._element = this.makeElement('div'); this._element.css('display', 'block'); this._element.css('margin', '0 0 13px 2px'); this._element.addClass('non-existing-tags'); this._warning = this.makeElement('p'); this._element.append(this._warning); this._tag_container = this.makeElement('ul'); this._tag_container.addClass('tags'); this._element.append(this._tag_container); this._element.append($('
')); this._element.hide(); }; TagWarningBox.prototype.clear = function(){ this._tags = []; if (this._tag_container){ this._tag_container.empty(); } this._warning.hide(); this._element.hide(); }; TagWarningBox.prototype.addTag = function(tag_name){ var tag = new Tag(); tag.setName(tag_name); tag.setLinkable(false); tag.setDeletable(false); var elem = this.getElement(); this._tag_container.append(tag.getElement()); this._tag_container.css('display', 'block'); this._tags.push(tag); elem.show(); }; TagWarningBox.prototype.showWarning = function(){ this._warning.html( ngettext( 'Sorry, this tag does not exist', 'Sorry, these tags do not exist', this._tags.length ) ); this._warning.show(); }; /** * @constructor * tool tip to be shown on top of the search input */ var InputToolTip = function() { WrappedElement.call(this); this._promptText = gettext('search or ask your question'); }; inherits(InputToolTip, WrappedElement); InputToolTip.prototype.show = function() { this._element.removeClass('dimmed'); this._element.show(); }; InputToolTip.prototype.hide = function() { this._element.removeClass('dimmed'); this._element.hide(); }; InputToolTip.prototype.dim = function() { this._element.addClass('dimmed'); }; InputToolTip.prototype.setPromptText = function(text) { this._promptText = text; }; InputToolTip.prototype.setClickHandler = function(handler) { this._clickHandler = handler; }; InputToolTip.prototype.createDom = function() { var element = this.makeElement('div'); this._element = element; element.addClass('input-tool-tip'); element.html(this._promptText); this.decorate(element); }; InputToolTip.prototype.decorate = function(element) { this._element = element; var handler = this._clickHandler; var me = this; element.click(function() { handler(); me.dim(); return false; }); $(document).click(function() { if (element.css('display') === 'block') { element.removeClass('dimmed'); } }); }; /** * @constructor * provides full text search functionality * which re-draws contents of the main page * in response to the search query */ var FullTextSearch = function() { WrappedElement.call(this); this._running = false; this._baseUrl = askbot['urls']['questions']; this._q_list_sel = 'question-list';//id of question listing div /** @todo: the questions/ needs translation... */ this._searchUrl = '/scope:all/sort:activity-desc/page:1/' this._askButtonEnabled = true; }; inherits(FullTextSearch, WrappedElement); /** * @param {{boolean=}} optional, if given then function is setter * otherwise it is a getter * isRunning returns `true` when search is running */ FullTextSearch.prototype.isRunning = function(val) { if (val === undefined) { return this._running; } else { this._running = val; } }; FullTextSearch.prototype.setAskButtonEnabled = function(isEnabled) { this._askButtonEnabled = isEnabled; } /** * @param {{string}} url for the page displaying search results */ FullTextSearch.prototype.setSearchUrl = function(url) { this._searchUrl = url; }; FullTextSearch.prototype.getSearchUrl = function() { return this._searchUrl; }; FullTextSearch.prototype.renderTagWarning = function(tag_list){ if ( !tag_list ) { return; } var tagWarningBox = this._tag_warning_box; tagWarningBox.clear(); $.each(tag_list, function(idx, tag_name){ tagWarningBox.addTag(tag_name); }); tagWarningBox.showWarning(); }; FullTextSearch.prototype.runTagSearch = function() { var search_tags = $('#ab-tag-search').val().split(/\s+/); if (search_tags.length === 0) { return; } var searchUrl = this.getSearchUrl(); //add all tags to the url searchUrl = QSutils.add_search_tag(searchUrl, search_tags); var url = this._baseUrl + searchUrl; var me = this; $.ajax({ url: url, dataType: 'json', success: function(data, text_status, xhr){ me.renderFullTextSearchResult(data, text_status, xhr); $('#ab-tag-search').val(''); }, }); this.updateHistory(url); }; FullTextSearch.prototype.updateHistory = function(url) { var context = { state:1, rand:Math.random() }; History.pushState( context, "Questions", url ); setTimeout(function(){ /* HACK: For some weird reson, sometimes * overrides the above pushState so we re-aplly it * This might be caused by some other JS plugin. * The delay of 10msec allows the other plugin to override the URL. */ History.replaceState(context, "Questions", url); }, 10 ); }; FullTextSearch.prototype.activateTagSearchInput = function() { //the autocomplete is set up in tag_selector.js var button = $('#ab-tag-search-add'); if (button.length === 0){//may be absent return; } var me = this; var ac = new AutoCompleter({ url: askbot['urls']['get_tag_list'], minChars: 1, useCache: true, matchInside: true, maxCacheLength: 100, maxItemsToShow: 20, onItemSelect: function(){ me.runTagSearch(); }, delay: 10 }); ac.decorate($('#ab-tag-search')); setupButtonEventHandlers( button, function() { me.runTagSearch() } ); }; FullTextSearch.prototype.sendTitleSearchQuery = function(query_text) { this.isRunning(true); this._prevText = query_text; var data = {query_text: query_text}; var me = this; $.ajax({ url: askbot['urls']['apiGetQuestions'], data: data, dataType: 'json', success: function(data, text_status, xhr){ me.renderTitleSearchResult(data, text_status, xhr); }, complete: function(){ me.isRunning(false); me.evalTitleSearchQuery(); }, cache: false }); }; FullTextSearch.prototype.sendFullTextSearchQuery = function(query_text) { this.isRunning(true); var searchUrl = this.getSearchUrl(); var prevText = this._prevText; if(!prevText && query_text && askbot['settings']['showSortByRelevance']) { /* If there was no query but there is some * query now - and we support relevance search * - then switch to it */ searchUrl = QSutils.patch_query_string( searchUrl, 'sort:relevance-desc' ); } this._prevText = this.updateQueryString(query_text); /* if something has changed, then reset the page no. */ searchUrl = QSutils.patch_query_string(searchUrl, 'page:1'); var url = this._baseUrl + searchUrl; var me = this; $.ajax({ url: url, dataType: 'json', success: function(data, text_status, xhr){ me.renderFullTextSearchResult(data, text_status, xhr); }, complete: function(){ me.isRunning(false); }, cache: false }); this.updateHistory(url); }; FullTextSearch.prototype.refresh = function() { this.sendFullTextSearchQuery();/* used for tag search, maybe not necessary */ }; FullTextSearch.prototype.getSearchQuery = function() { return $.trim(this._query.val()); }; /** * renders title search result in the dropdown under the search input */ FullTextSearch.prototype.renderTitleSearchResult = function(data) { var menu = this._dropMenu; menu.hideWaitIcon(); if (data.length > 0) { menu.hideHeader(); } menu.setData(data); menu.render(); menu.show(); }; FullTextSearch.prototype.renderFullTextSearchResult = function(data) { if (data['questions'].length === 0) { return; } $('#pager').toggle(data['paginator'] !== '').html(data['paginator']); $('#questionCount').html(data['question_counter']); this.renderSearchTags(data['query_data']['tags'], data['query_string']); if(data['faces'].length > 0) { $('#contrib-users > a').remove(); $('#contrib-users').append(data['faces'].join('')); } this.renderRelatedTags(data['related_tags'], data['query_string']); this.renderRelevanceSortTab(data['query_string']); this.renderTagWarning(data['non_existing_tags']); this.setActiveSortTab( data['query_data']['sort_order'], data['query_string'] ); if(data['feed_url']){ // Change RSS URL $("#ContentLeft a.rss:first").attr("href", data['feed_url']); } // Patch scope selectors var baseUrl = this._baseUrl; $('#scopeWrapper > a.scope-selector').each(function(index) { var old_qs = $(this).attr('href').replace(baseUrl, ''); var scope = QSutils.get_query_string_selector_value(old_qs, 'scope'); qs = QSutils.patch_query_string(data['query_string'], 'scope:' + scope); $(this).attr('href', baseUrl + qs); }); // Patch "Ask your question" var askButton = $('#askButton'); var askHrefBase = askButton.attr('href').split('?')[0]; askButton.attr( 'href', askHrefBase + data['query_data']['ask_query_string'] ); /* INFO: ask_query_string should already be URL-encoded! */ this._query.focus(); var old_list = $('#' + this._q_list_sel); var new_list = $('').hide().html(data['questions']); new_list.find('.timeago').timeago(); var q_list_sel = this._q_list_sel; old_list.stop(true).after(new_list).fadeOut(200, function() { //show new div with a fadeIn effect old_list.remove(); new_list.attr('id', q_list_sel); new_list.fadeIn(400); }); }; FullTextSearch.prototype.evalTitleSearchQuery = function() { var cur_query = this.getSearchQuery(); var prevText = this._prevText; if (cur_query !== prevText && this.isRunning() === false){ if (cur_query.length >= askbot['settings']['minSearchWordLength']){ this.sendTitleSearchQuery(cur_query); } else if (cur_query.length === 0){ this.reset(); } } }; FullTextSearch.prototype.reset = function() { this._prevText = ''; this._dropMenu.reset(); this._element.val(''); this._element.focus(); this._xButton.hide(); this._toolTip.show(); }; FullTextSearch.prototype.refreshXButton = function() { if(this.getSearchQuery().length > 0){ if (this._query.hasClass('searchInput')){ $('#searchBar').addClass('cancelable'); this._xButton.show(); } } else { this._xButton.hide(); $('#searchBar').removeClass('cancelable'); } }; FullTextSearch.prototype.updateQueryString = function(query_text) { if (query_text === undefined) { // handle missing parameter query_text = this.getSearchQuery(); } var newUrl = QSutils.patch_query_string( this._searchUrl, 'query:' + encodeURIComponent(query_text), query_text === '' // remove if empty ); this.setSearchUrl(newUrl); return query_text; }; FullTextSearch.prototype.renderRelatedTags = function(tags, query_string){ if (tags.length === 0) return; var html_list = []; for (var i=0; i