calibre/src/pyj/book_list/comments_editor.pyj

504 lines
18 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals
from elementmaker import E
from gettext import gettext as _
from book_list.theme import get_color
from dom import add_extra_css, build_rule, clear, ensure_id, svgicon
from iframe_comm import IframeClient, IframeWrapper
from modals import create_custom_dialog
from utils import html_escape
from widgets import create_button
CLASS_NAME = 'comments-editor'
TOOLBAR_CLASS = 'comments-editor-toolbar'
add_extra_css(def():
sel = '.' + TOOLBAR_CLASS + ' '
style = ''
style += build_rule(sel, display='flex', flex_wrap='wrap', padding_top='0.25ex', padding_bottom='0.25ex')
sel += ' > div'
style += build_rule(sel, padding='0.5ex', margin_right='0.5ex', cursor='pointer')
style += build_rule(sel + ':hover', color='red')
style += build_rule(sel + '.activated', color=get_color('window-background'), background=get_color('window-foreground'))
sel += '.sep'
style += build_rule(sel, border_left='solid 2px currentColor', cursor='auto')
style += build_rule(sel + ':hover', color='currentColor')
return style
)
def choose_block_style(proceed):
create_custom_dialog(_('Choose style'), def(parent, close_modal):
def action(which):
close_modal()
proceed(which)
def s(text, tag):
ans = create_button(text, action=action.bind(None, tag))
ans.style.marginBottom = '1ex'
return ans
spc = '\xa0'
parent.appendChild(E.div(
E.div(_('Choose the block style you would like to use')),
E.div(class_='button-box', style='flex-wrap: wrap',
s(_('Normal'), 'div'), spc, s('H1', 'h1'), spc, s('H2', 'h2'), spc, s('H3', 'h3'), spc, s('H4', 'h4'), spc, s('H5', 'h5'), spc, s('H6', 'h6'), spc, s(_('Quote'), 'blockquote')
)
))
)
def insert_heading(editor_id, cmd):
editor = registry[editor_id]
if cmd:
editor.exec_command('formatBlock', f'<{cmd}>')
editor.focus()
def insert_link(title, msg, proceed):
create_custom_dialog(title, def(parent, close_modal):
def action(ok, evt):
close_modal()
c = evt.currentTarget.closest('.il-modal-container')
url = c.querySelector('input[name="url"]').value
name = c.querySelector('input[name="name"]').value
proceed(ok, url, name)
parent.appendChild(E.div(class_='il-modal-container',
E.div(msg),
E.table(style='width: 95%; margin-top: 1ex',
E.tr(E.td('URL:'), E.td(style='padding-bottom: 1ex', E.input(type='url', name='url', style='width: 100%'))),
E.tr(E.td(_('Name (optional):')), E.td(E.input(type='text', name='name', style='width: 100%'))),
),
E.div(class_='button-box',
create_button(_('OK'), action=action.bind(None, True), highlight=True),
'\xa0',
create_button(_('Cancel'), action=action.bind(None, False), highlight=False),
),
))
)
def insert_link_or_image(editor_id, is_image, ok, url, title):
editor = registry[editor_id]
if ok:
if title:
markup = '<img src="{}" title="{}"></img>' if is_image else '<a href="{}">{}</a>'
editor.exec_command('insertHTML', markup.format(html_escape(url), html_escape(title)))
else:
cmd = 'insertImage' if is_image else 'createLink'
editor.exec_command(cmd, url)
editor.focus()
def set_color(title, msg, proceed):
create_custom_dialog(title, def(parent, close_modal):
def action(ok, evt):
close_modal()
c = evt.currentTarget.closest('.sc-modal-container')
color = c.querySelector('input[name="color"]').value
proceed(ok, color)
parent.appendChild(E.div(class_='sc-modal-container',
E.div(msg),
E.div(E.input(type='color', name='color')),
E.div(class_='button-box',
create_button(_('OK'), action=action.bind(None, True), highlight=True),
'\xa0',
create_button(_('Cancel'), action=action.bind(None, False), highlight=False),
),
))
)
def change_color(editor_id, which, ok, color):
editor = registry[editor_id]
if ok:
editor.exec_command(which, color)
editor.focus()
def all_editor_actions(): # {{{
if not all_editor_actions.ans:
all_editor_actions.ans = {
'select-all': {
'icon': 'select-all',
'title': _('Select all'),
'execute': def (editor, activated):
editor.exec_command('selectAll')
},
'remove-format': {
'icon': 'eraser',
'title': _('Remove formatting'),
'execute': def (editor, activated):
editor.exec_command('removeFormat')
},
'undo': {
'icon': 'undo',
'title': _('Undo'),
'execute': def (editor, activated):
editor.exec_command('undo')
},
'redo': {
'icon': 'redo',
'title': _('Redo'),
'execute': def (editor, activated):
editor.exec_command('redo')
},
'bold': {
'icon': 'bold',
'title': _('Bold'),
'execute': def (editor, activated):
editor.exec_command('bold')
},
'italic': {
'icon': 'italic',
'title': _('Italic'),
'execute': def (editor, activated):
editor.exec_command('italic')
},
'underline': {
'icon': 'underline',
'title': _('Underline'),
'execute': def (editor, activated):
editor.exec_command('underline')
},
'strikethrough': {
'icon': 'strikethrough',
'title': _('Strikethrough'),
'execute': def (editor, activated):
editor.exec_command('strikeThrough')
},
'superscript': {
'icon': 'superscript',
'title': _('Superscript'),
'execute': def (editor, activated):
editor.exec_command('superscript')
},
'subscript': {
'icon': 'subscript',
'title': _('Subscript'),
'execute': def (editor, activated):
editor.exec_command('subscript')
},
'hr': {
'icon': 'hr',
'title': _('Insert separator'),
'execute': def (editor, activated):
editor.exec_command('insertHorizontalRule')
},
'format-block': {
'icon': 'heading',
'title': _('Style the selected text block'),
'execute': def (editor, activated):
choose_block_style(insert_heading.bind(None, editor.id))
},
'ul': {
'icon': 'ul',
'title': _('Unordered list'),
'execute': def (editor, activated):
editor.exec_command('insertUnorderedList')
},
'ol': {
'icon': 'ol',
'title': _('Ordered list'),
'execute': def (editor, activated):
editor.exec_command('insertOrderedList')
},
'indent': {
'icon': 'indent',
'title': _('Increase indentation'),
'execute': def (editor, activated):
editor.exec_command('indent')
},
'outdent': {
'icon': 'outdent',
'title': _('Decrease indentation'),
'execute': def (editor, activated):
editor.exec_command('outdent')
},
'justify-left': {
'icon': 'justify-left',
'title': _('Align left'),
'execute': def (editor, activated):
editor.exec_command('justifyLeft')
},
'justify-full': {
'icon': 'justify-full',
'title': _('Align justified'),
'execute': def (editor, activated):
editor.exec_command('justifyFull')
},
'justify-center': {
'icon': 'justify-center',
'title': _('Align center'),
'execute': def (editor, activated):
editor.exec_command('justifyCenter')
},
'justify-right': {
'icon': 'justify-right',
'title': _('Align right'),
'execute': def (editor, activated):
editor.exec_command('justifyRight')
},
'insert-link': {
'icon': 'insert-link',
'title': _('Insert a link or linked image'),
'execute': def (editor, activated):
insert_link(_('Insert a link'), _('Enter the link URL and optionally the link name'), insert_link_or_image.bind(None, editor.id, False))
},
'insert-image': {
'icon': 'image',
'title': _('Insert an image'),
'execute': def (editor, activated):
insert_link(_('Insert an image'), _('Enter the image URL and optionally the image name'), insert_link_or_image.bind(None, editor.id, True))
},
'fg': {
'icon': 'fg',
'title': _('Change the text color'),
'execute': def (editor, activated):
set_color(_('Choose text color'), _('Choose the color below'), change_color.bind(None, editor.id, 'foreColor'))
},
'bg': {
'icon': 'bg',
'title': _('Change the highlight color'),
'execute': def (editor, activated):
set_color(_('Choose highlight color'), _('Choose the color below'), change_color.bind(None, editor.id, 'hiliteColor'))
},
}
return all_editor_actions.ans
# }}}
class CommentsEditorBoss:
def __init__(self):
handlers = {
'initialize': self.initialize,
'set_html': self.set_html,
'get_html': self.get_html,
'exec_command': self.exec_command,
'focus': self.focus,
}
self.comm = IframeClient(handlers)
def initialize(self, data):
window.onerror = self.onerror
clear(document.body)
document.execCommand("defaultParagraphSeparator", False, "div")
document.execCommand("styleWithCSS", False, False)
document.body.style.margin = '0'
document.body.style.padding = '0'
document.documentElement.style.height = document.body.style.height = '100%'
document.documentElement.style.overflow = document.body.style.overflow = 'hidden'
document.body.style.fontFamily = window.default_font_family
document.body.appendChild(E.div(style='width: 100%; height: 100%; padding: 0; margin: 0; border: solid 3px transparent; box-sizing: border-box'))
document.body.lastChild.contentEditable = True
document.body.lastChild.addEventListener('keyup', self.update_state)
document.body.lastChild.addEventListener('mouseup', self.update_state)
document.body.lastChild.focus()
self.update_state()
def focus(self):
document.body.lastChild.focus()
def onerror(self, msg, script_url, line_number, column_number, error_object):
if error_object is None:
# This happens for cross-domain errors (probably javascript injected
# into the browser via extensions/ userscripts and the like). It also
# happens all the time when using Chrome on Safari
console.log(f'Unhandled error from external javascript, ignoring: {msg} {script_url} {line_number}')
else:
console.log(error_object)
def set_html(self, data):
document.body.lastChild.innerHTML = data.html
def get_html(self, data):
self.comm.send_message('html', html=document.body.lastChild.innerHTML)
def exec_command(self, data):
document.execCommand(data.name, False, data.value)
self.update_state()
def update_state(self):
state = {name: document.queryCommandState(name) for name in 'bold italic underline superscript subscript'.split(' ')}
state.strikethrough = document.queryCommandState('strikeThrough')
self.comm.send_message('update_state', state=state)
registry = {}
def add_editor(editor):
for k in Object.keys(registry):
if not document.getElementById(k):
registry[k].destroy()
v'delete registry[k]'
registry[editor.id] = editor
class Editor:
def __init__(self, iframe):
handlers = {
'ready': self.on_iframe_ready,
'html': self.on_html_received,
'update_state': self.update_state,
}
self.iframe_wrapper = IframeWrapper(handlers, iframe, 'book_list.comments_editor', _('Loading comments editor...'))
self.id = ensure_id(iframe)
self.ready = False
self.pending_set_html = None
self.get_html_callbacks = v'[]'
def init(self):
self.iframe_wrapper.init()
def destroy(self):
self.iframe_wrapper.destroy()
self.get_html_callbacks = v'[]'
def focus(self):
self.iframe.contentWindow.focus()
if self.ready:
self.iframe_wrapper.send_message('focus')
@property
def iframe(self):
return self.iframe_wrapper.iframe
def on_iframe_ready(self, msg):
self.ready = True
return self.after_iframe_initialized
def after_iframe_initialized(self):
if self.pending_set_html is not None:
self.set_html(self.pending_set_html)
self.pending_set_html = None
if self.get_html_callbacks.length:
self.get_html(self.get_html_callback)
def set_html(self, html):
if not self.ready:
self.pending_set_html = html
return
self.iframe_wrapper.send_message('set_html', html=html)
def get_html(self, proceed):
self.get_html_callbacks.push(proceed)
if self.ready:
self.iframe_wrapper.send_message('get_html')
def on_html_received(self, data):
if self.get_html_callbacks.length:
for f in self.get_html_callbacks:
f(data.html)
self.get_html_callbacks = v'[]'
def exec_command(self, name, value=None):
if self.ready:
self.iframe_wrapper.send_message('exec_command', name=name, value=value)
def update_state(self, data):
c = self.iframe.closest('.' + CLASS_NAME)
for name in Object.keys(data.state):
div = c.querySelector(f'.{TOOLBAR_CLASS} > [name="{name}"]')
if div:
if data.state[name]:
div.classList.add('activated')
else:
div.classList.remove('activated')
def create_editor():
iframe = E.iframe(sandbox='allow-scripts', seamless=True, style='flex-grow: 10', id=self.id)
editor = Editor(iframe)
add_editor(editor)
return iframe, editor
def action_activated(editor_id, ac_name, evt):
editor = registry[editor_id]
if not editor:
return
action = all_editor_actions()[ac_name]
if not action:
return
button = evt.currentTarget
action.execute(editor, button.classList.contains('activated'))
editor.focus()
def add_action(toolbar, ac_name, action, editor_id):
b = E.div(svgicon(action.icon), title=action.title, onclick=action_activated.bind(None, editor_id, ac_name), name=ac_name)
toolbar.appendChild(b)
def create_comments_editor(container):
iframe, editor = create_editor()
toolbars = E.div(style='flex-grow: 0')
toolbar1 = E.div(class_=TOOLBAR_CLASS)
toolbar2 = E.div(class_=TOOLBAR_CLASS)
toolbars.appendChild(toolbar1), toolbars.appendChild(toolbar2)
acmap = all_editor_actions()
def add(toolbar, ac_name):
if ac_name:
if acmap[ac_name]:
add_action(toolbar, ac_name, acmap[ac_name], editor.id)
else:
toolbar.appendChild(E.div(class_='sep'))
for ac_name in 'undo redo select-all remove-format | bold italic underline strikethrough | hr superscript subscript format-block'.split(' '):
add(toolbar1, ac_name)
for ac_name in 'ul ol indent outdent | justify-left justify-center justify-right justify-full | insert-link insert-image fg bg'.split(' '):
add(toolbar2, ac_name)
container.setAttribute('style', (container.getAttribute('style') or '') + ';display: flex; flex-direction: column; align-items: stretch')
container.appendChild(toolbars)
container.appendChild(iframe)
container.classList.add(CLASS_NAME)
return editor
def focus_comments_editor(container):
iframe = container.querySelector('iframe')
editor = registry[iframe.getAttribute('id')]
if editor:
editor.focus()
def set_comments_html(container, html):
iframe = container.querySelector('iframe')
eid = iframe.getAttribute('id')
editor = registry[eid]
editor.set_html(html or '')
def get_comments_html(container, proceed):
iframe = container.querySelector('iframe')
eid = iframe.getAttribute('id')
editor = registry[eid]
editor.get_html(proceed)
def develop(container):
container.setAttribute('style', 'width: 100%; min-height: 90vh; display: flex; flex-direction: column; align-items: stretch')
editor = create_comments_editor(container)
set_comments_html(container, '<p>Testing, <i>testing</i> 123...')
focus_comments_editor(container)
editor.init()
def main():
main.boss = CommentsEditorBoss()