Fixed caret position when inserting quotes and shortly after
[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-2016 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLabSuite/Core/Ui/Redactor/Quote
8 */
9 define(['Core', 'EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function (Core, EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog) {
10 "use strict";
11
12 var _headerHeight = 0;
13
14 /**
15 * @param {Object} editor editor instance
16 * @param {jQuery} button toolbar button
17 * @constructor
18 */
19 function UiRedactorQuote(editor, button) { this.init(editor, button); }
20 UiRedactorQuote.prototype = {
21 /**
22 * Initializes the quote management.
23 *
24 * @param {Object} editor editor instance
25 * @param {jQuery} button toolbar button
26 */
27 init: function(editor, button) {
28 this._blockquote = null;
29 this._editor = editor;
30 this._elementId = this._editor.$element[0].id;
31
32 EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
33
34 this._editor.button.addCallback(button, this._click.bind(this));
35
36 // support for active button marking
37 this._editor.opts.activeButtonsStates.blockquote = 'woltlabQuote';
38
39 // static bind to ensure that removing works
40 this._callbackEdit = this._edit.bind(this);
41
42 // bind listeners on init
43 this._observeLoad();
44
45 // quote manager
46 EventHandler.add('com.woltlab.wcf.redactor2', 'insertQuote_' + this._elementId, this._insertQuote.bind(this));
47 },
48
49 /**
50 * Inserts a quote.
51 *
52 * @param {Object} data quote data
53 * @protected
54 */
55 _insertQuote: function (data) {
56 this._editor.buffer.set();
57
58 // caret must be within a `<p>`, if it is not move it
59 /** @type Node */
60 var block = this._editor.selection.block();
61 if (block === false) {
62 this._editor.selection.restore();
63
64 block = this._editor.selection.block();
65 }
66
67 if (block.nodeName !== 'P') {
68 var redactor = this._editor.core.editor()[0];
69
70 // find parent before Redactor
71 while (block.parentNode !== redactor) {
72 block = block.parentNode;
73 }
74
75 // caret.after() requires a following element
76 var next = this._editor.caret.next(block);
77 if (next === undefined || next.nodeName !== 'P') {
78 var p = elCreate('p');
79 p.textContent = '\u200B';
80
81 DomUtil.insertAfter(p, block);
82 }
83
84 this._editor.caret.after(block);
85 }
86
87 var content = '';
88 if (data.isText) content = this._editor.marker.html();
89 else content = data.content;
90
91 var quoteId = Core.getUuid();
92 this._editor.insert.html('<blockquote id="' + quoteId + '">' + content + '</blockquote>');
93
94 var quote = elById(quoteId);
95 elData(quote, 'author', data.author);
96 elData(quote, 'link', data.link);
97
98 if (data.isText) {
99 this._editor.selection.restore();
100 this._editor.insert.text(data.content);
101 }
102
103 quote.removeAttribute('id');
104
105 this._editor.caret.after(quote);
106
107 this._editor.buffer.set();
108 },
109
110 /**
111 * Toggles the quote block on button click.
112 *
113 * @protected
114 */
115 _click: function() {
116 this._editor.button.toggle({}, 'blockquote', 'func', 'block.format');
117
118 var blockquote = this._editor.selection.block();
119 if (blockquote && blockquote.nodeName === 'BLOCKQUOTE') {
120 this._setTitle(blockquote);
121
122 blockquote.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
123 }
124 },
125
126 /**
127 * Binds event listeners and sets quote title on both editor
128 * initialization and when switching back from code view.
129 *
130 * @protected
131 */
132 _observeLoad: function() {
133 elBySelAll('blockquote', this._editor.$editor[0], (function(blockquote) {
134 blockquote.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
135 this._setTitle(blockquote);
136 }).bind(this));
137 },
138
139 /**
140 * Opens the dialog overlay to edit the quote's properties.
141 *
142 * @param {Event} event event object
143 * @protected
144 */
145 _edit: function(event) {
146 var blockquote = event.currentTarget;
147
148 if (_headerHeight === 0) {
149 _headerHeight = ~~window.getComputedStyle(blockquote).paddingTop.replace(/px$/, '');
150
151 var styles = window.getComputedStyle(blockquote, '::before');
152 _headerHeight += ~~styles.paddingTop.replace(/px$/, '');
153 _headerHeight += ~~styles.height.replace(/px$/, '');
154 _headerHeight += ~~styles.paddingBottom.replace(/px$/, '');
155 }
156
157 // check if the click hit the header
158 var offset = DomUtil.offset(blockquote);
159 if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
160 event.preventDefault();
161
162 this._blockquote = blockquote;
163
164 UiDialog.open(this);
165 }
166 },
167
168 /**
169 * Saves the changes to the quote's properties.
170 *
171 * @param {Event} event event object
172 * @protected
173 */
174 _save: function(event) {
175 event.preventDefault();
176
177 var id = 'redactor-quote-' + this._elementId;
178 var urlInput = elById(id + '-url');
179 var innerError = elBySel('.innerError', urlInput.parentNode);
180 if (innerError !== null) elRemove(innerError);
181
182 var url = urlInput.value.replace(/\u200B/g, '').trim();
183 // simple test to check if it at least looks like it could be a valid url
184 if (url.length && !/^https?:\/\/[^\/]+/.test(url)) {
185 innerError = elCreate('small');
186 innerError.className = 'innerError';
187 innerError.textContent = Language.get('wcf.editor.quote.url.error.invalid');
188 urlInput.parentNode.insertBefore(innerError, urlInput.nextElementSibling);
189 return;
190 }
191
192 // set author
193 elData(this._blockquote, 'author', elById(id + '-author').value);
194
195 // set url
196 elData(this._blockquote, 'url', url);
197
198 this._setTitle(this._blockquote);
199 this._editor.caret.after(this._blockquote);
200
201 UiDialog.close(this);
202 },
203
204 /**
205 * Sets or updates the quote's header title.
206 *
207 * @param {Element} blockquote quote element
208 * @protected
209 */
210 _setTitle: function(blockquote) {
211 var title = Language.get('wcf.editor.quote.title', {
212 author: elData(blockquote, 'author'),
213 url: elData(blockquote, 'url')
214 });
215
216 if (elData(blockquote, 'title') !== title) {
217 elData(blockquote, 'title', title);
218 }
219 },
220
221 _dialogSetup: function() {
222 var id = 'redactor-quote-' + this._elementId,
223 idAuthor = id + '-author',
224 idButtonSave = id + '-button-save',
225 idUrl = id + '-url';
226
227 return {
228 id: id,
229 options: {
230 onSetup: (function() {
231 elById(idButtonSave).addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
232 }).bind(this),
233
234 onShow: (function() {
235 elById(idAuthor).value = elData(this._blockquote, 'author');
236 elById(idUrl).value = elData(this._blockquote, 'url');
237 }).bind(this),
238
239 title: Language.get('wcf.editor.quote.edit')
240 },
241 source: '<div class="section">'
242 + '<dl>'
243 + '<dt><label for="' + idAuthor + '">' + Language.get('wcf.editor.quote.author') + '</label></dt>'
244 + '<dd>'
245 + '<input type="text" id="' + idAuthor + '" class="long">'
246 + '</dd>'
247 + '</dl>'
248 + '<dl>'
249 + '<dt><label for="' + idUrl + '">' + Language.get('wcf.editor.quote.url') + '</label></dt>'
250 + '<dd>'
251 + '<input type="text" id="' + idUrl + '" class="long">'
252 + '<small>' + Language.get('wcf.editor.quote.url.description') + '</small>'
253 + '</dd>'
254 + '</dl>'
255 + '</div>'
256 + '<div class="formSubmit">'
257 + '<button id="' + idButtonSave + '" class="buttonPrimary">' + Language.get('wcf.global.button.save') + '</button>'
258 + '</div>'
259 };
260 }
261 };
262
263 return UiRedactorQuote;
264 });