PageSourceSearch

https://actionnetwork.org/includes/js/redactor_image_editing_email.js?v=71

js actionnetwork.org collected 2026-09-24 07:03:53 UTC 23,827 bytes, 685 lines download raw bytes

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.