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 * ‌ 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>‌<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 = ''; 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 ? ' · <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.