aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorAndreas Gohr <andi@splitbrain.org>2022-07-14 22:37:03 +0200
committerAndreas Gohr <andi@splitbrain.org>2022-07-14 22:37:03 +0200
commite4085da24142cfafce95aa1368431a05fb6cecd3 (patch)
tree387bd9b02c93ad24770bc2dbec8b3ffaad6d3949
parentd1d65d581a25ebc7241a71984748ad049bec40dc (diff)
downloaddokuwiki-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.php1
-rw-r--r--lib/scripts/edit.js126
-rw-r--r--lib/scripts/editor.js386
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]);
+});
+