Convert `Ui/Redactor/Code` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Wed, 4 Nov 2020 12:06:47 +0000 (13:06 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 4 Nov 2020 12:06:47 +0000 (13:06 +0100)
package-lock.json
package.json
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Code.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Code.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Code.ts [new file with mode: 0644]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Editor.ts

index 922feececf3ec9523e24de4bf2942ea4645d8060..4b08d05163e67f3ce6d8cc4147c98400cd78f9c8 100644 (file)
       "integrity": "sha512-KJPg2vGt1l03VaDK1SPKDcm/I5RVexO5Jyo/kGPlaS7SqVOkY83O3f1iyff981UnSzbF3Tg1Zw0r6vX6vB6JxA==",
       "dev": true
     },
+    "@types/prismjs": {
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.16.2.tgz",
+      "integrity": "sha512-1M/j21xgTde7RPtpJVQebW5rzrquj7S+wnqt4x9uWrIPpr0Ya/uXypcqC2aUQL5gtLXFCKSH7GnjfAijMdfbuA==",
+      "dev": true
+    },
     "@types/sizzle": {
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
index cae4a51704e79dda3dc9a203453702b1425a390f..a141704ed3bb92cc8d620e185401986175a6f676 100644 (file)
@@ -5,6 +5,7 @@
     "@types/facebook-js-sdk": "^3.3.1",
     "@types/jquery": "^3.5.4",
     "@types/pica": "^5.1.2",
+    "@types/prismjs": "^1.16.2",
     "@typescript-eslint/eslint-plugin": "^4.6.0",
     "@typescript-eslint/parser": "^4.6.0",
     "@woltlab/zxcvbn": "git+https://github.com/WoltLab/zxcvbn.git#master",
index dd8038f2e5c9b847a9f97b55544f096260c0f4d4..11588f12bf1dc62573c94c58d0e3f737962d3470 100644 (file)
  * Manages code blocks.
  *
  * @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/Code
  */
-define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', './PseudoHeader', 'prism/prism-meta'], function (EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog, UiRedactorPseudoHeader, PrismMeta) {
+define(["require", "exports", "tslib", "../../Core", "../../Dom/Util", "../../Event/Handler", "../../Language", "../../StringUtil", "../Dialog", "./PseudoHeader"], function (require, exports, tslib_1, Core, Util_1, EventHandler, Language, StringUtil, Dialog_1, UiRedactorPseudoHeader) {
     "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        var Fake = function () { };
-        Fake.prototype = {
-            init: function () { },
-            _bbcodeCode: function () { },
-            _observeLoad: function () { },
-            _edit: function () { },
-            _setTitle: function () { },
-            _delete: function () { },
-            _dialogSetup: function () { },
-            _dialogSubmit: function () { }
-        };
-        return Fake;
-    }
-    var _headerHeight = 0;
-    /**
-     * @param       {Object}        editor  editor instance
-     * @constructor
-     */
-    function UiRedactorCode(editor) { this.init(editor); }
-    UiRedactorCode.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);
+    UiRedactorPseudoHeader = tslib_1.__importStar(UiRedactorPseudoHeader);
+    let _headerHeight = 0;
+    class UiRedactorCode {
         /**
          * Initializes the source code management.
-         *
-         * @param       {Object}        editor  editor instance
          */
-        init: function (editor) {
+        constructor(editor) {
+            this._pre = null;
             this._editor = editor;
             this._elementId = this._editor.$element[0].id;
-            this._pre = null;
-            EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_code_' + this._elementId, this._bbcodeCode.bind(this));
-            EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
+            EventHandler.add("com.woltlab.wcf.redactor2", `bbcode_code_${this._elementId}`, (data) => this._bbcodeCode(data));
+            EventHandler.add("com.woltlab.wcf.redactor2", `observe_load_${this._elementId}`, () => this._observeLoad());
             // support for active button marking
-            this._editor.opts.activeButtonsStates.pre = 'code';
+            this._editor.opts.activeButtonsStates.pre = "code";
             // static bind to ensure that removing works
             this._callbackEdit = this._edit.bind(this);
             // bind listeners on init
             this._observeLoad();
-        },
+        }
         /**
          * Intercepts the insertion of `[code]` tags and uses a native `<pre>` instead.
-         *
-         * @param       {Object}        data    event data
-         * @protected
          */
-        _bbcodeCode: function (data) {
+        _bbcodeCode(data) {
             data.cancel = true;
-            var pre = this._editor.selection.block();
-            if (pre && pre.nodeName === 'PRE' && pre.classList.contains('woltlabHtml')) {
+            let pre = this._editor.selection.block();
+            if (pre && pre.nodeName === "PRE" && pre.classList.contains("woltlabHtml")) {
                 return;
             }
-            this._editor.button.toggle({}, 'pre', 'func', 'block.format');
+            this._editor.button.toggle({}, "pre", "func", "block.format");
             pre = this._editor.selection.block();
-            if (pre && pre.nodeName === 'PRE' && !pre.classList.contains('woltlabHtml')) {
-                if (pre.childElementCount === 1 && pre.children[0].nodeName === 'BR') {
+            if (pre && pre.nodeName === "PRE" && !pre.classList.contains("woltlabHtml")) {
+                if (pre.childElementCount === 1 && pre.children[0].nodeName === "BR") {
                     // drop superfluous linebreak
                     pre.removeChild(pre.children[0]);
                 }
                 this._setTitle(pre);
-                pre.addEventListener('click', this._callbackEdit);
+                pre.addEventListener("click", this._callbackEdit);
                 // work-around for Safari
                 this._editor.caret.end(pre);
             }
-        },
+        }
         /**
          * Binds event listeners and sets quote title on both editor
          * initialization and when switching back from code view.
-         *
-         * @protected
          */
-        _observeLoad: function () {
-            elBySelAll('pre:not(.woltlabHtml)', this._editor.$editor[0], (function (pre) {
-                pre.addEventListener('mousedown', this._callbackEdit);
+        _observeLoad() {
+            this._editor.$editor[0].querySelectorAll("pre:not(.woltlabHtml)").forEach((pre) => {
+                pre.addEventListener("mousedown", this._callbackEdit);
                 this._setTitle(pre);
-            }).bind(this));
-        },
+            });
+        }
         /**
          * Opens the dialog overlay to edit the code's properties.
-         *
-         * @param       {Event}         event           event object
-         * @protected
          */
-        _edit: function (event) {
-            var pre = event.currentTarget;
+        _edit(event) {
+            const pre = event.currentTarget;
             if (_headerHeight === 0) {
                 _headerHeight = UiRedactorPseudoHeader.getHeight(pre);
             }
             // check if the click hit the header
-            var offset = DomUtil.offset(pre);
-            if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
+            const offset = Util_1.default.offset(pre);
+            if (event.pageY > offset.top && event.pageY < offset.top + _headerHeight) {
                 event.preventDefault();
                 this._editor.selection.save();
                 this._pre = pre;
-                UiDialog.open(this);
+                Dialog_1.default.open(this);
             }
-        },
+        }
         /**
          * Saves the changes to the code's properties.
-         *
-         * @protected
          */
-        _dialogSubmit: function () {
-            var id = 'redactor-code-' + this._elementId;
-            ['file', 'highlighter', 'line'].forEach((function (attr) {
-                elData(this._pre, attr, elById(id + '-' + attr).value);
-            }).bind(this));
-            this._setTitle(this._pre);
-            this._editor.caret.after(this._pre);
-            UiDialog.close(this);
-        },
+        _dialogSubmit() {
+            const id = "redactor-code-" + this._elementId;
+            const pre = this._pre;
+            ["file", "highlighter", "line"].forEach((attr) => {
+                const input = document.getElementById(`${id}-${attr}`);
+                pre.dataset[attr] = input.value;
+            });
+            this._setTitle(pre);
+            this._editor.caret.after(pre);
+            Dialog_1.default.close(this);
+        }
         /**
          * Sets or updates the code's header title.
-         *
-         * @param       {Element}       pre     code element
-         * @protected
          */
-        _setTitle: function (pre) {
-            var file = elData(pre, 'file'), highlighter = elData(pre, 'highlighter');
-            //noinspection JSUnresolvedVariable
-            highlighter = (this._editor.opts.woltlab.highlighters.indexOf(highlighter) !== -1) ? PrismMeta[highlighter].title : '';
-            var title = Language.get('wcf.editor.code.title', {
-                file: file,
-                highlighter: highlighter
+        _setTitle(pre) {
+            const file = pre.dataset.file;
+            let highlighter = pre.dataset.highlighter;
+            highlighter =
+                this._editor.opts.woltlab.highlighters.indexOf(highlighter) !== -1 ? PrismMeta[highlighter].title : "";
+            const title = Language.get("wcf.editor.code.title", {
+                file,
+                highlighter,
             });
-            if (elData(pre, 'title') !== title) {
-                elData(pre, 'title', title);
+            if (pre.dataset.title !== title) {
+                pre.dataset.title = title;
             }
-        },
-        _delete: function (event) {
+        }
+        _delete(event) {
             event.preventDefault();
-            var caretEnd = this._pre.nextElementSibling || this._pre.previousElementSibling;
-            if (caretEnd === null && this._pre.parentNode !== this._editor.core.editor()[0]) {
-                caretEnd = this._pre.parentNode;
+            const pre = this._pre;
+            let caretEnd = pre.nextElementSibling || pre.previousElementSibling;
+            if (caretEnd === null && pre.parentElement !== this._editor.core.editor()[0]) {
+                caretEnd = pre.parentElement;
             }
             if (caretEnd === null) {
-                this._editor.code.set('');
+                this._editor.code.set("");
                 this._editor.focus.end();
             }
             else {
-                elRemove(this._pre);
+                pre.remove();
                 this._editor.caret.end(caretEnd);
             }
-            UiDialog.close(this);
-        },
-        _dialogSetup: function () {
-            var id = 'redactor-code-' + this._elementId, idButtonDelete = id + '-button-delete', idButtonSave = id + '-button-save', idFile = id + '-file', idHighlighter = id + '-highlighter', idLine = id + '-line';
+            Dialog_1.default.close(this);
+        }
+        _dialogSetup() {
+            const id = `redactor-code-${this._elementId}`;
+            const idButtonDelete = `${id}-button-delete`;
+            const idButtonSave = `${id}-button-save`;
+            const idFile = `${id}-file`;
+            const idHighlighter = `${id}-highlighter`;
+            const idLine = `${id}-line`;
             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));
+                        Dialog_1.default.destroy(this);
+                    },
+                    onSetup: () => {
+                        document.getElementById(idButtonDelete).addEventListener("click", (ev) => this._delete(ev));
                         // set highlighters
-                        var highlighters = '<option value="">' + Language.get('wcf.editor.code.highlighter.detect') + '</option>';
-                        highlighters += '<option value="plain">' + Language.get('wcf.editor.code.highlighter.plain') + '</option>';
-                        //noinspection JSUnresolvedVariable
-                        var values = this._editor.opts.woltlab.highlighters.map(function (highlighter) {
+                        let highlighters = `<option value="">${Language.get("wcf.editor.code.highlighter.detect")}</option>
+            <option value="plain">${Language.get("wcf.editor.code.highlighter.plain")}</option>`;
+                        const values = this._editor.opts.woltlab.highlighters.map((highlighter) => {
                             return [highlighter, PrismMeta[highlighter].title];
                         });
                         // sort by label
-                        values.sort(function (a, b) {
+                        values.sort((a, b) => {
                             if (a[1] < b[1]) {
                                 return -1;
                             }
@@ -180,48 +161,59 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                             }
                             return 0;
                         });
-                        values.forEach((function (value) {
-                            highlighters += '<option value="' + value[0] + '">' + StringUtil.escapeHTML(value[1]) + '</option>';
-                        }).bind(this));
-                        elById(idHighlighter).innerHTML = highlighters;
-                    }).bind(this),
-                    onShow: (function () {
-                        elById(idHighlighter).value = elData(this._pre, 'highlighter');
-                        var line = elData(this._pre, 'line');
-                        elById(idLine).value = (line === '') ? 1 : ~~line;
-                        elById(idFile).value = elData(this._pre, 'file');
-                    }).bind(this),
-                    title: Language.get('wcf.editor.code.edit')
+                        values.forEach((value) => {
+                            highlighters += `<option value="${value[0]}">${StringUtil.escapeHTML(value[1])}</option>`;
+                        });
+                        document.getElementById(idHighlighter).innerHTML = highlighters;
+                    },
+                    onShow: () => {
+                        const pre = this._pre;
+                        const highlighter = document.getElementById(idHighlighter);
+                        highlighter.value = pre.dataset.highlighter;
+                        const line = ~~(pre.dataset.line || 1);
+                        const lineInput = document.getElementById(idLine);
+                        lineInput.value = line.toString();
+                        const filename = document.getElementById(idFile);
+                        filename.value = pre.dataset.file;
+                    },
+                    title: Language.get("wcf.editor.code.edit"),
                 },
-                source: '<div class="section">'
-                    + '<dl>'
-                    + '<dt><label for="' + idHighlighter + '">' + Language.get('wcf.editor.code.highlighter') + '</label></dt>'
-                    + '<dd>'
-                    + '<select id="' + idHighlighter + '"></select>'
-                    + '<small>' + Language.get('wcf.editor.code.highlighter.description') + '</small>'
-                    + '</dd>'
-                    + '</dl>'
-                    + '<dl>'
-                    + '<dt><label for="' + idLine + '">' + Language.get('wcf.editor.code.line') + '</label></dt>'
-                    + '<dd>'
-                    + '<input type="number" id="' + idLine + '" min="0" value="1" class="long" data-dialog-submit-on-enter="true">'
-                    + '<small>' + Language.get('wcf.editor.code.line.description') + '</small>'
-                    + '</dd>'
-                    + '</dl>'
-                    + '<dl>'
-                    + '<dt><label for="' + idFile + '">' + Language.get('wcf.editor.code.file') + '</label></dt>'
-                    + '<dd>'
-                    + '<input type="text" id="' + idFile + '" class="long" data-dialog-submit-on-enter="true">'
-                    + '<small>' + Language.get('wcf.editor.code.file.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="${idHighlighter}">${Language.get("wcf.editor.code.highlighter")}</label>
+            </dt>
+            <dd>
+              <select id="${idHighlighter}"></select>
+              <small>${Language.get("wcf.editor.code.highlighter.description")}</small>
+            </dd>
+          </dl>
+          <dl>
+            <dt>
+              <label for="${idLine}">${Language.get("wcf.editor.code.line")}</label>
+            </dt>
+            <dd>
+              <input type="number" id="${idLine}" min="0" value="1" class="long" data-dialog-submit-on-enter="true">
+              <small>${Language.get("wcf.editor.code.line.description")}</small>
+            </dd>
+          </dl>
+          <dl>
+            <dt>
+              <label for="${idFile}">${Language.get("wcf.editor.code.file")}</label>
+            </dt>
+            <dd>
+              <input type="text" id="${idFile}" class="long" data-dialog-submit-on-enter="true">
+              <small>${Language.get("wcf.editor.code.file.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(UiRedactorCode);
     return UiRedactorCode;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Code.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Code.js
deleted file mode 100644 (file)
index 2799276..0000000
+++ /dev/null
@@ -1,278 +0,0 @@
-/**
- * Manages code blocks.
- *
- * @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/Code
- */
-define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', './PseudoHeader', 'prism/prism-meta'], function (EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog, UiRedactorPseudoHeader, PrismMeta) {
-       "use strict";
-       
-       if (!COMPILER_TARGET_DEFAULT) {
-               var Fake = function() {};
-               Fake.prototype = {
-                       init: function() {},
-                       _bbcodeCode: function() {},
-                       _observeLoad: function() {},
-                       _edit: function() {},
-                       _setTitle: function() {},
-                       _delete: function() {},
-                       _dialogSetup: function() {},
-                       _dialogSubmit: function() {}
-               };
-               return Fake;
-       }
-       
-       var _headerHeight = 0;
-       
-       /**
-        * @param       {Object}        editor  editor instance
-        * @constructor
-        */
-       function UiRedactorCode(editor) { this.init(editor); }
-       UiRedactorCode.prototype = {
-               /**
-                * Initializes the source code management.
-                * 
-                * @param       {Object}        editor  editor instance
-                */
-               init: function(editor) {
-                       this._editor = editor;
-                       this._elementId = this._editor.$element[0].id;
-                       this._pre = null;
-                       
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_code_' + this._elementId, this._bbcodeCode.bind(this));
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
-                       
-                       // support for active button marking
-                       this._editor.opts.activeButtonsStates.pre = 'code';
-                       
-                       // static bind to ensure that removing works
-                       this._callbackEdit = this._edit.bind(this);
-                       
-                       // bind listeners on init
-                       this._observeLoad();
-               },
-               
-               /**
-                * Intercepts the insertion of `[code]` tags and uses a native `<pre>` instead.
-                * 
-                * @param       {Object}        data    event data
-                * @protected
-                */
-               _bbcodeCode: function(data) {
-                       data.cancel = true;
-                       
-                       var pre = this._editor.selection.block();
-                       if (pre && pre.nodeName === 'PRE' && pre.classList.contains('woltlabHtml')) {
-                               return;
-                       }
-                       
-                       this._editor.button.toggle({}, 'pre', 'func', 'block.format');
-                       
-                       pre = this._editor.selection.block();
-                       if (pre && pre.nodeName === 'PRE' && !pre.classList.contains('woltlabHtml')) {
-                               if (pre.childElementCount === 1 && pre.children[0].nodeName === 'BR') {
-                                       // drop superfluous linebreak
-                                       pre.removeChild(pre.children[0]);
-                               }
-                               
-                               this._setTitle(pre);
-                               
-                               pre.addEventListener('click', this._callbackEdit);
-                               
-                               // work-around for Safari
-                               this._editor.caret.end(pre);
-                       }
-               },
-               
-               /**
-                * Binds event listeners and sets quote title on both editor
-                * initialization and when switching back from code view.
-                * 
-                * @protected
-                */
-               _observeLoad: function() {
-                       elBySelAll('pre:not(.woltlabHtml)', this._editor.$editor[0], (function(pre) {
-                               pre.addEventListener('mousedown', this._callbackEdit);
-                               this._setTitle(pre);
-                       }).bind(this));
-               },
-               
-               /**
-                * Opens the dialog overlay to edit the code's properties.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _edit: function(event) {
-                       var pre = event.currentTarget;
-                       
-                       if (_headerHeight === 0) {
-                               _headerHeight = UiRedactorPseudoHeader.getHeight(pre);
-                       }
-                       
-                       // check if the click hit the header
-                       var offset = DomUtil.offset(pre);
-                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
-                               event.preventDefault();
-                               
-                               this._editor.selection.save();
-                               this._pre = pre;
-                               
-                               UiDialog.open(this);
-                       }
-               },
-               
-               /**
-                * Saves the changes to the code's properties.
-                * 
-                * @protected
-                */
-               _dialogSubmit: function() {
-                       var id = 'redactor-code-' + this._elementId;
-                       
-                       ['file', 'highlighter', 'line'].forEach((function (attr) {
-                               elData(this._pre, attr, elById(id + '-' + attr).value);
-                       }).bind(this));
-                       
-                       this._setTitle(this._pre);
-                       this._editor.caret.after(this._pre);
-                       
-                       UiDialog.close(this);
-               },
-               
-               /**
-                * Sets or updates the code's header title.
-                * 
-                * @param       {Element}       pre     code element
-                * @protected
-                */
-               _setTitle: function(pre) {
-                       var file = elData(pre, 'file'),
-                           highlighter = elData(pre, 'highlighter');
-                       
-                       //noinspection JSUnresolvedVariable
-                       highlighter = (this._editor.opts.woltlab.highlighters.indexOf(highlighter) !== -1) ? PrismMeta[highlighter].title : '';
-                       
-                       var title = Language.get('wcf.editor.code.title', {
-                               file: file,
-                               highlighter: highlighter
-                       });
-                       
-                       if (elData(pre, 'title') !== title) {
-                               elData(pre, 'title', title);
-                       }
-               },
-               
-               _delete: function (event) {
-                       event.preventDefault();
-                       
-                       var caretEnd = this._pre.nextElementSibling || this._pre.previousElementSibling;
-                       if (caretEnd === null && this._pre.parentNode !== this._editor.core.editor()[0]) {
-                               caretEnd = this._pre.parentNode;
-                       }
-                       
-                       if (caretEnd === null) {
-                               this._editor.code.set('');
-                               this._editor.focus.end();
-                       }
-                       else {
-                               elRemove(this._pre);
-                               this._editor.caret.end(caretEnd);
-                       }
-                       
-                       UiDialog.close(this);
-               },
-               
-               _dialogSetup: function() {
-                       var id = 'redactor-code-' + this._elementId,
-                           idButtonDelete = id + '-button-delete',
-                           idButtonSave = id + '-button-save',
-                           idFile = id + '-file',
-                           idHighlighter = id + '-highlighter',
-                           idLine = id + '-line';
-                       
-                       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));
-                                               
-                                               // set highlighters
-                                               var highlighters = '<option value="">' + Language.get('wcf.editor.code.highlighter.detect') + '</option>';
-                                               highlighters += '<option value="plain">' + Language.get('wcf.editor.code.highlighter.plain') + '</option>';
-                                               
-                                               //noinspection JSUnresolvedVariable
-                                               var values = this._editor.opts.woltlab.highlighters.map(function (highlighter) {
-                                                       return [highlighter, PrismMeta[highlighter].title];
-                                               });
-                                               
-                                               // sort by label
-                                               values.sort(function(a, b) {
-                                                       if (a[1] < b[1]) {
-                                                               return  -1;
-                                                       }
-                                                       else if (a[1] > b[1]) {
-                                                               return 1;
-                                                       }
-                                                       
-                                                       return 0;
-                                               });
-                                               
-                                               values.forEach((function(value) {
-                                                       highlighters += '<option value="' + value[0] + '">' + StringUtil.escapeHTML(value[1]) + '</option>';
-                                               }).bind(this));
-                                               
-                                               elById(idHighlighter).innerHTML = highlighters;
-                                       }).bind(this),
-                                       
-                                       onShow: (function() {
-                                               elById(idHighlighter).value = elData(this._pre, 'highlighter');
-                                               var line = elData(this._pre, 'line');
-                                               elById(idLine).value = (line === '') ? 1 : ~~line;
-                                               elById(idFile).value = elData(this._pre, 'file');
-                                       }).bind(this),
-                                       
-                                       title: Language.get('wcf.editor.code.edit')
-                               },
-                               source: '<div class="section">'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idHighlighter + '">' + Language.get('wcf.editor.code.highlighter') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<select id="' + idHighlighter + '"></select>'
-                                                       + '<small>' + Language.get('wcf.editor.code.highlighter.description') + '</small>'
-                                               + '</dd>'
-                                       + '</dl>'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idLine + '">' + Language.get('wcf.editor.code.line') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="number" id="' + idLine + '" min="0" value="1" class="long" data-dialog-submit-on-enter="true">'
-                                                       + '<small>' + Language.get('wcf.editor.code.line.description') + '</small>'
-                                               + '</dd>'
-                                       + '</dl>'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idFile + '">' + Language.get('wcf.editor.code.file') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="text" id="' + idFile + '" class="long" data-dialog-submit-on-enter="true">'
-                                                       + '<small>' + Language.get('wcf.editor.code.file.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 UiRedactorCode;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Code.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Code.ts
new file mode 100644 (file)
index 0000000..5b3f807
--- /dev/null
@@ -0,0 +1,288 @@
+/**
+ * Manages code blocks.
+ *
+ * @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/Code
+ */
+
+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 { DialogCallbackObject, DialogCallbackSetup } from "../Dialog/Data";
+import { RedactorEditor } from "./Editor";
+import * as UiRedactorPseudoHeader from "./PseudoHeader";
+
+// TODO: "prism/prism-meta" is missing -- START
+
+interface PrismLang {
+  title: string;
+  file: string;
+}
+
+type Identifier = string;
+
+type PrismMetaData = Record<Identifier, PrismLang>;
+
+declare const PrismMeta: PrismMetaData;
+
+// TODO: "prism/prism-meta" is missing -- END
+
+type Highlighter = [string, string];
+
+let _headerHeight = 0;
+
+class UiRedactorCode implements DialogCallbackObject {
+  protected readonly _callbackEdit: (ev: MouseEvent) => void;
+  protected readonly _editor: RedactorEditor;
+  protected readonly _elementId: string;
+  protected _pre: HTMLElement | null = null;
+
+  /**
+   * Initializes the source code management.
+   */
+  constructor(editor: RedactorEditor) {
+    this._editor = editor;
+    this._elementId = this._editor.$element[0].id;
+
+    EventHandler.add("com.woltlab.wcf.redactor2", `bbcode_code_${this._elementId}`, (data) => this._bbcodeCode(data));
+    EventHandler.add("com.woltlab.wcf.redactor2", `observe_load_${this._elementId}`, () => this._observeLoad());
+
+    // support for active button marking
+    this._editor.opts.activeButtonsStates.pre = "code";
+
+    // static bind to ensure that removing works
+    this._callbackEdit = this._edit.bind(this);
+
+    // bind listeners on init
+    this._observeLoad();
+  }
+
+  /**
+   * Intercepts the insertion of `[code]` tags and uses a native `<pre>` instead.
+   */
+  protected _bbcodeCode(data: { cancel: boolean }): void {
+    data.cancel = true;
+
+    let pre = this._editor.selection.block();
+    if (pre && pre.nodeName === "PRE" && pre.classList.contains("woltlabHtml")) {
+      return;
+    }
+
+    this._editor.button.toggle({}, "pre", "func", "block.format");
+
+    pre = this._editor.selection.block();
+    if (pre && pre.nodeName === "PRE" && !pre.classList.contains("woltlabHtml")) {
+      if (pre.childElementCount === 1 && pre.children[0].nodeName === "BR") {
+        // drop superfluous linebreak
+        pre.removeChild(pre.children[0]);
+      }
+
+      this._setTitle(pre);
+
+      pre.addEventListener("click", this._callbackEdit);
+
+      // work-around for Safari
+      this._editor.caret.end(pre);
+    }
+  }
+
+  /**
+   * Binds event listeners and sets quote title on both editor
+   * initialization and when switching back from code view.
+   */
+  protected _observeLoad(): void {
+    this._editor.$editor[0].querySelectorAll("pre:not(.woltlabHtml)").forEach((pre: HTMLElement) => {
+      pre.addEventListener("mousedown", this._callbackEdit);
+      this._setTitle(pre);
+    });
+  }
+
+  /**
+   * Opens the dialog overlay to edit the code's properties.
+   */
+  protected _edit(event: MouseEvent): void {
+    const pre = event.currentTarget as HTMLPreElement;
+
+    if (_headerHeight === 0) {
+      _headerHeight = UiRedactorPseudoHeader.getHeight(pre);
+    }
+
+    // check if the click hit the header
+    const offset = DomUtil.offset(pre);
+    if (event.pageY > offset.top && event.pageY < offset.top + _headerHeight) {
+      event.preventDefault();
+
+      this._editor.selection.save();
+      this._pre = pre;
+
+      UiDialog.open(this);
+    }
+  }
+
+  /**
+   * Saves the changes to the code's properties.
+   */
+  _dialogSubmit(): void {
+    const id = "redactor-code-" + this._elementId;
+    const pre = this._pre!;
+
+    ["file", "highlighter", "line"].forEach((attr) => {
+      const input = document.getElementById(`${id}-${attr}`) as HTMLInputElement;
+      pre.dataset[attr] = input.value;
+    });
+
+    this._setTitle(pre);
+    this._editor.caret.after(pre);
+
+    UiDialog.close(this);
+  }
+
+  /**
+   * Sets or updates the code's header title.
+   */
+  protected _setTitle(pre: HTMLElement): void {
+    const file = pre.dataset.file!;
+    let highlighter = pre.dataset.highlighter!;
+
+    highlighter =
+      this._editor.opts.woltlab.highlighters.indexOf(highlighter) !== -1 ? PrismMeta[highlighter].title : "";
+
+    const title = Language.get("wcf.editor.code.title", {
+      file,
+      highlighter,
+    });
+
+    if (pre.dataset.title !== title) {
+      pre.dataset.title = title;
+    }
+  }
+
+  protected _delete(event: MouseEvent): void {
+    event.preventDefault();
+
+    const pre = this._pre!;
+    let caretEnd = pre.nextElementSibling || pre.previousElementSibling;
+    if (caretEnd === null && pre.parentElement !== this._editor.core.editor()[0]) {
+      caretEnd = pre.parentElement;
+    }
+
+    if (caretEnd === null) {
+      this._editor.code.set("");
+      this._editor.focus.end();
+    } else {
+      pre.remove();
+      this._editor.caret.end(caretEnd);
+    }
+
+    UiDialog.close(this);
+  }
+
+  _dialogSetup(): ReturnType<DialogCallbackSetup> {
+    const id = `redactor-code-${this._elementId}`;
+    const idButtonDelete = `${id}-button-delete`;
+    const idButtonSave = `${id}-button-save`;
+    const idFile = `${id}-file`;
+    const idHighlighter = `${id}-highlighter`;
+    const idLine = `${id}-line`;
+
+    return {
+      id: id,
+      options: {
+        onClose: () => {
+          this._editor.selection.restore();
+
+          UiDialog.destroy(this);
+        },
+
+        onSetup: () => {
+          document.getElementById(idButtonDelete)!.addEventListener("click", (ev) => this._delete(ev));
+
+          // set highlighters
+          let highlighters = `<option value="">${Language.get("wcf.editor.code.highlighter.detect")}</option>
+            <option value="plain">${Language.get("wcf.editor.code.highlighter.plain")}</option>`;
+
+          const values: Highlighter[] = this._editor.opts.woltlab.highlighters.map((highlighter) => {
+            return [highlighter, PrismMeta[highlighter].title];
+          });
+
+          // sort by label
+          values.sort((a, b) => {
+            if (a[1] < b[1]) {
+              return -1;
+            } else if (a[1] > b[1]) {
+              return 1;
+            }
+
+            return 0;
+          });
+
+          values.forEach((value) => {
+            highlighters += `<option value="${value[0]}">${StringUtil.escapeHTML(value[1])}</option>`;
+          });
+
+          document.getElementById(idHighlighter)!.innerHTML = highlighters;
+        },
+
+        onShow: () => {
+          const pre = this._pre!;
+
+          const highlighter = document.getElementById(idHighlighter) as HTMLSelectElement;
+          highlighter.value = pre.dataset.highlighter!;
+          const line = ~~(pre.dataset.line || 1);
+
+          const lineInput = document.getElementById(idLine) as HTMLInputElement;
+          lineInput.value = line.toString();
+
+          const filename = document.getElementById(idFile) as HTMLInputElement;
+          filename.value = pre.dataset.file!;
+        },
+
+        title: Language.get("wcf.editor.code.edit"),
+      },
+      source: `<div class="section">
+          <dl>
+            <dt>
+              <label for="${idHighlighter}">${Language.get("wcf.editor.code.highlighter")}</label>
+            </dt>
+            <dd>
+              <select id="${idHighlighter}"></select>
+              <small>${Language.get("wcf.editor.code.highlighter.description")}</small>
+            </dd>
+          </dl>
+          <dl>
+            <dt>
+              <label for="${idLine}">${Language.get("wcf.editor.code.line")}</label>
+            </dt>
+            <dd>
+              <input type="number" id="${idLine}" min="0" value="1" class="long" data-dialog-submit-on-enter="true">
+              <small>${Language.get("wcf.editor.code.line.description")}</small>
+            </dd>
+          </dl>
+          <dl>
+            <dt>
+              <label for="${idFile}">${Language.get("wcf.editor.code.file")}</label>
+            </dt>
+            <dd>
+              <input type="text" id="${idFile}" class="long" data-dialog-submit-on-enter="true">
+              <small>${Language.get("wcf.editor.code.file.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(UiRedactorCode);
+
+export = UiRedactorCode;
index 338f40c154305da4e016744b1bd28b3a9fbfa481..757f4d958ea4f30f748a5306b131eb3638a44f04 100644 (file)
@@ -1,26 +1,47 @@
 export interface RedactorEditor {
   $editor: JQuery;
+  $element: JQuery;
+
+  opts: {
+    [key: string]: any;
+  };
 
   buffer: {
-    set: () => void;
+    set(): void;
+  };
+  button: {
+    toggle(event: MouseEvent | object, btnName: string, type: string, callback: string, args?: object): void;
+  };
+  caret: {
+    after(node: Node): void;
+    end(node: Node): void;
   };
   clean: {
-    onSync: (html: string) => string;
+    onSync(html: string): string;
   };
   code: {
-    get: () => string;
-    start: (html: string) => void;
+    get(): string;
+    set(html: string): void;
+    start(html: string): void;
   };
   core: {
-    box: () => JQuery;
-    editor: () => JQuery;
-    element: () => JQuery;
-    textarea: () => JQuery;
+    box(): JQuery;
+    editor(): JQuery;
+    element(): JQuery;
+    textarea(): JQuery;
+  };
+  focus: {
+    end(): void;
   };
   insert: {
-    text: (text: string) => void;
+    text(text: string): void;
+  };
+  selection: {
+    block(): HTMLElement | false;
+    restore(): void;
+    save(): void;
   };
   utils: {
-    isEmpty: (html: string) => boolean;
+    isEmpty(html: string): boolean;
   };
 }