Merge branch '5.3' into 5.4
authorMatthias Schmidt <gravatronics@live.com>
Tue, 15 Jun 2021 05:39:49 +0000 (07:39 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Tue, 15 Jun 2021 05:39:49 +0000 (07:39 +0200)
1  2 
ts/WoltLabSuite/Core/Ui/Redactor/Link.ts
ts/WoltLabSuite/Core/Ui/Redactor/Quote.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Link.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Quote.js
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 79ea1979d2a064771115838793af6483f6f6c2b4,0000000000000000000000000000000000000000..e6de9bf698e0004e5d5b68f11f6af10c2d9a5ee3
mode 100644,000000..100644
--- /dev/null
@@@ -1,110 -1,0 +1,106 @@@
-         onShow: () => {
-           const url = document.getElementById("redactor-link-url") as HTMLInputElement;
-           url.focus();
-         },
 +/**
 + * @woltlabExcludeBundle tiny
 + */
 +
 +import DomUtil from "../../Dom/Util";
 +import * as Language from "../../Language";
 +import UiDialog from "../Dialog";
 +import { DialogCallbackObject, DialogCallbackSetup } from "../Dialog/Data";
 +
 +type SubmitCallback = () => boolean;
 +
 +interface LinkOptions {
 +  insert: boolean;
 +  submitCallback: SubmitCallback;
 +}
 +
 +class UiRedactorLink implements DialogCallbackObject {
 +  private boundListener = false;
 +  private submitCallback: SubmitCallback;
 +
 +  open(options: LinkOptions) {
 +    UiDialog.open(this);
 +
 +    UiDialog.setTitle(this, Language.get("wcf.editor.link." + (options.insert ? "add" : "edit")));
 +
 +    const submitButton = document.getElementById("redactor-modal-button-action")!;
 +    submitButton.textContent = Language.get("wcf.global.button." + (options.insert ? "insert" : "save"));
 +
 +    this.submitCallback = options.submitCallback;
 +
 +    // Redactor might modify the button, thus we cannot bind it in the dialog's `onSetup()` callback.
 +    if (!this.boundListener) {
 +      this.boundListener = true;
 +
 +      submitButton.addEventListener("click", () => this.submit());
 +    }
 +  }
 +
 +  private submit(): void {
 +    if (this.submitCallback()) {
 +      UiDialog.close(this);
 +    } else {
 +      const url = document.getElementById("redactor-link-url") as HTMLInputElement;
 +
 +      const errorMessage = url.value.trim() === "" ? "wcf.global.form.error.empty" : "wcf.editor.link.error.invalid";
 +      DomUtil.innerError(url, Language.get(errorMessage));
 +    }
 +  }
 +
 +  _dialogSetup(): ReturnType<DialogCallbackSetup> {
 +    return {
 +      id: "redactorDialogLink",
 +      options: {
 +        onClose: () => {
 +          const url = document.getElementById("redactor-link-url") as HTMLInputElement;
 +          const small = url.nextElementSibling;
 +          if (small && small.nodeName === "SMALL") {
 +            small.remove();
 +          }
 +        },
 +        onSetup: (content) => {
 +          const submitButton = content.querySelector(".formSubmit > .buttonPrimary") as HTMLButtonElement;
 +
 +          if (submitButton !== null) {
 +            content.querySelectorAll('input[type="url"], input[type="text"]').forEach((input: HTMLInputElement) => {
 +              input.addEventListener("keyup", (event) => {
 +                if (event.key === "Enter") {
 +                  submitButton.click();
 +                }
 +              });
 +            });
 +          }
 +        },
 +      },
 +      source: `<dl>
 +          <dt>
 +            <label for="redactor-link-url">${Language.get("wcf.editor.link.url")}</label>
 +          </dt>
 +          <dd>
 +            <input type="url" id="redactor-link-url" class="long">
 +          </dd>
 +        </dl>
 +        <dl>
 +          <dt>
 +            <label for="redactor-link-url-text">${Language.get("wcf.editor.link.text")}</label>
 +          </dt>
 +          <dd>
 +            <input type="text" id="redactor-link-url-text" class="long">
 +          </dd>
 +        </dl>
 +        <div class="formSubmit">
 +          <button id="redactor-modal-button-action" class="buttonPrimary"></button>
 +        </div>`,
 +    };
 +  }
 +}
 +
 +let uiRedactorLink: UiRedactorLink;
 +
 +export function showDialog(options: LinkOptions): void {
 +  if (!uiRedactorLink) {
 +    uiRedactorLink = new UiRedactorLink();
 +  }
 +
 +  uiRedactorLink.open(options);
 +}
index ad379d9ddec595b356042d2b7982ff72f6223b51,0000000000000000000000000000000000000000..5afd1f9ef6bf89e89a17438ed12c1e4b2c278c5f
mode 100644,000000..100644
--- /dev/null
@@@ -1,303 -1,0 +1,305 @@@
-           this._editor.selection.restore();
 +/**
 + * 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
 + * @woltlabExcludeBundle tiny
 + */
 +
 +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 readonly _knownElements = new WeakSet<HTMLElement>();
 +  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) => {
 +      if (!this._knownElements.has(quote)) {
 +        quote.addEventListener("mousedown", (ev) => this._edit(ev));
 +        this._setTitle(quote);
 +
 +        this._knownElements.add(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: () => {
++          window.setTimeout(() => {
++            this._editor.selection.restore();
++          }, 100);
 +
 +          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;
index 7c3c77b7deb86dc89dcd62f86103eb60602f4bb1,d56a9ce64e0b3bb42e403531d49edf9261e80be7..f1956bc9d333aef49345215693fedda5a4c38d78
@@@ -1,95 -1,84 +1,91 @@@
 -define(['Core', 'EventKey', 'Language', 'Ui/Dialog'], function(Core, EventKey, Language, UiDialog) {
 -      "use strict";
 -      
 -      if (!COMPILER_TARGET_DEFAULT) {
 -              var Fake = function() {};
 -              Fake.prototype = {
 -                      showDialog: function() {},
 -                      _submit: function() {},
 -                      _dialogSetup: function() {}
 -              };
 -              return Fake;
 -      }
 -      
 -      var _boundListener = false;
 -      var _callback = null;
 -      
 -      return {
 -              showDialog: function(options) {
 -                      UiDialog.open(this);
 -                      
 -                      UiDialog.setTitle(this, Language.get('wcf.editor.link.' + (options.insert ? 'add' : 'edit')));
 -                      
 -                      var submitButton = elById('redactor-modal-button-action');
 -                      submitButton.textContent = Language.get('wcf.global.button.' + (options.insert ? 'insert' : 'save'));
 -                      
 -                      _callback = options.submitCallback;
 -                      
 -                      if (!_boundListener) {
 -                              _boundListener = true;
 -                              
 -                              submitButton.addEventListener(WCF_CLICK_EVENT, this._submit.bind(this));
 -                      }
 -              },
 -              
 -              _submit: function() {
 -                      if (_callback()) {
 -                              UiDialog.close(this);
 -                      }
 -                      else {
 -                              var url = elById('redactor-link-url');
 -                              elInnerError(url, Language.get((url.value.trim() === '' ? 'wcf.global.form.error.empty' : 'wcf.editor.link.error.invalid')));
 -                      }
 -              },
 -              
 -              _dialogSetup: function() {
 -                      return {
 -                              id: 'redactorDialogLink',
 -                              options: {
 -                                      onClose: function() {
 -                                              var url = elById('redactor-link-url');
 -                                              var small = (url.nextElementSibling && url.nextElementSibling.nodeName === 'SMALL') ? url.nextElementSibling : null;
 -                                              if (small !== null) {
 -                                                      elRemove(small);
 -                                              }
 -                                      },
 -                                      onSetup: function (content) {
 -                                              var submitButton = elBySel('.formSubmit > .buttonPrimary', content);
 -                                              
 -                                              if (submitButton !== null) {
 -                                                      elBySelAll('input[type="url"], input[type="text"]', content, function (input) {
 -                                                              input.addEventListener('keyup', function (event) {
 -                                                                      if (EventKey.Enter(event)) {
 -                                                                              Core.triggerEvent(submitButton, 'click');
 -                                                                      }
 -                                                              });
 -                                                      });
 -                                              }
 -                                      }
 -                              },
 -                              source: '<dl>'
 -                                              + '<dt><label for="redactor-link-url">' + Language.get('wcf.editor.link.url') + '</label></dt>'
 -                                              + '<dd><input type="url" id="redactor-link-url" class="long"></dd>'
 -                                      + '</dl>'
 -                                      + '<dl>'
 -                                              + '<dt><label for="redactor-link-url-text">' + Language.get('wcf.editor.link.text') + '</label></dt>'
 -                                              + '<dd><input type="text" id="redactor-link-url-text" class="long"></dd>'
 -                                      + '</dl>'
 -                                      + '<div class="formSubmit">'
 -                                              + '<button id="redactor-modal-button-action" class="buttonPrimary"></button>'
 -                                      + '</div>'
 -                      };
 -              }
 -      };
 +/**
 + * @woltlabExcludeBundle tiny
 + */
 +define(["require", "exports", "tslib", "../../Dom/Util", "../../Language", "../Dialog"], function (require, exports, tslib_1, Util_1, Language, Dialog_1) {
 +    "use strict";
 +    Object.defineProperty(exports, "__esModule", { value: true });
 +    exports.showDialog = void 0;
 +    Util_1 = tslib_1.__importDefault(Util_1);
 +    Language = tslib_1.__importStar(Language);
 +    Dialog_1 = tslib_1.__importDefault(Dialog_1);
 +    class UiRedactorLink {
 +        constructor() {
 +            this.boundListener = false;
 +        }
 +        open(options) {
 +            Dialog_1.default.open(this);
 +            Dialog_1.default.setTitle(this, Language.get("wcf.editor.link." + (options.insert ? "add" : "edit")));
 +            const submitButton = document.getElementById("redactor-modal-button-action");
 +            submitButton.textContent = Language.get("wcf.global.button." + (options.insert ? "insert" : "save"));
 +            this.submitCallback = options.submitCallback;
 +            // Redactor might modify the button, thus we cannot bind it in the dialog's `onSetup()` callback.
 +            if (!this.boundListener) {
 +                this.boundListener = true;
 +                submitButton.addEventListener("click", () => this.submit());
 +            }
 +        }
 +        submit() {
 +            if (this.submitCallback()) {
 +                Dialog_1.default.close(this);
 +            }
 +            else {
 +                const url = document.getElementById("redactor-link-url");
 +                const errorMessage = url.value.trim() === "" ? "wcf.global.form.error.empty" : "wcf.editor.link.error.invalid";
 +                Util_1.default.innerError(url, Language.get(errorMessage));
 +            }
 +        }
 +        _dialogSetup() {
 +            return {
 +                id: "redactorDialogLink",
 +                options: {
 +                    onClose: () => {
 +                        const url = document.getElementById("redactor-link-url");
 +                        const small = url.nextElementSibling;
 +                        if (small && small.nodeName === "SMALL") {
 +                            small.remove();
 +                        }
 +                    },
 +                    onSetup: (content) => {
 +                        const submitButton = content.querySelector(".formSubmit > .buttonPrimary");
 +                        if (submitButton !== null) {
 +                            content.querySelectorAll('input[type="url"], input[type="text"]').forEach((input) => {
 +                                input.addEventListener("keyup", (event) => {
 +                                    if (event.key === "Enter") {
 +                                        submitButton.click();
 +                                    }
 +                                });
 +                            });
 +                        }
 +                    },
-                     onShow: () => {
-                         const url = document.getElementById("redactor-link-url");
-                         url.focus();
-                     },
 +                },
 +                source: `<dl>
 +          <dt>
 +            <label for="redactor-link-url">${Language.get("wcf.editor.link.url")}</label>
 +          </dt>
 +          <dd>
 +            <input type="url" id="redactor-link-url" class="long">
 +          </dd>
 +        </dl>
 +        <dl>
 +          <dt>
 +            <label for="redactor-link-url-text">${Language.get("wcf.editor.link.text")}</label>
 +          </dt>
 +          <dd>
 +            <input type="text" id="redactor-link-url-text" class="long">
 +          </dd>
 +        </dl>
 +        <div class="formSubmit">
 +          <button id="redactor-modal-button-action" class="buttonPrimary"></button>
 +        </div>`,
 +            };
 +        }
 +    }
 +    let uiRedactorLink;
 +    function showDialog(options) {
 +        if (!uiRedactorLink) {
 +            uiRedactorLink = new UiRedactorLink();
 +        }
 +        uiRedactorLink.open(options);
 +    }
 +    exports.showDialog = showDialog;
  });
index 9d22917eb6259b8bb6be7af00c6a1fddbfcad84b,64b0238c0415d42fe0d7ed1a175dd58688db5ad1..02d8bc2dd933dad272eec4941892f42ee7d09d73
   * 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
 + * @woltlabExcludeBundle tiny
   */
 -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(WCF_CLICK_EVENT, 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 () {
 -                                              window.setTimeout((function () {
 -                                                      this._editor.selection.restore();
 -                                              }).bind(this), 100);
 -                                              
 -                                              UiDialog.destroy(this);
 -                                      }).bind(this),
 -                                      
 -                                      onSetup: (function() {
 -                                              elById(idButtonDelete).addEventListener(WCF_CLICK_EVENT, 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;
 +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";
 +    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.
 +         */
 +        constructor(editor, button) {
 +            this._knownElements = new WeakSet();
 +            this._quote = null;
 +            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.
 +         */
 +        _insertQuote(data) {
 +            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();
 +            }
 +            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.
 +         */
 +        _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", (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.
 +         */
 +        _observeLoad() {
 +            document.querySelectorAll("woltlab-quote").forEach((quote) => {
 +                if (!this._knownElements.has(quote)) {
 +                    quote.addEventListener("mousedown", (ev) => this._edit(ev));
 +                    this._setTitle(quote);
 +                    this._knownElements.add(quote);
 +                }
 +            });
 +        }
 +        /**
 +         * Opens the dialog overlay to edit the quote's properties.
 +         */
 +        _edit(event) {
 +            const quote = event.currentTarget;
 +            if (_headerHeight === 0) {
 +                _headerHeight = UiRedactorPseudoHeader.getHeight(quote);
 +            }
 +            // check if the click hit the header
 +            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;
 +                Dialog_1.default.open(this);
 +            }
 +        }
 +        /**
 +         * Saves the changes to the quote's properties.
 +         *
 +         * @protected
 +         */
 +        _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)) {
 +                Util_1.default.innerError(urlInput, Language.get("wcf.editor.quote.url.error.invalid"));
 +                return;
 +            }
 +            else {
 +                Util_1.default.innerError(urlInput, false);
 +            }
 +            const quote = this._quote;
 +            // set author
 +            const author = document.getElementById(id + "-author");
 +            quote.dataset.author = author.value;
 +            // set url
 +            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.
 +         */
 +        _setTitle(quote) {
 +            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;
 +            }
 +        }
 +        _delete(event) {
 +            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);
 +            }
 +            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: () => {
-                         this._editor.selection.restore();
++                        window.setTimeout(() => {
++                            this._editor.selection.restore();
++                        }, 100);
 +                        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>`,
 +            };
 +        }
 +    }
 +    Core.enableLegacyInheritance(UiRedactorQuote);
 +    return UiRedactorQuote;
  });
index 8d0a6f9002d0cfe3a98aef53a047e83d61fc52c8,fb8254014bb2c1a7c885e604557e8d7a0084b7b7..888f69aaf8a2c8d0acac2c81ca4c55e784723e85
        <category name="wcf.acp.devtools">
                <item name="wcf.acp.devtools.project.add"><![CDATA[Projekt hinzufügen]]></item>
                <item name="wcf.acp.devtools.project.edit"><![CDATA[Projekt bearbeiten]]></item>
-               <item name="wcf.acp.devtools.project.introduction"><![CDATA[Bitte {if LANGUAGE_USE_INFORMAL_VARIANT}beachte{else}beachten Sie{/if} die <a href="https://docs.woltlab.com/getting-started_quick-start.html#developer-tools" class="externalURL">Hinweise zur Benutzung</a> in der Entwickler-Dokumentation.]]></item>
 -              <item name="wcf.acp.devtools.project.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} das Projekt <span class="confirmationObject">{$object->name}</span> wirklich löschen?]]></item>
 -              <item name="wcf.acp.devtools.project.introduction"><![CDATA[Bitte {if LANGUAGE_USE_INFORMAL_VARIANT}beachte{else}beachten Sie{/if} die <a href="https://docs.woltlab.com/5.3/getting-started_quick-start.html#developer-tools" class="externalURL">Hinweise zur Benutzung</a> in der Entwickler-Dokumentation.]]></item>
++              <item name="wcf.acp.devtools.project.introduction"><![CDATA[Bitte {if LANGUAGE_USE_INFORMAL_VARIANT}beachte{else}beachten Sie{/if} die <a href="https://docs.woltlab.com/latest/getting-started/#developer-tools" class="externalURL">Hinweise zur Benutzung</a> in der Entwickler-Dokumentation.]]></item>
                <item name="wcf.acp.devtools.project.list"><![CDATA[Projekte]]></item>
                <item name="wcf.acp.devtools.project.name"><![CDATA[Name]]></item>
                <item name="wcf.acp.devtools.project.name.error.notUnique"><![CDATA[Der Name wird bereits von einem anderen Projekt verwendet.]]></item>
index ba88751f93fbfadf67c258d510a10f54485c2623,6ae06cd2059d799480ba01b9467845fb846b9206..3b6d47dc4ee2caf8b520a8af50901cb96b34fbdf
        <category name="wcf.acp.devtools">
                <item name="wcf.acp.devtools.project.add"><![CDATA[Add Project]]></item>
                <item name="wcf.acp.devtools.project.edit"><![CDATA[Edit Project]]></item>
-               <item name="wcf.acp.devtools.project.introduction"><![CDATA[Please read the <a href="https://docs.woltlab.com/getting-started_quick-start.html#developer-tools" class="externalURL">usage instructions</a> in the developer documentation.]]></item>
 -              <item name="wcf.acp.devtools.project.delete.confirmMessage"><![CDATA[Do you really want to delete the project <span class="confirmationObject">{$object->name}</span>?]]></item>
 -              <item name="wcf.acp.devtools.project.introduction"><![CDATA[Please read the <a href="https://docs.woltlab.com/5.3/getting-started_quick-start.html#developer-tools" class="externalURL">usage instructions</a> in the developer documentation.]]></item>
++              <item name="wcf.acp.devtools.project.introduction"><![CDATA[Please read the <a href="https://docs.woltlab.com/latest/getting-started/#developer-tools" class="externalURL">usage instructions</a> in the developer documentation.]]></item>
                <item name="wcf.acp.devtools.project.list"><![CDATA[Projects]]></item>
                <item name="wcf.acp.devtools.project.name"><![CDATA[Name]]></item>
                <item name="wcf.acp.devtools.project.name.error.notUnique"><![CDATA[The name is already used by another project.]]></item>