Commit | Line | Data |
---|---|---|
c088b28c AE |
1 | /** |
2 | * BBCode Plugin for CKEditor | |
3 | * | |
4 | * @author Marcel Werk | |
ca4ba303 | 5 | * @copyright 2001-2014 WoltLab GmbH |
c088b28c | 6 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
d45eaff6 | 7 | */ |
d45eaff6 MW |
8 | (function() { |
9 | var $pasted = false; | |
10 | var $insertedText = null; | |
11 | ||
12 | CKEDITOR.on('instanceReady', function(event) { | |
13 | /** | |
14 | * Fixes issues with pasted html. | |
15 | */ | |
16 | event.editor.on('paste', function(ev) { | |
17 | if (ev.data.type == 'html') { | |
18 | var $value = ev.data.dataValue; | |
19 | ||
20 | // Convert <br> to line breaks. | |
21 | $value = $value.replace(/<br><\/p>/gi,"\n\n"); | |
22 | $value = $value.replace(/<br>/gi, "\n"); | |
23 | $value = $value.replace(/<\/p>/gi,"\n\n"); | |
24 | $value = $value.replace(/ /gi," "); | |
ceea0870 AE |
25 | |
26 | // convert div-separated content into new lines | |
8b7c6738 AE |
27 | $value = $value.replace(/<div([^>])>/gi, ''); |
28 | $value = $value.replace(/<\/div>/gi, "\n"); | |
d45eaff6 | 29 | |
ceea0870 AE |
30 | // convert lists into new lines |
31 | $value = $value.replace(/<\/li>/gi, "\n"); | |
d45eaff6 MW |
32 | // remove html tags |
33 | $value = $value.replace(/<[^>]+>/g, ''); | |
8b7c6738 | 34 | |
d45eaff6 MW |
35 | // fix multiple new lines |
36 | $value = $value.replace(/\n{3,}/gi,"\n\n"); | |
37 | ||
38 | ev.data.dataValue = $value; | |
39 | ||
40 | $pasted = true; | |
41 | } | |
42 | }, null, null, 9); | |
43 | ||
6414c2f1 | 44 | // prevent drag and drop of images in Firefox |
93e30a24 AE |
45 | if ($.browser.mozilla) { |
46 | event.editor.document.on('drop', function(ev) { | |
47 | ev.data.preventDefault(true); | |
48 | }); | |
49 | } | |
6414c2f1 | 50 | |
d45eaff6 MW |
51 | event.editor.on('insertText', function(ev) { |
52 | $insertedText = ev.data; | |
53 | }, null, null, 1); | |
54 | event.editor.on('mode', function(ev) { | |
8cacfb77 AE |
55 | if ($.browser.mozilla && ev.editor.mode === 'wysiwyg') { |
56 | fixFirefox(); | |
57 | } | |
58 | ||
d45eaff6 MW |
59 | ev.editor.focus(); |
60 | ||
61 | insertFakeSubmitButton(ev); | |
62 | }); | |
63 | event.editor.on('afterSetData', function(ev) { | |
64 | insertFakeSubmitButton(ev); | |
65 | }); | |
66 | ||
67 | event.editor.on('key', function(ev) { | |
68 | if (ev.data.keyCode == CKEDITOR.ALT + 83) { // [Alt] + [S] | |
69 | WCF.Message.Submit.execute(ev.editor.name); | |
70 | } | |
71 | }); | |
72 | ||
73 | insertFakeSubmitButton(event); | |
78e9a6ca MW |
74 | |
75 | // remove stupid title tag | |
6e725d5c MW |
76 | // @todo: obsolete in ckeditor 4.2 |
77 | $(event.editor.container.$).find('.cke_wysiwyg_div').removeAttr('title'); | |
8cacfb77 AE |
78 | |
79 | if ($.browser.mozilla) { | |
80 | fixFirefox(); | |
81 | } | |
d45eaff6 MW |
82 | }); |
83 | ||
84 | /** | |
85 | * Inserts a fake submit button, Chrome only. | |
86 | * | |
87 | * @param object event | |
88 | */ | |
89 | function insertFakeSubmitButton(event) { | |
90 | if (event.editor.mode === 'source' || !WCF.Browser.isChrome()) { | |
91 | return; | |
92 | } | |
93 | ||
94 | // place button outside of <body> to prevent it being removed once deleting content | |
6414c2f1 | 95 | $('<button accesskey="s" />').hide().appendTo($(event.editor.container.$).find('.cke_wysiwyg_div')); |
d45eaff6 MW |
96 | |
97 | } | |
98 | ||
8cacfb77 AE |
99 | /** |
100 | * Disables object resizing and table handles in Firefox. | |
101 | */ | |
102 | function fixFirefox() { | |
103 | document.designMode = 'on'; | |
104 | document.execCommand('enableObjectResizing', false, false); | |
105 | document.execCommand('enableInlineTableEditing', false, false); | |
106 | document.designMode = 'off'; | |
107 | } | |
108 | ||
d45eaff6 MW |
109 | /** |
110 | * Removes obsolete dialog elements. | |
111 | */ | |
112 | CKEDITOR.on('dialogDefinition', function(event) { | |
113 | var $tab; | |
114 | var $name = event.data.name; | |
115 | var $definition = event.data.definition; | |
d45eaff6 MW |
116 | if ($name == 'link') { |
117 | $definition.removeContents('target'); | |
118 | $definition.removeContents('upload'); | |
119 | $definition.removeContents('advanced'); | |
120 | $tab = $definition.getContents('info'); | |
121 | $tab.remove('emailSubject'); | |
122 | $tab.remove('emailBody'); | |
123 | } | |
124 | else if ($name == 'image') { | |
125 | $definition.removeContents('advanced'); | |
126 | $tab = $definition.getContents('Link'); | |
127 | $tab.remove('cmbTarget'); | |
128 | $tab = $definition.getContents('info'); | |
129 | $tab.remove('txtAlt'); | |
130 | $tab.remove('basic'); | |
487d6cd8 AE |
131 | |
132 | // remove preview, do NOT use $tab.remove() because that breaks the plugin | |
133 | $definition.dialog.on('show', function(event) { | |
134 | var $container = $(event.sender._.element.$).find('div[name=info]') | |
135 | $container.find('> table > tbody > tr:eq(1)').hide(); | |
136 | $container.parent().css('height', 'auto'); | |
137 | }); | |
d45eaff6 MW |
138 | } |
139 | else if ($name == 'table') { | |
140 | $definition.removeContents('advanced'); | |
141 | $definition.width = 210; | |
142 | $definition.height = 1; | |
143 | ||
144 | $tab = $definition.getContents('info'); | |
145 | ||
146 | $tab.remove('selHeaders'); | |
147 | $tab.remove('cmbAlign'); | |
148 | $tab.remove('txtHeight'); | |
149 | $tab.remove('txtCaption'); | |
150 | $tab.remove('txtSummary'); | |
151 | ||
152 | // don't remove these fields as we need their default values | |
153 | $tab.get('txtBorder').style = 'display: none'; | |
154 | $tab.get('txtWidth').style = 'display: none'; | |
155 | $tab.get('txtCellSpace').style = 'display: none'; | |
156 | $tab.get('txtCellPad').style = 'display: none'; | |
157 | } | |
158 | else if ($name == 'smiley') { | |
159 | $definition.contents[0].elements[0].onClick = function(ev) { | |
160 | var $target = ev.data.getTarget(); | |
161 | var $targetName = $target.getName(); | |
162 | ||
163 | if ($targetName == 'a') { | |
164 | $target = $target.getChild( 0 ); | |
165 | } | |
166 | else if ($targetName != 'img') { | |
167 | return; | |
168 | } | |
169 | ||
170 | var $src = $target.getAttribute('cke_src'); | |
171 | var $title = $target.getAttribute('title'); | |
172 | ||
173 | event.editor.insertText(' ' + $title + ' '); | |
174 | ||
175 | $definition.dialog.hide(); | |
176 | ev.data.preventDefault(); | |
177 | }; | |
178 | } | |
179 | }); | |
180 | ||
181 | /** | |
182 | * Enables this plugin. | |
183 | */ | |
184 | CKEDITOR.plugins.add('wbbcode', { | |
185 | requires: ['htmlwriter'], | |
186 | init: function(editor) { | |
187 | editor.dataProcessor = new CKEDITOR.htmlDataProcessor(editor); | |
188 | editor.dataProcessor.toHtml = toHtml; | |
189 | editor.dataProcessor.toDataFormat = toDataFormat; | |
190 | } | |
191 | }); | |
192 | ||
193 | /** | |
194 | * Removes the unicode zero width space (0x200B). | |
195 | * | |
196 | * @param string string | |
197 | * @return string | |
198 | */ | |
199 | var removeCrap = function(string) { | |
200 | var $string = ''; | |
201 | ||
202 | for (var $i = 0, $length = string.length; $i < $length; $i++) { | |
203 | var $byte = string.charCodeAt($i).toString(16); | |
204 | if ($byte != '200b') { | |
205 | $string += string[$i]; | |
206 | } | |
207 | } | |
208 | ||
209 | return $string; | |
210 | } | |
211 | ||
212 | /** | |
213 | * Converts bbcodes to html. | |
214 | */ | |
215 | var toHtml = function(data, fixForBody) { | |
67ae95eb | 216 | //if ($.trim(data) === "") return "<p></p>"; |
adffb9ac | 217 | |
d45eaff6 MW |
218 | // remove 0x200B (unicode zero width space) |
219 | data = removeCrap(data); | |
220 | ||
221 | if ($insertedText !== null) { | |
222 | data = $insertedText; | |
223 | $insertedText = null; | |
224 | ||
225 | if (data == ' ') return ' '; | |
226 | } | |
227 | ||
228 | if (!$pasted) { | |
229 | // Convert & to its HTML entity. | |
230 | data = data.replace(/&/g, '&'); | |
231 | ||
232 | // Convert < and > to their HTML entities. | |
233 | data = data.replace(/</g, '<'); | |
234 | data = data.replace(/>/g, '>'); | |
235 | } | |
236 | ||
237 | // Convert line breaks to <br>. | |
238 | data = data.replace(/(?:\r\n|\n|\r)/g, '<br>'); | |
239 | ||
240 | if ($pasted) { | |
241 | $pasted = false; | |
242 | // skip | |
243 | return data; | |
244 | } | |
245 | ||
246 | // cache code tags | |
247 | var $cachedCodes = { }; | |
248 | data = data.replace(/\[code(.+?)\[\/code]/gi, function(match) { | |
249 | var $key = match.hashCode(); | |
89dd0b2c | 250 | $cachedCodes[$key] = match.replace(/\$/g, '$$$$'); |
d45eaff6 MW |
251 | return '@@' + $key + '@@'; |
252 | }); | |
253 | ||
254 | // [url] | |
255 | data = data.replace(/\[url\]([^"]+?)\[\/url]/gi, '<a href="$1">$1</a>'); | |
13996ba9 MW |
256 | data = data.replace(/\[url\='([^'"]+)'](.+?)\[\/url]/gi, '<a href="$1">$2</a>'); |
257 | data = data.replace(/\[url\=([^'"\]]+)](.+?)\[\/url]/gi, '<a href="$1">$2</a>'); | |
d45eaff6 MW |
258 | |
259 | // [email] | |
260 | data = data.replace(/\[email\]([^"]+?)\[\/email]/gi, '<a href="mailto:$1">$1</a>'); | |
261 | data = data.replace(/\[email\=([^"\]]+)](.+?)\[\/email]/gi, '<a href="mailto:$1">$2</a>'); | |
262 | ||
263 | // [b] | |
264 | data = data.replace(/\[b\](.*?)\[\/b]/gi, '<b>$1</b>'); | |
265 | ||
266 | // [i] | |
267 | data = data.replace(/\[i\](.*?)\[\/i]/gi, '<i>$1</i>'); | |
268 | ||
269 | // [u] | |
270 | data = data.replace(/\[u\](.*?)\[\/u]/gi, '<u>$1</u>'); | |
271 | ||
272 | // [s] | |
679b13aa | 273 | data = data.replace(/\[s\](.*?)\[\/s]/gi, '<s>$1</s>'); |
d45eaff6 MW |
274 | |
275 | // [sub] | |
276 | data = data.replace(/\[sub\](.*?)\[\/sub]/gi, '<sub>$1</sub>'); | |
277 | ||
278 | // [sup] | |
279 | data = data.replace(/\[sup\](.*?)\[\/sup]/gi, '<sup>$1</sup>'); | |
280 | ||
281 | // [img] | |
282 | data = data.replace(/\[img\]([^"]+?)\[\/img\]/gi,'<img src="$1" />'); | |
25e14f52 | 283 | data = data.replace(/\[img='?([^"]*?)'?,'?(left|right)'?\]\[\/img\]/gi,'<img src="$1" style="float: $2" />'); |
d45eaff6 MW |
284 | data = data.replace(/\[img='?([^"]*?)'?\]\[\/img\]/gi,'<img src="$1" />'); |
285 | ||
286 | // [quote] | |
287 | // data = data.replace(/\[quote\]/gi, '<blockquote>'); | |
288 | // data = data.replace(/\[\/quote\]/gi, '</blockquote>'); | |
289 | ||
290 | // [size] | |
291 | data = data.replace(/\[size=(\d+)\](.*?)\[\/size\]/gi,'<span style="font-size: $1pt">$2</span>'); | |
292 | ||
293 | // [color] | |
294 | data = data.replace(/\[color=([#a-z0-9]*?)\](.*?)\[\/color\]/gi,'<span style="color: $1">$2</span>'); | |
295 | ||
296 | // [font] | |
297 | data = data.replace(/\[font='?([a-z,\- ]*?)'?\](.*?)\[\/font\]/gi,'<span style="font-family: $1">$2</span>'); | |
298 | ||
299 | // [align] | |
300 | data = data.replace(/\[align=(left|right|center|justify)\](.*?)\[\/align\]/gi,'<div style="text-align: $1">$2</div>'); | |
301 | ||
302 | // [*] | |
303 | data = data.replace(/\[\*\](.*?)(?=\[\*\]|\[\/list\])/gi,'<li>$1</li>'); | |
304 | ||
305 | // [list] | |
306 | data = data.replace(/\[list\]/gi, '<ul>'); | |
307 | data = data.replace(/\[list=1\]/gi, '<ul style="list-style-type: decimal">'); | |
2ee70bbe MW |
308 | data = data.replace(/\[list=a\]/gi, '<ul style="list-style-type: lower-latin">'); |
309 | data = data.replace(/\[list=(none|circle|square|disc|decimal|lower-roman|upper-roman|decimal-leading-zero|lower-greek|lower-latin|upper-latin|armenian|georgian)\]/gi, '<ul style="list-style-type: $1">'); | |
d45eaff6 MW |
310 | data = data.replace(/\[\/list]/gi, '</ul>'); |
311 | ||
312 | // [table] | |
313 | data = data.replace(/\[table\]/gi, '<table border="1" cellspacing="1" cellpadding="1" style="width: 500px;">'); | |
314 | data = data.replace(/\[\/table\]/gi, '</table>'); | |
315 | // [tr] | |
316 | data = data.replace(/\[tr\]/gi, '<tr>'); | |
317 | data = data.replace(/\[\/tr\]/gi, '</tr>'); | |
318 | // [td] | |
319 | data = data.replace(/\[td\]/gi, '<td>'); | |
320 | data = data.replace(/\[\/td\]/gi, '</td>'); | |
321 | ||
322 | // smileys | |
323 | for (var i = 0; i < this.editor.config.smiley_descriptions.length; i++) { | |
324 | var smileyCode = this.editor.config.smiley_descriptions[i].replace(/</g, '<').replace(/>/g, '>'); | |
325 | var regExp = new RegExp('(\\s|>|^)'+WCF.String.escapeRegExp(smileyCode)+'(?=\\s|<|$)', 'gi'); | |
326 | data = data.replace(regExp, '$1<img src="' + this.editor.config.smiley_path + this.editor.config.smiley_images[i] + '" class="smiley" alt="'+smileyCode+'" />'); | |
327 | } | |
328 | ||
329 | // remove "javascript:" | |
330 | data = data.replace(/(javascript):/gi, '$1<span></span>:'); | |
331 | ||
332 | // insert codes | |
333 | if ($.getLength($cachedCodes)) { | |
334 | for (var $key in $cachedCodes) { | |
335 | var $regex = new RegExp('@@' + $key + '@@', 'g'); | |
336 | data = data.replace($regex, $cachedCodes[$key]); | |
337 | } | |
338 | } | |
339 | ||
340 | return data; | |
341 | }; | |
342 | ||
343 | /** | |
344 | * Converts html to bbcodes. | |
345 | */ | |
346 | var toDataFormat = function(html, fixForBody) { | |
347 | if (html == '<br>' || html == '<p><br></p>') { | |
348 | return ""; | |
349 | } | |
350 | ||
351 | // Convert <br> to line breaks. | |
352 | html = html.replace(/<br><\/p>/gi,"\n"); | |
353 | html = html.replace(/<br(?=[ \/>]).*?>/gi, '\r\n'); | |
354 | html = html.replace(/<p>/gi,""); | |
355 | html = html.replace(/<\/p>/gi,"\n"); | |
356 | html = html.replace(/ /gi," "); | |
357 | ||
358 | // [email] | |
4efc488e | 359 | html = html.replace(/<a [^>]*?href=(["'])mailto:(.+?)\1.*?>([\s\S]+?)<\/a>/gi, '[email=$2]$3[/email]'); |
d45eaff6 MW |
360 | |
361 | // [url] | |
4efc488e | 362 | html = html.replace(/<a [^>]*?href=(["'])(.+?)\1.*?>([\s\S]+?)<\/a>/gi, function(match, x, url, text) { |
49630032 MW |
363 | if (url == text) return '[url]' + url + '[/url]'; |
364 | ||
365 | return "[url='" + url + "']" + text + "[/url]"; | |
366 | }); | |
d45eaff6 MW |
367 | |
368 | // [b] | |
369 | html = html.replace(/<(?:b|strong)>/gi, '[b]'); | |
370 | html = html.replace(/<\/(?:b|strong)>/gi, '[/b]'); | |
371 | ||
372 | // [i] | |
373 | html = html.replace(/<(?:i|em)>/gi, '[i]'); | |
374 | html = html.replace(/<\/(?:i|em)>/gi, '[/i]'); | |
375 | ||
376 | // [u] | |
377 | html = html.replace(/<u>/gi, '[u]'); | |
378 | html = html.replace(/<\/u>/gi, '[/u]'); | |
379 | ||
380 | // [s] | |
679b13aa MW |
381 | html = html.replace(/<s>/gi, '[s]'); |
382 | html = html.replace(/<\/s>/gi, '[/s]'); | |
d45eaff6 | 383 | |
4efc488e | 384 | // [sub] |
d45eaff6 MW |
385 | html = html.replace(/<sub>/gi, '[sub]'); |
386 | html = html.replace(/<\/sub>/gi, '[/sub]'); | |
387 | ||
388 | // [sup] | |
389 | html = html.replace(/<sup>/gi, '[sup]'); | |
390 | html = html.replace(/<\/sup>/gi, '[/sup]'); | |
391 | ||
392 | // smileys | |
393 | html = html.replace(/<img [^>]*?alt="([^"]+?)" class="smiley".*?>/gi, '$1'); // firefox | |
394 | html = html.replace(/<img [^>]*?class="smiley" alt="([^"]+?)".*?>/gi, '$1'); // chrome, ie | |
395 | ||
396 | // [img] | |
397 | html = html.replace(/<img [^>]*?src=(["'])([^"']+?)\1 style="float: (left|right)".*?>/gi, "[img='$2',$3][/img]"); | |
398 | html = html.replace(/<img [^>]*?src=(["'])([^"']+?)\1.*?>/gi, '[img]$2[/img]'); | |
399 | ||
400 | // [quote] | |
401 | // html = html.replace(/<blockquote>/gi, '[quote]'); | |
402 | // html = html.replace(/\n*<\/blockquote>/gi, '[/quote]'); | |
403 | ||
404 | // [color] | |
405 | html = html.replace(/<span style="color: ?rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\);?">([\s\S]*?)<\/span>/gi, function(match, r, g, b, text) { | |
406 | var $hex = ("0123456789ABCDEF".charAt((r - r % 16) / 16) + '' + "0123456789ABCDEF".charAt(r % 16)) + '' + ("0123456789ABCDEF".charAt((g - g % 16) / 16) + '' + "0123456789ABCDEF".charAt(g % 16)) + '' + ("0123456789ABCDEF".charAt((b - b % 16) / 16) + '' + "0123456789ABCDEF".charAt(b % 16)); | |
407 | ||
408 | return "[color=#" + $hex + "]" + text + "[/color]"; | |
409 | }); | |
410 | html = html.replace(/<span style="color: ?(.*?);?">([\s\S]*?)<\/span>/gi, "[color=$1]$2[/color]"); | |
411 | ||
412 | // [size] | |
413 | html = html.replace(/<span style="font-size: ?(\d+)pt;?">([\s\S]*?)<\/span>/gi, "[size=$1]$2[/size]"); | |
414 | ||
415 | // [font] | |
416 | html = html.replace(/<span style="font-family: ?(.*?);?">([\s\S]*?)<\/span>/gi, "[font='$1']$2[/font]"); | |
417 | ||
418 | // [align] | |
419 | html = html.replace(/<div style="text-align: ?(left|center|right|justify);? ?">([\s\S]*?)<\/div>/gi, "[align=$1]$2[/align]"); | |
420 | ||
421 | // [*] | |
422 | html = html.replace(/<li>/gi, '[*]'); | |
423 | html = html.replace(/<\/li>/gi, ''); | |
424 | ||
425 | // [list] | |
426 | html = html.replace(/<ul>/gi, '[list]'); | |
427 | html = html.replace(/<(ol|ul style="list-style-type: decimal")>/gi, '[list=1]'); | |
2ee70bbe | 428 | html = html.replace(/<ul style="list-style-type: (none|circle|square|disc|decimal|lower-roman|upper-roman|decimal-leading-zero|lower-greek|lower-latin|upper-latin|armenian|georgian)">/gi, '[list=$1]'); |
d45eaff6 MW |
429 | html = html.replace(/<\/(ul|ol)>/gi, '[/list]'); |
430 | ||
431 | // [table] | |
432 | html = html.replace(/<table[^>]*>/gi, '[table]'); | |
433 | html = html.replace(/<\/table>/gi, '[/table]'); | |
434 | ||
435 | // remove empty <tr>s | |
436 | html = html.replace(/<tr><\/tr>/gi, ''); | |
437 | // [tr] | |
438 | html = html.replace(/<tr>/gi, '[tr]'); | |
439 | html = html.replace(/<\/tr>/gi, '[/tr]'); | |
440 | ||
d124bc6e MW |
441 | // [td]+[align] |
442 | html = html.replace(/<td style="text-align: ?(left|center|right|justify);? ?">([\s\S]*?)<\/td>/gi, "[td][align=$1]$2[/align][/td]"); | |
443 | ||
d45eaff6 MW |
444 | // [td] |
445 | html = html.replace(/<td>/gi, '[td]'); | |
446 | html = html.replace(/<\/td>/gi, '[/td]'); | |
447 | ||
448 | // Remove remaining tags. | |
449 | html = html.replace(/<[^>]+>/g, ''); | |
450 | ||
451 | // Restore <, > and & | |
452 | html = html.replace(/</g, '<'); | |
453 | html = html.replace(/>/g, '>'); | |
454 | html = html.replace(/&/g, '&'); | |
455 | ||
456 | // Restore (and ) | |
457 | html = html.replace(/%28/g, '('); | |
458 | html = html.replace(/%29/g, ')'); | |
459 | ||
460 | // Restore %20 | |
461 | html = html.replace(/%20/g, ' '); | |
462 | ||
463 | return html; | |
7fd6e30b | 464 | }; |
d45eaff6 | 465 | })(); |