1/* 2 Redactor Edit Image (Email Version) 3 by Jason Rosenbaum, the Action Network 4 5 Version 2.0 6 Released December 29, 2015 7 8 9 Instructions: 10 11 This plugin edits how images are handled in redactor, adding width and source input boxes, storing width/height and alignments as attributes instead of inline styles, stopping image resizing and selecting in IE, and automatically enabling responsive image styles with max-width, all useful for using redactor as a visual editor for email. For Redactor II. 12 13 14 MIT license: 15 16 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 17 18 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 19 20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21*/ 22 23$.Redactor.prototype.edit_image_email = function() { 24 return { 25 getTemplate: function(template) 26 { 27 if (template == 'edit_image') { 28 return String() 29 + '<div class="redactor-modal-tab redactor-group" data-title="General">' 30 + '<div id="redactor-image-preview" class="redactor-modal-tab-side">' 31 + '</div>' 32 + '<div class="redactor-modal-tab-area">' 33 + '<section>' 34 + '<label>Source (usually should start with http://)</label>' 35 + '<input type="text" id="redactor_file_src" />' 36 + '<label class="mt5"><a href="javascript:void(0);" class="js-redactor_image_replace_upload">Replace image with a new upload</a></label>' 37 + '</section>' 38 + '<section>' 39 + '<label>' + this.lang.get('title') + ' / Alt Text</label>' 40 + '<input type="text" id="redactor-image-title" />' 41 + '</section>' 42 + '<section>' 43 + '<label>Attribution</label>' 44 + '<input type="text" id="redactor-image-attribution" />' 45 + '</section>' 46 + '<section>' 47 + '<label>' + this.lang.get('link') + ' (usually should start with http://)' + '</label>' 48 + '<input type="text" id="redactor-image-link" aria-label="' + this.lang.get('link') + '" />' 49 + '<label class="checkbox"><input type="checkbox" id="redactor-image-link-blank" aria-label="' + this.lang.get('link-in-new-tab') + '"> ' + this.lang.get('link-in-new-tab') + '</label>' 50 + '</section>' 51 + '<section>' 52 + '<label>Position</label>' 53 + '<select id="redactor_form_image_align">' 54 + '<option value="none">None</option>' 55 + '<option value="left">Left</option>' 56 + '<option value="center">Center</option>' 57 + '<option value="right">Right</option>' 58 + '</select>' 59 + '</section>' 60 + '<section>' 61 + '<label>Width (typically in px or %, ex: 200px)</label>' 62 + '<input type="text" id="redactor_image_width" />' 63 + '</section>' 64 + '<section>' 65 + '<button id="redactor-modal-button-action">Insert</button>' 66 + '<button id="redactor-modal-button-cancel">Cancel</button>' 67 + '<button id="redactor-modal-button-delete" class="redactor-modal-button-offset">Delete</button>' 68 + '</section>' 69 + '</div>' 70 + '</div>' 71 } else if (template == 'upload_image') { 72 return String() 73 + '<div class="redactor-modal-tab js-redactor-modal-tab_image_upload" data-title="Upload">' 74 // Start AN-2192 75 + '<div id="redactor-insert-image-tabs" class="js-redactor-insert-image-tabs">' 76 + '<ul id="tabs" class="light-tabs clear clearfix js-tabs">' 77 + '<li><a href="#tab_image_library" id="current">Image Library</a></li>' 78 + '<li><a href="#tab_image_upload">Upload</a></li>' 79 + '<li><a href="#tab_image_url">URL</a></li>' 80 + '</ul>' 81 + '</div>' 82 + '<div id="tab_image_library" class="tab js-tab">' 83 + '<div class="input-with-button">' 84 + '<div class="input-container"><input type="text" placeholder="Search images by file name" class="js-redactor-image-library-search_input"></div>' 85 + '<div class="button-container"><a class="js-redactor-image-library-search button">Search</a></div>' 86 + '</div>' 87 + '<div class="mt5 mb5">' 88 + 'Showing the most recently uploaded images. ' 89 + '<a class="js-redactor-image-library-more">Load more images</a> ' 90 + '</div>' 91 + '<section id="image-library-container" class="redactor-image-library js-redactor-image-library">Loading...' 92 + '</section>' 93 + '<section>' 94 + '<button id="redactor-modal-button-action" class="js-redactor-image-library-insert">Insert image</button>' 95 + '<button id="redactor-modal-button-cancel">Cancel</button>' 96 + '<span id="image-library-selected-filename" class="js-image-library-selected-filename"></span>' 97 + '</section>' 98 + '</div>' 99 + '<div id="tab_image_upload" class="tab hide js-tab">' 100 + '<div id="redactor-modal-image-droparea"></div>' 101 + '</div>' 102 + '<div id="tab_image_url" class="tab hide js-tab">' 103 + '<section>' 104 + '<label>Image URL (usually starts with http://)</label>' 105 + '<input type="text" id="redactor_image_upload_link" />' 106 + '</section>' 107 + '<section>' 108 + '<button id="redactor-modal-button-action">Insert image</button>' 109 + '<button id="redactor-modal-button-cancel">Cancel</button>' 110 + '</section>' 111 + '</div>' 112 + '</div>'; 113 } 114 }, 115 init: function() { 116 this.image.showEdit = $.proxy( 117 function($image) 118 { 119 //console.log('showedit'); 120 if (this.events.imageEditing) 121 { 122 return; 123 } 124 125 this.observe.image = $image; 126 127 //if (this.detect.isIe()) { 128 129 130 //} 131 132 var $link = $image.closest('a', this.$editor[0]); 133 134 this.modal.addTemplate('edit_image_email', this.edit_image_email.getTemplate('edit_image')); 135 this.modal.load('edit_image_email', 'Edit', 705); 136 137 //this.modal.load('image-edit', this.lang.get('edit'), 705); 138 139 this.image.buttonDelete = this.modal.getDeleteButton().text(this.lang.get('delete')); 140 this.image.buttonSave = this.modal.getActionButton().text(this.lang.get('save')); 141 142 this.image.buttonDelete.on('click', $.proxy(this.image.remove, this)); 143 this.image.buttonSave.on('click', $.proxy(this.image.update, this)); 144 145 $('.js-redactor_image_replace_upload').on('click', $.proxy( 146 function() { 147 var that = this; 148 redactor_open_image_swap = true; 149 redactor_image_swap = $image; 150 this.core.element().off('modalClosed.callback.redactor'); 151 this.core.element().on('modalClosed.callback.redactor', function() { 152 153 if (typeof(redactor_open_image_swap) != "undefined") { 154 delete redactor_open_image_swap; 155 setTimeout(function() { 156 that.image.show(); 157 }, 100); 158 } else { 159 delete redactor_image_swap; 160 } 161 162 }); 163 this.modal.close(); 164 }, this) 165 ); 166 167 if (this.opts.imageCaption === false) 168 { 169 $('#redactor-image-caption').val('').hide().prev().hide(); 170 } 171 else 172 { 173 var $next = $image.next(); 174 if ($next.length !== 0 && $next[0].tagName === 'FIGCAPTION') 175 { 176 $('#redactor-image-caption').val($next.text()).show(); 177 } 178 } 179 180 $('#redactor-image-preview').html($('<img src="' + $image.attr('src') + '" style="max-width: 100%;">')); 181 $('#redactor-image-title').val($image.attr('alt')); 182 $('#redactor-image-attribution').val($image.parents('.redactor-editor').find('figcaption[data-src="' + $image.attr('src') + '"]').html()); 183 184 var $redactorImageLink = $('#redactor-image-link'); 185 $redactorImageLink.attr('href', $image.attr('src')); 186 if ($link.length !== 0) 187 { 188 $redactorImageLink.val($link.attr('href')); 189 if ($link.attr('target') === '_blank') 190 { 191 $('#redactor-image-link-blank').prop('checked', true); 192 } 193 } 194 195 $('#redactor_file_src').val($image.attr('src')); 196 197 if ($image.prop('align') == 'center' || $image.prop('align') == 'middle') 198 { 199 $('#redactor_form_image_align').val('center'); 200 } 201 else if ($image.prop('align') == 'left') 202 { 203 $('#redactor_form_image_align').val('left'); 204 } 205 else if ($image.prop('align') == 'right') 206 { 207 $('#redactor_form_image_align').val('right'); 208 } 209 else 210 { 211 $('#redactor_form_image_align').val('none'); 212 } 213 214 if ($image.prop('style').maxWidth) { 215 $('#redactor_image_width').val($image.prop('style').maxWidth); 216 } else if ($image.prop('style').width) { 217 $('#redactor_image_width').val($image.prop('style').width); 218 } 219 220 // hide link's tooltip 221 $('.redactor-link-tooltip').remove(); 222 223 this.modal.show(); 224 225 // focus 226 if (this.detect.isDesktop()) 227 { 228 $('#redactor-image-title').focus(); 229 } 230 231 //don't make images selectable in IE 232 if (this.detect.isIe()) { 233 $image.removeAttr('unselectable'); 234 } 235 236 }, 237 this); 238 239 this.image.update = $.proxy( 240 function() 241 { 242 //console.log('update'); 243 var $image = this.observe.image; 244 var $link = $image.closest('a', this.core.editor()[0]); 245 246 var title = $('#redactor-image-title').val().replace(/(<([^>]+)>)/ig,""); 247 var attribution = $('#redactor-image-attribution').val(); 248 249 $image.attr('alt', title).attr('title', title); 250 251 $image.attr('src', $('#redactor_file_src').val()); 252 //console.log('figcaption#' + $('#redactor_file_src').val() + ''); 253 $image.parents('.redactor-editor').find('figcaption[data-src="' + $('#redactor_file_src').val() + '"]').remove(); 254 //console.log($image); 255 if (attribution != '') { 256 $image.after('<figcaption class="redactor_caption" data-src="' + $('#redactor_file_src').val() + '">' + attribution + '</figcaption>'); 257 } 258 259 var floating = $('#redactor_form_image_align').val(); 260 var margin = ''; 261 262 if ($('#redactor_image_width').val() != '') { 263 if ($('#redactor_image_width').val().indexOf('%') > -1) { 264 var attr_width = $('#redactor_image_width').val(); 265 } else { 266 var attr_width = parseInt($('#redactor_image_width').val()); 267 } 268 269 if (/^[0-9]+$/.test($('#redactor_image_width').val())) { 270 var max_width = $('#redactor_image_width').val() + 'px'; 271 } else { 272 var max_width = $('#redactor_image_width').val(); 273 } 274 275 var css_width = '100%'; 276 } else { 277 var attr_width = ''; 278 var max_width = ''; 279 var css_width = ''; 280 } 281 282 283 if (floating === 'left') 284 { 285 margin = '0 10px 10px 0'; 286 $image.css({ 'margin': margin, 'width': css_width, 'max-width': max_width }); 287 $image.prop('align', 'left').attr('width', attr_width); 288 } 289 else if (floating === 'right') 290 { 291 margin = '0 0 10px 10px'; 292 $image.css({ 'margin': margin, 'width': css_width, 'max-width': max_width }); 293 $image.prop('align', 'right').attr('width', attr_width); 294 } 295 else if (floating === 'center') 296 { 297 $image.css({ 'display': 'block', 'margin': 'auto', 'width': css_width, 'max-width': max_width }); 298 $image.prop('align', 'center').attr('width', attr_width); 299 } 300 else 301 { 302 $image.css({ 'display': '', 'margin': '', 'width': css_width, 'max-width': max_width }); 303 $image.prop('align', '').attr('width', attr_width); 304 } 305 306 //if (navigator.appVersion.indexOf("MSIE") !== -1 || navigator.appVersion.indexOf("Trident") !== -1) { 307 //$image.attr('unselectable','on'); 308 //} 309 310 // as link 311 var link = $.trim($('#redactor-image-link').val()).replace(/(<([^>]+)>)/ig,""); 312 if (link !== '') 313 { 314 // test url (add protocol) 315 var pattern = '((xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}'; 316 var re = new RegExp('^(http|ftp|https)://' + pattern, 'i'); 317 var re2 = new RegExp('^' + pattern, 'i'); 318 319 if (link.search(re) === -1 && link.search(re2) === 0 && this.opts.linkProtocol) 320 { 321 link = this.opts.linkProtocol + '://' + link; 322 } 323 324 var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false;
325 326 if ($link.length === 0) 327 { 328 var a = $('<a href="' + link + '">' + this.utils.getOuterHtml($image) + '</a>'); 329 if (target) 330 { 331 a.attr('target', '_blank'); 332 } 333 334 $image.replaceWith(a); 335 } 336 else 337 { 338 $link.attr('href', link); 339 if (target) 340 { 341 $link.attr('target', '_blank'); 342 } 343 else 344 { 345 $link.removeAttr('target'); 346 } 347 } 348 } 349 else if ($link.length !== 0) 350 { 351 $link.replaceWith(this.utils.getOuterHtml($image)); 352 353 } 354 355 356 this.modal.close(); 357 358 // buffer 359 this.buffer.set(); 360 361 }, 362 this); 363 364 this.image.show = $.proxy( 365 function() 366 { 367 //console.log('show'); 368 // build modal 369 this.modal.addTemplate('edit_image_email', this.edit_image_email.getTemplate('upload_image')); 370 this.modal.load('edit_image_email', 'Image', 700); 371 372 //this.modal.load('image', this.lang.get('image'), 700); 373 374 // build upload 375 this.upload.init('#redactor-modal-image-droparea', this.opts.imageUpload, this.image.insert); 376 this.modal.show(); 377 378 this.image.buttonSave = this.modal.getActionButton(); 379 var that = this; 380 this.image.buttonSave.on('click', function() { 381 if ($('.js-tabs #current').attr('href') == '#tab_image_library') { 382 that.image.insert({ "url": $('.js-redactor-image-library input[type="radio"]:checked').val() }); 383 } else { 384 that.image.insert({ "url": $('#redactor_image_upload_link').val() }); 385 } 386 }); 387 388 if ($('#redactor_image_upload_link').val() == '') { 389 that.image.buttonSave.attr('disabled',true); 390 that.image.buttonSave.addClass('disabled'); 391 } 392 393 $('#redactor_image_upload_link').on('change keyup focusout', function() { 394 if ($(this).val() == '') { 395 that.image.buttonSave.attr('disabled',true); 396 that.image.buttonSave.addClass('disabled'); 397 } else { 398 that.image.buttonSave.attr('disabled',false); 399 that.image.buttonSave.removeClass('disabled'); 400 } 401 }); 402 403 404 // init stuff 405 // tabs 406 $('.js-redactor-insert-image-tabs a').click(function(e) { 407 e.preventDefault(); 408 // Clear current tab 409 $('.js-redactor-insert-image-tabs .js-tabs a').removeAttr('id'); 410 $('.js-redactor-modal-tab_image_upload .js-tab').hide(); 411 // Select this tab 412 $(this).attr('id', 'current'); 413 var tab = $(this).attr('href'); 414 $(tab).show(); 415 }); 416 417 //hide file library if user 418 if ($('.js-redactor_image_upload_group_id').val() == '' || typeof($('.js-redactor_image_upload_group_id').val()) == 'undefined') { 419 $('.js-tabs a[href="#tab_image_library"]').parent().hide(); 420 $('.js-tabs a[href="#tab_image_upload"]').trigger('click'); 421 } 422 423 // search floatlabel 424 $('.js-floatlabel').floatlabel(); 425 426 // Image Library 427 $('body').on('change', '.js-redactor-image-library input[type="radio"]', function() { 428 $('.js-image-library-selected-filename').text($(this).data('file_name')); 429 // Enable save button when an image is selected 430 // TODO This doesn't work because it's controlled by the URL input 431 // & the button style is only available for #redactor-modal-button-action 432 $('.js-redactor-image-library-insert') 433 .removeAttr('disabled') 434 .removeClass('disabled'); 435 }); 436 437 var group_link = this.$textarea.data('group_link'); 438 var page = 1; 439 440 var load_images_ajax = function(group_link, page, search, clear) { 441 if (clear) { 442 $('.js-redactor-image-library').html('Loading...'); 443 } 444 445 $('.js-redactor-image-library').addClass('disabled'); 446 447 $.get('/groups/' + group_link + '/image_library?page=' + page + '&search=' + search, function(data) { 448 if (clear) { 449 $('.js-redactor-image-library').html(''); 450 } 451 if (data.images.length) { 452 $.each(data.images, function(i, e) { 453 $('.js-redactor-image-library').append('' 454 + '<div class="redactor-library-image-container">' 455 + '<img src="' + e.image_url + '"/>' 456 + '<label for="' + e.image_url + '"><span>' + e.data_file_name + '</span></label>' 457 + '<input type="radio" data-file_name="' + e.data_file_name + '" name="image-library-radios" id="' + e.image_url + '" value="' + e.image_url + '">' 458 + '</div>' 459 ); 460 }); 461 } else { 462 if (clear) { 463 $('.js-redactor-image-library').html('No images found.'); 464 } 465 } 466 467 $('.js-redactor-image-library').removeClass('disabled'); 468 }).fail(function() { 469 $('.js-redactor-image-library').removeClass('disabled').html('Failed to fetch images. Refresh the page and try again.'); 470 }); 471 } 472 473 if (typeof(group_link) != 'undefined' && group_link != 'undefined') { 474 load_images_ajax(group_link, page, "", true); 475 } 476 477 $('.js-redactor-image-library-more').click(function() { 478 page++; 479 load_images_ajax(group_link, page, $('.js-redactor-image-library-search_input').val(), false); 480 }); 481 482 $('.js-redactor-image-library-search').click(function() { 483 page = 1; 484 load_images_ajax(group_link, page, $('.js-redactor-image-library-search_input').val(), true); 485 }); 486 487 //todo: make group ID attach during upload 488 }, 489 this); 490 491 this.image.insert = $.proxy( 492 function(json, direct, e) 493 { 494 //console.log('insert'); 495 //console.log(json); 496 //console.log(direct); 497 //console.log(e); 498 var $img; 499 500 // error callback 501 if (typeof json.error !== 'undefined') 502 { 503 this.modal.close(); 504 this.events.dropImage = false;
505 this.core.callback('imageUploadError', json, e); 506 return; 507 } 508 509 // change image 510 if (this.events.dropImage !== false) 511 { 512 $img = $(this.events.dropImage); 513 514 this.core.callback('imageDelete', $img[0].src, $img); 515 516 $img.attr('src', json.url); 517 518 this.events.dropImage = false; 519 this.core.callback('imageUpload', $img, json); 520 return; 521 } 522 523 this.placeholder.hide(); 524 //var $figure = $('<figure>'); 525 526 if (typeof(redactor_image_swap) != 'undefined') { 527 redactor_image_swap.attr('src', json.url); 528 } 529 530 $img = $('<img>'); 531 $img.attr('src', json.url); 532 533 534 // set id 535 var id = (typeof json.id === 'undefined') ? '' : json.id; 536 var type = (typeof json.s3 === 'undefined') ? 'image' : 's3'; 537 //$img.attr('data-' + type, id); 538 539 if (typeof(redactor_image_swap) == 'undefined') { 540 var theImage = new Image(); 541 theImage.src = $img.attr("src"); 542 543 // Get accurate measurements from that. 544 $(theImage).load(function(){ 545 var imageWidth = this.width; 546 //console.log(imageWidth); 547 // Get accurate measurements from that. 548 var imageWidth = theImage.width; 549 $img.css({'width':'100%', 'max-width': imageWidth }).attr('width', imageWidth); 550 }); 551 } 552 553 554 555 //if (navigator.appVersion.indexOf("MSIE") !== -1 || navigator.appVersion.indexOf("Trident") !== -1) { 556 //$img.attr('unselectable','on'); 557 //} 558 559 //$figure.append($img); 560 561 var pre = this.utils.isTag(this.selection.current(), 'pre'); 562 563 if (direct) 564 { 565 this.air.collapsed(); 566 this.marker.remove(); 567 568 var node = this.insert.nodeToPoint(e, this.marker.get()); 569 var $next = $(node).next(); 570 571 this.selection.restore(); 572 573 // buffer 574 this.buffer.set(); 575 576 // insert 577 if (typeof $next !== 'undefined' && $next.length !== 0 && $next[0].tagName === 'IMG') 578 { 579 if (typeof(redactor_image_swap) != 'undefined') { 580 $img = redactor_image_swap; 581 } else { 582 // delete callback 583 this.core.callback('imageDelete', $next[0].src, $next); 584 585 // replace 586 $next.closest('p', this.core.editor()[0]).replaceWith($img); 587 this.caret.after($img); 588 } 589 590 } 591 else 592 { 593 if (pre) 594 { 595 if (typeof(redactor_image_swap) != 'undefined') { 596 $img = redactor_image_swap; 597 } else { 598 $(pre).after($img); 599 } 600 } 601 else 602 { 603 if (typeof(redactor_image_swap) != 'undefined') { 604 $img = redactor_image_swap; 605 } else { 606 this.insert.node($img); 607 } 608 } 609 610 this.caret.after($img); 611 } 612 613 } 614 else 615 { 616 this.modal.close(); 617 618 // buffer 619 this.buffer.set(); 620 621 // insert 622 this.air.collapsed(); 623 624 if (pre) 625 { 626 if (typeof(redactor_image_swap) != 'undefined') { 627 $img = redactor_image_swap; 628 } else { 629 $(pre).after($img); 630 } 631 } 632 else 633 { 634 if (typeof(redactor_image_swap) != 'undefined') { 635 $img = redactor_image_swap; 636 } else { 637 this.insert.node($img); 638 } 639 } 640 641 this.caret.after($img); 642 } 643 644 this.events.dropImage = false; 645 646 this.storage.add({ type: type, node: $img[0], url: $img[0].src, id: id }); 647 648 if (direct !== null) 649 { 650 this.core.callback('imageUpload', $img, json); 651 } 652 653 if (json.large_image) { 654 alert('We\'ve detected that you have uploaded an image larger than 1MB. This image may take a long time to download when activists read your email, especially on mobile phones. Consider compressing or using a smaller image for a better experience.'); 655 } 656 }, 657 this); 658 659 this.observe.images = $.proxy( 660 function() { 661 this.core.editor().find('img').each($.proxy(function(i, img) 662 { 663 var $img = $(img); 664 665 666 667 // IE fix (when we clicked on an image and then press backspace IE does goes to image's url) 668 $img.closest('a', this.$editor[0]).on('click', function(e) { e.preventDefault(); }); 669 670 this.image.setEditable($img); 671 672 //don't make images selectable in IE 673 if (this.detect.isIe()) { 674 $img.off('mousedown.redactor_ie_image_fix').on('mousedown.redactor_ie_image_fix', function() { 675 $img.attr('unselectable', 'on'); 676 }); 677 } 678 679 680 }, this)); 681 }, 682 this); 683 } 684 } 685}
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.