PageSourceSearch

https://community.bipolaruk.org/plugins/editor/js/editor.js?v=6abec633

js bipolaruk.org collected 2026-10-03 19:30:16 UTC 91,731 bytes, 1,883 lines download raw bytes

1(function($) {
2    $.fn.setAsEditor = function(selector) {
3        selector = selector || '.BodyBox,.js-bodybox';
4
5        // If editor can be loaded, add class to body
6        $('body').addClass('editor-active');
7
8        /**
9         * Determine editor format to load, and asset path, default to Wysiwyg
10         */
11        var editor,
12            editorName,
13            editorRules = {}, // for Wysiwyg
14            editorCacheBreakValue = Math.random(),
15            editorVersion = gdn.definition('editorVersion', editorCacheBreakValue),
16            formatOriginal = gdn.definition('editorInputFormat', 'Wysiwyg'),
17            format = formatOriginal.toLowerCase(),
18            assets = gdn.definition('editorPluginAssets', '/plugins/editor'),
19            debug = false;
20
21        /**
22         * Load relevant stylesheets into editor iframe. The first one loaded is
23         * the actual editor.css required for the plugin. The others are extra,
24         * grabbed from the source of parent to iframe, as different communities
25         * may have styles they want injected into the iframe.
26         */
27
28        if (debug) {
29            editorVersion += '&cachebreak=' + editorCacheBreakValue;
30        }
31
32        var stylesheetsInclude = [assets + '/design/editor.css?v=' + editorVersion];
33
34
35        // Some communities may want to modify just the styling of the Wysiwyg
36        // while editing, so this file will let them.
37        var editorWysiwygCSS = gdn.definition('editorWysiwygCSS', '');
38        if (editorWysiwygCSS != '') {
39            stylesheetsInclude.push(editorWysiwygCSS + '?v=' + editorVersion);
40        }
41
42        /**
43         * Fullpage actions--available to all editor views on page load.
44         *
45         * TODO: add fullpage class to iframe bodybox if in wysiwyg, for easier
46         * style overwriting.
47         */
48        var fullPageInit = function(wysiwygInstance) {
49
50            // Hack to push toolbar left 15px when vertical scrollbar appears, so it
51            // is always aligned with the textarea. This is for clearing interval.
52            var toolbarInterval;
53
54            var toggleFullpage = function(e) {
55
56                // either user clicks on fullpage toggler button, or escapes out with key
57                var toggleButton = (typeof e != 'undefined')
58                    ? e.target
59                    : $('#editor-fullpage-candidate').find('.editor-toggle-fullpage-button');
60
61                var bodyEl = $('body');
62
63                // New wrapper classes added to form. If the first is not availble,
64                // fall back to second one. This was added to core, so core will
65                // need to be updated alongside the plugin if site owners want
66                // latest feature.
67                var formWrapper = ($(toggleButton).closest('.fullpage-wrap').length)
68                    ? $(toggleButton).closest('.fullpage-wrap')
69                    : $(toggleButton).closest('.bodybox-wrap');
70
71                // Not all parts of the site have same surrounding markup, so if that
72                // fails, grab nearest parent element that might enclose it. The
73                // exception this was made for is the signatures plugin.
74                if (typeof formWrapper == 'undefined') {
75                    formWrapper = $(toggleButton).parent().parent();
76                }
77
78                var fullPageCandidate = $('#editor-fullpage-candidate');
79
80                // If no fullpage, enable it
81                if (!bodyEl.hasClass('editor-fullpage')) {
82                    $(formWrapper).attr('id', 'editor-fullpage-candidate');
83                    bodyEl.addClass('editor-fullpage');
84                    $(toggleButton).addClass('icon-resize-small');
85
86                    var editorToolbar = $(fullPageCandidate).find('.editor');
87
88                    // When textarea pushes beyond viewport of its container, a
89                    // scrollbar appears, which pushes the textarea left, while the
90                    // fixed editor toolbar does not move, so push it over.
91                    // Opted to go this route because support for the flow events is
92                    // limited, webkit/moz both have their own implementations, while
93                    // IE has no support for them. See below for example, commented out.
94
95                    // Only Firefox seems to have this issue (unless this is
96                    // mac-specific. Chrome & Safari on mac do n
96ot shift content over.
97                    /*
98                     if (typeof InstallTrigger !== 'undefined') {
99                     toolbarInterval = setInterval(function() {
100                     if ($(fullPageCandidate)[0].clientHeight < $(fullPageCandidate)[0].scrollHeight) {
101                     // console.log('scrollbar');
102                     $(editorToolbar).css('right', '15px');
103                     } else {
104                     // console.log('no scrollbar');
105                     $(editorToolbar).css('right', '0');
106                     }
107                     }, 10);
108                     }
109                     */
110                } else {
111                    clearInterval(toolbarInterval);
112
113                    // wysiwhtml5 editor area sometimes overflows beyond wrapper
114                    // when exiting fullpage, and it reflows on window resize, so
115                    // trigger resize event to get it done.
116                    $('.' + editorName).css('width', '100%');
117
118                    // else disable fullpage
119                    $(formWrapper).attr('id', '');
120                    bodyEl.removeClass('editor-fullpage');
121                    $(toggleButton).removeClass('icon-resize-small');
122
123                    // If in wysiwyg fullpage mode with lights toggled off, then
124                    // exit, the lights off remains.
125                    // TODO move into own function
126                    var ifr = $(fullPageCandidate).find('.wysihtml5-sandbox');
127                    if (ifr.length) {
128                        var iframeBodyBox = ifr.contents().find('.BodyBox,.js-bodybox');
129                        //$(iframeBodyBox).addClass('iframe-bodybox-lightsoff');
130                        iframeBodyBox.off('focus blur');
131                        $(fullPageCandidate).removeClass('editor-lights-candidate');
132                        $(iframeBodyBox).removeClass('iframe-bodybox-lightsoff');
133                        $(iframeBodyBox).removeClass('iframe-bodybox-lightson');
134                    }
135
136                    // Auto scroll to correct location upon exiting fullpage.
137                    var scrollto = $(toggleButton).closest('.Comment');
138                    if (!scrollto.length) {
139                        scrollto = $(toggleButton).closest('.CommentForm');
140                    }
141
142                    // Just in case I haven't covered all bases.
143                    if (scrollto.length) {
144                        $('html, body').animate({
145                            scrollTop: $(scrollto).offset().top
146                        }, 400);
147                    }
148                }
149
150                // Toggle lights while in fullpage (lights off essentially makes
151                // the background black and the buttons lighter.
152                toggleLights();
153
154                // Set focus to composer when going fullpage and exiting.
155                if (typeof wysiwygInstance != 'undefined') {
156                    wysiwygInstance.focus();
157                } else {
158                    editorSetCaretFocusEnd($(formWrapper).find('.BodyBox,.js-bodybox')[0]);
159                }
160            };
161
162            /**
163             * Attach fullpage toggling events
164             */
165
166            // click fullpage
167            var clickFullPage = (function() {
168                $(".editor-toggle-fullpage-button")
169                    .off('click')
170                    .on('click', toggleFullpage);
171            }());
172
173            // exit fullpage on esc
174            var closeFullPageEsc = (function() {
175                $(document)
176                    .off('keyup')
177                    .on('keyup', function(e) {
178                        if ($('body').hasClass('editor-fullpage') && e.which == 27) {
179                            toggleFullpage();
180                        }
181                    });
182            }());
183
184            /**
185             * If full page and the user saves/cancels/previews comment,
186             * exit out of full page.
187             * Not smart in the sense that a failed post will also exit out of
188             * full page, but the text will remain in editor, so not big issue.
189             */
190            var postCommentCloseFullPageEvent = (function() {
191                $('.Button')
192                    .off('click.closefullpage')
193                    .on('click.closefullpage', function() {
194                        // Prevent auto-saving drafts from exiting fullpage
195                        if (!$(this).hasClass('DraftButton')) {
196                            if ($('body').hasClass('editor-fullpage')) {
197                                toggleFullpage();
198                            }
199                        }
200                    });
201            }());
202
203            /**
204             * Lights on/off in fullpage
205             *
206             * Note: Wysiwyg makes styling the BodyBox more difficult as it's an
207             * iframe. Consequently, JavaScript has to override all the styles
208             * that skip the iframe, and tie into the focus and blur events to
209             * override the Wysihtml5 inline style events.
210             */
211            var toggleLights = function() {
212
213                // Set initial lights settings on/off. This setting is defined
214                // in global scope, so that plugins can define an initial state
215                var initialLightState = (typeof editorLights != 'undefined')
216                    ? editorLights // should be off, typically
217                    : 'on';
218
219                var toggleLights = $('.editor-toggle-lights-button');
220                var fullPageCandidate = $('#editor-fullpage-candidate');
221                var ifr = {};
222
223                if (fullPageCandidate.length) {
224                    $(toggleLights).attr('style', 'display:inline-block !important');
225
226                    // Due to wysiwyg styles embedded inline and states changed
227                    // using JavaScript, all the styles have to be duplicated from
228                    // the external stylesheet and override the iframe inlines.
229                    ifr = $(fullPageCandidate).find('.wysihtml5-sandbox');
230                    if (ifr.length) {
231                        var iframeBodyBox = ifr.contents().find('.BodyBox,.js-bodybox');
232                        iframeBodyBox.css({
233                            "transition": "background-color 0.4s ease, color 0.4s ease"
234                        });
235
236                        // By default, black text on white background. Some themes
237                        // prevent text from being readable, so make sure it can.
238                        iframeBodyBox.addClass('iframe-bodybox-lightson');
239                    }
240
241                    // Set initial state.
242                    if (initialLightState == 'off') {
243                        $(fullPageCandidate).addClass('editor-lights-candidate');
244                        if (ifr.length) {
245                            // if wysiwyg, need to manipulate content in iframe
246                            iframeBodyBox.removeClass('iframe-bodybox-lightson');
247                            iframeBodyBox.addClass('iframe-bodybox-lightsoff');
248
249                            iframeBodyBox.on('focus blur', function(e) {
250                                $(this).addClass('iframe-bodybox-lightsoff');
251                            });
252                        }
253                    }
254                } else {
255                    $(toggleLights).attr('style', '');
256                }
257
258                $(toggleLights).off('click').on('click', function() {
259                    if (!$(fullPageCandidate).hasClass('editor-lights-candidate')) {
260                        $(fullPageCandidate).addClass('editor-lights-candidate');
261
262                        // Again, for Wysiwyg, override styles
263                        if (ifr.length) {
264                            // if wysiwyg, need to manipulate content in iframe
265                            iframeBodyBox.removeClass('iframe-bodybox-lightson');
266                            iframeBodyBox.addClass('iframe-bodybox-lightsoff');
267
268                            iframeBodyBox.on('focus blur', function(e) {
269                                $(this).addClass('iframe-bodybox-lightsoff');
270                            });
271                        }
272                    } else {
273                        $(fullPageCandidate).removeClass('editor-lights-candidate');
274
275                        // Wysiwyg override styles
276                        if (ifr.length) {
277                            iframeBodyBox.off('focus blur');
278
279                            // if wysiwyg, need to manipulate content in iframe
280                            iframeBodyBox.removeClass('iframe-bodybox-lightsoff');
281                            iframeBodyBox.addClass('iframe-bodybox-lightson');
282                        }
283                    }
284                });
285            };
286        };
287
288        /**
289         * When rendering editor, load correct helpt text message
290         */
291        var editorSetHelpText = function(format, editorAreaObj) {
292            format = format.toLowerCase();
293            if (format != 'wysiwyg' && format != 'text' && format != 'textex') {
294                // If the helpt text is already there, don't insert it again.
295                if (!$(editorAreaObj).parent().find('.editor-help-text').length) {
296                    $("<div></div>")
297                        .addClass('editor-help-text')
298                        .html(gdn.definition(format + 'HelpText'))
299                        .insertAfter(editorAreaObj);
300                }
301            }
302        };
303
304        /**
305         * For non-wysiwyg views. Wysiwyg focus() automatically places caret
306         * at the end of a string of text.
307         */
308        var editorSetCaretFocusEnd = function(obj) {
309            obj.selectionStart = obj.selectionEnd = obj.value.length;
310            // Hack to work around jQuery's autogrow, which requires focus to init
311            // the feature, but setting focus immediately here prevents that.
312            // Considered using trigger() and triggerHandler(), but do not work.
313            setTimeout(function() {
314                obj.focus();
315            }, 250);
316        };
317
318        /**
319         * Helper function to select whole text of an input or textarea on focus
320         */
321        var editorSelectAllInput = function(obj) {
322            // Check if can access selection, as programmatically triggering the
323            // dd close event throws an error here.
324            obj.focus();
325            if (obj.value.length) {
326                // selectionStart is implied 0
327                obj.selectionEnd = obj.value.length;
328                obj.select();
329            }
330        };
331
332        /**
333         * Call to quickly close all open dropdowns. Refactor this into some of
334         * the areas below that are performing this function.
335         */
336        var editorDropdownsClose = function() {
337            $('.editor-dropdown').each(function(i, el) {
338                $(el).removeClass('editor-dropdown-open');
339                $(el).find('.wysihtml5-command-dialog-opened').removeClass('wysihtml5-command-dialog-opened');
340                $(this).setFlyoutAttributes();
341            });
342        };
343
344        /**
345         * Deal with clashing JS for opening dialogs on click, and do not let
346         * more than one dialog/dropdown appear at once.
347         */
348        var editorSetupDropdowns = function(editorInstance) {
349            $(document).on('click touchstart', function() {
350                editorDropdownsClose();
351            });
352
353            $(document).on('click touchstart', '.editor-dropdown', function(e) {
354                e.stopPropagation();
355            });
356
357
358            $('.editor-dropdown .editor-action')
359                .off('click.dd')
360                .on('click.dd', function(e) {
361
362                    var $parentEl = $(e.target).closest('.editor-dropdown');
363
364                    // Again, tackling with clash from multiple codebases.
365                    $('.editor-insert-dialog').each(function(i, el) {
366                        setTimeout(function() {
367                            $(el).removeAttr('style');
368                        }, 0);
369                    });
370
371                    if ($parentEl.hasClass('editor-dropdown') && $parentEl.hasClass('editor-dropdown-open')) {
372                        $parentEl.removeClass('editor-dropdown-open');
373                        //$($parentEl).find('.wysihtml5-command-dialog-opened').removeClass('wysihtml5-command-dialog-opened');
374                    } else {
375                        // clear other opened dropdowns before opening this one
376                        $($parentEl).parent('.editor').find('.editor-dropdown-open').each(function(i, el) {
377                            $(el).removeClass('editor-dropdown-open');
378                            $(el).find('.wysihtml5-command-dialog-opened').removeClass('wysihtml5-command-dialog-opened');
379                        });
380
381                        // If the editor action buttons have been disabled (by switching
382                        // to HTML code view, then do not allow dropdowns. CSS pointer-
383                        // events should have taken care of this, but JS still fires the
384                        // event regardless, so disable them here as well.
385                        if (!$parentEl.hasClass('wysihtml5-commands-disabled')) {
386                            $parentEl.addClass('editor-dropdown-open');
387
388                            // if has input, focus and move caret to end of text
389                            var inputBox = $parentEl.find('.InputBox');
390                            if (inputBox.length) {
391                                editorSelectAllInput(inputBox[0]);
392                            }
393                        }
394                    }
395
396                    $parentEl.setFlyoutAttributes();
397
398                }).on('keypress', function(e){
399                    var key = e.keyCode || e.which;
400                    if (key == 13 || key == 32) {
401                        $(this).trigger('click.dd');
402                    }
403                });
404
405            // Handle Enter key
406            $('.editor-dropdown').find('.InputBox').on('keydown', function(e) {
407                var key = e.keyCode || e.which;
408                if (key === 13 || key === 32) {
409                    // Cancel enter key submissions on these values.
410                    if (this.value == ''
411                        || this.value == 'http://'
412                        || this.value == 'https://') {
413                        e.stopPropagation();
414                        e.preventDefault();
415                        return false;
416                    }
417
418                    // Fire event programmatically to do what needs to be done in
419                    // ButtonBar code.
420                    $(this).parent().find('.Button').trigger('click.insertData');
421
422                    e.stopPropagation();
423                    e.preventDefault();
424                    return false;
425                }
426            });
427
428            // Clicking into an editor area should close the dropdown, but keep
429            // it open for anything else.
430            $('.TextBoxWrapper').add($('.wysihtml5-sandbox').contents().find('html')).each(function(i, el) {
431                $(el).addClass('editor-dialog-fire-close');
432            });
433
434            // Target all elements in the document that fire the dropdown close
435            // from within the iframe, and attach the relevant callbacks to events.
436            $('.wysihtml5-sandbox').contents().find('.editor-dialog-fire-close')
437                .off('mouseup.fireclose dragover.fireclose')
438                .on('mouseup.fireclose dragover.fireclose', function(e) {
439                    $('.editor-dropdown').each(function(i, el) {
440                        $(el).removeClass('editor-dropdown-open');
441                        $(el).find('.wysihtml5-command-dialog-opened').removeClass('wysihtml5-command-dialog-opened');
442                        $(el).setFlyoutAttributes();
443                    });
444                });
445        };
446
447        /**
448         * Editor does not play well with Quotes plugin in Wysiwyg mode.
449         */
450        var editorHandleQuotesPlugin = function(editorInstance) {
451            var editor = editorInstance;
452
453            /*
454             // handle Quotes plugin using own logic.
455             $('.MessageList')
456             .on('mouseup.QuoteReply', 'a.ReactButton.Quote', function(e) {
457             // For the quotes plugin to insert the quoted text, it
458             // requires that the textarea be pastable, which is not true
459             // when not displayed, so momentarily toggle to it, then,
460             // unavoidable, wait short interval to then allow wysihtml5
461             // to toggle back and render the content.
462             editor.fire("change_view", "textarea");
463             $(editor.textarea.element).css({"opacity":"0.50"});
464             var initialText = $(editor.textarea.element).val();
465             var si = setInterval(function() {
466             if ($(editor.textarea.element).val() !== initialText) {
467             clearInterval(si);
468             $(editor.textarea.element).css({"opacity":""});
469             editor.fire("change_view", "composer");
470             // Inserting a quote at the end prevents editor from
471             // breaking out of quotation, which means everything
472             // typed after the inserted quotation, will be wrapped
473             // in a blockquote.
474             editor.composer.selection.setAfter(editor.composer.element.lastChild);
475             editor.composer.commands.exec("insertHTML", "<p></p>");
476
477             // editor.composer.setValue(editor.composer.getValue() + "<p><br></p>");
478             // editor.fire("focus:composer");
479             }
480             }, 0);
481             });
482             */
483
484            $(editor.textarea.element).on('appendHtml', function(e, data) {
485
486                // The quotes plugin tends to add line breaks to the end of the
487                // quoted string, which upsets wysihtml5 paragraphing, so replace
488                // it with proper block ending to make sure paragraphs continue.
489//            data = data.replace(/<br\s?\/?>$/, '<p><br></p>');
490
491                // Read nullFix function for full explanation. Essentially,
492                // placeholder does not get removed, so remove it manually if
493                // one is set.
494                if (editor.composer.placeholderSet) {
495                    // Just clear it on Firefox, then insert null fix.
496                    editor.composer.setValue('');
497                }
498
499//            if (window.chrome || navigator.userAgent.toLowerCase().indexOf('safari') > -1) {
500//               var initial_value = editor.composer.getValue();
501//
502//               if (!initial_value.length
503//                  || initial_value.toString() === '<p></p>') {
504//                  editor.composer.setValue('foo: ');
505//               }
506//            }
507
508                editor.focus();
509                editor.composer.commands.exec("insertHTML", data);
510
511                // Reported bug: Chrome does not handle wysihtml5's insertHTML
512                // command properly. The downside to this workaround is that the
513                // caret will be placed at the beginning of the text box.
514                // Safari also does not handle this command properly, so check
515                // for that as well. Can just check for the safari or webkit
516                // strings, but there's no telling when Chrome will change their
517                // user agent string to reflect Blink and other changes.
518//            if (window.chrome || navigator.userAgent.toLowerCase().indexOf('safari') > -1) {
519//               var initial_value = editor.composer.getValue();
520//
521//               if (!initial_value.length
522//               || initial_value.toString() === '<p></p>') {
523//                  editor.composer.setValue(initial_value + data);
524//               } else {
525//                  editor.composer.setValue(initial_value);
526//               }
527//            }
528
529//            editor.focus();
530            });
531
532        };
533
534        /**
535         * This is just to make sure that editor, upon choosing to edit an
536         * inline post, will be scrolled to the correct location on the page.
537         * Some sites may have plugins that interfere on edit, so take care of
538         * those possibilities here.
539         */
540        var scrollToEditorContainer = function(textarea) {
541            var scrollto = $(textarea).closest('.Comment');
542
543            if (!scrollto.length) {
544                scrollto = $(textarea).closest('.CommentForm');
545            }
546
547            if (scrollto.length) {
548                $('html, body').animate({
549                    scrollTop: $(scrollto).offset().top
550                }, 400);
551            }
552        };
553
554        /**
555         * Inserting images into the wysi editor does not automatically adjust
556         * the height. It tends to require a hover over, so call this function
557         * to auto-adjust it. This will add the extra height onto the current
558         * height.
559         */
560        var wysiInsertAdjustHeight = function(editorInstance, addHeight) {
561            editorInstance.focus();
562            var iframeElement = $(editorInstance.composer.iframe);
563            var newHeight = parseInt($(iframeElement).css('min-height')) + addHeight + 'px';
564            $(iframeElement).css('min-height', newHeight);
565        };
566
567        /**
568         * Chrome wraps span around content. Firefox prepends b.
569         * No real need to detect browsers.
570         */
571        var wysiPasteFix = function(editorInstance) {
572            var editor = editorInstance;
573            editor.observe("paste:composer", function(e) {
574                // Cancel out this function's operation for now. On the original
575                // 0.3.0 version, pasting google docs would wrap either a span or
576                // b tag around the content. Now, since moving to 0.4.0pre, the
577                // paragraphing messes this up severaly. Moreover, pasting
578                // through this function sets caret to end of composer.
579                // Originally found this bug through a client site mentioning paste
580                // issue, which opened up larger issue of pasting with new version
581                // of wysihtml5. For now, disable paste filtering to make sure
582                // pasting and the caret remain in same position.
583                // TODO.
584                //return;
585                // Grab paste value
586                ////var paste = this.composer.getValue();
587                // Just need to remove first one, and wysihtml5 will auto
588                // make sure the pasted html has all tags closed, so the
589                // last will just be stripped automatically. sweet.
590                ////paste = paste.replace(/^<(span|b)>/m, ''); // just match first
591                // Insert into composer
592                ////this.composer.setValue(paste);
593            });
594        };
595
596
597        /**
598         * Debugging lazyloaded scripts impossible with jQuery getScript/get,
599         * so make sure available.
600         *
601         * http://balpha.de/2011/10/jquery-script-insertion-and-its-consequences-for-debugging/
602         */
603        function loadScript(path) {
604            var result = $.Deferred(),
605                script = document.createElement("script");
606            script.async = "async";
607            script.type = "text/javascript";
608            script.src = path;
609            script.onload = script.onreadystatechange = function(_, isAbort) {
610                if (!script.readyState || /loaded|complete/.test(script.readyState)) {
611                    if (isAbort)
612                        result.reject();
613                    else
614                        result.resolve();
615                }
616            };
617            script.onerror = function() {
618                result.reject();
619            };
620            $("head")[0].appendChild(script);
621            return result.promise();
622        }
623
624        /**
625         * Strange bug when editing a comment, then returning
626         * to main editor at bottom of discussion. For now,
627         * just use this temp hack. I noticed that if there
628         * was text in the main editor before choosing to edit
629         * a comment further up the discussion, that the main
630         * one would be fine, so insert a zero-width character
631         * that will virtually disappear to everyone and
632         * everything--except wysihtml5.
633         * Actual console error:
634         * NS_ERROR_INVALID_POINTER: Component returned failure code: 0x80004003 (NS_ERROR_INVALID_POINTER) [nsISelection.addRange]
635         * this.nativeSelection.addRange(getNativeRange(range));
636         * LINE: 2836 in wysihtml5-0.4.0pre.js
637         *
638         * &zwnj;
639         * wysihtml5.INVISIBLE_SPACE = \uFEFF
640         */
641        var nullFix = function(editorInstance) {
642            return;
643            var editor = editorInstance;
644            //var text = editor.composer.getValue();
645            //editor.composer.setValue(text + "<p>&zwnj;<br></p>");
646
647            // Problem with this is being able to post "empty", because inv
647isible
648            // space is counted as a character. However, many forums could
649            // implemented a character minimum, so this will
650            // not happen everywhere. Regardless, this is only a bandaid. A real
651            // fix will need to be figured out. The wysihtml5 source was pointing
652            // to a few things, but it largely also utilizes hacks like this, and
653            // in fact does insert an initial p tag in the editor to signal that
654            // paragraphs should follow.
655            var insertNull = function() {
656                if (!window.chrome) {
657                    // When placeholder attribute set, Firefox does not clear it, and this
658                    // is due to this nullfix. Chrome handles it okay, though this
659                    // whole function (nullFix) is just a mess of flimflam.
660                    // Paragraphing in Wysihtml5 added many exceptions.
661                    if (editor.composer.placeholderSet) {
662                        // Just clear it on Firefox, then insert null fix.
663                        editor.composer.setValue('');
664                    }
665
666                    editor.composer.commands.exec("insertHTML", "<span></span>");
667                } else {
668                    editor.composer.setValue(editor.composer.getValue() + "<span></span>");
669                }
670                editor.fire("blur", "composer");
671                editor.focus();
672            };
673
674            editor.on("focus", function() {
675                if (!editor.composer.getValue().length) {
676                    insertNull();
677                }
678            });
679
680            // On Chrome, when loading page, first editing a post, then going to
681            // reply, ctrl+a all body of new reply, delete, then start typing,
682            // and paragraphing is gone. This is because I'm only checking and
683            // inserting null on backspace when empty. I could just interval
684            // check for emptiness, but this nullFix is already too hackish.
685            // OKAY no. Return to this problem in future.
686//         $(editor.composer.doc).on('keyup', function(e) {
687//            // Backspace
688//            if (e.which == 8) {
689//               if (!editor.composer.getValue().length) {
690//                  insertNull();
691//               }
692//            }
693//         });
694        };
695
696        /**
697         * Generic helper to generate correct image code based on format.
698         */
699        var buildImgTag = function(href, editorFormat) {
700            // Default for text and textex. If in future they require special
701            // handling, then add them to switch statement.
702            var imgTag = href;
703
704            switch (editorFormat.toLowerCase()) {
705                case 'wysiwyg':
706                case 'html':
707                    imgTag = '<img src="' + href + '" alt="" />';
708                    break;
709                case 'bbcode':
710                    imgTag = '[img]' + href + '[/img]';
711                    break;
712                case 'markdown':
713                    imgTag = '![](' + href + ' "")';
714                    break;
715                case 'text':
716                case 'textex':
717                    imgTag = '';
718                    break;
719            }
720
721            return imgTag;
722        };
723
724        /* Note, this depends heavily on blueimp's file upload, and just a bit
725         * on Tim Down's rangyinputs (originally loaded for buttonbarplus).
726         */
727        var fileUploadsInit = function(dropElement, editorInstance) {
728
729            if (!jQuery.fn.fileupload) {
730                //console.warn('Editor missing fileupload dependency.');
731                return false;
732            }
733
734            var canUpload = (gdn.definition('canUpload', false)) ? 1 : 0;
735            if (canUpload != 1) {
736                return false;
737            }
738
739            // Disable default browser behaviour of file drops
740            $(document).on('drop dragover', function(e) {
741                e.preventDefault();
742            });
743
744            // If drag and drop unavailable, remove its cue from the attachment
745            // dropdown menu, so it will just have the file input and URL input.
746            if (!window.FileReader) {
747                $('.js-can-drop').hide();
748            }
749
750            // Multi upload element. If an iframe, it will need to be checked
751            // in the loop far below. Redundant, but let's see.
752            var dropZone = dropElement;
753
754            // Pass editor instance to uploader, to access methods
755            var editor = (editorInstance)
756                ? editorInstance
757                : dropElement;
758
759            var maxUploadSize = gdn.definition('maxUploadSize');
760            var editorFileInputName = gdn.definition('editorFileInputName');
761            var allowedFileExtensions = gdn.definition('allowedFileExtensions');
762            var maxFileUploads = gdn.definition('maxFileUploads');
763
764            // Add CSS class to this element to style children on dragover
765            var $dndCueWrapper = $(dropElement).closest('.bodybox-wrap');
766
767            // Determine if element passed is an iframe or local element.
768            var handleIframe = false;
769            var iframeElement = '';
770            if ($(dropElement)[0].contentWindow) {
771                iframeElement = dropElement;
772                dropElement = $(dropElement)[0].contentWindow;
773                handleIframe = true;
774            }
775
776            // Insert container for displaying all uploads. All successful
777            // uploads will be inserted here.
778            $dndCueWrapper.find('.TextBoxWrapper').after('<div class="editor-upload-previews"></div>');
779            $editorUploadPreviews = $dndCueWrapper.find('.editor-upload-previews');
780
781            // Handle drop effect as UX cue
782            $(dropElement).on('dragenter dragover', function(e) {
783                $dndCueWrapper.addClass('editor-drop-cue');
784
785                // Chrome cannot handle the influx of events and adding the CSS
786                // class, which causes the border to become very buggy, so just
787                // force it for now. Also, this is only on Wysiwyg format.
788                if (handleIframe) {
789                    $(editor).focus();
790                    $(this).css({
791                        'border': '2px dashed rgba(0,0,0,0.25)',
792                        'border-radius': '2px'
793                    });
794                }
795            }).on('drop dragend dragleave', function(e) {
796                $dndCueWrapper.removeClass('editor-drop-cue');
797            });
798
799
800            // Get current comment or discussion post box
801
802            // This is the key that finds .editor-upload-saved
803            var editorKey = 'editor-uploads-';
804            var editorForm = $dndCueWrapper.closest('form');
805
806            var savedUploadsContainer = savedContainer = '';
807            var mainCommentForm = '';
808            if (editorForm) {
809                var formCommentId = $(editorForm).find('#Form_CommentID');
810                var formDiscussionId = $(editorForm).find('#Form_DiscussionID');
811                var formConversationId = $(editorForm).find('#Form_ConversationID');
812
813                // Determine if bodybox loaded is the main comment one. This one
814                // will never have any saved uploads, so make sure it never grabs
815                // saved ones when switching.
816                var mainCommentBox = false;
817                if ((formCommentId.length
818                    && formCommentId[0].value == ''
819                    && formDiscussionId.length
820                    && parseInt(formDiscussionId[0].value) > 0)
821                    || (formConversationId.length
822                    && parseInt(formConversationId[0].value) > 0)) {
823                    mainCommentBox = true;
824                    mainCommentForm = editorForm;
825                    mainCommentPreviews = $editorUploadPreviews;
826                }
827
828                // Build editorKey
829                if (formCommentId.length
830                    && parseInt(formCommentId[0].value) > 0) {
831                    var type = 'CommentID';
832                    var id = formCommentId[0].value;
833                    editorKey += 'commentid' + formCommentId[0].value;
834                } else if (formDiscussionId.length
835                    && parseInt(formDiscussionId[0].value) > 0) {
836                    var type = 'DiscussionID';
837                    var id = formDiscussionId[0].value;
838                    editorKey += 'discussionid' + formDiscussionId[0].value;
839                } else if (formConversationId.length
840                    && parseInt(formConversationId[0].value) > 0) {
841                    var type = 'ConversationID';
842                    var id = formConversationId[0].value;
843                    editorKey += 'conversationid' + formConversationId[0].value;
844                }
845
846                // Make saved files editable from the form
847                if (!mainCommentBox && editorKey != 'editor-uploads-') {
848                    var savedContainer = $('#' + editorKey);
849                    if (savedContainer.length && savedContainer.html().trim() != '') {
850                        savedContainer.hide();
851                        // Move existing uploads into preview container for better UX.
852                        var form = $('#Form_' + type + ':input[value="' + id + '"]').closest('form').find('.bodybox-wrap');
853                        form.children('.editor-upload-previews').html(savedContainer.html());
854                        savedUploadsContainer = form.children('.editor-upload-previews');
855                    }
856                }
857            }
858
859            var insertImageIntoBody = function(filePreviewContainer) {
860                $(filePreviewContainer).removeClass('editor-file-removed');
861                var file = $(filePreviewContainer).find('a.filename');
862                var type = $(file).data('type');
863                var href = file.attr('href');
864            };
865
866            var removeImageFromBody = function(filePreviewContainer) {
867                $(filePreviewContainer).addClass('editor-file-removed');
868                var file = $(filePreviewContainer).find('a.filename');
869                var type = $(file).data('type');
870                var href = file.attr('href');
871            };
872
873            /**
874             * Remove and reattach files in a live upload session
875             */
876            $editorUploadPreviews
877                .on('click.live-file-remove', '.editor-file-remove', function(e) {
878                    var $editorFilePreview = $(this).closest('.editor-file-preview');
879                    $editorFilePreview.find('input').attr('name', 'RemoveMediaIDs[]');
880
881                    // Remove element from editor body
882                    removeImageFromBody($editorFilePreview);
883                })
884                .on('click.live-file-reattach', '.editor-file-reattach', function(e) {
885                    var $editorFilePreview = $(this).closest('.editor-file-preview');
886                    $editorFilePreview.find('input').attr('name', 'MediaIDs[]');
887
888                    // Re-attach
889                    insertImageIntoBody($editorFilePreview);
890                });
891
892            /**
893             * Remove and reattach files in a saved upload session, typically
894             * through editing.
895             */
896            if (savedUploadsContainer) {
897                // Turn read-only mode on. Event is fired from conversations.js
898                // and discussion.js.
899                $(editorForm).on('clearCommentForm', function(e) {
900                    $(savedContainer).addClass('editor-upload-readonly');
901                    $(savedContainer).show();
902                });
903
904                $(editorForm).on('clearMessageForm', function(e) {
905                    $(savedContainer).addClass('editor-upload-readonly');
906                    $(savedContainer).show();
907                });
908
909                $(savedUploadsContainer)
910                    // Turn read-only mode off.
911                    .removeClass('editor-upload-readonly')
912                    // Remove saved file. This will add hidden input to form
913                    .on('click.saved-file-remove', '.editor-file-remove', function(e) {
914                        var $editorFilePreview = $(this).closest('.editor-file-preview');
915                        var mediaId = $editorFilePreview.find('input').val();
916
917                        // Add hidden input to form so it knows to remove files.
918                        $('<input>').attr({
919                            type: 'hidden',
920                            id: 'file-remove-' + mediaId,
921                            name: 'RemoveMediaIDs[]',
922                            value: mediaId
923                        }).appendTo($(editorForm));
924                    })
925                    // This will remove the hidden input
926                    .on('click.saved-file-reattach', '.editor-file-reattach', function(e) {
927                        var $editorFilePreview = $(this).closest('.editor-file-preview');
928                        var mediaId = $editorFilePreview.find('input').val();
929
930                        // Remove hidden input from form
931                        $('#file-remove-' + mediaId).remove();
932                    });
933            }
934
935            // Clear session preview files--this for main comment box.
936            if (mainCommentBox) {
937                // When closing editor with new uploads in session, typically
938                // commentbox at bottom of discussion, remove the uploads
939                $(editorForm).on('clearCommentForm', function(e) {
940                    // Empty out the session previews.
941                    $(mainCommentPreviews).empty();
942                });
943
944                $(editorForm).on('clearMessageForm', function(e) {
945                    // Empty out the session previews.
946                    $(mainCommentPreviews).empty();
947                });
948            }
949
950            // List of already uploaded files.
951            var getCurrentEditorFiles = function() {
952                var filesInDiscussion = $(editor).closest('.FormWrapper').find('.editor-file-preview');
953                var fileNames = [];
954
955                filesInDiscussion.each(function(i, el) {
956                    fileNames.push($(el).find('.filename').text().trim());
957                });
958
959                return fileNames;
960            };
961
962            $(dropZone).each(function(i, el) {
963                var $init = $(this);
964                var cssDropInitClass = 'editor-dropzone-init';
965
966                if (!$(this).hasClass(cssDropInitClass)) {
967                    $(this).addClass(cssDropInitClass);
968
969                    if ($init[0].contentWindow) {
970                        $init = $init[0].contentWindow;
971                    }
972                }
973
974                // Save original title for progress meter title percentages.
975                documentTitle = document.title;
976
977                // Attach voodoo to dropzone.
978                // Originally was just bodybox-wrap, but that was causing multiple
979                // dropzone issues, then was just 'this', but that was preventing
980                // any file input from working in the form, so this is best.
981                $(this).closest('.bodybox-wrap').fileupload({
982
983                    // Options
984                    url: gdn.url('/post/editorupload'),
985                    paramName: editorFileInputName,
986                    dropZone: $init,
987                    forceIframeTransport: false,
988                    dataType: 'json',
989                    progressInterval: 5,
990                    autoUpload: true,
991
992                    // Fired on entire drop, and contains entire batch.
993                    drop: function(e, data) {
994
995                        // This is from PHP's max_file_uploads setting
996                        if (data.files.length > maxFileUploads) {
997                            var message = 'You cannot upload more than ' + maxFileUploads + ' files at once.';
998                            gdn.informMessage(message);
999                            return false;
1000                        }
1001                    },
1002
1003                    // Where to process and validate files.
1004                    add: function(e, data) {
1005                        if (data.autoUpload
1006                            || (data.autoUpload !== false
1007                            && $(this).fileupload('option', 'autoUpload'))) {
1008                            data.process().done(function() {
1009                                // Single upload per request, files[] is always
1010                                // going to be an array of 1.
1011                                var file = data.files[0];
1012                                var type = file.type.split('/').pop().toLowerCase();
1013                                var filename = file.name;
1014                                var extension = filename.split('.').pop().toLowerCase();
1015                                var allowedExtensions = JSON.parse(allowedFileExtensions);
1016
1017                                if (data.fileInput !== undefined) {
1018                                    // We're using either the fileupload or imageupload dropdown to upload the file(s).
1019                                    // Type hint which dropdown the user is using so we know whether to insert the
1020                                    // upload into the post (as an image) or attach it to the post.
1021                                    var uploadType = $(data.fileInput).data('uploadType');
1022                                    data.url = gdn.url('/post/editorupload/' + uploadType);
1023                                }
1024
1025                                // Make absolutely sure that the file extensions
1026                                // provided are all lowercase.
1027                                $.each(allowedExtensions, function(index, item) {
1028                                    allowedExtensions[index] = item.toLowerCase();
1029                                });
1030
1031                                var validSize = (file.size <= maxUploadSize)
1032                                    ? true
1033                                    : false;
1034
1035                                // Check against provided file type and file extension.
1036                                // Note: the file type check was removed. Not all mime
1037                                // types are formatted like images (image/jpeg), where
1038                                // the subtype (jpeg) can match a file extension.
1039                                //var validFile = ($.inArray(type, allowedExtensions) > -1 && $.inArray(extension, allowedExtensions) > -1)
1040                                var validFile = ($.inArray(extension, allowedExtensions) > -1)
1041                                    ? true
1042                                    : false;
1043
1044                                // Check if the file is already a part of the
1045                                // current post.
1046                                var fileAlreadyExists = ($.inArray(filename, getCurrentEditorFiles()) > -1)
1047                                    ? true
1048                                    : false;
1049
1050                                // Apple devices upload files with very generic names.
1051                                // For example, uploading an image will produce a name
1052                                // like "image.jpeg" for every jpeg image. Disable
1053                                // superficial file duplication check for these cases.
1054                                if (/ip(hone|ad|od)/i.test(navigator.userAgent)) {
1055                                    fileAlreadyExists = false;
1056                                }
1057
1058                                if (validSize && validFile && !fileAlreadyExists) {
1059                                    data.submit();
1060
1061                                    // If selecting a file through traditional file
1062                                    // input, close the dropdown.
1063                                    editorDropdownsClose();
1064                                } else {
1065                                    // File dropped is not allowed!
1066                                    var message;
1067
1068                                    if (!validSize) {
1069                                        message = gdn.definition('fileErrorSize', 'editor') + ' (max ' + maxUploadSize + ' bytes)';
1070                                        gdn.informMessage(message);
1071                                    }
1072
1073                                    if (!validFile) {
1074                                        message = gdn.definition('fileErrorFormat', 'editor');
1075                                        gdn.informMessage(message);
1076                                    }
1077
1078                                    if (fileAlreadyExists) {
1079                                        message = gdn.definition('fileErrorAlreadyExists', 'editor');
1080                                        gdn.informMessage(message);
1081                                    }
1082                                }
1083                            });
1084                        }
1085                    },
1086
1087                    // Fired on every file.
1088                    //send: function(e, data) {},
1089
1090                    // There is also `progress` per file upload.
1091                    progressall: function(e, data) {
1092                        var progress = parseInt(data.loaded / data.total * 100, 10);
1093                        var $progressMeter = $(this).closest('form').find('.editor-upload-progress');
1094                        document.title = '(' + progress + '%) ' + documentTitle;
1095                        $progressMeter.css({
1096                            'opacity': 1,
1097                            'width': progress + '%'
1098                        });
1099                    },
1100
1101                    // Fired on successful upload
1102                    done: function(e, data) {
1103
1104                        var result = data.result;
1105
1106                        if (!result.error) {
1107                            var payload = result.payload;
1108
1109                            var imgTag = buildImgTag(payload.original_url, format);
1110
1111                            // If we uploaded an image and we have an image tag, insert directly into editor area.
1112                            if (payload.upload_type === 'image' && imgTag) {
1113                                // Determine max height for sample. They can resize it
1114                                // afterwards.
1115                                var maxHeight = (payload.original_height >= 400)
1116                                    ? 400
1117                                    : payload.original_height;
1118
1119                                var payloadHeight = payload.original_height;
1120                                var payloadWidth = payload.original_width;
1121                                var editorWidth = $(this).find('.BodyBox,.js-bodybox').width();
1122
1123                                // Image max-width is 100%. Change the height to refect scaling down the width.
1124                                if (editorWidth < payloadWidth) {
1125                                    payloadHeight = (editorWidth * payload.original_height) / payload.original_width;
1126                                }
1127
1128                                if (handleIframe) {
1129                                    editor.focus();
1130                                    editor.composer.commands.exec("insertImage", {src: payload.original_url, alt: ''});
1131                                    //editor.composer.commands.exec('insertHTML', '<p>' + imgTag + '</p>');
1132                                    wysiInsertAdjustHeight(editor, payloadHeight);
1133                                } else {
1134                                    try {
1135                                        // i don't know why adding [0] to this when iframe
1136                                        // matters, and clear up part of t problem.
1137                                        if (imgTag) {
1138                                            $(editor).replaceSelectedText(imgTag + '\n');
1139                                        }
1140                                    } catch (ex) {
1141                                    }
1142                                }
1143                            } else {
1144                                // If has thumbnail, display it instead of generic file icon.
1145                                var filePreviewCss = (payload.thumbnail_url)
1146                                    ? '<i class="file-preview img" style="background-image: url(' + payload.thumbnail_url + ')"></i>'
1147                                    : '<i class="file-preview icon icon-file"></i>';
1148
1149                                // If it's an image, then indicate that it's been embedded
1150                                // in the post
1151                                var imageEmbeddedText = (payload.thumbnail_url)
1152                                    ? ' &middot; <em title="' + gdn.meta['fileUpload-insertedTooltip'] + '">' + gdn.meta['fileUpload-inserted'] + '</em>'
1153                                    : '';
1154
1155                                var html = ''
1156                                    + '<div class="editor-file-preview" id="media-id-' + payload.MediaID + '" title="' + payload.Filename + '">'
1157                                    + '<input type="hidden" name="MediaIDs[]" value="' + payload.MediaID + '" />'
1158                                    + filePreviewCss
1159                                    + '<div class="file-data">'
1160                                    + '<a class="filename" data-type="' + payload.type + '" data-width="' + payload.original_width + '" data-height="' + payload.original_height + '" href="' + payload.original_url + '" target="_blank">' + payload.Filename + '</a>'
1161                                    + '<span class="meta">' + payload.FormatFilesize + imageEmbeddedText + '</span>'
1162                                    + '</div>'
1163                                    + '<span class="editor-file-remove" title="' + window.gdn.meta['fileUpload-remove']  + '"></span>'
1164                                    + '<span class="editor-file-reattach" title="' + window.gdn.meta['fileUpload-reattatch']  + ' \'' + payload.Filename + '\'"></span>'
1165                                    + '</div>';
1166
1167
1168                                // Editor upload previews is getting found above, and
1169                                // does not change per upload dropzone, which causes
1170                                // files to preview on the last found preview zone.
1171                                $editorUploadPreviews = $(this).closest('form').find('.editor-upload-previews');
1172
1173                                // Add file blocksjust below editor area for easy removal
1174                                // and preview.
1175                                // Find it here.
1176                                $editorUploadPreviews.append(html);
1177                            }
1178                        }
1179                    },
1180
1181                    // Called after every file upload in a session
1182                    // Typically this is fired due to 400 bad request, because
1183                    // the file was probably not allowed, but passed client checks.
1184                    // That would tend to mean the client checks need to be updated.
1185                    fail: function(e, data) {
1186                        var message = gdn.definition('The file failed to upload.');
1187                        gdn.informMessage(message);
1188                        return false;
1189                    },
1190
1191                    // Called regardless of success or failure.
1192                    //always: function(e, data) {},
1193
1194                    // This is the last event that fires, and it's only fired once.
1195                    // Note, sometimes the upload progress meter never reaches the
1196                    // end, so this would be a good place to clear it as a backup.
1197                    stop: function(e) {
1198                        var $progressMeter = $(this).closest('form').find('.editor-upload-progress');
1199
1200                        // If progress meter didn't reach the end, force it.
1201                        $progressMeter.css({
1202                            'width': '100%'
1203                        });
1204
1205                        $progressMeter.addClass('fade-out');
1206
1207                        // Transition inserted above is 400ms, so remove it shortly
1208                        // after.
1209                        setTimeout(function() {
1210                            // Restore original document title
1211                            document.title = documentTitle;
1212                            // Remove transition class
1213                            $progressMeter.removeClass('fade-out');
1214                            // Reset width
1215                            $progressMeter.css({
1216                                'opacity': 0,
1217                                'width': 0
1218                            });
1219                        }, 710);
1220                    }
1221                });
1222            });
1223        };
1224
1225        /**
1226         * Combine the image URL input box with the file uploader dropdown, so
1227         * handle the event differently for wysi and non-wysi mode.
1228         */
1229        var insertImageUrl = function(editorInstance) {
1230
1231            $('.editor-file-image').on('dragover', function(e) {
1232                // Allow passthrough of dragover so main body can accept drop
1233                // event. Only need to flash the passthrough for a moment, then
1234                // remove the passthrough class.
1235                $(this).addClass('drag-passthrough');
1236                var that = this;
1237                setTimeout(function() {
1238                    editorDropdownsClose();
1239                    $(that).removeClass('drag-passthrough');
1240                }, 250);
1241            });
1242
1243            $('.editor-input-image').on('keyup', function(e) {
1244                // Match image URLs
1245                var imageUrl = e.target.value;
1246                // Doesn't have to be perfect, because URL can still not exist.
1247                var imageUrlRegex = /(?:\/\/)(?:.+)(\/)(.*\.(?:jpe?g|gif|png))(?:\?([^#]*))?(?:#(.*))?/i;
1248                if (imageUrlRegex.test(imageUrl)) {
1249
1250                    editorInstance.focus();
1251
1252                    // Working with wysihtml5
1253                    if (editorInstance.composer) {
1254                        editorInstance.composer.commands.exec("insertImage", {src: imageUrl, alt: ''});
1255                        // Consider autogrowing body when insert, as iframe doesn't
1256                        // grow until after hover over. This is done with the file
1257                        // uploader, but we know the height, so use JS and insert
1258                        // only on load.
1259                        var img = new Image();
1260                        img.src = imageUrl;
1261                        $(img).on('load', function(e) {
1262                            naturalHeight = this.height;
1263                            wysiInsertAdjustHeight(editorInstance, naturalHeight);
1264                        });
1265                    }
1266                    // Standard insert
1267                    else {
1268                        var imgTag = buildImgTag(imageUrl, format);
1269                        $(editorInstance).replaceSelectedText(imgTag + '\n');
1270                    }
1271
1272                    // Now clear input and close dropdown
1273                    var $dropDown = $(this).closest('.editor-dropdown-open');
1274                    $dropDown.removeClass('editor-dropdown-open');
1275                    $dropDown.setFlyoutAttributes();
1276                    $(this).val('');
1277
1278                }
1279            });
1280        };
1281
1282        /**
1283         * Mobile devices don't play too well with contenteditable within an
1284         * iFrame, particularly iOS.
1285         */
1286        var iOSwysiFix = function(editor) {
1287
1288            // iOS keyboard does not push content up initially,
1289            // thus blocking the actual content. Typing (spaces, newlines) also
1290            // jump the page up, so keep it in view.
1291            if (window.parent.location != window.location && (/ipad|iphone|ipod/i).test(navigator.userAgent)) {
1292
1293                var contentEditable = $(editor.composer.iframe).contents().find('body');
1294                contentEditable.attr('autocorrect', 'off');
1295                contentEditable.attr('autocapitalize', 'off');
1296
1297                try {
1298                    var iOSscrollFrame = $(window.parent.document).find('#vanilla-iframe').contents();
1299                    var iOSscrollTo = $(iOSscrollFrame).find('#' + editor.config.toolbar).closest('form').find('.Buttons');
1300
1301                    contentEditable.on('keydown keyup', function(e) {
1302                        Vanilla.scrollTo(iOSscrollTo);
1303                        editor.focus();
1304                    });
1305
1306                    editor.on('focus', function() {
1307                        //var postButton = $('#'+editor.config.toolbar).parents('form').find('.CommentButton');
1308                        setTimeout(function() {
1309                            Vanilla.scrollTo(iOSscrollTo);
1310                        }, 1);
1311                    });
1312                } catch (e) {
1313                    // "window.parent unsupported for iFrames in your browser"
1314                }
1315            }
1316        }
1317
1318        /**
1319         * This will only be called when debug=true;
1320         */
1321        var wysiDebug = function(editorInstance) {
1322            // Event examples that will come in handy--taken from source.
1323            //editor.fire("change_view", "composer");
1324            //editor.fire("change_view", "textarea");
1325            //this.editor.observe("change_view", function(view) {
1326            //this.editor.observe("destroy:composer", stopInterval);
1327            //editor.setValue('This will do it.');
1328            editorInstance.on("load", function() {
1329                console.log('load');
1330            })
1331                .on("focus", function() {
1332                    console.log('focus');
1333                })
1334                .on("blur", function() {
1335                    console.log('blur');
1336                })
1337                .on("change", function() {
1338                    console.log('change');
1339                })
1340                .on("paste", function() {
1341                    console.log('paste');
1342                })
1343                .on("newword:composer", function() {
1344                    console.log('newword:composer');
1345                })
1346                .on("undo:composer", function() {
1347                    console.log('undo:composer');
1348                })
1349                .on("redo:composer", function() {
1350                    console.log('redo:composer');
1351                })
1352                .on("change:textarea", function() {
1353                    console.log('change:textarea');
1354                })
1355                .on("change:composer", function() {
1356                    console.log('change:composer');
1357                })
1358                .on("paste:textarea", function() {
1359                    console.log('paste:textarea');
1360                })
1361                .on("paste:composer", function() {
1362                    console.log('paste:composer');
1363                })
1364                .on("blur:composer", function() {
1365                    console.log('change:composer');
1366                })
1367                .on("blur:textarea", function() {
1368                    console.log('change:composer');
1369                })
1370                .on("beforecommand:composer", function() {
1371                    console.log('beforecommand:composer');
1372                })
1373                .on("aftercommand:composer", function() {
1374                    console.log('aftercommand:composer');
1375                })
1376                .on("destroy:composer", function() {
1377                    console.log('destroy:composer');
1378                })
1379                .on("set_placeholder", function() {
1380                    console.log('set_placeholder');
1381                })
1382                .on("unset_placeholder", function(e) {
1383                    console.log('unset_placeholder');
1384                });
1385        };
1386
1387        /**
1388         * Initialize editor on every .BodyBox (or other element passed to this
1389         * jQuery plugin) on the page.
1390         *
1391         * Was originallt built for latest mutation observers, but far too
1392         * limited in support and VERY temperamental, so had to move to livequery.
1393         * The params checks are there in case in future want to use mutation
1394         * observers again. For livequery functionality, just pass empty string as
1395         * first param, and the textarea object to the second.
1396         */
1397        var editorInit = function(obj, textareaObj) {
1398            var $t = $(obj);
1399
1400            // if using mutation events, use this, and send mutation
1401            if (typeof obj.target != 'undefined') {
1402                $t = $(obj.target);
1403            }
1404
1405            // if using livequery, use this, and send blank string
1406            if (obj == '') {
1407                $t = $(textareaObj).closest('form');
1408            }
1409
1410            //var currentEditorFormat     = t.find('#Form_Format');
1411            var currentEditorFormat = $t.find('input[name="Format"]');
1412            var $currentEditorToolbar = '';
1413            var $currentEditableTextarea = '';
1414            var currentTextBoxWrapper = '';
1415
1416            // When previewing text in standard reply discussion controller,
1417            // the mutation will cause the iframe to be inserted AGAIN, thus
1418            // having multiple iframes with identical properties, which, upon
1419            // reverting back to editing mode, will break everything, so kill
1420            // mutation callback immediately, so check if iframe already exists.
1421            if ($t.find('iframe').hasClass('vanilla-editor-text')) {
1422                return false;
1423            }
1424
1425            if (currentEditorFormat.length) {
1426
1427                formatOriginal = currentEditorFormat[0].value;
1428                currentEditorFormat = currentEditorFormat[0].value.toLowerCase();
1429                format = currentEditorFormat + '';
1430                $currentEditorToolbar = $t.find('.editor-format-' + format);
1431                //currentEditableTextarea = t.find('#Form_Body');
1432                $currentEditableTextarea = $t.find('.BodyBox,.js-bodybox');
1433
1434                if (textareaObj) {
1435                    $currentEditableTextarea = textareaObj;
1436                }
1437
1438                currentTextBoxWrapper = $currentEditableTextarea.parent('.TextBoxWrapper');
1439
1440                // If singleInstance is false, then odds are the editor is being
1441                // loaded inline and there are other instances on page.
1442                var singleInstance = true;
1443
1444                // Determine if editing a comment, or not. When editing a comment,
1445                // it has a comment id, while adding a new comment has an empty
1446                // comment id. The value is a hidden input.
1447                var commentId = $(currentTextBoxWrapper).parent().find('#Form_CommentID').val();
1448                if (typeof commentId != 'undefined' && commentId != '') {
1449                    singleInstance = false;
1450                }
1451            }
1452
1453            // if found, perform operation
1454            if ($currentEditorToolbar.length
1455                && $currentEditableTextarea.length) {
1456
1457                var currentEditableCommentId = (new Date()).getTime();
1458                var editorName = 'vanilla-editor-text-' + currentEditableCommentId;
1459
1460                $(document).on('click', 'a.PreviewButton', function() {
1461                    $currentEditorToolbar.hide();
1462                });
1463
1464                $(document).on('click', 'a.WriteButton', function() {
1465                    $currentEditorToolbar.show();
1466                });
1467
1468                $(document).on('click', '#Form_PostComment', function() {
1469                    $currentEditorToolbar.show();
1470                });
1471
1472                switch (format) {
1473                    case 'wysiwyg':
1474                    case 'ipb':
1475                    case 'bbhtml':
1476                    case 'bbwysiwyg':
1477
1478                        // Lazyloading scripts, then run single callback
1479                        $.when(
1480                            loadScript(assets + '/js/wysihtml5-0.4.0pre.js?v=' + editorVersion),
1481                            loadScript(assets + '/js/advanced.js?v=' + editorVersion),
1482                            loadScript(assets + '/js/jquery.wysihtml5_size_matters.js?v=' + editorVersion)
1483                        ).done(function() {
1484
1485                                // Throw event for parsing rules, so external plugins
1486                                // can modify the parsing rules of the editor. This was
1487                                // made for one case where a forum's older posts had
1488                                // very heavy inline formatting, using style attributes and
1489                                // deprecated tags like font. This will merge and replace
1490                                // any parsing rules passed. There is an example of how to
1491                                // do this at the bottom of this script.
1492                                $.event.trigger({
1493                                    type: "editorParseRules",
1494                                    message: "Customize parsing rules by merging into e.rules.",
1495                                    rules: wysihtml5ParserRules
1496                                });
1497
1498                                var editorRules = {
1499                                    // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
1500                                    name: editorName,
1501                                    // Whether the editor should look like the textarea (by adopting styles)
1502                                    style: true,
1503                                    // Id of the toolbar element or DOM node, pass false value if you don't want any toolbar logic
1504                                    toolbar: $currentEditorToolbar.get(0),
1505                                    // Whether urls, entered by the user should automatically become clickable-links
1506                                    autoLink: false,
1507                                    // Object which includes parser rules to apply when html gets inserted via copy & paste
1508                                    // See parser_rules/*.js for examples
1509                                    parserRules: wysihtml5ParserRules,
1510                                    // Parser method to use when the user inserts content via copy & paste
1511                                    parser: wysihtml5.dom.parse,
1512                                    // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
1513                                    composerClassName: "editor-composer",
1514                                    // Class name to add to the body when the wysihtml5 editor is supported
1515                                    bodyClassName: "editor-active",
1516                                    // By default wysihtml5 will insert a <br> for line breaks, set this to false to use <p>
1517                                    useLineBreaks: true,
1518                                    // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
1519                                    stylesheets: stylesheetsInclude,
1520                                    // Placeholder text to use, defaults to the placeholder attribute on the textarea element
1521                                    //placeholderText:      "Write something!",
1522                                    // Whether the composer should allow the user to manually resize images, tables etc.
1523                                    allowObjectResizing: true,
1524                                    // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
1525                                    supportTouchDevices: true,
1526                                    // Whether senseless <span> elements (empty or without attributes) should be removed/replaced with their content
1527                                    cleanUp: true
1528                                };
1529
1530                                // instantiate new editor
1531                                var editor = new wysihtml5.Editor($currentEditableTextarea[0], editorRules);
1532
1533                                editor.on('load', function(e) {
1534                                    if (!editor.composer) {
1535                                        $currentEditorToolbar.hide();
1536                                        return;
1537                                    }
1538
1539                                    // enable auto-resize
1540                                    $(editor.composer.iframe).wysihtml5_size_matters();
1541                                    editorHandleQuotesPlugin(editor);
1542
1543                                    // Clear textarea/iframe content on submit.
1544                                    // This is not actually necessary here because
1545                                    // the whole editor is removed from the page on post.
1546                                    $currentEditableTextarea.closest('form').on('clearCommentForm', function() {
1547                                        editor.fire('clear');
1548                                        editor.composer.clear();
1549                                        this.reset();
1550                                        $currentEditableTextarea.val('');
1551                                        //$('iframe').contents().find('body').empty();
1552                                        $(editor.composer.iframe).css({"min-height": "inherit"});
1553                                    });
1554
1555                                    // Fix problem of editor losing its default p tag
1556                                    // when loading another instance on the same page.
1557//                         nullFix(editor);
1558
1559                                    // iOS
1560                                    iOSwysiFix(editor);
1561
1562                                    //wysiPasteFix(editor);
1563                                    fullPageInit(editor);
1564                                    editorSetupDropdowns(editor);
1565
1566                                    // If editor is being loaded inline, then focus it.
1567                                    if (!singleInstance) {
1568                                        //scrollToEditorContainer(editor.textarea.element);
1569                                        editor.focus();
1570                                    } else {
1571                                        // Bug in IE 10,11 where the caret is placed outside of the editor because it is hidden
1572                                        // The bug is caused by focusWithoutScrolling() from wysihtml5-0.4.0pre
1573                                        // To circumvent the issue let's focus the editor instead.
1574                                        var formWrapper = $('div.FormWrapper');
1575                                        formWrapper.on('focus', function() {
1576                                            editor.focus();
1577                                        });
1578                                    }
1579
1580                                    if (debug) {
1581                                        wysiDebug(editor);
1582                                    }
1583
1584                                    // Enable at-suggestions
1585                                    var iframe = $(editor.composer.iframe);
1586                                    var iframe_body = iframe.contents().find('body')[0];
1587                                    // Typically this is called from core on all BodyBox
1588                                    // elements, but the editor's iframe needs special
1589                                    // handling, so it's called directly.
1590                                    gdn.atCompleteInit(iframe_body, iframe[0]);
1591
1592                                    // Enable file uploads. Pass the iframe as the
1593                                    // drop target in wysiwyg, while the regular editor
1594                                    // modes will just require the standard textarea
1595                                    // element.
1596                                    fileUploadsInit(iframe, editor);
1597
1598                                    insertImageUrl(editor);
1599                                });
1600
1601
1602                                // extending whysihtml5 library for spoilers
1603                                (function(wysihtml5) {
1604                                    var undef,
1605                                        REG_EXP = /Spoiler/g;
1606
1607                                    wysihtml5.commands.spoiler = {
1608                                        exec: function(composer, command) {
1609                                            //return wysihtml5.commands.formatInline.exec(composer, command, "div", "Spoiler", REG_EXP);
1610                                            wysihtml5.commands.formatBlock.exec(composer, "formatBlock", "div", "Spoiler", REG_EXP);
1611
1612                                            // If block element chosen from last string in editor, there is no way to
1613                                            // click out of it and continue typing below it, so set the selection
1614                                            // after the insertion, and insert a break, because that will set the
1615                                            // caret to after the latest insertion.
1616                                            if ($(composer.element.lastChild).hasClass('Spoiler')) {
1617                                                var bookmark = composer.selection.getBookmark();
1618                                                composer.selection.setAfter(composer.element.lastChild);
1619                                                composer.commands.exec("insertHTML", "<p><br></p>");
1620                                                composer.selection.setBookmark(bookmark);
1621                                            }
1622                                        },
1623
1624                                        state: function(composer, command) {
1625                                            return wysihtml5.commands.formatBlock.state(composer, "formatBlock", "div", "Spoiler", REG_EXP);
1626                                        },
1627
1628                                        value: function() {
1629                                            return undef;
1630                                        }
1631                                    };
1632                                })(wysihtml5);
1633
1634                                // extending whysihtml5 library for blockquotes
1635                                (function(wysihtml5) {
1636                                    var undef,
1637                                        REG_EXP = /Quote/g;
1638
1639                                    wysihtml5.commands.blockquote = {
1640                                        exec: function(composer, command) {
1641                                            wysihtml5.commands.formatBlock.exec(composer, "formatBlock", "div", "Quote", REG_EXP);
1642                                            if ($(composer.element.lastChild).hasClass('Quote')) {
1643                                                var bookmark = composer.selection.getBookmark();
1644                                                composer.selection.setAfter(composer.element.lastChild);
1645                                                composer.commands.exec("insertHTML", "<p><br></p>");
1646                                                composer.selection.setBookmark(bookmark);
1647                                            }
1648                                        },
1649
1650                                        state: function(composer, command) {
1651                                            return wysihtml5.commands.formatBlock.state(composer, "formatBlock", "div", "Quote", REG_EXP);
1652                                        },
1653
1654                                        value: function() {
1655                                            return undef;
1656                                        }
1657                                    };
1658                                })(wysihtml5);
1659
1660                                // extending whysihtml5 library for code blocks
1661                                (function(wysihtml5) {
1662                                    var undef,
1663                                        REG_EXP = /CodeBlock/g;
1664
1665                                    wysihtml5.commands.code = {
1666                                        exec: function(composer) {
1667                                            var inCodeBlock = this.state(composer);
1668                                            if (inCodeBlock) {
1669                                                // caret is already within a <pre><code>...</code></pre>
1670                                                composer.selection.executeAndRestore(function() {
1671                                                    var pre = wysihtml5.dom.getParentElement(
1672                                                        composer.selection.getSelectedNode(),
1673                                                        {
1674                                                            className: "CodeBlock",
1675                                                            classRegExp: /CodeBlock/g,
1676                                                            nodeName: "PRE"
1677                                                        },
1678                                                        3
1679                                                    );
1680                                                    var code = pre.querySelector("code");
1681
1682                                                    wysihtml5.dom.replaceWithChildNodes(pre);
1683                                                    if (code) {
1684                                                        wysihtml5.dom.replaceWithChildNodes(code);
1685                                                    }
1686                                                });
1687                                            } else {
1688                                                // Wrap in <pre><code>...</code></pre>
1689                                                var range = composer.selection.getRange();
1690                                                var selectedNodes = range.extractContents();
1691                                                var pre = composer.doc.createElement("pre");
1692                                                var code = composer.doc.createElement("code");
1693
1694                                                pre.className = "CodeBlock";
1695                                                pre.appendChild(code);
1696                                                code.appendChild(selectedNodes);
1697                                                range.insertNode(pre);
1698                                                composer.selection.selectNode(code);
1699
1700                                                if ($(composer.element.lastChild).hasClass('CodeBlock')) {
1701                                                    var bookmark = composer.selection.getBookmark();
1702                                                    composer.selection.setAfter(composer.element.lastChild);
1703                                                    composer.commands.exec("insertHTML", "<p><br></p>");
1704                                                    composer.selection.setBookmark(bookmark);
1705                                                }
1706                                            }
1707                                        },
1708                                        state: function(composer) {
1709                                            // Determine if we're in a code block, inside a pre block.
1710                                            var selectedNode = composer.selection.getSelectedNode();
1711                                            var inCode = !!wysihtml5.dom.getParentElement(selectedNode, {
1712                                                nodeName: "CODE"
1713                                            }, 2);
1714                                            var parentIsPre = !!wysihtml5.dom.getParentElement(selectedNode, {
1715                                                className: "CodeBlock",
1716                                                classRegExp: /CodeBlock/g,
1717                                                nodeName: "PRE"
1718                                            }, 3);
1719
1720                                            return inCode && parentIsPre;
1721                                        },
1722
1723                                        value: function() {
1724                                            return undef;
1725                                        }
1726                                    };
1727                                })(wysihtml5);
1728
1729                                // extending whysihtml5 library for color highlights
1730                                (function(wysihtml5) {
1731                                    var undef,
1732                                        REG_EXP = /post-highlightcolor-[0-9a-z]+/g;
1733
1734                                    wysihtml5.commands.highlightcolor = {
1735                                        exec: function(composer, command, color) {
1736                                            wysihtml5.commands.formatInline.exec(composer, command, "span", "post-highlightcolor-" + color, REG_EXP);
1737                                        },
1738
1739                                        state: function(composer, command, color) {
1740                                            return wysihtml5.commands.formatInline.state(composer, command, "span", "post-highlightcolor-" + color, REG_EXP);
1741                                        },
1742
1743                                        value: function() {
1744                                            return undef;
1745                                        }
1746                                    };
1747                                })(wysihtml5);
1748
1749                                // extending whysihtml5 library for font families
1750                                (function(wysihtml5) {
1751                                    var undef,
1752                                        REG_EXP = /post-fontfamily-[0-9a-z-]+/g;
1753
1754                                    wysihtml5.commands.fontfamily = {
1755                                        exec: function(composer, command, fontfamily) {
1756                                            wysihtml5.commands.formatInline.exec(composer, command, "span", "post-fontfamily-" + fontfamily, REG_EXP);
1757                                        },
1758
1759                                        state: function(composer, command, fontfamily) {
1760                                            return wysihtml5.commands.formatInline.state(composer, command, "span", "post-fontfamily-" + fontfamily, REG_EXP);
1761                                        },
1762
1763                                        value: function() {
1764                                            return undef;
1765                                        }
1766                                    };
1767                                })(wysihtml5);
1768                            });
1769                        break;
1770
1771                    case 'html':
1772                    case 'bbcode':
1773                    case 'markdown':
1774                    case 'text':
1775                    case 'textex':
1776                        // Lazyloading scripts, then run single callback
1777                        $.when(
1778                            loadScript(assets + '/js/buttonbarplus.js?v=' + editorVersion),
1779                            loadScript(assets + '/js/jquery.hotkeys.js?v=' + editorVersion),
1780                            loadScript(assets + '/js/rangy.js?v=' + editorVersion)
1781                        ).done(function() {
1782                                ButtonBar.AttachTo($($currentEditableTextarea)[0], formatOriginal);
1783                                fullPageInit();
1784                                editorSetupDropdowns();
1785                                if (!singleInstance) {
1786                                    //scrollToEditorContainer($(currentEditableTextarea)[0]);
1787                                    editorSetCaretFocusEnd($currentEditableTextarea[0]);
1788                                }
1789
1790                                // Enable file uploads
1791                                fileUploadsInit($currentEditableTextarea[0], '');
1792
1793                                insertImageUrl($currentEditableTextarea);
1794                            });
1795                        break;
1796                }
1797
1798                // Set up on editor load
1799                editorSetHelpText(formatOriginal, currentTextBoxWrapper);
1800
1801                // some() loop requires true to end loop. every() requires false.
1802                return true;
1803            }
1804        } //editorInit
1805
1806        editorInit('', this);
1807
1808        // jQuery chaining
1809        return this;
1810    };
1811
1812    $(document).on('contentLoad', function(e) {
1813        $('.BodyBox,.js-bodybox', e.target).setAsEditor();
1814    });
1815}(jQuery));
1816
1817/*
1818 * This is an example of hooking into the custom parse event, to enable
1819 * passing custom parsing rules to the Advanced Editor's Wysiwyg format.
1820 // This is getting merged into Advanced Editor's
1821 // custom parsing rules in advanced.js.
1822 $(document).on('editorParseRules', function(e) {
1823 // Merge custom parsing object for Soompi's old posts.
1824 var newTagRules = {
1825 "a": {
1826 "check_attributes": {
1827 "href": "url",
1828 "style": "allow"
1829 },
1830 "set_attributes": {
1831 "rel": "nofollow",
1832 "target": "_blank"
1833 }
1834 },
1835 "img": {
1836 "check_attributes": {
1837 "width": "numbers",
1838 "alt": "alt",
1839 "src": "url", // if you compiled master manually then change this from 'url' to 'src'
1840 "height": "numbers",
1841 "style": "allow"
1842 },
1843 "add_class": {
1844 "align": "align_img"
1845 }
1846 },
1847 "div": {
1848 "add_class": {
1849 "align": "align_text"
1850 },
1851
1852 "check_attributes": {
1853 "style":"allow"
1854 }
1855 },
1856 "span": {
1857 "check_attributes": {
1858 "style":"allow"
1859 }
1860 },
1861 "font": {
1862 "add_class": {
1863 "size": "size_font"
1864 },
1865 "check_attributes": {
1866 "style":"allow",
1867 "face":"allow",
1868 "color":"allow",
1869 "size":"allow"
1870 }
1871 },
1872 "b": {
1873 "check_attributes": {
1874 "style":"allow"
1875 }
1876 }
1877 };
1878
1879 // Merge new tag rules with current ones, so advanced
1880 // editor can obey latest parse rules.
1881 e.rules['tags'] = $.extend(e.rules.tags, newTagRules);
1882 });
1883 */

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.