1if (typeof RedactorPlugins === 'undefined') var RedactorPlugins = {}; 2 3/* Generic draft support for osTicket. The plugins supports draft retrieval 4 * automatically, along with draft autosave, and image uploading. 5 * 6 * Configuration: 7 * draftNamespace: namespace for the draft retrieval 8 * draftObjectId: extension to the namespace for draft retrieval 9 * 10 * Caveats: 11 * Login (staff only currently) is required server-side for drafts and image 12 * uploads. Furthermore, the id of the staff is considered for the drafts, 13 * so one user will not retrieve drafts for another user. 14 */ 15RedactorPlugins.draft = function() { 16 return { 17 init: function() { 18 if (!this.opts.draftNamespace) 19 return; 20 21 this.opts.changeCallback = this.draft.hideDraftSaved; 22 var autosave_url = 'ajax.php/draft/' + this.opts.draftNamespace; 23 if (this.opts.draftObjectId) 24 autosave_url += '.' + this.opts.draftObjectId; 25 this.opts.autosave = this.opts.autoCreateUrl = autosave_url; 26 this.opts.autosaveInterval = 30; 27 this.opts.autosaveCallback = this.draft.afterUpdateDraft; 28 this.opts.autosaveErrorCallback = this.draft.autosaveFailed; 29 this.opts.imageUploadErrorCallback = this.draft.displayError; 30 if (this.opts.draftId) { 31 this.opts.autosave = 'ajax.php/draft/'+this.opts.draftId; 32 this.opts.clipboardUploadUrl = 33 this.opts.imageUpload = 34 'ajax.php/draft/'+this.opts.draftId+'/attach'; 35 } 36 else if (this.$textarea.hasClass('draft')) { 37 // Just upload the file. A draft will be created automatically 38 // and will be configured locally in the afterUpateDraft() 39 this.opts.clipboardUploadUrl = 40 this.opts.imageUpload = this.opts.autoCreateUrl + '/attach'; 41 this.opts.imageUploadCallback = this.afterUpdateDraft; 42 } 43 44 // FIXME: Monkey patch Redactor's autosave enable method to disable first 45 var oldAse = this.autosave.enable; 46 this.autosave.enable = function() { 47 this.autosave.disable(); 48 oldAse.call(this); 49 }.bind(this); 50 51 if (autosave_url) 52 this.autosave.enable(); 53 54 if (this.$textarea.hasClass('draft')) { 55 this.$draft_saved = $('<span>') 56 .addClass("pull-right draft-saved") 57 .hide() 58 .append($('<span>') 59 .text(__('Draft Saved'))); 60 // Float the [Draft Saved] box with the toolbar 61 this.$toolbar.append(this.$draft_saved); 62 63 // Add [Delete Draft] button to the toolbar 64 if (this.opts.draftDelete) { 65 var trash = this.draft.deleteButton = 66 this.button.add('deleteDraft', __('Delete Draft')) 67 this.button.addCallback(trash, this.draft.deleteDraft); 68 this.button.setAwesome('deleteDraft', 'icon-trash'); 69 trash.parent().addClass('pull-right'); 70 trash.addClass('delete-draft'); 71 if (!this.opts.draftId) 72 trash.hide(); 73 } 74 } 75 if (this.code.get()) 76 this.$box.trigger('draft:recovered'); 77 }, 78 afterUpdateDraft: function(name, data) { 79 // If the draft was created, a draft_id will be sent back â update 80 // the URL to send updates in the future 81 if (!this.opts.draftId && data.draft_id) { 82 this.opts.draftId = data.draft_id; 83 this.opts.autosave = 'ajax.php/draft/' + data.draft_id; 84 this.opts.clipboardUploadUrl = 85 this.opts.imageUpload = 86 'ajax.php/draft/'+this.opts.draftId+'/attach'; 87 if (!this.code.get()) 88 this.code.set(' ', false); 89 } 90 // Only show the [Draft Saved] notice if there is content in the 91 // field that has been touched 92 if (!this.draft.firstSave) { 93 this.draft.firstSave = true; 94 // No change yet â dont't show the button 95 return; 96 } 97 if (data && this.code.get()) { 98 this.$draft_saved.show().delay(5000).fadeOut(); 99 } 100 // Show the button if there is a draft to delete
101 if (this.opts.draftId && this.opts.draftDelete) 102 this.draft.deleteButton.show(); 103 this.$box.trigger('draft:saved'); 104 }, 105 autosaveFailed: function(error) { 106 if (error.code == 422) 107 // Unprocessable request (Empty message) 108 return; 109 110 this.draft.displayError(error); 111 // Cancel autosave 112 this.autosave.disable(); 113 this.hideDraftSaved(); 114 this.$box.trigger('draft:failed'); 115 }, 116 117 displayError: function(json) { 118 $.sysAlert(json.error, 119 __('Unable to save draft.') 120 + __('Refresh the current page to restore and continue your draft.')); 121 }, 122 123 hideDraftSaved: function() { 124 this.$draft_saved.hide(); 125 }, 126 127 deleteDraft: function() { 128 if (!this.opts.draftId) 129 // Nothing to delete 130 return; 131 var self = this; 132 $.ajax('ajax.php/draft/'+this.opts.draftId, { 133 type: 'delete', 134 async: false, 135 success: function() { 136 self.draft_id = self.opts.draftId = undefined; 137 self.draft.hideDraftSaved(); 138 self.code.set(self.opts.draftOriginal || '', false, false); 139 self.opts.autosave = self.opts.autoCreateUrl; 140 self.draft.deleteButton.hide(); 141 self.draft.firstSave = false; 142 self.$box.trigger('draft:deleted'); 143 } 144 }); 145 } 146 }; 147}; 148 149RedactorPlugins.autolock = function() { 150 return { 151 init: function() { 152 var code = this.$box.closest('form').find('[name=lockCode]'), 153 self = this; 154 if (code.length) 155 this.opts.keydownCallback = function(e) { 156 self.$box.closest('[data-lock-object-id]').exclusive('acquire'); 157 }; 158 } 159 }; 160} 161 162RedactorPlugins.signature = function() { 163 return { 164 init: function() { 165 var $el = $(this.$element.get(0)), 166 inner = $('<div class="inner"></div>'); 167 if ($el.data('signatureField')) { 168 this.$signatureBox = $('<div class="selected-signature"></div>') 169 .append(inner) 170 .appendTo(this.$box); 171 if ($el.data('signature')) 172 inner.html($el.data('signature')); 173 else 174 this.$signatureBox.hide(); 175 $('input[name='+$el.data('signatureField')+']', $el.closest('form')) 176 .on('change', false, false, $.proxy(this.signature.updateSignature, this)); 177 if ($el.data('deptField')) 178 $(':input[name='+$el.data('deptField')+']', $el.closest('form')) 179 .on('change', false, false, $.proxy(this.signature.updateSignature, this)); 180 // Expand on hover 181 var outer = this.$signatureBox, 182 inner = $('.inner', this.$signatureBox).get(0), 183 originalHeight = outer.height(), 184 hoverTimeout = undefined, 185 originalShadow = this.$signatureBox.css('box-shadow'); 186 this.$signatureBox.hover(function() { 187 hoverTimeout = setTimeout($.proxy(function() { 188 originalHeight = Math.max(originalHeight, outer.height()); 189 $(this).animate({ 190 'height': inner.offsetHeight 191 }, 'fast'); 192 $(this).css('box-shadow', 'none', 'important'); 193 }, this), 250); 194 }, function() { 195 clearTimeout(hoverTimeout); 196 $(this).stop().animate({ 197 'height': Math.min(inner.offsetHeight, originalHeight) 198 }, 'fast'); 199 $(this).css('box-shadow', originalShadow); 200 }); 201 this.$box.find('.redactor_editor').css('border-bottom-style', 'none', true); 202 } 203 }, 204 updateSignature: function(e) { 205 var $el = $(this.$element.get(0)); 206 selected = $(':input:checked[name='+$el.data('signatureField')+']', $el.closest('form')).val(), 207 type = $(e.target).val(), 208 dept = $(':input[name='+$el.data('deptField')+']', $el.closest('form')).val(), 209 url = 'ajax.php/content/signature/', 210 inner = $('.inner', this.$signatureBox); 211 e.preventDefault && e.preventDefault(); 212 if (selected == 'dept' && $el.data('deptId')) 213 url += 'dept/' + $el.data('deptId'); 214 else if (selected == 'dept' && $el.data('deptField')) { 215 if (dept) 216 url += 'dept/' + dept; 217 else 218 return inner.empty().parent().hide(); 219 } 220 else if (selected == 'theirs' && $el.data('posterId')) { 221 url += 'agent/' + $el.data('posterId'); 222 } 223 else if (type == 'none') 224 return inner.empty().parent().hide(); 225 else 226 url += selected; 227 228 inner.load(url).parent().show(); 229 } 230 } 231}; 232 233/* Redactor richtext init */ 234$(function() { 235 var captureImageSizes = function(html) { 236 $('img', this.$box).each(function(i, img) { 237 // TODO: Rewrite the entire <img> tag. Otherwise the @width 238 // and @height attributes will begin to accumulate 239 before = img.outerHTML; 240 if (img.clientWidth && img.clientHeight) 241 $(img).attr('width', img.clientWidth) 242 .attr('height',img.clientHeight); 243 html = html.replace(before, img.outerHTML); 244 }); 245 return html; 246 }, 247 redact = $.redact = function(el, options) { 248 var el = $(el), 249 sizes = {'small': 75, 'medium': 150, 'large': 225}, 250 selectedSize = sizes['medium']; 251 $.each(sizes, function(k, v) { 252 if (el.hasClass(k)) selectedSize = v; 253 }); 254 var options = $.extend({ 255 'air': el.hasClass('no-bar'), 256 'buttons': el.hasClass('no-bar') 257 ? ['formatting', '|', 'bold', 'italic', 'underline', 'deleted', '|', 'unorderedlist', 'orderedlist', 'outdent', 'indent', '|', 'link', 'image'] 258 : ['html', '|', 'formatting', '|', 'bold', 259 'italic', 'underline', 'deleted', '|', 'unorderedlist', 260 'orderedlist', 'outdent', 'indent', '|', 'image', 'video', 261 'file', 'table', 'link', '|', 'alignment', '|', 262 'horizontalrule'], 263 'buttonSource': !el.hasClass('no-bar'), 264 'autoresize': !el.hasClass('no-bar') && !el.closest('.dialog').length, 265 'maxHeight': el.closest('.dialog').length ? selectedSize : false, 266 'minHeight': selectedSize,
267 'focus': false, 268 'plugins': el.hasClass('no-bar') 269 ? ['imagemanager','definedlinks'] 270 : ['imagemanager','imageannotate','table','video','definedlinks','autolock'], 271 'imageUpload': el.hasClass('draft'), 272 'imageManagerJson': 'ajax.php/draft/images/browse', 273 'syncBeforeCallback': captureImageSizes, 274 'linebreaks': true, 275 'tabFocus': false, 276 'toolbarFixedBox': true, 277 'focusCallback': function() { this.$box.addClass('no-pjax'); }, 278 'initCallback': function() { 279 if (this.$element.data('width')) 280 this.$editor.width(this.$element.data('width')); 281 this.$editor.attr('spellcheck', 'true'); 282 var lang = this.$editor.closest('[lang]').attr('lang'); 283 if (lang) 284 this.$editor.attr('lang', lang); 285 }, 286 'linkSize': 100000, 287 'definedLinks': 'ajax.php/config/links' 288 }, options||{}); 289 if (el.data('redactor')) return; 290 var reset = $('input[type=reset]', el.closest('form')); 291 if (reset) { 292 reset.click(function() { 293 if (el.hasClass('draft')) 294 el.redactor('deleteDraft'); 295 else 296 el.redactor('set', '', false, false); 297 }); 298 } 299 $('input[type=submit]', el.closest('form')).on('click', function() { 300 // Some setups (IE v10 on Windows 7 at least) seem to have a bug 301 // where Redactor does not sync properly after adding an image. 302 // Therefore, the ::get() call will not include text added after 303 // the image was inserted. 304 el.redactor('code.sync'); 305 }); 306 if (!$.clientPortal) { 307 options['plugins'] = options['plugins'].concat( 308 'fontcolor', 'fontfamily', 'signature'); 309 } 310 if (el.hasClass('draft')) { 311 el.closest('form').append($('<input type="hidden" name="draft_id"/>')); 312 options['plugins'].push('draft'); 313 options['plugins'].push('imagepaste'); 314 options.draftDelete = el.hasClass('draft-delete'); 315 } 316 if (true || 'scp') { // XXX: Add this to SCP only 317 options['plugins'].push('contexttypeahead'); 318 } 319 if (el.hasClass('fullscreen')) 320 options['plugins'].push('fullscreen'); 321 if (el.data('translateTag')) 322 options['plugins'].push('translatable'); 323 if ($('#thread-items[data-thread-id]').length) 324 options['imageManagerJson'] += '?threadId=' + $('#thread-items').data('threadId'); 325 getConfig().then(function(c) { 326 if (c.lang && c.lang.toLowerCase() != 'en_us' && 327 $.Redactor.opts.langs[c.short_lang]) 328 options['lang'] = c.short_lang; 329 //if (c.has_rtl) 330 // options['plugins'].push('textdirection'); 331 if (el.find('rtl').length) 332 options['direction'] = 'rtl'; 333 el.redactor(options); 334 }); 335 }, 336 findRichtextBoxes = function() { 337 $('.richtext').each(function(i,el) { 338 if ($(el).hasClass('ifhtml')) 339 // Check if html_thread is enabled first 340 getConfig().then(function(c) { 341 if (c.html_thread) 342 redact(el); 343 }); 344 else 345 // Make a rich text editor immediately 346 redact(el); 347 }); 348 }, 349 cleanupRedactorElements = function() { 350 // Tear down redactor editors on this page 351 $('.richtext').each(function() { 352 var redactor = $(this).data('redactor'); 353 if (redactor) 354 redactor.core.destroy(); 355 }); 356 }; 357 findRichtextBoxes(); 358 $(document).ajaxStop(findRichtextBoxes); 359 $(document).on('pjax:success', findRichtextBoxes); 360 $(document).on('pjax:start', cleanupRedactorElements); 361}); 362 363$(document).on('focusout.redactor', 'div.redactor_richtext', function (e) { 364 $(this).siblings('textarea').trigger('change'); 365}); 366 367$(document).ajaxError(function(event, request, settings) { 368 if (settings.url.indexOf('ajax.php/draft') != -1 369 && settings.type.toUpperCase() == 'POST') { 370 $('.richtext').each(function() { 371 var redactor = $(this).data('redactor'); 372 if (redactor) { 373 redactor.autosave.disable(); 374 clearInterval(redactor.autosaveInterval); 375 } 376 }); 377 $.sysAlert(__('Unable to save draft.'), 378 __('Refresh the current page to restore and continue your draft.')); 379 } 380});
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.