const CoreView = require('backbone/core-view'); const Utils = require('builder/helpers/utils'); const PaginationModel = require('builder/components/pagination/pagination-model'); const template = require('./paged-search.tpl'); const pagedSearchDialogWrapperTemplate = require('./paged-search-dialog-wrapper.tpl'); const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl'); const loadingView = require('builder/components/loading/render-loading'); const noResultsView = require('builder/components/no-results/render-no-results.js'); const TabPane = require('dashboard/components/tabpane/tabpane'); const ViewFactory = require('builder/components/view-factory'); const PaginationView = require('builder/components/pagination/pagination-view'); const checkAndBuildOpts = require('builder/helpers/required-opts'); const REQUIRED_OPTS = [ 'collection', 'pagedSearchModel' ]; /** * View to render a searchable/pageable collection. * Also allows to filter/search list. * Set {isUsedInDialog: true} in view opts if intended to be used in a dialog, to have proper classes to position views * properly. * * - collection is a collection which has a PagedSearchModel. */ module.exports = CoreView.extend({ events: { 'click .js-search-link': '_onSearchClick', 'click .js-clean-search': '_onCleanSearchClick', 'keydown .js-search-input': '_onKeyDown', 'submit .js-search-form': 'killEvent' }, initialize: function (options) { checkAndBuildOpts(options, REQUIRED_OPTS, this); this.options.noResults = this.options.noResults || {}; const params = this._pagedSearchModel; this.paginationModel = new PaginationModel({ current_page: params.get('page'), total_count: this._collection.totalCount() || 0, per_page: params.get('per_page') }); this._initBinds(); this._pagedSearchModel.fetch(this._collection); }, _initBinds: function () { this.listenTo(this._collection, 'fetching', function () { this._toggleCleanSearchBtn(); this._activatePane('loading'); }); this.listenTo(this._collection, 'error', function (e) { // Old requests can be stopped, so aborted requests are not // considered as an error if (!e || (e && e.statusText !== 'abort')) { this._activatePane('error'); } this._toggleCleanSearchBtn(); }); this.listenTo(this._collection, 'sync', function (collection) { this.paginationModel.set({ total_count: this._collection.totalCount(), current_page: this._pagedSearchModel.get('page') }); this._activatePane(this._collection.totalCount() > 0 ? 'list' : 'no_results'); this._toggleCleanSearchBtn(); }); this.listenTo(this.paginationModel, 'change:current_page', function (model, newPage) { this._pagedSearchModel.set('page', newPage); this._pagedSearchModel.fetch(this._collection); }); }, render: function () { this.clearSubViews(); this._renderContent( template({ thinFilters: this.options.thinFilters || false, q: this._pagedSearchModel.get('q') }) ); this._initViews(); this._$cleanSearchBtn().hide(); this._renderExtraFilters(); return this; }, _renderExtraFilters: function () { if (this.options.filtersExtrasView) { this.$('.js-filters').append(this.options.filtersExtrasView.render().el); } }, _renderContent: function (html) { if (this.options.isUsedInDialog) { html = pagedSearchDialogWrapperTemplate({ htmlToWrap: html }); } this.$el.html(html); // Needs to be called after $el html changed: if (this.options.isUsedInDialog) { this.$el.addClass('Dialog-expandedSubContent'); this._$tabPane().addClass('Dialog-bodyInnerExpandedWithSubFooter'); } }, _toggleCleanSearchBtn: function () { this._$cleanSearchBtn().toggle(!!this._pagedSearchModel.get('q')); }, _initViews: function () { this._panes = new TabPane({ el: this._$tabPane() }); this.addView(this._panes); this._panes.addTab('list', ViewFactory.createListView([ () => this._createListView(), () => new PaginationView({ className: 'CDB-Text CDB-Size-medium Pagination Pagination--shareList', model: this.paginationModel }) ]) ); this._panes.addTab('error', ViewFactory.createByHTML(errorTemplate({ msg: '' })).render() ); this._panes.addTab('no_results', ViewFactory.createByHTML(noResultsView({ icon: this.options.noResults.icon || 'CDB-IconFont-defaultUser', title: this.options.noResults.title || 'Oh! No results', msg: this.options.noResults.msg || 'Unfortunately we could not find anything with these parameters' })).render() ); this._panes.addTab('loading', ViewFactory.createByHTML(loadingView({ title: 'Searching' })).render() ); if (this._pagedSearchModel.get('q')) { this._focusSearchInput(); } this._activatePane(this._chooseActivePaneName(this._collection.totalCount())); }, _createListView: function () { var view = this.options.createListView(); if (view instanceof CoreView) { return view; } else { console.error('createListView function must return a view'); // fallback for view to not fail miserably return new CoreView(); } }, _activatePane: function (name) { // Only change active pane if the panes is actually initialized if (this._panes && this._panes.size() > 0) { // explicit render required, since tabpane doesn't do it this._panes.active(name).render(); } }, _chooseActivePaneName: function (totalCount) { if (totalCount === 0) { return 'no_results'; } else if (totalCount > 0) { return 'list'; } else { return 'loading'; } }, _focusSearchInput: function () { // also selects the current search str on the focus this._$searchInput().focus().val(this._$searchInput().val()); }, _onSearchClick: function (ev) { this.killEvent(ev); this._$searchInput().focus(); }, _onCleanSearchClick: function (ev) { this.killEvent(ev); this._cleanSearch(); }, _onKeyDown: function (ev) { var enterPressed = (ev.key === 'Enter'); var escapePressed = (ev.key === 'Escape'); if (enterPressed) { this.killEvent(ev); this._submitSearch(); } else if (escapePressed) { this.killEvent(ev); if (this._pagedSearchModel.get('q')) { this._cleanSearch(); } } }, _submitSearch: function (e) { this._makeNewSearch(Utils.stripHTML(this._$searchInput().val().trim())); }, _cleanSearch: function () { this._$searchInput().val(''); this._makeNewSearch(); }, _makeNewSearch: function (query) { this._pagedSearchModel.set({ q: query, page: 1 }); this._pagedSearchModel.fetch(this._collection); }, _$searchInput: function () { return this.$('.js-search-input'); }, _$cleanSearchBtn: function () { return this.$('.js-clean-search'); }, _$tabPane: function () { return this.$('.js-tab-pane'); } });