From bdaa8088868eb5f11177d0347aa84c4a54a2839f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Jun 2016 09:08:54 +0530 Subject: [PATCH] Start work on completion popups --- src/pyj/book_list/ui.pyj | 2 + src/pyj/dom.pyj | 11 ++++ src/pyj/modals.pyj | 3 +- src/pyj/popups.pyj | 109 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/pyj/popups.pyj diff --git a/src/pyj/book_list/ui.pyj b/src/pyj/book_list/ui.pyj index 1da234140c..df10597b31 100644 --- a/src/pyj/book_list/ui.pyj +++ b/src/pyj/book_list/ui.pyj @@ -12,6 +12,7 @@ from book_list.prefs import PrefsPanel from book_list.book_details import BookDetailsPanel from gettext import gettext as _ from modals import error_dialog, ajax_progress_dialog +from popups import install_event_filters class BarState: @@ -88,6 +89,7 @@ class UI: ROOT_PANEL = 'books' def __init__(self, interface_data, book_list_container): + install_event_filters() self.top_bar = TopBar(book_list_container) self.books_view = BooksView(interface_data, book_list_container) self.items_view = ItemsView(interface_data, book_list_container) diff --git a/src/pyj/dom.pyj b/src/pyj/dom.pyj index 3337887425..903f7795dd 100644 --- a/src/pyj/dom.pyj +++ b/src/pyj/dom.pyj @@ -91,3 +91,14 @@ def element(elem_id, child_selector): if child_selector: ans = ans.querySelector(child_selector) return ans + +auto_id_count = 0 + +def ensure_id(w): + nonlocal auto_id_count + ans = w.getAttribute('id') + if not ans: + auto_id_count += 1 + ans = 'auto-id-' + auto_id_count + w.setAttribute('id', ans) + return ans diff --git a/src/pyj/modals.pyj b/src/pyj/modals.pyj index d53f4fde7f..41017b0b1b 100644 --- a/src/pyj/modals.pyj +++ b/src/pyj/modals.pyj @@ -7,6 +7,7 @@ from elementmaker import E from dom import set_css, clear, build_rule, svgicon from gettext import gettext as _ from book_list.theme import get_color, get_font_size +from popups import MODAL_Z_INDEX modal_container = None modal_count = 0 @@ -43,7 +44,7 @@ class ModalContainer: # Container style set_css(div, position='fixed', top='0', right='0', bottom='0', left='0', # Stretch over entire window - background_color='rgba(0,0,0,0.8)', z_index='1000', + background_color='rgba(0,0,0,0.8)', z_index=MODAL_Z_INDEX + '', display='none', text_align='center', user_select='none' ) diff --git a/src/pyj/popups.pyj b/src/pyj/popups.pyj new file mode 100644 index 0000000000..c63610130d --- /dev/null +++ b/src/pyj/popups.pyj @@ -0,0 +1,109 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal +from __python__ import hash_literals, bound_methods + +from dom import set_css, ensure_id +from elementmaker import E + +MODAL_Z_INDEX = 1000 +POPUP_Z_INDEX = MODAL_Z_INDEX + 1 +popup_count = 0 + +shown_popups = set() +associated_widgets = {} + +def element_contains_click_event(element, event): + r = element.getBoundingClientRect() + return r.left <= event.clientX <= r.right and r.top <= event.clientY <= r.bottom + +def check_for_open_popups(event): + if not shown_popups.length: + return False + for popup_id in shown_popups: + popup = document.getElementById(popup_id) + if element_contains_click_event(popup, event): + return False + w = associated_widgets[popup_id] + if w and w.length: + for wid in w: + widget = document.getElementById(wid) + if element_contains_click_event(widget, event): + return False + return True + +def filter_clicks(event): + if check_for_open_popups(event): + event.stopPropagation(), event.preventDefault() + for popup in list(shown_popups): + hide_popup(popup.getAttribute('id')) + +def install_event_filters(): + window.addEventListener('click', filter_clicks, True) + +def create_popup(parent, idprefix): + nonlocal popup_count + popup_count += 1 + pid = (idprefix or 'popup') + '-' + popup_count + div = E.div(id=pid, style='display: none; position: absolute; z-index: {}'.format(POPUP_Z_INDEX)) + parent = parent or document.body + parent.appendChild(div) + return pid + +def show_popup(popup_id, *associated_widget_ids): + elem = document.getElementById(popup_id) + elem.style.display = 'block' + shown_popups.add(popup_id) + associated_widgets[popup_id] = set() + for aid in associated_widget_ids: + associated_widgets[popup_id].add(aid) + +def hide_popup(popup_id): + elem = document.getElementById(popup_id) + elem.style.display = 'none' + shown_popups.discard(popup_id) + v'delete associated_widgets[popup_id]' + +class CompletionPopup: + + def __init__(self, parent=None, max_items=25): + self.max_items = max_items + self.container_id = create_popup(parent) + self.items = [] + c = self.container + set_css(c, user_select='none') + self.associated_widget_ids = set() + self.current_query, self.is_upwards = '', False + + @property + def container(self): + return document.getElementById(self.container_id) + + @property + def is_visible(self): + return self.container.style.display is not 'none' + + def set_all_items(self, items): + self.items = items + + def add_associated_widget(self, widget_or_id): + if type(widget_or_id) is not 'string': + widget_or_id = ensure_id(widget_or_id) + self.associated_widget_ids.add(widget_or_id) + + def show_at_widget(self, w): + br = w.getBoundingClientRect() + if br.top > window.innerHeight - br.bottom: + y, upwards = br.top, True + else: + y, upwards = br.bottom, False + self.show_at(br.left, y, br.width, upwards) + + def show_at(self, x, y, width, upwards): + self.is_upwards = upwards + c = self.container + cs = c.style + cs.left = x + 'px' + cs.top = 'auto' if upwards else y + 'px' + cs.bottom = y + 'px' if upwards else 'auto' + cs.width = width + 'px' + show_popup(self.container_id, *self.associated_widget_ids)