# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2015, Kovid Goyal from __python__ import hash_literals from elementmaker import E from gettext import gettext as _ from ajax import ajax, ajax_send from book_list.theme import get_color, get_font_size from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id from popups import MODAL_Z_INDEX from utils import safe_set_inner_html from widgets import create_button from book_list.globals import get_session_data 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( '.button-box', display='flex', justify_content='flex-end', padding='1rem 0rem', overflow='hidden' ) style += build_rule('.button-box .calibre-push-button', transform_origin='right') return style ) class Modal: def __init__(self, create_func, on_close, show_close, onkeydown): nonlocal modal_count self.create_func, self.on_close, self.show_close = create_func, on_close, show_close self.onkeydown = onkeydown modal_count += 1 self.id = modal_count class ModalContainer: def __init__(self): div = E.div(id='modal-container', tabindex='0', E.div( # popup E.div(), # content area E.a(svgicon('close'), title=_('Close')) ) ) div.addEventListener('keydown', self.onkeydown.bind(self), {'passive': False}) 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, onkeydown=None): self.modals.push(Modal(create_func, on_close, show_close, onkeydown)) modal_id = self.modals[-1].id self.update() window.setTimeout(def(): self.modal_container.focus();, 0) 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 close_all_modals(self): while self.current_modal is not None: self.close_current_modal() def onkeydown(self, event): if self.current_modal is not None and self.current_modal.onkeydown: return self.current_modal.onkeydown(event, self.clear_current_modal.bind(self)) if (event.key is 'Escape' or event.key is 'Esc') and not event.altKey and not event.ctrlKey and not event.metaKey and not event.shiftKey: event.preventDefault(), event.stopPropagation() self.close_current_modal(event) def create_simple_dialog_markup(title, msg, details, icon, prefix, parent): details = details or '' show_details = E.a(class_='blue-link', style='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: safe_set_inner_html(html_container, msg) details_container = E.span() if /<[a-zA-Z]/.test(details): safe_set_inner_html(details_container, details) else: details_container.textContent = details if prefix: prefix = E.span(' ' + prefix + ' ', style='white-space:pre; font-variant: small-caps') else: prefix = '\xa0' parent.appendChild( E.div( style='max-width:40em; text-align: left', E.h2( E.span(svgicon(icon), style='color:red'), prefix, 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' ) ) ) ) def create_simple_dialog(title, msg, details, icon, prefix, on_close=None): show_modal(create_simple_dialog_markup.bind(None, title, msg, details, icon, prefix), on_close=on_close) def create_custom_dialog(title, content_generator_func, on_close=None, onkeydown=None): def create_func(parent, close_modal): content_div = E.div() content_generator_func(content_div, close_modal) parent.appendChild( E.div( style='max-width:60em; text-align: left', E.h2(title, style='font-weight: bold; font-size: ' + get_font_size('title')), E.div(content_div, style='padding-top: 1em; margin-top: 1em; border-top: 1px solid currentColor'), )) show_modal(create_func, on_close=on_close, onkeydown=onkeydown) def get_text_dialog(title, callback, initial_text=None, msg=None, rows=12): called = {} cid = unique_id() def keyaction(ok, close_modal): if called.done: return called.done = True text = document.getElementById(cid).value or '' if close_modal: close_modal() callback(ok, text) def on_keydown(event, close_modal): if event.altKey or event.ctrlKey or event.metaKey or event.shiftKey: return if event.key is 'Escape' or event.key is 'Esc': event.preventDefault(), event.stopPropagation() keyaction(False, close_modal) create_custom_dialog( title, def(parent, close_modal): parent.appendChild(E.div( E.textarea(initial_text or '', placeholder=msg or '', id=cid, rows=rows + '', style='min-width: min(40rem, 60vw)'), E.div(class_='button-box', create_button(_('OK'), 'check', keyaction.bind(None, True, close_modal), highlight=True), '\xa0', create_button(_('Cancel'), 'close', keyaction.bind(None, False, close_modal)) ), )) window.setTimeout(def(): parent.querySelector('textarea').focus();, 10) , on_close=keyaction.bind(None, False, None), onkeydown=on_keydown ) def question_dialog( title, msg, callback, yes_text=None, no_text=None, skip_dialog_name=None, skip_dialog_msg=None, skip_dialog_skipped_value=True, skip_dialog_skip_precheck=True, ): yes_text = yes_text or _('Yes') no_text = no_text or _('No') called = {} def keyaction(yes, close_modal): if called.done: return called.done = True if skip_dialog_name: if not skip_box.querySelector('input').checked: sd = get_session_data() skipped_dialogs = Object.assign(v'{}', sd.get('skipped_dialogs', v'{}')) skipped_dialogs[skip_dialog_name] = Date().toISOString() sd.set('skipped_dialogs', skipped_dialogs) if close_modal: close_modal() callback(yes) def on_keydown(event, close_modal): if event.altKey or event.ctrlKey or event.metaKey or event.shiftKey: return if event.key is 'Escape' or event.key is 'Esc': event.preventDefault(), event.stopPropagation() keyaction(False, close_modal) if event.key is 'Enter' or event.key is 'Return' or event.key is 'Space': event.preventDefault(), event.stopPropagation() keyaction(True, close_modal) skip_box = E.div(style='margin-top: 2ex;') if skip_dialog_name: sd = get_session_data() skipped_dialogs = sd.get('skipped_dialogs', v'{}') if skipped_dialogs[skip_dialog_name]: return callback(skip_dialog_skipped_value) skip_dialog_msg = skip_dialog_msg or _('Show this confirmation again') skip_box.appendChild(E.label(E.input(type='checkbox', name='skip_dialog'), '\xa0', skip_dialog_msg)) if skip_dialog_skip_precheck: skip_box.querySelector('input').checked = True else: skip_box.style.display = 'none' create_custom_dialog( title, def(parent, close_modal): parent.appendChild(E.div( E.div(msg), skip_box, E.div(class_='button-box', create_button(yes_text, 'check', keyaction.bind(None, True, close_modal), highlight=True), '\xa0', create_button(no_text, 'close', keyaction.bind(None, False, close_modal)) )) ) , on_close=keyaction.bind(None, False, None), onkeydown=on_keydown ) 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): safe_set_inner_html(msg_div, 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, onkeydown=None): return modal_container.show_modal(create_func, on_close, show_close, onkeydown) def close_all_modals(): return modal_container.close_all_modals() def error_dialog(title, msg, details=None, on_close=None): create_simple_dialog(title, msg, details, 'bug', _('Error:'), on_close) def warning_dialog(title, msg, details=None, on_close=None): create_simple_dialog(title, msg, details, 'warning', _('Warning:'), on_close) 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