'use strict'; define('forum/search', [ 'search', 'storage', 'hooks', 'alerts', 'api', 'translator', 'categoryFilter', 'userFilter', ], function (searchModule, storage, hooks, alerts, api, translator, categoryFilter, userFilter) { const Search = {}; let selectedUsers = []; let selectedTags = []; let selectedCids = []; let searchFilters = {}; Search.init = function () { const searchIn = $('#search-in'); searchIn.on('change', function () { updateFormItemVisiblity(searchIn.val()); }); const searchQuery = $('#results').attr('data-search-query'); searchModule.highlightMatches( searchQuery, $('.search-results .content p, .search-results .topic-title') ); $('#advanced-search form').off('submit').on('submit', function (e) { e.preventDefault(); searchModule.query(getSearchDataFromDOM()); return false; }); handleSavePreferences(); categoryFilterDropdown(ajaxify.data.selectedCids); userFilterDropdown($('[component="user/filter"]'), ajaxify.data.userFilterSelected); tagFilterDropdown($('[component="tag/filter"]'), ajaxify.data.tagFilterSelected); $('[component="search/filters"]').on('hidden.bs.dropdown', '.dropdown', function () { const updateFns = { replies: updateReplyCountFilter, time: updateTimeFilter, sort: updateSortFilter, tag: updateTagFilter, }; if (updateFns[$(this).attr('data-filter-name')]) { updateFns[$(this).attr('data-filter-name')](); } const searchFiltersNew = getSearchDataFromDOM(); if (JSON.stringify(searchFilters) !== JSON.stringify(searchFiltersNew)) { searchFilters = searchFiltersNew; searchModule.query(searchFilters); } }); fillOutForm(); updateTimeFilter(); updateReplyCountFilter(); updateSortFilter(); searchFilters = getSearchDataFromDOM(); }; function updateTagFilter() { const isActive = selectedTags.length > 0; let labelText = '[[search:tags]]'; if (selectedTags.length) { labelText = translator.compile( 'search:tags-x', selectedTags.map(u => u.valueEscaped).join(', ') ); } $('[component="tag/filter/button"]').toggleClass( 'active-filter', isActive ).find('.filter-label').translateText(labelText); } function updateTimeFilter() { const isActive = $('#post-time-range').val() > 0; $('#post-time-button').toggleClass( 'active-filter', isActive ).find('.filter-label').translateText( isActive ? `[[search:time-${$('#post-time-filter').val()}-than-${$('#post-time-range').val()}]]` : `[[search:time]]` ); } function updateSortFilter() { const isActive = $('#post-sort-by').val() !== 'relevance' || $('#post-sort-direction').val() !== 'desc'; $('#sort-by-button').toggleClass( 'active-filter', isActive ).find('.filter-label').translateText( isActive ? `[[search:sort-by-${$('#post-sort-by').val()}-${$('#post-sort-direction').val()}]]` : `[[search:sort]]` ); } function updateReplyCountFilter() { const isActive = $('#reply-count').val() > 0; $('#reply-count-button').toggleClass( 'active-filter', isActive ).find('.filter-label').translateText( isActive ? `[[search:replies-${$('#reply-count-filter').val()}-count, ${$('#reply-count').val()}]]` : `[[search:replies]]` ); } function getSearchDataFromDOM() { const form = $('#advanced-search'); const searchData = { in: $('#search-in').val(), }; searchData.term = $('#search-input').val(); if (searchData.in === 'posts' || searchData.in === 'titlesposts' || searchData.in === 'titles') { searchData.matchWords = form.find('#match-words-filter').val(); searchData.by = selectedUsers.length ? selectedUsers.map(u => u.username) : undefined; searchData.categories = selectedCids.length ? selectedCids : undefined; searchData.searchChildren = form.find('#search-children').is(':checked'); searchData.hasTags = selectedTags.length ? selectedTags.map(t => t.value) : undefined; searchData.replies = form.find('#reply-count').val(); searchData.repliesFilter = form.find('#reply-count-filter').val(); searchData.timeFilter = form.find('#post-time-filter').val(); searchData.timeRange = form.find('#post-time-range').val(); searchData.sortBy = form.find('#post-sort-by').val(); searchData.sortDirection = form.find('#post-sort-direction').val(); searchData.showAs = form.find('#show-results-as').val(); } hooks.fire('action:search.getSearchDataFromDOM', { form: form, data: searchData, }); return searchData; } function updateFormItemVisiblity(searchIn) { const hideTitlePostFilters = !searchIn.includes('posts') && !searchIn.includes('titles'); $('.post-search-item').toggleClass('hidden', hideTitlePostFilters); } function fillOutForm() { const params = utils.params({ disableToType: true, }); const searchData = searchModule.getSearchPreferences(); const formData = utils.merge(searchData, params); if (formData) { if (ajaxify.data.term) { $('#search-input').val(ajaxify.data.term); } formData.in = formData.in || ajaxify.data.searchDefaultIn; $('#search-in').val(formData.in); updateFormItemVisiblity(formData.in); if (formData.matchWords) { $('#match-words-filter').val(formData.matchWords); } if (formData.showAs) { $('#show-results-as').val(formData.showAs); } if (formData.by) { formData.by = Array.isArray(formData.by) ? formData.by : [formData.by]; formData.by.forEach(function (by) { $('#posted-by-user').tagsinput('add', by); }); } if (formData.categories) { $('#posted-in-categories').val(formData.categories); } if (formData.searchChildren) { $('#search-children').prop('checked', true); } if (formData.hasTags) { formData.hasTags = Array.isArray(formData.hasTags) ? formData.hasTags : [formData.hasTags]; formData.hasTags.forEach(function (tag) { $('#has-tags').tagsinput('add', tag); }); } if (formData.replies) { $('#reply-count').val(formData.replies); $('#reply-count-filter').val(formData.repliesFilter); } if (formData.timeRange) { $('#post-time-range').val(formData.timeRange); $('#post-time-filter').val(formData.timeFilter); } if (formData.sortBy || ajaxify.data.searchDefaultSortBy) { $('#post-sort-by').val(formData.sortBy || ajaxify.data.searchDefaultSortBy); } $('#post-sort-direction').val(formData.sortDirection || 'desc'); hooks.fire('action:search.fillOutForm', { form: formData, }); } } function handleSavePreferences() { $('#save-preferences').on('click', function () { const data = getSearchDataFromDOM(); const fieldsToSave = [ 'matchWords', 'in', 'showAs', 'replies', 'repliesFilter', 'timeFilter', 'timeRange', 'sortBy', 'sortDirection', ]; const saveData = {}; fieldsToSave.forEach((key) => { saveData[key] = data[key]; }); storage.setItem('search-preferences', JSON.stringify(saveData)); alerts.success('[[search:search-preferences-saved]]'); return false; }); $('#clear-preferences').on('click', async function () { storage.removeItem('search-preferences'); const html = await app.parseAndTranslate('partials/search-filters', {}); $('[component="search/filters"]').replaceWith(html); $('#search-in').val(ajaxify.data.searchDefaultIn); $('#post-sort-by').val(ajaxify.data.searchDefaultSortBy); $('#match-words-filter').val('all'); $('#show-results-as').val('posts'); // clearing dom removes all event handlers, reinitialize userFilterDropdown($('[component="user/filter"]'), []); tagFilterDropdown($('[component="tag/filter"]'), []); categoryFilterDropdown([]); alerts.success('[[search:search-preferences-cleared]]'); return false; }); } function categoryFilterDropdown(_selectedCids) { ajaxify.data.allCategoriesUrl = ''; const dropdownEl = $('[component="category/filter"]'); categoryFilter.init(dropdownEl, { selectedCids: _selectedCids, updateButton: false, // prevent categoryFilter module from updating the button onHidden: async function (data) { const isActive = data.selectedCids.length > 0 && data.selectedCids[0] !== 'all'; let labelText = '[[search:categories]]'; ajaxify.data.selectedCids = data.selectedCids; selectedCids = data.selectedCids; if (data.selectedCids.length === 1 && data.selectedCids[0] === 'watched') { ajaxify.data.selectedCategory = { cid: 'watched' }; labelText = `[[search:categories-watched-categories]]`; } else if (data.selectedCids.length === 1 && data.selectedCids[0] === 'all') { ajaxify.data.selectedCategory = null; } else if (data.selectedCids.length > 0) { const categoryData = await api.get(`/categories/${data.selectedCids[0]}`); ajaxify.data.selectedCategory = categoryData; labelText = `[[search:categories-x, ${categoryData.name}]]`; } if (data.selectedCids.length > 1) { labelText = `[[search:categories-x, ${data.selectedCids.length}]]`; } $('[component="category/filter/button"]').toggleClass( 'active-filter', isActive ).find('.filter-label').translateText(labelText); }, localCategories: [ { cid: 'watched', name: '[[category:watched-categories]]', icon: '', }, ], }); } function userFilterDropdown(el, _selectedUsers) { userFilter.init(el, { selectedUsers: _selectedUsers, template: 'partials/search-filters', onSelect: function (_selectedUsers) { selectedUsers = _selectedUsers; }, onHidden: function (_selectedUsers) { const isActive = _selectedUsers.length > 0; let labelText = '[[search:posted-by]]'; if (isActive) { labelText = translator.compile( 'search:posted-by-usernames', selectedUsers.map(u => u.username).join(', ') ); } el.find('[component="user/filter/button"]').toggleClass( 'active-filter', isActive ).find('.filter-label').translateText(labelText); }, }); } function tagFilterDropdown(el, _selectedTags) { selectedTags = _selectedTags; async function renderSelectedTags() { const html = await app.parseAndTranslate('partials/search-filters', 'tagFilterSelected', { tagFilterSelected: selectedTags, }); el.find('[component="tag/filter/selected"]').html(html); } function tagValueToObject(value) { const escapedTag = utils.escapeHTML(value); return { value: value, valueEscaped: escapedTag, valueEncoded: encodeURIComponent(escapedTag), class: escapedTag.replace(/\s/g, '-'), }; } async function doSearch() { let result = { tags: [] }; const query = el.find('[component="tag/filter/search"]').val(); if (query && query.length > 1) { if (app.user.privileges['search:tags']) { result = await socket.emit('topics.searchAndLoadTags', { query: query }); } else { result = { tags: [tagValueToObject(query)], }; } } if (!result.tags.length) { el.find('[component="tag/filter/results"]').translateHtml( '[[tags:no-tags-found]]' ); return; } result.tags = result.tags.slice(0, 20); const tagMap = {}; result.tags.forEach((tag) => { tagMap[tag.valueEscaped] = tag; }); const html = await app.parseAndTranslate('partials/search-filters', 'tagFilterResults', { tagFilterResults: result.tags, }); el.find('[component="tag/filter/results"]').html(html); el.find('[component="tag/filter/results"] [data-tag]').on('click', async function () { selectedTags.push(tagMap[$(this).attr('data-tag')]); renderSelectedTags(); }); } el.find('[component="tag/filter/search"]').on('keyup', utils.debounce(function () { if (app.user.privileges['search:tags']) { doSearch(); } }, 1000)); el.on('click', '[component="tag/filter/delete"]', function () { const deleteTag = $(this).attr('data-tag'); selectedTags = selectedTags.filter(tag => tag.valueEscaped !== deleteTag); renderSelectedTags(); }); el.find('[component="tag/filter/search"]').on('keyup', (e) => { if (e.key === 'Enter' && !app.user.privileges['search:tags']) { const value = el.find('[component="tag/filter/search"]').val(); if (value && selectedTags.every(tag => tag.value !== value)) { selectedTags.push(tagValueToObject(value)); renderSelectedTags(); } el.find('[component="tag/filter/search"]').val(''); } }); el.on('shown.bs.dropdown', function () { el.find('[component="tag/filter/search"]').trigger('focus'); }); } return Search; });