Add basic formatting buttons to comments editor toolbar on EM page

This commit is contained in:
Kovid Goyal 2018-03-21 07:28:29 +05:30
parent 3b464dc99b
commit 88d4772ec8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 126 additions and 4 deletions

1
imgsrc/srv/bold.svg Normal file
View File

@ -0,0 +1 @@
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M747 1521q74 32 140 32 376 0 376-335 0-114-41-180-27-44-61.5-74t-67.5-46.5-80.5-25-84-10.5-94.5-2q-73 0-101 10 0 53-.5 159t-.5 158q0 8-1 67.5t-.5 96.5 4.5 83.5 12 66.5zm-14-746q42 7 109 7 82 0 143-13t110-44.5 74.5-89.5 25.5-142q0-70-29-122.5t-79-82-108-43.5-124-14q-50 0-130 13 0 50 4 151t4 152q0 27-.5 80t-.5 79q0 46 1 69zm-541 889l2-94q15-4 85-16t106-27q7-12 12.5-27t8.5-33.5 5.5-32.5 3-37.5.5-34v-65.5q0-982-22-1025-4-8-22-14.5t-44.5-11-49.5-7-48.5-4.5-30.5-3l-4-83q98-2 340-11.5t373-9.5q23 0 68.5.5t67.5.5q70 0 136.5 13t128.5 42 108 71 74 104.5 28 137.5q0 52-16.5 95.5t-39 72-64.5 57.5-73 45-84 40q154 35 256.5 134t102.5 248q0 100-35 179.5t-93.5 130.5-138 85.5-163.5 48.5-176 14q-44 0-132-3t-132-3q-106 0-307 11t-231 12z"/></svg>

After

Width:  |  Height:  |  Size: 833 B

1
imgsrc/srv/italic.svg Normal file
View File

@ -0,0 +1 @@
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M384 1662l17-85q6-2 81.5-21.5t111.5-37.5q28-35 41-101 1-7 62-289t114-543.5 52-296.5v-25q-24-13-54.5-18.5t-69.5-8-58-5.5l19-103q33 2 120 6.5t149.5 7 120.5 2.5q48 0 98.5-2.5t121-7 98.5-6.5q-5 39-19 89-30 10-101.5 28.5t-108.5 33.5q-8 19-14 42.5t-9 40-7.5 45.5-6.5 42q-27 148-87.5 419.5t-77.5 355.5q-2 9-13 58t-20 90-16 83.5-6 57.5l1 18q17 4 185 31-3 44-16 99-11 0-32.5 1.5t-32.5 1.5q-29 0-87-10t-86-10q-138-2-206-2-51 0-143 9t-121 11z"/></svg>

After

Width:  |  Height:  |  Size: 540 B

View File

@ -0,0 +1 @@
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1760 896q14 0 23 9t9 23v64q0 14-9 23t-23 9h-1728q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h1728zm-1277-64q-28-35-51-80-48-97-48-188 0-181 134-309 133-127 393-127 50 0 167 19 66 12 177 48 10 38 21 118 14 123 14 183 0 18-5 45l-12 3-84-6-14-2q-50-149-103-205-88-91-210-91-114 0-182 59-67 58-67 146 0 73 66 140t279 129q69 20 173 66 58 28 95 52h-743zm507 256h411q7 39 7 92 0 111-41 212-23 55-71 104-37 35-109 81-80 48-153 66-80 21-203 21-114 0-195-23l-140-40q-57-16-72-28-8-8-8-22v-13q0-108-2-156-1-30 0-68l2-37v-44l102-2q15 34 30 71t22.5 56 12.5 27q35 57 80 94 43 36 105 57 59 22 132 22 64 0 139-27 77-26 122-86 47-61 47-129 0-84-81-157-34-29-137-71z"/></svg>

After

Width:  |  Height:  |  Size: 750 B

1
imgsrc/srv/underline.svg Normal file
View File

@ -0,0 +1 @@
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M176 223q-37-2-45-4l-3-88q13-1 40-1 60 0 112 4 132 7 166 7 86 0 168-3 116-4 146-5 56 0 86-2l-1 14 2 64v9q-60 9-124 9-60 0-79 25-13 14-13 132 0 13 .5 32.5t.5 25.5l1 229 14 280q6 124 51 202 35 59 96 92 88 47 177 47 104 0 191-28 56-18 99-51 48-36 65-64 36-56 53-114 21-73 21-229 0-79-3.5-128t-11-122.5-13.5-159.5l-4-59q-5-67-24-88-34-35-77-34l-100 2-14-3 2-86h84l205 10q76 3 196-10l18 2q6 38 6 51 0 7-4 31-45 12-84 13-73 11-79 17-15 15-15 41 0 7 1.5 27t1.5 31q8 19 22 396 6 195-15 304-15 76-41 122-38 65-112 123-75 57-182 89-109 33-255 33-167 0-284-46-119-47-179-122-61-76-83-195-16-80-16-237v-333q0-188-17-213-25-36-147-39zm1488 1409v-64q0-14-9-23t-23-9h-1472q-14 0-23 9t-9 23v64q0 14 9 23t23 9h1472q14 0 23-9t9-23z"/></svg>

After

Width:  |  Height:  |  Size: 822 B

View File

@ -5,9 +5,60 @@ from __python__ import bound_methods, hash_literals
from elementmaker import E from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from dom import clear, ensure_id 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 iframe_comm import IframeClient, IframeWrapper
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 all_editor_actions():
if not all_editor_actions.ans:
all_editor_actions.ans = {
'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')
},
}
return all_editor_actions.ans
class CommentsEditorBoss: class CommentsEditorBoss:
@ -16,12 +67,15 @@ class CommentsEditorBoss:
'initialize': self.initialize, 'initialize': self.initialize,
'set_html': self.set_html, 'set_html': self.set_html,
'get_html': self.get_html, 'get_html': self.get_html,
'exec_command': self.exec_command,
'focus': self.focus,
} }
self.comm = IframeClient(handlers) self.comm = IframeClient(handlers)
def initialize(self, data): def initialize(self, data):
window.onerror = self.onerror window.onerror = self.onerror
clear(document.body) clear(document.body)
document.execCommand("defaultParagraphSeparator", False, "div")
document.body.style.margin = '0' document.body.style.margin = '0'
document.body.style.padding = '0' document.body.style.padding = '0'
document.documentElement.style.height = document.body.style.height = '100%' document.documentElement.style.height = document.body.style.height = '100%'
@ -29,6 +83,12 @@ class CommentsEditorBoss:
document.body.style.fontFamily = window.default_font_family 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.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.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() document.body.lastChild.focus()
def onerror(self, msg, script_url, line_number, column_number, error_object): def onerror(self, msg, script_url, line_number, column_number, error_object):
@ -46,6 +106,16 @@ class CommentsEditorBoss:
def get_html(self, data): def get_html(self, data):
self.comm.send_message('html', html=document.body.lastChild.innerHTML) 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'.split(' ')}
state.strikethrough = document.queryCommandState('strikeThrough')
self.comm.send_message('update_state', state=state)
registry = {} registry = {}
@ -63,6 +133,7 @@ class Editor:
handlers = { handlers = {
'ready': self.on_iframe_ready, 'ready': self.on_iframe_ready,
'html': self.on_html_received, 'html': self.on_html_received,
'update_state': self.update_state,
} }
self.iframe_wrapper = IframeWrapper(handlers, iframe, 'book_list.comments_editor', _('Loading comments editor...')) self.iframe_wrapper = IframeWrapper(handlers, iframe, 'book_list.comments_editor', _('Loading comments editor...'))
self.id = ensure_id(iframe) self.id = ensure_id(iframe)
@ -77,6 +148,11 @@ class Editor:
self.iframe_wrapper.destroy() self.iframe_wrapper.destroy()
self.get_html_callbacks = v'[]' self.get_html_callbacks = v'[]'
def focus(self):
self.iframe.contentWindow.focus()
if self.ready:
self.iframe_wrapper.send_message('focus')
@property @property
def iframe(self): def iframe(self):
return self.iframe_wrapper.iframe return self.iframe_wrapper.iframe
@ -109,6 +185,19 @@ class Editor:
f(data.html) f(data.html)
self.get_html_callbacks = v'[]' 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(): def create_editor():
@ -118,18 +207,47 @@ def create_editor():
return iframe, 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): def create_comments_editor(container):
iframe, editor = create_editor() iframe, editor = create_editor()
toolbar1 = E.div('TODO: add toolbar', style='flex-grow: 0') toolbars = E.div(style='flex-grow: 0')
toolbar1 = E.div(class_=TOOLBAR_CLASS)
toolbars.appendChild(toolbar1)
acmap = all_editor_actions()
for ac_name in 'bold italic underline strikethrough |'.split(' '):
if acmap[ac_name]:
add_action(toolbar1, ac_name, acmap[ac_name], editor.id)
else:
toolbar1.appendChild(E.div(class_='sep'))
container.setAttribute('style', (container.getAttribute('style') or '') + ';display: flex; flex-direction: column; align-items: stretch') container.setAttribute('style', (container.getAttribute('style') or '') + ';display: flex; flex-direction: column; align-items: stretch')
container.appendChild(toolbar1) container.appendChild(toolbars)
container.appendChild(iframe) container.appendChild(iframe)
container.classList.add(CLASS_NAME)
return editor return editor
def focus_comments_editor(container): def focus_comments_editor(container):
iframe = container.querySelector('iframe') iframe = container.querySelector('iframe')
iframe.contentWindow.focus() editor = registry[iframe.getAttribute('id')]
if editor:
editor.focus()
def set_comments_html(container, html): def set_comments_html(container, html):