diff options
author | Andreas Gohr <andi@splitbrain.org> | 2022-07-14 22:37:03 +0200 |
---|---|---|
committer | Andreas Gohr <andi@splitbrain.org> | 2022-07-14 22:37:03 +0200 |
commit | e4085da24142cfafce95aa1368431a05fb6cecd3 (patch) | |
tree | 387bd9b02c93ad24770bc2dbec8b3ffaad6d3949 | |
parent | d1d65d581a25ebc7241a71984748ad049bec40dc (diff) | |
download | dokuwiki-editorjs-cleanup.tar.gz dokuwiki-editorjs-cleanup.zip |
refactor editor.js completelyeditorjs-cleanup
This makes it simpler, cleaner and uses more modern JS APIs. jQuery
dependencies have been mostly (but not completely) removed.
This also fixes #3711
-rw-r--r-- | inc/Ui/Editor.php | 1 | ||||
-rw-r--r-- | lib/scripts/edit.js | 126 | ||||
-rw-r--r-- | lib/scripts/editor.js | 386 |
3 files changed, 226 insertions, 287 deletions
diff --git a/inc/Ui/Editor.php b/inc/Ui/Editor.php index 67ad081ff..be768d3c6 100644 --- a/inc/Ui/Editor.php +++ b/inc/Ui/Editor.php @@ -70,6 +70,7 @@ class Editor extends Ui $form->setHiddenField('prefix', $PRE .'.'); $form->setHiddenField('suffix', $SUF); $form->setHiddenField('changecheck', $check); + $form->setHiddenField('haschanged', $INPUT->int('haschanged')); // set by JavaScript // prepare data for EDIT_FORM_ALTERNATE event $data = array( diff --git a/lib/scripts/edit.js b/lib/scripts/edit.js index 2253d05cf..d8a4165fd 100644 --- a/lib/scripts/edit.js +++ b/lib/scripts/edit.js @@ -181,129 +181,3 @@ function currentHeadlineLevel(textboxId){ } return 7 - s.match(/^={2,6}/)[0].length; } - - -/** - * global var used for not saved yet warning - */ -window.textChanged = false; - -/** - * global var which stores original editor content - */ -window.doku_edit_text_content = ''; -/** - * Delete the draft before leaving the page - */ -function deleteDraft() { - if (is_opera || window.keepDraft) { - return; - } - - var $dwform = jQuery('#dw__editform'); - - if($dwform.length === 0) { - return; - } - - // remove a possibly saved draft using ajax - jQuery.post(DOKU_BASE + 'lib/exe/ajax.php', - { - call: 'draftdel', - id: $dwform.find('input[name=id]').val(), - sectok: $dwform.find('input[name=sectok]').val() - } - ); -} - -/** - * Activate "not saved" dialog, add draft deletion to page unload, - * add handlers to monitor changes - * Note: textChanged could be set by e.g. html_edit() as well - * - * Sets focus to the editbox as well - */ -jQuery(function () { - var $editform = jQuery('#dw__editform'); - if ($editform.length == 0) { - return; - } - - var $edit_text = jQuery('#wiki__text'); - if ($edit_text.length > 0) { - if($edit_text.attr('readOnly')) { - return; - } - - // set focus and place cursor at the start - var sel = DWgetSelection($edit_text[0]); - sel.start = 0; - sel.end = 0; - DWsetSelection(sel); - $edit_text.trigger('focus'); - - doku_edit_text_content = $edit_text.val(); - } - - var changeHandler = function() { - doku_hasTextBeenModified(); - - doku_summaryCheck(); - }; - - $editform.change(changeHandler); - $editform.keydown(changeHandler); - - window.onbeforeunload = function(){ - if(window.textChanged) { - return LANG.notsavedyet; - } - }; - window.onunload = deleteDraft; - - // reset change memory var on submit - jQuery('#edbtn__save').on('click', - function() { - window.onbeforeunload = ''; - textChanged = false; - } - ); - jQuery('#edbtn__preview').on('click', - function() { - window.onbeforeunload = ''; - textChanged = false; - window.keepDraft = true; // needed to keep draft on page unload - } - ); - - var $summary = jQuery('#edit__summary'); - $summary.on('change keyup', doku_summaryCheck); - - if (textChanged) doku_summaryCheck(); -}); - -/** - * Updates textChanged variable if content of the editor has been modified - */ -function doku_hasTextBeenModified() { - if (!textChanged) { - var $edit_text = jQuery('#wiki__text'); - - if ($edit_text.length > 0) { - textChanged = doku_edit_text_content != $edit_text.val(); - } else { - textChanged = true; - } - } -} - -/** - * Checks if a summary was entered - if not the style is changed - * - * @author Andreas Gohr <andi@splitbrain.org> - */ -function doku_summaryCheck(){ - var $sum = jQuery('#edit__summary'), - missing = $sum.val() === ''; - $sum.toggleClass('missing', missing).toggleClass('edit', !missing); -} diff --git a/lib/scripts/editor.js b/lib/scripts/editor.js index 988f54dc1..4aa255576 100644 --- a/lib/scripts/editor.js +++ b/lib/scripts/editor.js @@ -4,205 +4,269 @@ * These are the advanced features of the editor. It does NOT contain any * code for the toolbar buttons and its functions. See toolbar.js for that. */ - -let dw_editor = { +class DokuWikiEditor { + editor; + textarea; + summary; + btnSave; + btnPreview; + btnCancel; + haschanged; /** - * initialize the default editor functionality + * Initialize the Editor behavior * - * All other functions can also be called separately for non-default - * textareas + * @param {HTMLFormElement} form */ - init: function () { - const $editor = jQuery('#wiki__text'); - if ($editor.length === 0) { - return; - } + constructor(form) { + this.editor = form; + this.textarea = form.elements['wikitext']; + this.summary = form.elements['summary']; + this.haschanged = form.elements['haschanged']; + this.btnSave = form.elements['do[save]']; + this.btnPreview = form.elements['do[preview]']; + this.btnCancel = form.elements['do[cancel]']; - dw_editor.initSizeCtl('#size__ctl', $editor); + // this behavior applies to all editors + this.setupSize(); + this.setupWrap(); - if ($editor.attr('readOnly')) { - return; - } + // the following only to non-readonly ones + if (this.textarea.readOnly) return; - $editor.keydown(dw_editor.keyHandler); - - }, + this.setupChangeWarning(); + this.setupDraftDeletion(); + this.textarea.addEventListener('keydown', this.handleKeys.bind(this)); + this.summary.addEventListener('input', this.checkSummary.bind(this)); + } /** - * Add the edit window size and wrap controls - * - * Initial values are read from cookie if it exists - * - * @param {string|Element|jQuery} ctlarea the div to place the controls - * @param {string|Element|jQuery} editor the textarea to control + * Set and remember text area size using a cookie */ - initSizeCtl: function (ctlarea, editor) { - const $ctl = jQuery(ctlarea), - $textarea = jQuery(editor); + setupSize() { + const height = Math.max(25, parseInt(DokuCookie.getValue('sizeCtl'), 10)); + this.textarea.style.height = height + 'px'; - if ($ctl.length === 0 || $textarea.length === 0) { - return; - } - - $textarea.css('height', DokuCookie.getValue('sizeCtl') || '300px'); + new ResizeObserver(() => { + DokuCookie.setValue('sizeCtl', this.textarea.offsetHeight); + }).observe(this.textarea); + } + /** + * Allow switching the wrap beahviour and store it in a cookie + */ + setupWrap() { + // set stored wrap const wrp = DokuCookie.getValue('wrapCtl'); if (wrp) { - dw_editor.setWrap($textarea[0], wrp); - } // else use default value - - jQuery.each([ - ['larger', function () { - dw_editor.sizeCtl(editor, 100); - }], - ['smaller', function () { - dw_editor.sizeCtl(editor, -100); - }], - ['wrap', function () { - dw_editor.toggleWrap(editor); - }] - ], function (_, img) { - jQuery(document.createElement('img')) - .attr('src', DOKU_BASE + 'lib/images/' + img[0] + '.gif') - .attr('alt', '') - .on('click', img[1]) - .appendTo($ctl); + this.textarea.wrap = wrp; + } + + // create toggle element + const toggle = document.createElement('span'); + toggle.className = 'wraptoggle'; + toggle.innerText = '⏎'; + toggle.title = 'FIXME toggle line wrap'; + document.getElementById('size__ctl').append(toggle); // FIXME remove ID reliance + + // add click handler + toggle.addEventListener('click', () => { + const current = this.textarea.wrap.toLowerCase(); + this.textarea.wrap = (current === 'off') ? 'soft' : 'off'; + DokuCookie.setValue('wrapCtl', this.textarea.wrap); }); - }, + } /** - * This sets the vertical size of the editbox and adjusts the cookie - * - * @param {string|Element|jQuery} editor the textarea to control - * @param {int} val the relative value to resize in pixel + * Warn about unsaved changes when navigating away */ - sizeCtl: function (editor, val) { - const $textarea = jQuery(editor), - height = parseInt($textarea.css('height')) + val; - $textarea.css('height', height + 'px'); - DokuCookie.setValue('sizeCtl', $textarea.css('height')); - }, + setupChangeWarning() { + // notice changes (also trigger summary check) + this.textarea.addEventListener('input', () => { + this.haschanged.value = '1'; + this.checkSummary(); + }); + + // show unsaved changes warning, when trying to navigate away + window.onbeforeunload = () => { + if (this.haschanged.value === '1') { + return LANG.notsavedyet; + } + }; + + // prevent warning on some buttons, by removing the handler + const prevent = () => { + window.onbeforeunload = null; + }; + this.btnPreview.addEventListener('click', prevent); + this.btnSave.addEventListener('click', prevent); + } /** - * Toggle the wrapping mode of the editor textarea and adjusts the - * cookie + * Remove a possibly saved draft using ajax * - * @param {string|Element|jQuery} editor the textarea to control + * Note: draft saving is currently handled in locktimer.js */ - toggleWrap: function (editor) { - const $textarea = jQuery(editor), - wrap = $textarea.attr('wrap'); - dw_editor.setWrap($textarea[0], - (wrap && wrap.toLowerCase() === 'off') ? 'soft' : 'off'); - DokuCookie.setValue('wrapCtl', $textarea.attr('wrap')); - }, + setupDraftDeletion() { + window.onunload = () => { + // FIXME replace jQuery dependency + jQuery.post(DOKU_BASE + 'lib/exe/ajax.php', + { + call: 'draftdel', + id: this.editor.elements['id'].value, + sectok: this.editor.elements['sectok'].value + } + ); + }; + + // do not delete the draft on preview + this.btnPreview.addEventListener('click', () => { + window.onunload = null; + }); + } /** - * Set the wrapping mode of a textarea - * - * @author Fluffy Convict <fluffyconvict@hotmail.com> - * @author <shutdown@flashmail.com> - * @link http://news.hping.org/comp.lang.javascript.archive/12265.html - * @link https://bugzilla.mozilla.org/show_bug.cgi?id=41464 - * @param {Element} textarea - * @param {string} wrapAttrValue + * Set the class of the summary based on it's content and the text change status */ - setWrap: function (textarea, wrapAttrValue) { - textarea.setAttribute('wrap', wrapAttrValue); - - // Fix display for mozilla - const parNod = textarea.parentNode; - const nxtSib = textarea.nextSibling; - parNod.removeChild(textarea); - parNod.insertBefore(textarea, nxtSib); - }, + checkSummary() { + if (this.haschanged.value === '1' && this.summary.value === '') { + this.summary.classList.add('missing'); + } else { + this.summary.classList.remove('missing'); + } + } /** - * Make intended formattings easier to handle + * Make indented formattings easier to handle * - * Listens to all key inputs and handle indentions - * of lists and code blocks + * Listens to all key inputs and handle indentions of lists and code blocks * - * Currently handles space, backspace, enter and - * ctrl-enter presses + * Handles space, backspace, enter and ctrl-enter presses * - * @author Andreas Gohr <andi@splitbrain.org> - * @fixme handle tabs * @param {KeyboardEvent} e - the key press event object */ - keyHandler: function (e) { - const selection = DWgetSelection(this); + handleKeys(e) { + // Save on CTRL+Enter + if (e.key === 'Enter' && e.ctrlKey) { + this.btnSave.click(); + e.preventDefault(); // prevent enter key + return; + } + + // Handle text transformations below + const selection = DWgetSelection(this.textarea); if (selection.getLength() > 0) { - return; //there was text selected, keep standard behavior + return; //there was text selected, keep standard behavior. we're done } - let search = "\n" + this.value.substring(0, selection.start); - const linestart = Math.max( - search.lastIndexOf("\n"), - search.lastIndexOf("\r") //IE workaround + + let line = "\n" + this.textarea.value.substring(0, selection.start); + const linepos = Math.max( + line.lastIndexOf("\n"), + line.lastIndexOf("\r") //IE workaround ); - search = search.substring(linestart); + line = line.substring(linepos); - if (e.key === 'Enter' && e.ctrlKey) { - // Submit current edit - jQuery('#edbtn__save').trigger('click'); - e.preventDefault(); // prevent enter key - return false; - } else if (e.key === 'Enter') { - // keep current indention for lists and code - const match = search.match(/(\n +([*-] ?)?)/); - if (match) { - const scroll = this.scrollHeight; - const match2 = search.match(/^\n +[*-]\s*$/); - // Cancel list if the last item is empty (i.e. two times enter) - if (match2 && this.value.substring(selection.start).match(/^($|\r?\n)/)) { - this.value = this.value.substring(0, linestart) + "\n" + - this.value.substring(selection.start); - selection.start = linestart + 1; - selection.end = linestart + 1; - DWsetSelection(selection); - } else { - insertAtCarret(this.id, match[1]); - } - this.scrollTop += (this.scrollHeight - scroll); - e.preventDefault(); // prevent enter key - return false; - } + if (e.key === 'Enter') { + this.handleKeyEnter(line, linepos, selection) && e.preventDefault(); } else if (e.key === 'Backspace') { - // unindent lists - const match = search.match(/(\n +)([*-] ?)$/); - if (match) { - const spaces = match[1].length - 1; - - if (spaces > 3) { // unindent one level - this.value = this.value.substring(0, linestart) + - this.value.substring(linestart + 2); - selection.start = selection.start - 2; - selection.end = selection.start; - } else { // delete list point - this.value = this.value.substring(0, linestart) + - this.value.substring(selection.start); - selection.start = linestart; - selection.end = linestart; - } - DWsetSelection(selection); - e.preventDefault(); // prevent backspace - return false; - } + this.handleKeyBackspace(line, linepos, selection) && e.preventDefault(); } else if (e.key === ' ') { // Space - // intend list item - const match = search.match(/(\n +)([*-] )$/); - if (match) { - this.value = this.value.substring(0, linestart) + ' ' + - this.value.substring(linestart); - selection.start = selection.start + 2; - selection.end = selection.start; - DWsetSelection(selection); - e.preventDefault(); // prevent space - return false; - } + this.handleKeySpace(line, linepos, selection) && e.preventDefault(); + } + } + + /** + * Handle enter presses in the textarea + * + * @param {string} line The current line (and following) + * @param {int} linepos The start position of the current line + * @param {selection_class} selection A DokuWiki Text Selection object (current cursor) + * @returns {boolean} true if the event was handled and the default should be cancelled + */ + handleKeyEnter(line, linepos, selection) { + // only handle indented lines + const isIndented = line.match(/(\n +([*-] ?)?)/); + if (!isIndented) return false; + + // remember scroll position + const scroll = this.textarea.scrollHeight; + + // Cancel list if the last item is empty (i.e. two times enter) + const isEmptyListItem = line.match(/^\n +[*-]\s*$/); + if (isEmptyListItem && this.textarea.value.substring(selection.start).match(/^($|\r?\n)/)) { + this.textarea.value = + this.textarea.value.substring(0, linepos) + "\n" + + this.textarea.value.substring(selection.start); + selection.start = linepos + 1; + selection.end = linepos + 1; + DWsetSelection(selection); + } else { + insertAtCarret(this.textarea.id, isIndented[1]); } + + // restore scroll postion + this.textarea.scrollTop += (this.textarea.scrollHeight - scroll); + return true; + } + + /** + * Handle backspace presses in the textarea + * + * @param {string} line The current line (and following) + * @param {int} linepos The start position of the current line + * @param {selection_class} selection A DokuWiki Text Selection object (current cursor) + * @returns {boolean} true if the event was handled and the default should be cancelled + */ + handleKeyBackspace(line, linepos, selection) { + const isListItem = line.match(/(\n +)([*-] ?)$/); + if (!isListItem) return false; + + const spaces = isListItem[1].length - 1; + + if (spaces > 3) { // unindent one level + this.textarea.value = + this.textarea.value.substring(0, linepos) + + this.textarea.value.substring(linepos + 2); + selection.start = selection.start - 2; + selection.end = selection.start; + } else { // delete list point + this.textarea.value = + this.textarea.value.substring(0, linepos) + + this.textarea.value.substring(selection.start); + selection.start = linepos; + selection.end = linepos; + } + DWsetSelection(selection); + return true; } -}; -jQuery(dw_editor.init); + /** + * Handle space presses in the textarea + * + * @param {string} line The current line (and following) + * @param {int} linepos The start position of the current line + * @param {selection_class} selection A DokuWiki Text Selection object (current cursor) + * @returns {boolean} true if the event was handled and the default should be cancelled + */ + handleKeySpace(line, linepos, selection) { + // intend list item + const isListItem = line.match(/(\n +)([*-] )$/); + if (!isListItem) return false; + + this.textarea.value = + this.textarea.value.substring(0, linepos) + ' ' + + this.textarea.value.substring(linepos); + selection.start = selection.start + 2; + selection.end = selection.start; + DWsetSelection(selection); + + return true; + } +} + +// FIXME drop jQuery Dependency +jQuery(function () { + const $editform = jQuery('#dw__editform'); + if ($editform.length) new DokuWikiEditor($editform[0]); +}); + |