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