From 88d4772ec8b6866ae2b5a19ec5bdfec8d4097ead Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Mar 2018 07:28:29 +0530 Subject: [PATCH] Add basic formatting buttons to comments editor toolbar on EM page --- imgsrc/srv/bold.svg | 1 + imgsrc/srv/italic.svg | 1 + imgsrc/srv/strikethrough.svg | 1 + imgsrc/srv/underline.svg | 1 + src/pyj/book_list/comments_editor.pyj | 126 +++++++++++++++++++++++++- 5 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 imgsrc/srv/bold.svg create mode 100644 imgsrc/srv/italic.svg create mode 100644 imgsrc/srv/strikethrough.svg create mode 100644 imgsrc/srv/underline.svg diff --git a/imgsrc/srv/bold.svg b/imgsrc/srv/bold.svg new file mode 100644 index 0000000000..656d81d93f --- /dev/null +++ b/imgsrc/srv/bold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/imgsrc/srv/italic.svg b/imgsrc/srv/italic.svg new file mode 100644 index 0000000000..8691a38d64 --- /dev/null +++ b/imgsrc/srv/italic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/imgsrc/srv/strikethrough.svg b/imgsrc/srv/strikethrough.svg new file mode 100644 index 0000000000..cfe467ed37 --- /dev/null +++ b/imgsrc/srv/strikethrough.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/imgsrc/srv/underline.svg b/imgsrc/srv/underline.svg new file mode 100644 index 0000000000..7acc33ff2e --- /dev/null +++ b/imgsrc/srv/underline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pyj/book_list/comments_editor.pyj b/src/pyj/book_list/comments_editor.pyj index 49f4b19e6a..0de0950e2e 100644 --- a/src/pyj/book_list/comments_editor.pyj +++ b/src/pyj/book_list/comments_editor.pyj @@ -5,9 +5,60 @@ from __python__ import bound_methods, hash_literals from elementmaker import E 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 +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: @@ -16,12 +67,15 @@ class CommentsEditorBoss: '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.body.style.margin = '0' document.body.style.padding = '0' 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.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): @@ -46,6 +106,16 @@ class CommentsEditorBoss: 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'.split(' ')} + state.strikethrough = document.queryCommandState('strikeThrough') + self.comm.send_message('update_state', state=state) + + registry = {} @@ -63,6 +133,7 @@ class Editor: 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) @@ -77,6 +148,11 @@ class Editor: 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 @@ -109,6 +185,19 @@ class Editor: 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(): @@ -118,18 +207,47 @@ def create_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() - 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.appendChild(toolbar1) + container.appendChild(toolbars) container.appendChild(iframe) + container.classList.add(CLASS_NAME) return editor def focus_comments_editor(container): iframe = container.querySelector('iframe') - iframe.contentWindow.focus() + editor = registry[iframe.getAttribute('id')] + if editor: + editor.focus() def set_comments_html(container, html):