
committed by
GitHub

No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 224 additions and 0 deletions
@ -0,0 +1,203 @@ |
|||
class MarkdownEditor { |
|||
// State holder for debouncing.
|
|||
_saveTimer = null; |
|||
id = 'markdown_editor'; |
|||
|
|||
constructor() { |
|||
// TODO: I18n: https://github.com/nhn/tui.editor/blob/master/docs/en/i18n.md
|
|||
// TODO: handle switching from+to mailvelope (optional, it's disabled in TinyMCE as well).
|
|||
// TODO: handle toggling Rechtschreibprüfung
|
|||
|
|||
this.editorEl = html('div', { id: 'markdown_editor' }); |
|||
|
|||
// Replace textarea with markdown editor.
|
|||
const container = document.getElementById('composebodycontainer'); |
|||
this.textarea = container.querySelector('#composebody'); |
|||
this.textarea.style.display = 'none'; |
|||
container.append(this.editorEl); |
|||
|
|||
// Make the server handle the input as HTML.
|
|||
rcmail.gui_objects.messageform.querySelector('[name="_is_html"]').value = '1'; |
|||
|
|||
let toastUiConfig = { |
|||
usageStatistics: false, |
|||
el: this.editorEl, |
|||
hideModeSwitch: true, |
|||
height: '500px', |
|||
initialValue: this.textarea.value, |
|||
events: { |
|||
change: () => this.save(), |
|||
}, |
|||
}; |
|||
|
|||
this.tuiEditor = new toastui.Editor(toastUiConfig); |
|||
this.contenteditableEl = this.editorEl.querySelector('[contenteditable="true"]'); |
|||
|
|||
this.orig_editor = rcmail.editor; |
|||
rcmail.editor = this; |
|||
} |
|||
|
|||
get_content(args = {}) { |
|||
let text = null; |
|||
if (args.selection) { |
|||
text = this.tuiEditor.getSelectedText(); |
|||
} |
|||
if (!text) { |
|||
if (args.format === 'html') { |
|||
text = this.tuiEditor.getHTML(); |
|||
} else { |
|||
text = this.tuiEditor.getMarkdown(); |
|||
} |
|||
} |
|||
|
|||
if (args.nosig) { |
|||
sigstart = text.indexOf('-- \n'); |
|||
if (sigstart > 0) { |
|||
text = text.substring(0, sigstart); |
|||
} |
|||
} |
|||
|
|||
return text; |
|||
} |
|||
|
|||
get_language() { |
|||
return rcmail.env.spell_lang; |
|||
} |
|||
|
|||
focus() { |
|||
this.tuiEditor.focus(); |
|||
} |
|||
|
|||
is_html() { |
|||
return false; |
|||
} |
|||
|
|||
replace(input) { |
|||
if (!input) { |
|||
return false; |
|||
} |
|||
if (typeof input !== 'string') { |
|||
input = input.text || ''; |
|||
} |
|||
this.tuiEditor.replaceSelection(input); |
|||
|
|||
} |
|||
|
|||
save() { |
|||
// Debounce writing to the textarea using a delay of 1s.
|
|||
if (this._saveTimer) { |
|||
clearTimeout(this._saveTimer); |
|||
} |
|||
this._saveTimer = setTimeout(() => { |
|||
// this.textarea.value = this.tuiEditor.getMarkdown();
|
|||
this.textarea.value = this.tuiEditor.getHTML(); |
|||
}, 1000); |
|||
}; |
|||
|
|||
|
|||
// This gets also called on identity change, in which case a different signature (that belongs to the previously
|
|||
// used identity) needs to be replaced.
|
|||
change_signature(sig_id, show_sig) { |
|||
if (!show_sig || !rcmail.env.signatures) { |
|||
return; |
|||
} |
|||
|
|||
// Remove the old signature.
|
|||
let message = this.tuiEditor.getMarkdown(); |
|||
const old_sig_id = rcmail.env.identity; |
|||
let p; |
|||
if (old_sig_id && rcmail.env.signatures[old_sig_id]) { |
|||
const old_sig_text = rcmail.env.signatures[old_sig_id].text.replace(/\r\n/g, '\n'); |
|||
p = rcmail.env.top_posting ? message.indexOf(old_sig_text) : message.lastIndexOf(old_sig_text); |
|||
if (p >= 0) { |
|||
message = message.substring(0, p) + message.substring(p + old_sig_text.length, message.length); |
|||
if (message.endsWith('\n\n')) { |
|||
// Strip two newlines that this method probably introduced (see below).
|
|||
message = message.substring(0, message.length-2); |
|||
} |
|||
this.tuiEditor.setMarkdown(message, false); |
|||
} |
|||
} |
|||
|
|||
if (!rcmail.env.signatures[sig_id]) { |
|||
return; |
|||
} |
|||
|
|||
let sig_text = rcmail.env.signatures[sig_id].text.replace(/\r\n/g, '\n'); |
|||
|
|||
if (p >= 0) { |
|||
// If a signature was removed, insert the new signature at the same position.
|
|||
this.tuiEditor.insertText(sig_text); |
|||
return; |
|||
} |
|||
|
|||
// TODO: Fix this, it would the handling of some edge cases cleaner (switching between identities if the signature isn't the last block of the text anymore).
|
|||
// // Insert sig at next line if the line of the cursor is not empty.
|
|||
// const currentPosition = this.tuiEditor.getSelection();
|
|||
// console.info({ currentPosition });
|
|||
// const currentLineNumber = this.tuiEditor.getSelection()[0][0];
|
|||
// console.info({ currentLineNumber });
|
|||
// // TODO: Can we make this work without using internal APIs (toastMark)?
|
|||
// const currentLineText = this.tuiEditor.toastMark.getLineTexts()[currentLineNumber - 1];
|
|||
// console.info({ currentLineText });
|
|||
// if (currentLineText !== '') {
|
|||
// sig_text = `\n\n${sig_text}`;
|
|||
// }
|
|||
|
|||
if (!message) { |
|||
// If the message is empty, insert at the top, preceeded by a blank line.
|
|||
this.tuiEditor.moveCursorToStart(); |
|||
this.tuiEditor.insertText(`\n\n${sig_text}`); |
|||
return; |
|||
} |
|||
|
|||
// Insert at the cursor position if top_posting is enabled and sig_below is disabled.
|
|||
if (rcmail.env.top_posting && !rcmail.env.sig_below) { |
|||
this.tuiEditor.insertText(`\n${sig_text}\n\n`); |
|||
return; |
|||
} |
|||
|
|||
// Insert at the bottom
|
|||
if (message.slice(-1) !== '\n\n') { |
|||
// Prepend a newline if the last line isn't blank.
|
|||
sig_text = `\n\n${sig_text}`; |
|||
} |
|||
this.tuiEditor.moveCursorToEnd(); |
|||
this.tuiEditor.insertText(sig_text); |
|||
|
|||
}; |
|||
} |
|||
|
|||
const html = (type, ...args) => { |
|||
args = args.flat(); |
|||
let content; |
|||
let attributes; |
|||
const elem = document.createElement(type); |
|||
if (args[0]?.constructor?.name === 'Object') { |
|||
attributes = args.shift(); |
|||
for (const [key, value] of Object.entries(attributes)) { |
|||
if (key.slice(0, 2) === 'on' && typeof value === 'function') { |
|||
const eventName = key.slice(2).toLowerCase(); |
|||
elem.addEventListener(eventName, value); |
|||
} else { |
|||
elem.setAttribute(key, value); |
|||
} |
|||
} |
|||
} |
|||
content = args; |
|||
if (content?.constructor?.name !== 'Array') { |
|||
content = [content]; |
|||
} |
|||
for (const thing of content) { |
|||
if (thing) { |
|||
if (typeof thing === 'string') { |
|||
elem.append(document.createTextNode(thing)); |
|||
} else if (thing.nodeName) { |
|||
elem.append(thing); |
|||
} |
|||
} |
|||
} |
|||
return elem; |
|||
}; |
|||
|
|||
rcmail.addEventListener('init', () => new MarkdownEditor()); |
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
class markdown_editor extends rcube_plugin |
|||
{ |
|||
public $task = 'mail'; |
|||
|
|||
#[Override]
|
|||
public function init() |
|||
{ |
|||
$this->add_hook('message_compose_body', [$this, 'load']); |
|||
} |
|||
|
|||
public function load($args) |
|||
{ |
|||
// TODO: Download and serve these files ourselves
|
|||
$this->include_script('https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js'); |
|||
$this->include_stylesheet('https://uicdn.toast.com/editor/latest/toastui-editor.min.css'); |
|||
$this->include_script('markdown_editor.js'); |
|||
return $args; |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue