PageSourceSearch

https://forum.nikonrumors.com/plugins/editor/js/editor.js?v=1.7.7

js nikonrumors.com collected 2026-10-02 03:28:55 UTC 89,192 bytes, 1,855 lines download raw bytes

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

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.