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