Merge branch '5.3' into 5.4
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Ui / Redactor / Quote.js
1 /**
2 * Manages quotes.
3 *
4 * @author Alexander Ebert
5 * @copyright 2001-2019 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLabSuite/Core/Ui/Redactor/Quote
8 * @woltlabExcludeBundle tiny
9 */
10 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) {
11 "use strict";
12 Core = tslib_1.__importStar(Core);
13 Util_1 = tslib_1.__importDefault(Util_1);
14 EventHandler = tslib_1.__importStar(EventHandler);
15 Language = tslib_1.__importStar(Language);
16 StringUtil = tslib_1.__importStar(StringUtil);
17 Dialog_1 = tslib_1.__importDefault(Dialog_1);
18 UiRedactorMetacode = tslib_1.__importStar(UiRedactorMetacode);
19 UiRedactorPseudoHeader = tslib_1.__importStar(UiRedactorPseudoHeader);
20 let _headerHeight = 0;
21 class UiRedactorQuote {
22 /**
23 * Initializes the quote management.
24 */
25 constructor(editor, button) {
26 this._knownElements = new WeakSet();
27 this._quote = null;
28 this._editor = editor;
29 this._elementId = this._editor.$element[0].id;
30 EventHandler.add("com.woltlab.wcf.redactor2", `observe_load_${this._elementId}`, () => this._observeLoad());
31 this._editor.button.addCallback(button, this._click.bind(this));
32 // bind listeners on init
33 this._observeLoad();
34 // quote manager
35 EventHandler.add("com.woltlab.wcf.redactor2", `insertQuote_${this._elementId}`, (data) => this._insertQuote(data));
36 }
37 /**
38 * Inserts a quote.
39 */
40 _insertQuote(data) {
41 if (this._editor.WoltLabSource.isActive()) {
42 return;
43 }
44 EventHandler.fire("com.woltlab.wcf.redactor2", "showEditor");
45 const editor = this._editor.core.editor()[0];
46 this._editor.selection.restore();
47 this._editor.buffer.set();
48 // caret must be within a `<p>`, if it is not: move it
49 let block = this._editor.selection.block();
50 if (block === false) {
51 this._editor.focus.end();
52 block = this._editor.selection.block();
53 }
54 while (block && block.parentElement !== editor) {
55 block = block.parentElement;
56 }
57 const quote = document.createElement("woltlab-quote");
58 quote.dataset.author = data.author;
59 quote.dataset.link = data.link;
60 let content = data.content;
61 if (data.isText) {
62 content = StringUtil.escapeHTML(content);
63 content = `<p>${content}</p>`;
64 content = content.replace(/\n\n/g, "</p><p>");
65 content = content.replace(/\n/g, "<br>");
66 }
67 else {
68 content = UiRedactorMetacode.convertFromHtml(this._editor.$element[0].id, content);
69 }
70 // bypass the editor as `insert.html()` doesn't like us
71 quote.innerHTML = content;
72 const blockParent = block.parentElement;
73 blockParent.insertBefore(quote, block.nextSibling);
74 if (block.nodeName === "P" && (block.innerHTML === "<br>" || block.innerHTML.replace(/\u200B/g, "") === "")) {
75 blockParent.removeChild(block);
76 }
77 // avoid adjacent blocks that are not paragraphs
78 let sibling = quote.previousElementSibling;
79 if (sibling && sibling.nodeName !== "P") {
80 sibling = document.createElement("p");
81 sibling.textContent = "\u200B";
82 quote.insertAdjacentElement("beforebegin", sibling);
83 }
84 this._editor.WoltLabCaret.paragraphAfterBlock(quote);
85 this._editor.buffer.set();
86 }
87 /**
88 * Toggles the quote block on button click.
89 */
90 _click() {
91 this._editor.button.toggle({}, "woltlab-quote", "func", "block.format");
92 const quote = this._editor.selection.block();
93 if (quote && quote.nodeName === "WOLTLAB-QUOTE") {
94 this._setTitle(quote);
95 quote.addEventListener("click", (ev) => this._edit(ev));
96 // work-around for Safari
97 this._editor.caret.end(quote);
98 }
99 }
100 /**
101 * Binds event listeners and sets quote title on both editor
102 * initialization and when switching back from code view.
103 */
104 _observeLoad() {
105 document.querySelectorAll("woltlab-quote").forEach((quote) => {
106 if (!this._knownElements.has(quote)) {
107 quote.addEventListener("mousedown", (ev) => this._edit(ev));
108 this._setTitle(quote);
109 this._knownElements.add(quote);
110 }
111 });
112 }
113 /**
114 * Opens the dialog overlay to edit the quote's properties.
115 */
116 _edit(event) {
117 const quote = event.currentTarget;
118 if (_headerHeight === 0) {
119 _headerHeight = UiRedactorPseudoHeader.getHeight(quote);
120 }
121 // check if the click hit the header
122 const offset = Util_1.default.offset(quote);
123 if (event.pageY > offset.top && event.pageY < offset.top + _headerHeight) {
124 event.preventDefault();
125 this._editor.selection.save();
126 this._quote = quote;
127 Dialog_1.default.open(this);
128 }
129 }
130 /**
131 * Saves the changes to the quote's properties.
132 *
133 * @protected
134 */
135 _dialogSubmit() {
136 const id = `redactor-quote-${this._elementId}`;
137 const urlInput = document.getElementById(`${id}-url`);
138 const url = urlInput.value.replace(/\u200B/g, "").trim();
139 // simple test to check if it at least looks like it could be a valid url
140 if (url.length && !/^https?:\/\/[^/]+/.test(url)) {
141 Util_1.default.innerError(urlInput, Language.get("wcf.editor.quote.url.error.invalid"));
142 return;
143 }
144 else {
145 Util_1.default.innerError(urlInput, false);
146 }
147 const quote = this._quote;
148 // set author
149 const author = document.getElementById(id + "-author");
150 quote.dataset.author = author.value;
151 // set url
152 quote.dataset.link = url;
153 this._setTitle(quote);
154 this._editor.caret.after(quote);
155 Dialog_1.default.close(this);
156 }
157 /**
158 * Sets or updates the quote's header title.
159 */
160 _setTitle(quote) {
161 const title = Language.get("wcf.editor.quote.title", {
162 author: quote.dataset.author,
163 url: quote.dataset.url,
164 });
165 if (quote.dataset.title !== title) {
166 quote.dataset.title = title;
167 }
168 }
169 _delete(event) {
170 event.preventDefault();
171 const quote = this._quote;
172 let caretEnd = quote.nextElementSibling || quote.previousElementSibling;
173 if (caretEnd === null && quote.parentElement !== this._editor.core.editor()[0]) {
174 caretEnd = quote.parentElement;
175 }
176 if (caretEnd === null) {
177 this._editor.code.set("");
178 this._editor.focus.end();
179 }
180 else {
181 quote.remove();
182 this._editor.caret.end(caretEnd);
183 }
184 Dialog_1.default.close(this);
185 }
186 _dialogSetup() {
187 const id = `redactor-quote-${this._elementId}`;
188 const idAuthor = `${id}-author`;
189 const idButtonDelete = `${id}-button-delete`;
190 const idButtonSave = `${id}-button-save`;
191 const idUrl = `${id}-url`;
192 return {
193 id: id,
194 options: {
195 onClose: () => {
196 window.setTimeout(() => {
197 this._editor.selection.restore();
198 }, 100);
199 Dialog_1.default.destroy(this);
200 },
201 onSetup: () => {
202 const button = document.getElementById(idButtonDelete);
203 button.addEventListener("click", (ev) => this._delete(ev));
204 },
205 onShow: () => {
206 const author = document.getElementById(idAuthor);
207 author.value = this._quote.dataset.author || "";
208 const url = document.getElementById(idUrl);
209 url.value = this._quote.dataset.link || "";
210 },
211 title: Language.get("wcf.editor.quote.edit"),
212 },
213 source: `<div class="section">
214 <dl>
215 <dt>
216 <label for="${idAuthor}">${Language.get("wcf.editor.quote.author")}</label>
217 </dt>
218 <dd>
219 <input type="text" id="${idAuthor}" class="long" data-dialog-submit-on-enter="true">
220 </dd>
221 </dl>
222 <dl>
223 <dt>
224 <label for="${idUrl}">${Language.get("wcf.editor.quote.url")}</label>
225 </dt>
226 <dd>
227 <input type="text" id="${idUrl}" class="long" data-dialog-submit-on-enter="true">
228 <small>${Language.get("wcf.editor.quote.url.description")}</small>
229 </dd>
230 </dl>
231 </div>
232 <div class="formSubmit">
233 <button id="${idButtonSave}" class="buttonPrimary" data-type="submit">${Language.get("wcf.global.button.save")}</button>
234 <button id="${idButtonDelete}">${Language.get("wcf.global.button.delete")}</button>
235 </div>`,
236 };
237 }
238 }
239 Core.enableLegacyInheritance(UiRedactorQuote);
240 return UiRedactorQuote;
241 });