mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
More work on the completion popup
This commit is contained in:
parent
193db4879e
commit
7c0d90f7b4
85
src/pyj/complete.pyj
Normal file
85
src/pyj/complete.pyj
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
from __python__ import hash_literals, bound_methods
|
||||||
|
|
||||||
|
from dom import ensure_id
|
||||||
|
from elementmaker import E
|
||||||
|
from keycodes import get_key
|
||||||
|
from popups import CompletionPopup
|
||||||
|
|
||||||
|
class EditWithComplete:
|
||||||
|
|
||||||
|
def __init__(self, name, placeholder=None, tooltip=None, parent=None, input_type='text', onenterkey=None):
|
||||||
|
inpt = E.input(type=input_type, name=name, title=tooltip or '', placeholder=placeholder or '')
|
||||||
|
self.input_id = ensure_id(inpt)
|
||||||
|
self.onenterkey = onenterkey
|
||||||
|
self.completion_popup = CompletionPopup(parent=parent)
|
||||||
|
self.ignore_next_input = False
|
||||||
|
inpt.addEventListener('keydown', self.onkeydown)
|
||||||
|
inpt.addEventListener('input', self.oninput)
|
||||||
|
self.completion_popup.add_associated_widget(inpt)
|
||||||
|
if parent:
|
||||||
|
parent.appendChild(inpt)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text_input(self):
|
||||||
|
return document.getElementById(self.input_id)
|
||||||
|
|
||||||
|
def apply_completion(self):
|
||||||
|
self.ignore_next_input = True
|
||||||
|
text = self.completion_popup.current_text
|
||||||
|
self.text_input.value = text
|
||||||
|
|
||||||
|
def onkeydown(self, event):
|
||||||
|
k = get_key(event)
|
||||||
|
if self.completion_popup.is_visible and self.completion_popup.handle_keydown(k):
|
||||||
|
event.preventDefault(), event.stopPropagation()
|
||||||
|
return
|
||||||
|
if k is 'enter':
|
||||||
|
if self.onenterkey:
|
||||||
|
event.preventDefault(), event.stopPropagation()
|
||||||
|
self.enter_pressed()
|
||||||
|
elif k is 'tab':
|
||||||
|
if self.completion_popup.is_visible:
|
||||||
|
self.apply_completion()
|
||||||
|
event.preventDefault(), event.stopPropagation()
|
||||||
|
|
||||||
|
def oninput(self, event):
|
||||||
|
if self.ignore_next_input:
|
||||||
|
self.ignore_next_input = False
|
||||||
|
else:
|
||||||
|
ti = self.text_input
|
||||||
|
self.completion_popup.set_query(ti.value or '')
|
||||||
|
self.completion_popup.popup(ti)
|
||||||
|
|
||||||
|
def hide_completion_popup(self):
|
||||||
|
self.completion_popup.hide()
|
||||||
|
|
||||||
|
def add_associated_widget(self, widget_or_id):
|
||||||
|
self.completion_popup.add_associated_widget(widget_or_id)
|
||||||
|
|
||||||
|
def set_all_items(self, items):
|
||||||
|
self.completion_popup.set_all_items(items)
|
||||||
|
|
||||||
|
|
||||||
|
def create_search_bar(action, name, tooltip=None, placeholder=None, button=None):
|
||||||
|
|
||||||
|
parent = E.div()
|
||||||
|
ewc = EditWithComplete(name, parent=parent, tooltip=tooltip, placeholder=placeholder)
|
||||||
|
|
||||||
|
def trigger():
|
||||||
|
ewc.hide_completion_popup()
|
||||||
|
action()
|
||||||
|
ewc.onenterkey = trigger
|
||||||
|
|
||||||
|
if button:
|
||||||
|
ewc.add_associated_widget(button)
|
||||||
|
button.addEventListener('click', trigger)
|
||||||
|
return parent
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def main(container):
|
||||||
|
ewc = EditWithComplete(parent=container, placeholder='Testing edit with complete')
|
||||||
|
ewc.set_all_items('a a1 a11 a12 a13 b b1 b2 b3'.split(' '))
|
||||||
|
ewc.text_input.focus()
|
@ -2,7 +2,8 @@
|
|||||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
from __python__ import hash_literals, bound_methods
|
from __python__ import hash_literals, bound_methods
|
||||||
|
|
||||||
from dom import set_css, ensure_id
|
from book_list.theme import get_color
|
||||||
|
from dom import set_css, ensure_id, clear, build_rule, add_extra_css
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
|
|
||||||
MODAL_Z_INDEX = 1000
|
MODAL_Z_INDEX = 1000
|
||||||
@ -16,26 +17,26 @@ def element_contains_click_event(element, event):
|
|||||||
r = element.getBoundingClientRect()
|
r = element.getBoundingClientRect()
|
||||||
return r.left <= event.clientX <= r.right and r.top <= event.clientY <= r.bottom
|
return r.left <= event.clientX <= r.right and r.top <= event.clientY <= r.bottom
|
||||||
|
|
||||||
def check_for_open_popups(event):
|
def click_in_popup(event):
|
||||||
if not shown_popups.length:
|
|
||||||
return False
|
|
||||||
for popup_id in shown_popups:
|
for popup_id in shown_popups:
|
||||||
popup = document.getElementById(popup_id)
|
popup = document.getElementById(popup_id)
|
||||||
if element_contains_click_event(popup, event):
|
if popup and element_contains_click_event(popup, event):
|
||||||
return False
|
return True
|
||||||
w = associated_widgets[popup_id]
|
w = associated_widgets[popup_id]
|
||||||
if w and w.length:
|
if w and w.length:
|
||||||
for wid in w:
|
for wid in w:
|
||||||
widget = document.getElementById(wid)
|
widget = document.getElementById(wid)
|
||||||
if element_contains_click_event(widget, event):
|
if widget and element_contains_click_event(widget, event):
|
||||||
return False
|
return True
|
||||||
return True
|
return False
|
||||||
|
|
||||||
def filter_clicks(event):
|
def filter_clicks(event):
|
||||||
if check_for_open_popups(event):
|
if shown_popups.length:
|
||||||
event.stopPropagation(), event.preventDefault()
|
event.stopPropagation(), event.preventDefault()
|
||||||
for popup in list(shown_popups):
|
if not click_in_popup(event):
|
||||||
hide_popup(popup.getAttribute('id'))
|
for popup_id in shown_popups:
|
||||||
|
hide_popup(popup_id)
|
||||||
|
shown_popups.clear()
|
||||||
|
|
||||||
def install_event_filters():
|
def install_event_filters():
|
||||||
window.addEventListener('click', filter_clicks, True)
|
window.addEventListener('click', filter_clicks, True)
|
||||||
@ -49,30 +50,33 @@ def create_popup(parent, idprefix):
|
|||||||
parent.appendChild(div)
|
parent.appendChild(div)
|
||||||
return pid
|
return pid
|
||||||
|
|
||||||
def show_popup(popup_id, *associated_widget_ids):
|
def show_popup(popup_id, associated_widget_ids=None):
|
||||||
elem = document.getElementById(popup_id)
|
elem = document.getElementById(popup_id)
|
||||||
elem.style.display = 'block'
|
elem.style.display = 'block'
|
||||||
shown_popups.add(popup_id)
|
shown_popups.add(popup_id)
|
||||||
associated_widgets[popup_id] = set()
|
associated_widgets[popup_id] = associated_widget_ids
|
||||||
for aid in associated_widget_ids:
|
|
||||||
associated_widgets[popup_id].add(aid)
|
|
||||||
|
|
||||||
def hide_popup(popup_id):
|
def hide_popup(popup_id):
|
||||||
elem = document.getElementById(popup_id)
|
elem = document.getElementById(popup_id)
|
||||||
elem.style.display = 'none'
|
elem.style.display = 'none'
|
||||||
shown_popups.discard(popup_id)
|
|
||||||
v'delete associated_widgets[popup_id]'
|
v'delete associated_widgets[popup_id]'
|
||||||
|
|
||||||
class CompletionPopup:
|
class CompletionPopup:
|
||||||
|
|
||||||
|
CLASS = 'popup-completion-items'
|
||||||
|
CURRENT_ITEM_CLASS = 'popup-completion-current-item'
|
||||||
|
|
||||||
def __init__(self, parent=None, max_items=25):
|
def __init__(self, parent=None, max_items=25):
|
||||||
self.max_items = max_items
|
self.max_items = max_items
|
||||||
self.container_id = create_popup(parent)
|
self.container_id = create_popup(parent)
|
||||||
self.items = []
|
self.items = []
|
||||||
|
self.matches = []
|
||||||
c = self.container
|
c = self.container
|
||||||
set_css(c, user_select='none')
|
set_css(c, user_select='none')
|
||||||
|
c.appendChild(E.div(class_=self.CLASS, onclick=self._item_clicked))
|
||||||
self.associated_widget_ids = set()
|
self.associated_widget_ids = set()
|
||||||
self.current_query, self.is_upwards = '', False
|
self.current_query, self.is_upwards = '', False
|
||||||
|
self.applied_query = self.current_query
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def container(self):
|
def container(self):
|
||||||
@ -84,21 +88,75 @@ class CompletionPopup:
|
|||||||
|
|
||||||
def set_all_items(self, items):
|
def set_all_items(self, items):
|
||||||
self.items = items
|
self.items = items
|
||||||
|
self.matches = []
|
||||||
|
self.applied_query = ''
|
||||||
|
|
||||||
def add_associated_widget(self, widget_or_id):
|
def add_associated_widget(self, widget_or_id):
|
||||||
if type(widget_or_id) is not 'string':
|
if type(widget_or_id) is not 'string':
|
||||||
widget_or_id = ensure_id(widget_or_id)
|
widget_or_id = ensure_id(widget_or_id)
|
||||||
self.associated_widget_ids.add(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):
|
def show_at_widget(self, w):
|
||||||
br = w.getBoundingClientRect()
|
br = w.getBoundingClientRect()
|
||||||
if br.top > window.innerHeight - br.bottom:
|
if br.top > window.innerHeight - br.bottom:
|
||||||
y, upwards = br.top, True
|
y, upwards = br.top, True
|
||||||
else:
|
else:
|
||||||
y, upwards = br.bottom, False
|
y, upwards = br.bottom, False
|
||||||
self.show_at(br.left, y, br.width, upwards)
|
self._show_at(br.left, y, br.width, upwards)
|
||||||
|
|
||||||
def show_at(self, x, y, 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'
|
||||||
|
|
||||||
|
def highlight_up(self):
|
||||||
|
self.move_highlight(True)
|
||||||
|
|
||||||
|
def highlight_down(self):
|
||||||
|
self.move_highlight(False)
|
||||||
|
|
||||||
|
def handle_keydown(self, key):
|
||||||
|
if key is 'escape':
|
||||||
|
self.hide()
|
||||||
|
return True
|
||||||
|
if key is 'up':
|
||||||
|
self.highlight_up()
|
||||||
|
return True
|
||||||
|
if key is 'down':
|
||||||
|
self.highlight_down()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_item(self):
|
||||||
|
c = self.container
|
||||||
|
return c.querySelector('{} > div.{}'.format(self.CLASS, self.CURRENT_ITEM_CLASS))
|
||||||
|
|
||||||
|
def move_highlight(self, up=True):
|
||||||
|
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
|
||||||
|
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
|
self.is_upwards = upwards
|
||||||
c = self.container
|
c = self.container
|
||||||
cs = c.style
|
cs = c.style
|
||||||
@ -107,4 +165,41 @@ class CompletionPopup:
|
|||||||
cs.bottom = y + 'px' if upwards else 'auto'
|
cs.bottom = y + 'px' if upwards else 'auto'
|
||||||
cs.width = width + 'px'
|
cs.width = width + 'px'
|
||||||
cs.maxHeight = ((y if upwards else window.innerHeight - y) - 10) + 'px'
|
cs.maxHeight = ((y if upwards else window.innerHeight - y) - 10) + 'px'
|
||||||
show_popup(self.container_id, *self.associated_widget_ids)
|
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))
|
||||||
|
|
||||||
|
def _item_clicked(self, ev):
|
||||||
|
self.hide()
|
||||||
|
|
||||||
|
add_extra_css(def():
|
||||||
|
sel = 'div.' + CompletionPopup.CLASS
|
||||||
|
style = build_rule(sel, background_color=get_color('window-background'),
|
||||||
|
border='solid 1px ' + get_color('window-foreground'))
|
||||||
|
sel += ' > div'
|
||||||
|
style += build_rule(sel, cursor='pointer', margin='1ex 1rem')
|
||||||
|
sel += ' > div.' + CompletionPopup.CURRENT_ITEM_CLASS
|
||||||
|
style += build_rule(sel, color=get_color('list-hover-foreground'), background_color=get_color('list-hover-background'))
|
||||||
|
return style
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user