# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal from __python__ import hash_literals, bound_methods from book_list.theme import get_color from dom import set_css, ensure_id, clear, build_rule, add_extra_css 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 click_in_popup(event): for popup_id in shown_popups: popup = document.getElementById(popup_id) if popup and element_contains_click_event(popup, event): return True w = associated_widgets[popup_id] if w and w.length: for wid in w: widget = document.getElementById(wid) if widget and element_contains_click_event(widget, event): return True return False def filter_clicks(event): if shown_popups.length: if not click_in_popup(event): for popup_id in shown_popups: hide_popup(popup_id) shown_popups.clear() event.stopPropagation(), event.preventDefault() 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=None): elem = document.getElementById(popup_id) elem.style.display = 'block' shown_popups.add(popup_id) associated_widgets[popup_id] = associated_widget_ids def hide_popup(popup_id): elem = document.getElementById(popup_id) elem.style.display = 'none' v'delete associated_widgets[popup_id]' class CompletionPopup: CLASS = 'popup-completion-items' CURRENT_ITEM_CLASS = 'popup-completion-current-item' def __init__(self, parent=None, max_items=25, onselect=None): self.max_items = max_items self.container_id = create_popup(parent) self.onselect = onselect self.items = [] self.matches = [] c = self.container set_css(c, user_select='none') c.appendChild(E.div(class_=self.CLASS)) self.associated_widget_ids = set() self.current_query, self.is_upwards = '', False self.applied_query = self.current_query @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 self.matches = [] self.applied_query = '' 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 popup(self, widget): if not self.is_visible: if self.applied_query is not self.current_query: self._apply_query() if self.matches.length: self.show_at_widget(widget) 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 set_query(self, query): self.current_query = query if self.is_visible and self.applied_query is not self.current_query: self._apply_query() if not self.matches.length: self.hide() def hide(self): self.container.style.display = 'none' c = self.current_item if c: c.classList.remove(self.CURRENT_ITEM_CLASS) def handle_keydown(self, key): if key is 'escape': self.hide() return True if key is 'up': self.move_highlight(True) return True if key is 'down': self.move_highlight(False) return True return False @property def current_item(self): c = self.container return c.querySelector('div.{} > div.{}'.format(self.CLASS, self.CURRENT_ITEM_CLASS)) @property def current_text(self): return self.current_item?.textContent def move_highlight(self, up=None): if up is None: up = self.is_upwards ans = None div = self.current_item if div: div.classList.remove(self.CURRENT_ITEM_CLASS) ans = div.previousSibling if up else div.nextSibling if not ans: c = self.container.firstChild ans = c.lastChild if up else c.firstChild if ans: ans.classList.add(self.CURRENT_ITEM_CLASS) 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' cs.maxHeight = ((y if upwards else window.innerHeight - y) - 10) + 'px' show_popup(self.container_id, self.associated_widget_ids) def _apply_query(self): q = self.current_query.toLowerCase() self.matches.clear() self.applied_query = self.current_query if not q: self.matches = list(self.items[:self.max_items + 1]) else: i = 0 while self.matches.length < self.max_items and i < self.items.length: if self.items[i].toLowerCase().startswith(q): self.matches.push(self.items[i]) i += 1 self._render_matches() def _render_matches(self): c = self.container clear(c.firstChild) items = self.matches if self.is_upwards: items = reversed(items) for m in items: c.firstChild.appendChild(E.div(m, onmouseenter=self.onmouseenter, onclick=self.onclick)) def onmouseenter(self, event): div = self.current_item if div: div.classList.remove(self.CURRENT_ITEM_CLASS) event.currentTarget.classList.add(self.CURRENT_ITEM_CLASS) def onclick(self, event): self.onmouseenter(event) try: if self.onselect: self.onselect(self.current_text) finally: self.hide() add_extra_css(def(): sel = 'div.' + CompletionPopup.CLASS style = build_rule(sel, overflow='hidden', background_color=get_color('window-background'), border='solid 1px ' + get_color('window-foreground')) sel += ' > div' style += build_rule(sel, cursor='pointer', padding='1ex 1rem', white_space='nowrap', text_overflow='ellipsis', overflow='hidden') sel += '.' + CompletionPopup.CURRENT_ITEM_CLASS style += build_rule(sel, color=get_color('list-hover-foreground'), background_color=get_color('list-hover-background')) return style )