Convert `Ui/Redactor/Quote` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Wed, 4 Nov 2020 23:48:30 +0000 (00:48 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 4 Nov 2020 23:48:30 +0000 (00:48 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Quote.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Editor.ts
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Quote.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Quote.ts [new file with mode: 0644]

index c7b10ec872a982f9bab3df39f88d6d6d6af339e3..4cad2cafe95d3202ff48b5e0d132fe0c5fd0885a 100644 (file)
  * Manages quotes.
  *
  * @author      Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
+ * @copyright  2001-2019 WoltLab GmbH
  * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module      WoltLabSuite/Core/Ui/Redactor/Quote
  */
-define(['Core', 'EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', './Metacode', './PseudoHeader'], function (Core, EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog, UiRedactorMetacode, UiRedactorPseudoHeader) {
+define(["require", "exports", "tslib", "../../Core", "../../Dom/Util", "../../Event/Handler", "../../Language", "../../StringUtil", "../Dialog", "./Metacode", "./PseudoHeader"], function (require, exports, tslib_1, Core, Util_1, EventHandler, Language, StringUtil, Dialog_1, UiRedactorMetacode, UiRedactorPseudoHeader) {
     "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        var Fake = function () { };
-        Fake.prototype = {
-            init: function () { },
-            _insertQuote: function () { },
-            _click: function () { },
-            _observeLoad: function () { },
-            _edit: function () { },
-            _save: function () { },
-            _setTitle: function () { },
-            _delete: function () { },
-            _dialogSetup: function () { },
-            _dialogSubmit: function () { }
-        };
-        return Fake;
-    }
-    var _headerHeight = 0;
-    /**
-     * @param       {Object}        editor  editor instance
-     * @param       {jQuery}        button  toolbar button
-     * @constructor
-     */
-    function UiRedactorQuote(editor, button) { this.init(editor, button); }
-    UiRedactorQuote.prototype = {
+    Core = tslib_1.__importStar(Core);
+    Util_1 = tslib_1.__importDefault(Util_1);
+    EventHandler = tslib_1.__importStar(EventHandler);
+    Language = tslib_1.__importStar(Language);
+    StringUtil = tslib_1.__importStar(StringUtil);
+    Dialog_1 = tslib_1.__importDefault(Dialog_1);
+    UiRedactorMetacode = tslib_1.__importStar(UiRedactorMetacode);
+    UiRedactorPseudoHeader = tslib_1.__importStar(UiRedactorPseudoHeader);
+    let _headerHeight = 0;
+    class UiRedactorQuote {
         /**
          * Initializes the quote management.
-         *
-         * @param       {Object}        editor  editor instance
-         * @param       {jQuery}        button  toolbar button
          */
-        init: function (editor, button) {
+        constructor(editor, button) {
             this._quote = null;
-            this._quotes = elByTag('woltlab-quote', editor.$editor[0]);
             this._editor = editor;
             this._elementId = this._editor.$element[0].id;
-            EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
+            EventHandler.add("com.woltlab.wcf.redactor2", `observe_load_${this._elementId}`, () => this._observeLoad());
             this._editor.button.addCallback(button, this._click.bind(this));
-            // static bind to ensure that removing works
-            this._callbackEdit = this._edit.bind(this);
             // bind listeners on init
             this._observeLoad();
             // quote manager
-            EventHandler.add('com.woltlab.wcf.redactor2', 'insertQuote_' + this._elementId, this._insertQuote.bind(this));
-        },
+            EventHandler.add("com.woltlab.wcf.redactor2", `insertQuote_${this._elementId}`, (data) => this._insertQuote(data));
+        }
         /**
          * Inserts a quote.
-         *
-         * @param       {Object}        data            quote data
-         * @protected
          */
-        _insertQuote: function (data) {
+        _insertQuote(data) {
             if (this._editor.WoltLabSource.isActive()) {
                 return;
             }
-            EventHandler.fire('com.woltlab.wcf.redactor2', 'showEditor');
-            var editor = this._editor.core.editor()[0];
+            EventHandler.fire("com.woltlab.wcf.redactor2", "showEditor");
+            const editor = this._editor.core.editor()[0];
             this._editor.selection.restore();
             this._editor.buffer.set();
             // caret must be within a `<p>`, if it is not: move it
-            var block = this._editor.selection.block();
+            let block = this._editor.selection.block();
             if (block === false) {
                 this._editor.focus.end();
                 block = this._editor.selection.block();
             }
-            while (block && block.parentNode !== editor) {
-                block = block.parentNode;
+            while (block && block.parentElement !== editor) {
+                block = block.parentElement;
             }
-            var quote = elCreate('woltlab-quote');
-            elData(quote, 'author', data.author);
-            elData(quote, 'link', data.link);
-            var content = data.content;
+            const quote = document.createElement("woltlab-quote");
+            quote.dataset.author = data.author;
+            quote.dataset.link = data.link;
+            let content = data.content;
             if (data.isText) {
                 content = StringUtil.escapeHTML(content);
-                content = '<p>' + content + '</p>';
-                content = content.replace(/\n\n/g, '</p><p>');
-                content = content.replace(/\n/g, '<br>');
+                content = `<p>${content}</p>`;
+                content = content.replace(/\n\n/g, "</p><p>");
+                content = content.replace(/\n/g, "<br>");
             }
             else {
-                //noinspection JSUnresolvedFunction
                 content = UiRedactorMetacode.convertFromHtml(this._editor.$element[0].id, content);
             }
             // bypass the editor as `insert.html()` doesn't like us
             quote.innerHTML = content;
-            block.parentNode.insertBefore(quote, block.nextSibling);
-            if (block.nodeName === 'P' && (block.innerHTML === '<br>' || block.innerHTML.replace(/\u200B/g, '') === '')) {
-                block.parentNode.removeChild(block);
+            const blockParent = block.parentElement;
+            blockParent.insertBefore(quote, block.nextSibling);
+            if (block.nodeName === "P" && (block.innerHTML === "<br>" || block.innerHTML.replace(/\u200B/g, "") === "")) {
+                blockParent.removeChild(block);
             }
             // avoid adjacent blocks that are not paragraphs
-            var sibling = quote.previousElementSibling;
-            if (sibling && sibling.nodeName !== 'P') {
-                sibling = elCreate('p');
-                sibling.textContent = '\u200B';
-                quote.parentNode.insertBefore(sibling, quote);
+            let sibling = quote.previousElementSibling;
+            if (sibling && sibling.nodeName !== "P") {
+                sibling = document.createElement("p");
+                sibling.textContent = "\u200B";
+                quote.insertAdjacentElement("beforebegin", sibling);
             }
             this._editor.WoltLabCaret.paragraphAfterBlock(quote);
             this._editor.buffer.set();
-        },
+        }
         /**
          * Toggles the quote block on button click.
-         *
-         * @protected
          */
-        _click: function () {
-            this._editor.button.toggle({}, 'woltlab-quote', 'func', 'block.format');
-            var quote = this._editor.selection.block();
-            if (quote && quote.nodeName === 'WOLTLAB-QUOTE') {
+        _click() {
+            this._editor.button.toggle({}, "woltlab-quote", "func", "block.format");
+            const quote = this._editor.selection.block();
+            if (quote && quote.nodeName === "WOLTLAB-QUOTE") {
                 this._setTitle(quote);
-                quote.addEventListener('click', this._callbackEdit);
+                quote.addEventListener("click", (ev) => this._edit(ev));
                 // work-around for Safari
                 this._editor.caret.end(quote);
             }
-        },
+        }
         /**
          * Binds event listeners and sets quote title on both editor
          * initialization and when switching back from code view.
-         *
-         * @protected
          */
-        _observeLoad: function () {
-            var quote;
-            for (var i = 0, length = this._quotes.length; i < length; i++) {
-                quote = this._quotes[i];
-                quote.addEventListener('mousedown', this._callbackEdit);
+        _observeLoad() {
+            document.querySelectorAll("woltlab-quote").forEach((quote) => {
+                quote.addEventListener("mousedown", (ev) => this._edit(ev));
                 this._setTitle(quote);
-            }
-        },
+            });
+        }
         /**
          * Opens the dialog overlay to edit the quote's properties.
-         *
-         * @param       {Event}         event           event object
-         * @protected
          */
-        _edit: function (event) {
-            var quote = event.currentTarget;
+        _edit(event) {
+            const quote = event.currentTarget;
             if (_headerHeight === 0) {
                 _headerHeight = UiRedactorPseudoHeader.getHeight(quote);
             }
             // check if the click hit the header
-            var offset = DomUtil.offset(quote);
-            if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
+            const offset = Util_1.default.offset(quote);
+            if (event.pageY > offset.top && event.pageY < offset.top + _headerHeight) {
                 event.preventDefault();
                 this._editor.selection.save();
                 this._quote = quote;
-                UiDialog.open(this);
+                Dialog_1.default.open(this);
             }
-        },
+        }
         /**
          * Saves the changes to the quote's properties.
          *
          * @protected
          */
-        _dialogSubmit: function () {
-            var id = 'redactor-quote-' + this._elementId;
-            var urlInput = elById(id + '-url');
-            var url = urlInput.value.replace(/\u200B/g, '').trim();
+        _dialogSubmit() {
+            const id = `redactor-quote-${this._elementId}`;
+            const urlInput = document.getElementById(`${id}-url`);
+            const url = urlInput.value.replace(/\u200B/g, "").trim();
             // simple test to check if it at least looks like it could be a valid url
-            if (url.length && !/^https?:\/\/[^\/]+/.test(url)) {
-                elInnerError(urlInput, Language.get('wcf.editor.quote.url.error.invalid'));
+            if (url.length && !/^https?:\/\/[^/]+/.test(url)) {
+                Util_1.default.innerError(urlInput, Language.get("wcf.editor.quote.url.error.invalid"));
                 return;
             }
             else {
-                elInnerError(urlInput, false);
+                Util_1.default.innerError(urlInput, false);
             }
+            const quote = this._quote;
             // set author
-            elData(this._quote, 'author', elById(id + '-author').value);
+            const author = document.getElementById(id + "-author");
+            quote.dataset.author = author.value;
             // set url
-            elData(this._quote, 'link', url);
-            this._setTitle(this._quote);
-            this._editor.caret.after(this._quote);
-            UiDialog.close(this);
-        },
+            quote.dataset.link = url;
+            this._setTitle(quote);
+            this._editor.caret.after(quote);
+            Dialog_1.default.close(this);
+        }
         /**
          * Sets or updates the quote's header title.
-         *
-         * @param       {Element}       quote     quote element
-         * @protected
          */
-        _setTitle: function (quote) {
-            var title = Language.get('wcf.editor.quote.title', {
-                author: elData(quote, 'author'),
-                url: elData(quote, 'url')
+        _setTitle(quote) {
+            const title = Language.get("wcf.editor.quote.title", {
+                author: quote.dataset.author,
+                url: quote.dataset.url,
             });
-            if (elData(quote, 'title') !== title) {
-                elData(quote, 'title', title);
+            if (quote.dataset.title !== title) {
+                quote.dataset.title = title;
             }
-        },
-        _delete: function (event) {
+        }
+        _delete(event) {
             event.preventDefault();
-            var caretEnd = this._quote.nextElementSibling || this._quote.previousElementSibling;
-            if (caretEnd === null && this._quote.parentNode !== this._editor.core.editor()[0]) {
-                caretEnd = this._quote.parentNode;
+            const quote = this._quote;
+            let caretEnd = quote.nextElementSibling || quote.previousElementSibling;
+            if (caretEnd === null && quote.parentElement !== this._editor.core.editor()[0]) {
+                caretEnd = quote.parentElement;
             }
             if (caretEnd === null) {
-                this._editor.code.set('');
+                this._editor.code.set("");
                 this._editor.focus.end();
             }
             else {
-                elRemove(this._quote);
+                quote.remove();
                 this._editor.caret.end(caretEnd);
             }
-            UiDialog.close(this);
-        },
-        _dialogSetup: function () {
-            var id = 'redactor-quote-' + this._elementId, idAuthor = id + '-author', idButtonDelete = id + '-button-delete', idButtonSave = id + '-button-save', idUrl = id + '-url';
+            Dialog_1.default.close(this);
+        }
+        _dialogSetup() {
+            const id = `redactor-quote-${this._elementId}`;
+            const idAuthor = `${id}-author`;
+            const idButtonDelete = `${id}-button-delete`;
+            const idButtonSave = `${id}-button-save`;
+            const idUrl = `${id}-url`;
             return {
                 id: id,
                 options: {
-                    onClose: (function () {
+                    onClose: () => {
                         this._editor.selection.restore();
-                        UiDialog.destroy(this);
-                    }).bind(this),
-                    onSetup: (function () {
-                        elById(idButtonDelete).addEventListener('click', this._delete.bind(this));
-                    }).bind(this),
-                    onShow: (function () {
-                        elById(idAuthor).value = elData(this._quote, 'author');
-                        elById(idUrl).value = elData(this._quote, 'link');
-                    }).bind(this),
-                    title: Language.get('wcf.editor.quote.edit')
+                        Dialog_1.default.destroy(this);
+                    },
+                    onSetup: () => {
+                        const button = document.getElementById(idButtonDelete);
+                        button.addEventListener("click", (ev) => this._delete(ev));
+                    },
+                    onShow: () => {
+                        const author = document.getElementById(idAuthor);
+                        author.value = this._quote.dataset.author || "";
+                        const url = document.getElementById(idUrl);
+                        url.value = this._quote.dataset.link || "";
+                    },
+                    title: Language.get("wcf.editor.quote.edit"),
                 },
-                source: '<div class="section">'
-                    + '<dl>'
-                    + '<dt><label for="' + idAuthor + '">' + Language.get('wcf.editor.quote.author') + '</label></dt>'
-                    + '<dd>'
-                    + '<input type="text" id="' + idAuthor + '" class="long" data-dialog-submit-on-enter="true">'
-                    + '</dd>'
-                    + '</dl>'
-                    + '<dl>'
-                    + '<dt><label for="' + idUrl + '">' + Language.get('wcf.editor.quote.url') + '</label></dt>'
-                    + '<dd>'
-                    + '<input type="text" id="' + idUrl + '" class="long" data-dialog-submit-on-enter="true">'
-                    + '<small>' + Language.get('wcf.editor.quote.url.description') + '</small>'
-                    + '</dd>'
-                    + '</dl>'
-                    + '</div>'
-                    + '<div class="formSubmit">'
-                    + '<button id="' + idButtonSave + '" class="buttonPrimary" data-type="submit">' + Language.get('wcf.global.button.save') + '</button>'
-                    + '<button id="' + idButtonDelete + '">' + Language.get('wcf.global.button.delete') + '</button>'
-                    + '</div>'
+                source: `<div class="section">
+          <dl>
+            <dt>
+              <label for="${idAuthor}">${Language.get("wcf.editor.quote.author")}</label>
+            </dt>
+            <dd>
+              <input type="text" id="${idAuthor}" class="long" data-dialog-submit-on-enter="true">
+            </dd>
+          </dl>
+          <dl>
+            <dt>
+              <label for="${idUrl}">${Language.get("wcf.editor.quote.url")}</label>
+            </dt>
+            <dd>
+              <input type="text" id="${idUrl}" class="long" data-dialog-submit-on-enter="true">
+              <small>${Language.get("wcf.editor.quote.url.description")}</small>
+            </dd>
+          </dl>
+        </div>
+        <div class="formSubmit">
+          <button id="${idButtonSave}" class="buttonPrimary" data-type="submit">${Language.get("wcf.global.button.save")}</button>
+          <button id="${idButtonDelete}">${Language.get("wcf.global.button.delete")}</button>
+        </div>`,
             };
         }
-    };
+    }
+    Core.enableLegacyInheritance(UiRedactorQuote);
     return UiRedactorQuote;
 });
index a22ac4252542a178c89e66ff38e14abe289ab9ad..714bd4e386b50b490529275fdc22287fc302fa29 100644 (file)
@@ -11,6 +11,7 @@ export interface RedactorEditor {
     set(): void;
   };
   button: {
+    addCallback(button: JQuery, callback: () => void): void;
     toggle(event: MouseEvent | object, btnName: string, type: string, callback: string, args?: object): void;
   };
   caret: {
@@ -46,9 +47,15 @@ export interface RedactorEditor {
     isEmpty(html: string): boolean;
   };
 
+  WoltLabCaret: {
+    paragraphAfterBlock(quote: HTMLElement): void;
+  };
   WoltLabEvent: {
     register(event: string, callback: (data: WoltLabEventData) => void): void;
   };
+  WoltLabSource: {
+    isActive(): boolean;
+  };
 }
 
 export interface WoltLabEventData {
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Quote.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Quote.js
deleted file mode 100644 (file)
index 2541bed..0000000
+++ /dev/null
@@ -1,310 +0,0 @@
-/**
- * Manages quotes.
- *
- * @author      Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module      WoltLabSuite/Core/Ui/Redactor/Quote
- */
-define(['Core', 'EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', './Metacode', './PseudoHeader'], function (Core, EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog, UiRedactorMetacode, UiRedactorPseudoHeader) {
-       "use strict";
-       
-       if (!COMPILER_TARGET_DEFAULT) {
-               var Fake = function() {};
-               Fake.prototype = {
-                       init: function() {},
-                       _insertQuote: function() {},
-                       _click: function() {},
-                       _observeLoad: function() {},
-                       _edit: function() {},
-                       _save: function() {},
-                       _setTitle: function() {},
-                       _delete: function() {},
-                       _dialogSetup: function() {},
-                       _dialogSubmit: function() {}
-               };
-               return Fake;
-       }
-       
-       var _headerHeight = 0;
-       
-       /**
-        * @param       {Object}        editor  editor instance
-        * @param       {jQuery}        button  toolbar button
-        * @constructor
-        */
-       function UiRedactorQuote(editor, button) { this.init(editor, button); }
-       UiRedactorQuote.prototype = {
-               /**
-                * Initializes the quote management.
-                * 
-                * @param       {Object}        editor  editor instance
-                * @param       {jQuery}        button  toolbar button
-                */
-               init: function(editor, button) {
-                       this._quote = null;
-                       this._quotes = elByTag('woltlab-quote', editor.$editor[0]);
-                       this._editor = editor;
-                       this._elementId = this._editor.$element[0].id;
-                       
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
-                       
-                       this._editor.button.addCallback(button, this._click.bind(this));
-                       
-                       // static bind to ensure that removing works
-                       this._callbackEdit = this._edit.bind(this);
-                       
-                       // bind listeners on init
-                       this._observeLoad();
-                       
-                       // quote manager
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'insertQuote_' + this._elementId, this._insertQuote.bind(this));
-               },
-               
-               /**
-                * Inserts a quote.
-                * 
-                * @param       {Object}        data            quote data
-                * @protected
-                */
-               _insertQuote: function (data) {
-                       if (this._editor.WoltLabSource.isActive()) {
-                               return;
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'showEditor');
-                       
-                       var editor = this._editor.core.editor()[0];
-                       this._editor.selection.restore();
-                       
-                       this._editor.buffer.set();
-                       
-                       // caret must be within a `<p>`, if it is not: move it
-                       var block = this._editor.selection.block();
-                       if (block === false) {
-                               this._editor.focus.end();
-                               block = this._editor.selection.block();
-                       }
-                       
-                       while (block && block.parentNode !== editor) {
-                               block = block.parentNode;
-                       }
-                       
-                       var quote = elCreate('woltlab-quote');
-                       elData(quote, 'author', data.author);
-                       elData(quote, 'link', data.link);
-                       
-                       var content = data.content;
-                       if (data.isText) {
-                               content = StringUtil.escapeHTML(content);
-                               content = '<p>' + content + '</p>';
-                               content = content.replace(/\n\n/g, '</p><p>');
-                               content = content.replace(/\n/g, '<br>');
-                       }
-                       else {
-                               //noinspection JSUnresolvedFunction
-                               content = UiRedactorMetacode.convertFromHtml(this._editor.$element[0].id, content);
-                       }
-                       
-                       // bypass the editor as `insert.html()` doesn't like us
-                       quote.innerHTML = content;
-                       
-                       block.parentNode.insertBefore(quote, block.nextSibling);
-                       
-                       if (block.nodeName === 'P' && (block.innerHTML === '<br>' || block.innerHTML.replace(/\u200B/g, '') === '')) {
-                               block.parentNode.removeChild(block);
-                       }
-                       
-                       // avoid adjacent blocks that are not paragraphs
-                       var sibling = quote.previousElementSibling;
-                       if (sibling && sibling.nodeName !== 'P') {
-                               sibling = elCreate('p');
-                               sibling.textContent = '\u200B';
-                               quote.parentNode.insertBefore(sibling, quote);
-                       }
-                       
-                       this._editor.WoltLabCaret.paragraphAfterBlock(quote);
-                       
-                       this._editor.buffer.set();
-               },
-               
-               /**
-                * Toggles the quote block on button click.
-                * 
-                * @protected
-                */
-               _click: function() {
-                       this._editor.button.toggle({}, 'woltlab-quote', 'func', 'block.format');
-                       
-                       var quote = this._editor.selection.block();
-                       if (quote && quote.nodeName === 'WOLTLAB-QUOTE') {
-                               this._setTitle(quote);
-                               
-                               quote.addEventListener('click', this._callbackEdit);
-                               
-                               // work-around for Safari
-                               this._editor.caret.end(quote);
-                       }
-               },
-               
-               /**
-                * Binds event listeners and sets quote title on both editor
-                * initialization and when switching back from code view.
-                * 
-                * @protected
-                */
-               _observeLoad: function() {
-                       var quote;
-                       for (var i = 0, length = this._quotes.length; i < length; i++) {
-                               quote = this._quotes[i];
-                               
-                               quote.addEventListener('mousedown', this._callbackEdit);
-                               this._setTitle(quote);
-                       }
-               },
-               
-               /**
-                * Opens the dialog overlay to edit the quote's properties.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _edit: function(event) {
-                       var quote = event.currentTarget;
-                       
-                       if (_headerHeight === 0) {
-                               _headerHeight = UiRedactorPseudoHeader.getHeight(quote);
-                       }
-                       
-                       // check if the click hit the header
-                       var offset = DomUtil.offset(quote);
-                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
-                               event.preventDefault();
-                               
-                               this._editor.selection.save();
-                               this._quote = quote;
-                               
-                               UiDialog.open(this);
-                       }
-               },
-               
-               /**
-                * Saves the changes to the quote's properties.
-                * 
-                * @protected
-                */
-               _dialogSubmit: function() {
-                       var id = 'redactor-quote-' + this._elementId;
-                       var urlInput = elById(id + '-url');
-                       
-                       var url = urlInput.value.replace(/\u200B/g, '').trim();
-                       // simple test to check if it at least looks like it could be a valid url
-                       if (url.length && !/^https?:\/\/[^\/]+/.test(url)) {
-                               elInnerError(urlInput, Language.get('wcf.editor.quote.url.error.invalid'));
-                               return;
-                       }
-                       else {
-                               elInnerError(urlInput, false);
-                       }
-                       
-                       // set author
-                       elData(this._quote, 'author', elById(id + '-author').value);
-                       
-                       // set url
-                       elData(this._quote, 'link', url);
-                       
-                       this._setTitle(this._quote);
-                       this._editor.caret.after(this._quote);
-                       
-                       UiDialog.close(this);
-               },
-               
-               /**
-                * Sets or updates the quote's header title.
-                * 
-                * @param       {Element}       quote     quote element
-                * @protected
-                */
-               _setTitle: function(quote) {
-                       var title = Language.get('wcf.editor.quote.title', {
-                               author: elData(quote, 'author'),
-                               url: elData(quote, 'url')
-                       });
-                       
-                       if (elData(quote, 'title') !== title) {
-                               elData(quote, 'title', title);
-                       }
-               },
-               
-               _delete: function (event) {
-                       event.preventDefault();
-                       
-                       var caretEnd = this._quote.nextElementSibling || this._quote.previousElementSibling;
-                       if (caretEnd === null && this._quote.parentNode !== this._editor.core.editor()[0]) {
-                               caretEnd = this._quote.parentNode;
-                       }
-                       
-                       if (caretEnd === null) {
-                               this._editor.code.set('');
-                               this._editor.focus.end();
-                       }
-                       else {
-                               elRemove(this._quote);
-                               this._editor.caret.end(caretEnd);
-                       }
-                       
-                       UiDialog.close(this);
-               },
-               
-               _dialogSetup: function() {
-                       var id = 'redactor-quote-' + this._elementId,
-                           idAuthor = id + '-author',
-                           idButtonDelete = id + '-button-delete',
-                           idButtonSave = id + '-button-save',
-                           idUrl = id + '-url';
-                       
-                       return {
-                               id: id,
-                               options: {
-                                       onClose: (function () {
-                                               this._editor.selection.restore();
-                                               
-                                               UiDialog.destroy(this);
-                                       }).bind(this),
-                                       
-                                       onSetup: (function() {
-                                               elById(idButtonDelete).addEventListener('click', this._delete.bind(this));
-                                       }).bind(this),
-                                       
-                                       onShow: (function() {
-                                               elById(idAuthor).value = elData(this._quote, 'author');
-                                               elById(idUrl).value = elData(this._quote, 'link');
-                                       }).bind(this),
-                                       
-                                       title: Language.get('wcf.editor.quote.edit')
-                               },
-                               source: '<div class="section">'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idAuthor + '">' + Language.get('wcf.editor.quote.author') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="text" id="' + idAuthor + '" class="long" data-dialog-submit-on-enter="true">'
-                                               + '</dd>'
-                                       + '</dl>'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idUrl + '">' + Language.get('wcf.editor.quote.url') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="text" id="' + idUrl + '" class="long" data-dialog-submit-on-enter="true">'
-                                                       + '<small>' + Language.get('wcf.editor.quote.url.description') + '</small>'
-                                               + '</dd>'
-                                       + '</dl>'
-                               + '</div>'
-                               + '<div class="formSubmit">'
-                                       + '<button id="' + idButtonSave + '" class="buttonPrimary" data-type="submit">' + Language.get('wcf.global.button.save') + '</button>'
-                                       + '<button id="' + idButtonDelete + '">' + Language.get('wcf.global.button.delete') + '</button>'
-                               + '</div>'
-                       };
-               }
-       };
-       
-       return UiRedactorQuote;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Quote.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Quote.ts
new file mode 100644 (file)
index 0000000..78090e7
--- /dev/null
@@ -0,0 +1,297 @@
+/**
+ * Manages quotes.
+ *
+ * @author      Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module      WoltLabSuite/Core/Ui/Redactor/Quote
+ */
+
+import * as Core from "../../Core";
+import DomUtil from "../../Dom/Util";
+import * as EventHandler from "../../Event/Handler";
+import * as Language from "../../Language";
+import * as StringUtil from "../../StringUtil";
+import UiDialog from "../Dialog";
+import { DialogCallbackSetup } from "../Dialog/Data";
+import { RedactorEditor } from "./Editor";
+import * as UiRedactorMetacode from "./Metacode";
+import * as UiRedactorPseudoHeader from "./PseudoHeader";
+
+interface QuoteData {
+  author: string;
+  content: string;
+  isText: boolean;
+  link: string;
+}
+
+let _headerHeight = 0;
+
+class UiRedactorQuote {
+  protected readonly _editor: RedactorEditor;
+  protected readonly _elementId: string;
+  protected _quote: HTMLElement | null = null;
+
+  /**
+   * Initializes the quote management.
+   */
+  constructor(editor: RedactorEditor, button: JQuery) {
+    this._editor = editor;
+    this._elementId = this._editor.$element[0].id;
+
+    EventHandler.add("com.woltlab.wcf.redactor2", `observe_load_${this._elementId}`, () => this._observeLoad());
+
+    this._editor.button.addCallback(button, this._click.bind(this));
+
+    // bind listeners on init
+    this._observeLoad();
+
+    // quote manager
+    EventHandler.add("com.woltlab.wcf.redactor2", `insertQuote_${this._elementId}`, (data) => this._insertQuote(data));
+  }
+
+  /**
+   * Inserts a quote.
+   */
+  protected _insertQuote(data: QuoteData): void {
+    if (this._editor.WoltLabSource.isActive()) {
+      return;
+    }
+
+    EventHandler.fire("com.woltlab.wcf.redactor2", "showEditor");
+
+    const editor = this._editor.core.editor()[0];
+    this._editor.selection.restore();
+
+    this._editor.buffer.set();
+
+    // caret must be within a `<p>`, if it is not: move it
+    let block = this._editor.selection.block();
+    if (block === false) {
+      this._editor.focus.end();
+      block = this._editor.selection.block() as HTMLElement;
+    }
+
+    while (block && block.parentElement !== editor) {
+      block = block.parentElement!;
+    }
+
+    const quote = document.createElement("woltlab-quote");
+    quote.dataset.author = data.author;
+    quote.dataset.link = data.link;
+
+    let content = data.content;
+    if (data.isText) {
+      content = StringUtil.escapeHTML(content);
+      content = `<p>${content}</p>`;
+      content = content.replace(/\n\n/g, "</p><p>");
+      content = content.replace(/\n/g, "<br>");
+    } else {
+      content = UiRedactorMetacode.convertFromHtml(this._editor.$element[0].id, content);
+    }
+
+    // bypass the editor as `insert.html()` doesn't like us
+    quote.innerHTML = content;
+
+    const blockParent = block.parentElement!;
+    blockParent.insertBefore(quote, block.nextSibling);
+
+    if (block.nodeName === "P" && (block.innerHTML === "<br>" || block.innerHTML.replace(/\u200B/g, "") === "")) {
+      blockParent.removeChild(block);
+    }
+
+    // avoid adjacent blocks that are not paragraphs
+    let sibling = quote.previousElementSibling;
+    if (sibling && sibling.nodeName !== "P") {
+      sibling = document.createElement("p");
+      sibling.textContent = "\u200B";
+      quote.insertAdjacentElement("beforebegin", sibling);
+    }
+
+    this._editor.WoltLabCaret.paragraphAfterBlock(quote);
+
+    this._editor.buffer.set();
+  }
+
+  /**
+   * Toggles the quote block on button click.
+   */
+  protected _click(): void {
+    this._editor.button.toggle({}, "woltlab-quote", "func", "block.format");
+
+    const quote = this._editor.selection.block();
+    if (quote && quote.nodeName === "WOLTLAB-QUOTE") {
+      this._setTitle(quote);
+
+      quote.addEventListener("click", (ev) => this._edit(ev));
+
+      // work-around for Safari
+      this._editor.caret.end(quote);
+    }
+  }
+
+  /**
+   * Binds event listeners and sets quote title on both editor
+   * initialization and when switching back from code view.
+   */
+  protected _observeLoad(): void {
+    document.querySelectorAll("woltlab-quote").forEach((quote: HTMLElement) => {
+      quote.addEventListener("mousedown", (ev) => this._edit(ev));
+      this._setTitle(quote);
+    });
+  }
+
+  /**
+   * Opens the dialog overlay to edit the quote's properties.
+   */
+  protected _edit(event: MouseEvent): void {
+    const quote = event.currentTarget as HTMLElement;
+
+    if (_headerHeight === 0) {
+      _headerHeight = UiRedactorPseudoHeader.getHeight(quote);
+    }
+
+    // check if the click hit the header
+    const offset = DomUtil.offset(quote);
+    if (event.pageY > offset.top && event.pageY < offset.top + _headerHeight) {
+      event.preventDefault();
+
+      this._editor.selection.save();
+      this._quote = quote;
+
+      UiDialog.open(this);
+    }
+  }
+
+  /**
+   * Saves the changes to the quote's properties.
+   *
+   * @protected
+   */
+  _dialogSubmit(): void {
+    const id = `redactor-quote-${this._elementId}`;
+    const urlInput = document.getElementById(`${id}-url`) as HTMLInputElement;
+
+    const url = urlInput.value.replace(/\u200B/g, "").trim();
+    // simple test to check if it at least looks like it could be a valid url
+    if (url.length && !/^https?:\/\/[^/]+/.test(url)) {
+      DomUtil.innerError(urlInput, Language.get("wcf.editor.quote.url.error.invalid"));
+
+      return;
+    } else {
+      DomUtil.innerError(urlInput, false);
+    }
+
+    const quote = this._quote!;
+
+    // set author
+    const author = document.getElementById(id + "-author") as HTMLInputElement;
+    quote.dataset.author = author.value;
+
+    // set url
+    quote.dataset.link = url;
+
+    this._setTitle(quote);
+    this._editor.caret.after(quote);
+
+    UiDialog.close(this);
+  }
+
+  /**
+   * Sets or updates the quote's header title.
+   */
+  protected _setTitle(quote: HTMLElement): void {
+    const title = Language.get("wcf.editor.quote.title", {
+      author: quote.dataset.author!,
+      url: quote.dataset.url!,
+    });
+
+    if (quote.dataset.title !== title) {
+      quote.dataset.title = title;
+    }
+  }
+
+  protected _delete(event: MouseEvent): void {
+    event.preventDefault();
+
+    const quote = this._quote!;
+
+    let caretEnd = quote.nextElementSibling || quote.previousElementSibling;
+    if (caretEnd === null && quote.parentElement !== this._editor.core.editor()[0]) {
+      caretEnd = quote.parentElement;
+    }
+
+    if (caretEnd === null) {
+      this._editor.code.set("");
+      this._editor.focus.end();
+    } else {
+      quote.remove();
+      this._editor.caret.end(caretEnd);
+    }
+
+    UiDialog.close(this);
+  }
+
+  _dialogSetup(): ReturnType<DialogCallbackSetup> {
+    const id = `redactor-quote-${this._elementId}`;
+    const idAuthor = `${id}-author`;
+    const idButtonDelete = `${id}-button-delete`;
+    const idButtonSave = `${id}-button-save`;
+    const idUrl = `${id}-url`;
+
+    return {
+      id: id,
+      options: {
+        onClose: () => {
+          this._editor.selection.restore();
+
+          UiDialog.destroy(this);
+        },
+
+        onSetup: () => {
+          const button = document.getElementById(idButtonDelete) as HTMLButtonElement;
+          button.addEventListener("click", (ev) => this._delete(ev));
+        },
+
+        onShow: () => {
+          const author = document.getElementById(idAuthor) as HTMLInputElement;
+          author.value = this._quote!.dataset.author || "";
+
+          const url = document.getElementById(idUrl) as HTMLInputElement;
+          url.value = this._quote!.dataset.link || "";
+        },
+
+        title: Language.get("wcf.editor.quote.edit"),
+      },
+      source: `<div class="section">
+          <dl>
+            <dt>
+              <label for="${idAuthor}">${Language.get("wcf.editor.quote.author")}</label>
+            </dt>
+            <dd>
+              <input type="text" id="${idAuthor}" class="long" data-dialog-submit-on-enter="true">
+            </dd>
+          </dl>
+          <dl>
+            <dt>
+              <label for="${idUrl}">${Language.get("wcf.editor.quote.url")}</label>
+            </dt>
+            <dd>
+              <input type="text" id="${idUrl}" class="long" data-dialog-submit-on-enter="true">
+              <small>${Language.get("wcf.editor.quote.url.description")}</small>
+            </dd>
+          </dl>
+        </div>
+        <div class="formSubmit">
+          <button id="${idButtonSave}" class="buttonPrimary" data-type="submit">${Language.get(
+        "wcf.global.button.save",
+      )}</button>
+          <button id="${idButtonDelete}">${Language.get("wcf.global.button.delete")}</button>
+        </div>`,
+    };
+  }
+}
+
+Core.enableLegacyInheritance(UiRedactorQuote);
+
+export = UiRedactorQuote;