From 3869c4764b54ac14242fd9a9e09122c113f8dec4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 Nov 2015 15:49:38 +0530 Subject: [PATCH] Allow clicking on title bar to clear current search --- src/pyj/ajax.pyj | 27 ++++++++++++++++++--------- src/pyj/book_list/boss.pyj | 15 +++++++++++++-- src/pyj/book_list/theme.pyj | 1 + src/pyj/book_list/top_bar.pyj | 20 ++++++++++++++++++-- src/pyj/book_list/ui.pyj | 12 +++++++++++- src/pyj/book_list/views.pyj | 24 +++++++++++++++++++++++- 6 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/pyj/ajax.pyj b/src/pyj/ajax.pyj index 2701e02e95..a76c036ba9 100644 --- a/src/pyj/ajax.pyj +++ b/src/pyj/ajax.pyj @@ -3,6 +3,21 @@ from gettext import gettext as _ +def encode_query(query): + if not query: + return '' + keys = Object.keys(query) + has_query = False + path = '' + if keys.length: + for k in keys: + val = query[k] + if val is undefined or val is None: + continue + path += ('&' if has_query else '?') + encodeURIComponent(k) + '=' + encodeURIComponent(val.toString()) + has_query = True + return path + def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None, timeout=30*1000, ok_code=200): # Run an AJAX request. on_complete must be a function that accepts three # arguments: end_type, xhr, ev where end_type is one of 'abort', 'error', @@ -14,15 +29,9 @@ def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', q # Returns the xhr object, call xhr.send() to start the request. query = query or {} xhr = XMLHttpRequest() - keys = Object.keys(query) - has_query = False - if keys.length: - for k in keys: - val = query[k] - if val is undefined or val is None: - continue - path += ('&' if has_query else '?') + encodeURIComponent(k) + '=' + encodeURIComponent(val.toString()) - has_query = True + eq = encode_query(query) + has_query = eq.length > 0 + path += eq if bypass_cache: path += ('&' if has_query else '?') + Date().getTime() diff --git a/src/pyj/book_list/boss.pyj b/src/pyj/book_list/boss.pyj index 51ce2ebe93..a67100fe83 100644 --- a/src/pyj/book_list/boss.pyj +++ b/src/pyj/book_list/boss.pyj @@ -6,8 +6,9 @@ from elementmaker import E from modals import error_dialog, create_modal_container from gettext import gettext as _ from widgets import get_widget_css -from book_list.globals import get_session_data +from utils import parse_url_params +from book_list.globals import get_session_data from book_list.theme import get_color from book_list.ui import UI @@ -25,6 +26,9 @@ class Boss: document.body.appendChild(div) self.ui = UI(interface_data, div) window.onerror = self.onerror.bind(self) + setTimeout(def(): + window.onpopstate = self.onpopstate.bind(self) + , 0) # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load def update_window_title(self): document.title = 'calibre :: ' + self.current_library_name @@ -41,6 +45,13 @@ class Boss: except: console.error('There was an error in the unhandled exception handler') + def onpopstate(self, ev): + data = parse_url_params() + if not data.mode or data.mode == 'book_list': + search = data.search or '' + if search != self.ui.books_view.interface_data.search_result.query: + self.ui.books_view.change_search(search) + def change_books(self, data): data.search_result.sort = str.split(data.search_result.sort, ',')[:2].join(',') data.search_result.sort_order = str.split(data.search_result.sort_order, ',')[:2].join(',') @@ -50,4 +61,4 @@ class Boss: get_session_data().set('sort', str.rstrip(sval, ',')) self.interface_data.metadata = data.metadata self.interface_data.search_result = data.search_result - self.ui.books_view.refresh() + self.ui.refresh_books_view() diff --git a/src/pyj/book_list/theme.pyj b/src/pyj/book_list/theme.pyj index 4a0aa216ef..be377c6297 100644 --- a/src/pyj/book_list/theme.pyj +++ b/src/pyj/book_list/theme.pyj @@ -14,6 +14,7 @@ def get_color(name): # Top bar specific colors 'bar-background': DARK, 'bar-foreground': LIGHT, + 'bar-highlight': 'yellow', 'heart': '#B92111', # Item list colors diff --git a/src/pyj/book_list/top_bar.pyj b/src/pyj/book_list/top_bar.pyj index 312876aded..5760a317f3 100644 --- a/src/pyj/book_list/top_bar.pyj +++ b/src/pyj/book_list/top_bar.pyj @@ -16,6 +16,7 @@ class TopBar: def __init__(self, book_list_container): nonlocal bar_counter bar_counter += 1 + self.current_left_data = {} self.bar_id, self.dummy_bar_id = 'top-bar-' + bar_counter, 'dummy-top-bar-' + bar_counter self.throbber_name = self.bar_id + '-throbber' style = create_keyframes(self.throbber_name, 'from { transform: scale(1); } 50% { transform: scale(0.5); } to { transform: scale(1); }') @@ -28,6 +29,8 @@ class TopBar: style += build_rule(sel + ':hover', transform='scale(1.5)') style += build_rule(sel + ':active', transform='scale(2)') style += build_rule(sel + ':focus', outline='none') + style += build_rule(sel + '.top-bar-title:hover', transform='scale(1)', color=get_color('bar-highlight'), font_style='italic') + style += build_rule(sel + '.top-bar-title:active', transform='scale(1)', color=get_color('bar-highlight'), font_style='italic') for bid in self.dummy_bar_id, self.bar_id: bar = E.div( id=bid, @@ -53,18 +56,23 @@ class TopBar: return document.getElementById(self.dummy_bar_id) def set_left(self, title='calibre', icon_name='heart', action=None, tooltip='', run_animation=False): + self.current_left_data = {'title':title, 'icon_name':icon_name, 'action':action, 'tooltip':tooltip} if icon_name == 'heart': if not tooltip: tooltip = _('Donate to support calibre development') + title_action = title_tooltip = None if callable(title): - title = title() + data = title() + title, title_action, title_tooltip = data.title, data.title_action, data.title_tooltip for bar in self.bar, self.dummy_bar: left = bar.firstChild clear(left) + title_elem = 'a' if callable(title_action) else 'span' left.appendChild(E.a(title=tooltip, E.i(class_='fa fa-' + icon_name + ' fa-fw'))) - left.appendChild(E.span(title, style=str.format('margin-left: {0}; font-weight: bold; padding-top: {1}; padding-bottom: {1}; vertical-align: middle', self.SPACING, self.VSPACING))) + left.appendChild(E(title_elem, title, title=title_tooltip, class_='top-bar-title', style=str.format( + 'margin-left: {0}; font-weight: bold; padding-top: {1}; padding-bottom: {1}; vertical-align: middle', self.SPACING, self.VSPACING))) if bar is self.bar: a = left.firstChild if icon_name == 'heart': @@ -78,6 +86,14 @@ class TopBar: if action is not None: a.addEventListener('click', def(event): event.preventDefault(), action();) + if callable(title_action): + a = a.nextSibling + a.addEventListener('click', def(event): event.preventDefault(), title_action();) + + def refresh_left(self): + kw = self.current_left_data + self.set_left(**kw) + def add_button(self, icon_name=None, action=None, tooltip=''): if not icon_name: return diff --git a/src/pyj/book_list/ui.pyj b/src/pyj/book_list/ui.pyj index c29d06bf7b..9dfcabfbb0 100644 --- a/src/pyj/book_list/ui.pyj +++ b/src/pyj/book_list/ui.pyj @@ -90,7 +90,12 @@ class UI: self.search_panel = SearchPanel(interface_data, book_list_container) ibs = BarState(run_animation=True, title=def(): q = self.books_view.interface_data['search_result']['query'] - return (_('Books matching:') + ' ' + q) if q else 'calibre' + if q: + return {'title': _('Books matching') + ':: ' + q, 'title_tooltip':_('Click to clear this search'), + 'title_action':def(): + self.books_view.change_search('') + } + return {'title': 'calibre'} ) ibs.add_button(icon_name='sort-amount-desc', tooltip=_('Sort books'), action=show_panel_action('booklist-sort-menu')) ibs.add_button(icon_name='search', tooltip=_('Search for books'), action=show_panel_action('booklist-search')) @@ -125,3 +130,8 @@ class UI: def show_panel(self, state): self.states.append(state) self.apply_state(self.states[-1]) + + def refresh_books_view(self): + self.books_view.refresh() + if len(self.states) == 1: + self.top_bar.refresh_left() diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj index b2c6528f5c..d4fe9c988d 100644 --- a/src/pyj/book_list/views.pyj +++ b/src/pyj/book_list/views.pyj @@ -1,7 +1,7 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2015, Kovid Goyal -from ajax import ajax_send +from ajax import ajax_send, encode_query from dom import set_css, build_rule, clear from elementmaker import E from gettext import gettext as _ @@ -231,6 +231,28 @@ class BooksView: elif end_type != 'abort': error_dialog(_('Could not change sort order'), xhr.error_html) + def change_search(self, query): + query = query or '' + sd = get_session_data() + data = {'search':query, 'sort':sd.get('sort'), 'library_id':self.interface_data.library_id} + ajax_progress_dialog('interface-data/get-books', self.search_change_completed.bind(self), _( + 'Fetching data from server, please wait') + '…', query=data) + + def search_change_completed(self, end_type, xhr, ev): + if end_type == 'load': + boss = get_boss() + try: + data = JSON.parse(xhr.responseText) + boss.change_books(data) + except Exception as err: + return error_dialog(_('Could not change search query'), err + '', details=err.stack) + query = {'search':self.interface_data.search_result.query, 'library_id':self.interface_data.library_id} + window.history.pushState(None, '', encode_query(query)) + boss.ui.close_panel() + window.scrollTo(0, 0) + elif end_type != 'abort': + error_dialog(_('Could not change search query'), xhr.error_html) + def refresh(self): self.clear() self.render_ids()