calibre/src/pyj/popups.pyj
2016-07-28 10:43:36 +05:30

220 lines
7.2 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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
)