# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2015, Kovid Goyal from __python__ import hash_literals from ajax import ajax, ajax_send from elementmaker import E from dom import set_css, clear, build_rule, svgicon, add_extra_css 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 add_extra_css(def(): style = build_rule( '#modal-container > div > a:hover', color=get_color('dialog-foreground') + ' !important', background_color=get_color('dialog-background') + ' !important' ) style += build_rule( '#modal-container a.dialog-simple-link:hover', color='red !important' ) return style ) class Modal: def __init__(self, create_func, on_close, show_close): nonlocal modal_count self.create_func, self.on_close, self.show_close = create_func, on_close, show_close modal_count += 1 self.id = modal_count class ModalContainer: def __init__(self): div = E.div(id='modal-container', E.div( # popup E.div(), # content area E.a(svgicon('close'), title=_('Close')) ) ) document.body.appendChild(div) # 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=MODAL_Z_INDEX + '', display='none', text_align='center', user_select='none' ) # Popup style set_css(div.firstChild, position='relative', display='inline-block', top='50vh', transform='translateY(-50%)', min_width='25vw', max_width='70vw', # Needed for iPhone 5 border_radius='1em', padding='1em 2em', margin_right='1em', margin_left='1em', background=get_color('dialog-background'), color=get_color('dialog-foreground'), background_image=get_color('dialog-background-image'), get_color('dialog-background'), get_color('dialog-background2') ) # Close button style set_css(div.firstChild.lastChild, font_size='1.5em', line_height='100%', cursor='pointer', position='absolute', right='-0.5em', top='-0.5em', width='1em', height='1em', background_color=get_color('window-foreground'), color=get_color('window-background'), display='inline-box', border_radius='50%', padding='4px', text_align='center', box_shadow='1px 1px 3px black' ) div.firstChild.lastChild.addEventListener('click', def(event): event.preventDefault(), self.close_current_modal(event);) # Content container style set_css(div.firstChild.firstChild, user_select='text', max_height='60vh', overflow='auto') self.modals = v'[]' self.current_modal = None self.hide = self.close_current_modal.bind(self) @property def modal_container(self): return document.getElementById('modal-container') def show_modal(self, create_func, on_close=None, show_close=True): self.modals.push(Modal(create_func, on_close, show_close)) modal_id = self.modals[-1].id self.update() return modal_id def hide_modal(self, modal_id): if self.current_modal is not None and self.current_modal.id is modal_id: self.clear_current_modal() else: doomed_modal = None for i, modal in enumerate(self.modals): if modal.id is modal_id: doomed_modal = i break if doomed_modal is not None: self.modals.splice(doomed_modal, 1) def update(self): if self.current_modal is None and self.modals: self.current_modal = self.modals.shift() c = self.modal_container try: self.current_modal.create_func(c.firstChild.firstChild, self.hide) except: self.current_modal = None raise if c.style.display is 'none': set_css(c, display='block') c.firstChild.lastChild.style.visibility = 'visible' if self.current_modal.show_close else 'hidden' def clear_current_modal(self): self.current_modal = None c = self.modal_container clear(c.firstChild.firstChild) if self.modals.length is 0: set_css(c, display='none') else: self.update() def close_current_modal(self, event): if self.current_modal is not None: if self.current_modal.on_close is not None and self.current_modal.on_close(event) is True: return self.clear_current_modal() def create_simple_dialog(title, msg, details, icon, prefix): details = details or '' def create_func(parent): show_details = E.a(class_='dialog-simple-link', style='cursor:pointer; color: blue; padding-top:1em; display:inline-block; margin-left: auto', _('Show details')) show_details.addEventListener('click', def(): show_details.style.display = 'none' show_details.nextSibling.style.display = 'block' ) is_html_msg = /<[a-zA-Z]/.test(msg) html_container = E.div() if is_html_msg: html_container.innerHTML = msg details_container = E.span() if /<[a-zA-Z]/.test(details): details_container.innerHTML = details else: details_container.textContent = details parent.appendChild( E.div( style='max-width:60em; text-align: left', E.h2( E.span(svgicon(icon), style='color:red'), E.span('\xa0' + prefix + '\xa0', style='font-variant:small-caps'), title, style='font-weight: bold; font-size: ' + get_font_size('title') ), E.div((html_container if is_html_msg else msg), style='padding-top: 1em; margin-top: 1em; border-top: 1px solid currentColor'), E.div(style='display: ' + ('block' if details else 'none'), show_details, E.div(details_container, style='display:none; white-space:pre-wrap; font-size: smaller; margin-top: 1em; border-top: solid 1px currentColor; padding-top: 1em' ) ) ) ) show_modal(create_func) def create_progress_dialog(msg, on_close): msg = msg or _('Loading, please wait...') pbar, msg_div = E.progress(style='display:inline-block'), E.div(msg, style='text-align:center; padding-top:1ex') def create_func(parent): parent.appendChild(E.div(style='text-align: center', pbar, msg_div)) show_close = on_close is not None modal_id = show_modal(create_func, on_close, show_close) return { 'close': def(): modal_container.hide_modal(modal_id);, 'update_progress': def(amount, total): pbar.max, pbar.value = total, amount;, 'set_msg': def(new_msg): msg_div.innerHTML = new_msg;, } # def test_progress(): # counter = 0 # pd = progress_dialog('Testing progress dialog, please wait...', def(): # nonlocal counter # counter = 101 # console.log('pd canceled') # ) # def update(): # nonlocal counter # counter += 1 # pd.update_progress(counter, 100) # if counter < 100: # setTimeout(update, 150) # else: # pd.close() # update() # # def test_error(): # error_dialog( # 'Hello, world!', # 'Some long text to test rendering/line breaking in error dialogs that contain lots of text like this test error dialog from hell in a handbasket with fruits and muppets making a scene.', # ['a word ' for i in range(10000)].join(' ') # ) def create_modal_container(): nonlocal modal_container if modal_container is None: modal_container = ModalContainer() # window.setTimeout(def(): # document.getElementById('books-view-1').addEventListener('click', test_progress) # , 10) return modal_container def show_modal(create_func, on_close=None, show_close=True): return modal_container.show_modal(create_func, on_close, show_close) def error_dialog(title, msg, details=None): create_simple_dialog(title, msg, details, 'bug', _('Error:')) def warning_dialog(title, msg, details=None): create_simple_dialog(title, msg, details, 'warning', _('Warning:')) def progress_dialog(msg, on_close=None): # Show a modal dialog with a progress bar and an optional close button. # If the user clicks the close button, on_close is called and the dialog is closed. # Returns an object with the methods: close(), update_progress(amount, total), set_msg() # Call update_progress() to update the progress bar # Call set_msg() to change the displayed message # Call close() once the task being performed is finished return create_progress_dialog(msg, on_close) def ajax_progress_dialog(path, on_complete, msg, extra_data_for_callback=None, **kw): pd = None def on_complete_callback(event_type, xhr, ev): nonlocal pd pd.close() pd = undefined return on_complete(event_type, xhr, ev) def on_progress_callback(loaded, total, xhr): pd.update_progress(loaded, total) xhr = ajax(path, on_complete_callback, on_progress=on_progress_callback, **kw) xhr.extra_data_for_callback = extra_data_for_callback pd = progress_dialog(msg, xhr.abort.bind(xhr)) xhr.send() return xhr, pd def ajax_send_progress_dialog(path, data, on_complete, msg, **kw): pd = None def on_complete_callback(event_type, xhr, ev): nonlocal pd pd.close() pd = undefined return on_complete(event_type, xhr, ev) def on_progress_callback(loaded, total, xhr): pd.update_progress(loaded, total) xhr = ajax_send(path, data, on_complete_callback, on_progress=on_progress_callback, **kw) pd = progress_dialog(msg, xhr.abort.bind(xhr)) return xhr, pd