if (!RedactorPlugins) var RedactorPlugins = {}; RedactorPlugins.definedlinks = function() { return { init: function() { if (!this.opts.definedLinks) return; this.modal.addCallback('link', $.proxy(this.definedlinks.load, this)); }, load: function() { var $select = $('<select id="redactor-defined-links" />'); $('#redactor-modal-link-insert').prepend($select); this.definedlinks.storage = {}; $.getJSON(this.opts.definedLinks, $.proxy(function(data) { $.each(data, $.proxy(function(key, val) { this.definedlinks.storage[key] = val; $select.append($('<option>').val(key).html(val.name)); }, this)); $select.on('change', $.proxy(this.definedlinks.select, this)); }, this)); }, select: function(e) { var key = $(e.target).val(); var name = '', url = ''; if (key !== 0) { name = this.definedlinks.storage[key].name; url = this.definedlinks.storage[key].url; } $('#redactor-link-url').val(url); var $el = $('#redactor-link-url-text'); if ($el.val() === '') $el.val(name); } }; }; RedactorPlugins.fontcolor = function() { return { init: function() { var colors = [ '#ffffff', '#000000', '#eeece1', '#1f497d', '#4f81bd', '#c0504d', '#9bbb59', '#8064a2', '#4bacc6', '#f79646', '#ffff00', '#f2f2f2', '#7f7f7f', '#ddd9c3', '#c6d9f0', '#dbe5f1', '#f2dcdb', '#ebf1dd', '#e5e0ec', '#dbeef3', '#fdeada', '#fff2ca', '#d8d8d8', '#595959', '#c4bd97', '#8db3e2', '#b8cce4', '#e5b9b7', '#d7e3bc', '#ccc1d9', '#b7dde8', '#fbd5b5', '#ffe694', '#bfbfbf', '#3f3f3f', '#938953', '#548dd4', '#95b3d7', '#d99694', '#c3d69b', '#b2a2c7', '#b7dde8', '#fac08f', '#f2c314', '#a5a5a5', '#262626', '#494429', '#17365d', '#366092', '#953734', '#76923c', '#5f497a', '#92cddc', '#e36c09', '#c09100', '#7f7f7f', '#0c0c0c', '#1d1b10', '#0f243e', '#244061', '#632423', '#4f6128', '#3f3151', '#31859b', '#974806', '#7f6000' ]; var buttons = ['fontcolor', 'backcolor']; for (var i = 0; i < 2; i++) { var name = buttons[i]; var button = this.button.addBefore('deleted', name, this.lang.get(name)); var $dropdown = this.button.addDropdown(button); $dropdown.width(242); this.fontcolor.buildPicker($dropdown, name, colors); } }, buildPicker: function($dropdown, name, colors) { var rule = (name == 'backcolor') ? 'background-color' : 'color'; var len = colors.length; var self = this; var func = function(e) { e.preventDefault(); self.fontcolor.set($(this).data('rule'), $(this).attr('rel')); }; for (var z = 0; z < len; z++) { var color = colors[z]; var $swatch = $('<a rel="' + color + '" data-rule="' + rule +'" href="#" style="float: left; font-size: 0; border: 2px solid #fff; padding: 0; margin: 0; width: 22px; height: 22px;"></a>'); $swatch.css('background-color', color); $swatch.on('click', func); $dropdown.append($swatch); } var $elNone = $('<a href="#" style="display: block; clear: both; padding: 5px; font-size: 12px; line-height: 1;"></a>').html(this.lang.get('none')); $elNone.on('click', $.proxy(function(e) { e.preventDefault(); this.fontcolor.remove(rule); }, this)); $dropdown.append($elNone); }, set: function(rule, type) { this.inline.format('span', 'style', rule + ': ' + type + ';'); }, remove: function(rule) { this.inline.removeStyleRule(rule); } }; }; RedactorPlugins.fontfamily = function() { return { init: function () { var fonts = [ 'Arial', 'Helvetica', 'Georgia', 'Times New Roman', 'Monospace' ]; var that = this; var dropdown = {}; $.each(fonts, function(i, s) { dropdown['s' + i] = { title: '<span style="font-family:' + s.toLowerCase() + ';">' + s + '</span>', func: function() { that.fontfamily.set(s); }}; }); dropdown.remove = { title: __('Remove Font Family'), func: that.fontfamily.reset }; var button = this.button.addBefore('bold', 'fontfamily', __('Change Font Family')); this.button.addDropdown(button, dropdown); }, set: function (value) { this.inline.format('span', 'style', 'font-family:' + value + ';'); }, reset: function() { this.inline.removeStyleRule('font-family'); } }; }; RedactorPlugins.fullscreen = function() { return { init: function() { this.fullscreen.isOpen = false; var button = this.button.add('fullscreen', 'Fullscreen'); this.button.addCallback(button, this.fullscreen.toggle); if (this.opts.fullscreen) this.fullscreen.toggle(); }, enable: function() { this.button.changeIcon('fullscreen', 'normalscreen'); this.button.setActive('fullscreen'); this.fullscreen.isOpen = true; if (this.opts.toolbarExternal) { this.fullscreen.toolcss = {}; this.fullscreen.boxcss = {}; this.fullscreen.toolcss.width = this.$toolbar.css('width'); this.fullscreen.toolcss.top = this.$toolbar.css('top'); this.fullscreen.toolcss.position = this.$toolbar.css('position'); this.fullscreen.boxcss.top = this.$box.css('top'); } this.fullscreen.height = this.$editor.height(); if (this.opts.maxHeight) this.$editor.css('max-height', ''); if (this.opts.minHeight) this.$editor.css('min-height', ''); if (!this.$fullscreenPlaceholder) this.$fullscreenPlaceholder = $('<div/>'); this.$fullscreenPlaceholder.insertAfter(this.$box); this.$box.appendTo(document.body); this.$box.addClass('redactor-box-fullscreen'); $('body, html').css('overflow', 'hidden'); this.fullscreen.resize(); $(window).on('resize.redactor.fullscreen', $.proxy(this.fullscreen.resize, this)); $(document).scrollTop(0, 0); this.$editor.focus(); this.observe.load(); }, disable: function() { this.button.removeIcon('fullscreen', 'normalscreen'); this.button.setInactive('fullscreen'); this.fullscreen.isOpen = false; $(window).off('resize.redactor.fullscreen'); $('body, html').css('overflow', ''); this.$box.insertBefore(this.$fullscreenPlaceholder); this.$fullscreenPlaceholder.remove(); this.$box.removeClass('redactor-box-fullscreen').css({ width: 'auto', height: 'auto' }); this.code.sync(); if (this.opts.toolbarExternal) { this.$box.css('top', this.fullscreen.boxcss.top); this.$toolbar.css({ 'width': this.fullscreen.toolcss.width, 'top': this.fullscreen.toolcss.top, 'position': this.fullscreen.toolcss.position }); } if (this.opts.minHeight) this.$editor.css('minHeight', this.opts.minHeight); if (this.opts.maxHeight) this.$editor.css('maxHeight', this.opts.maxHeight); this.$editor.css('height', 'auto'); this.$editor.focus(); this.observe.load(); }, toggle: function() { if (this.fullscreen.isOpen) { this.fullscreen.disable(); } else { this.fullscreen.enable(); } }, resize: function() { if (!this.fullscreen.isOpen) return; var toolbarHeight = this.$toolbar.height(); var height = $(window).height() - toolbarHeight; this.$box.width($(window).width() - 2).height(height + toolbarHeight); if (this.opts.toolbarExternal) { this.$toolbar.css({ 'top': '0px', 'position': 'absolute', 'width': '100%' }); this.$box.css('top', toolbarHeight + 'px'); } this.$editor.height(height - 14); } }; }; RedactorPlugins.imagemanager = function() { return { init: function() { if (!this.opts.imageManagerJson) return; this.modal.addCallback('image', this.imagemanager.load); }, load: function() { var $modal = this.modal.getModal(); this.modal.createTabber($modal); this.modal.addTab(1, 'Upload', 'active'); this.modal.addTab(2, 'Choose'); $('#redactor-modal-image-droparea').addClass('redactor-tab redactor-tab1'); var $box = $('<div id="redactor-image-manager-box" style="overflow: auto; height: 300px;" class="redactor-tab redactor-tab2">').hide(); $modal.append($box); $.ajax({ dataType: "json", cache: false, url: this.opts.imageManagerJson, success: $.proxy(function(data) { $.each(data, $.proxy(function(key, val) { // title var thumbtitle = ''; if (typeof val.title !== 'undefined') thumbtitle = val.title; var img = $('<img src="' + val.thumb + '" rel="' + val.image + '" title="' + thumbtitle + '" style="width: 100px; height: 75px; cursor: pointer;" />'); $('#redactor-image-manager-box').append(img); $(img).click($.proxy(this.imagemanager.insert, this)); }, this)); }, this) }); }, insert: function(e) { this.image.insert('<img src="' + $(e.target).attr('rel') + '" alt="' + $(e.target).attr('title') + '">'); } }; }; RedactorPlugins.table = function() { return { getTemplate: function() { return String() + '<section id="redactor-modal-table-insert">' + '<label>' + this.lang.get('rows') + '</label>' + '<input type="text" size="5" value="2" id="redactor-table-rows" />' + '<label>' + this.lang.get('columns') + '</label>' + '<input type="text" size="5" value="3" id="redactor-table-columns" />' + '</section>'; }, init: function() { var dropdown = {}; dropdown.insert_table = { title: this.lang.get('insert_table'), func: this.table.show }; dropdown.insert_row_above = { title: this.lang.get('insert_row_above'), func: this.table.addRowAbove }; dropdown.insert_row_below = { title: this.lang.get('insert_row_below'), func: this.table.addRowBelow }; dropdown.insert_column_left = { title: this.lang.get('insert_column_left'), func: this.table.addColumnLeft }; dropdown.insert_column_right = { title: this.lang.get('insert_column_right'), func: this.table.addColumnRight }; dropdown.add_head = { title: this.lang.get('add_head'), func: this.table.addHead }; dropdown.delete_head = { title: this.lang.get('delete_head'), func: this.table.deleteHead }; dropdown.delete_column = { title: this.lang.get('delete_column'), func: this.table.deleteColumn }; dropdown.delete_row = { title: this.lang.get('delete_row'), func: this.table.deleteRow }; dropdown.delete_table = { title: this.lang.get('delete_table'), func: this.table.deleteTable }; this.observe.addButton('td', 'table'); this.observe.addButton('th', 'table'); var button = this.button.addBefore('link', 'table', this.lang.get('table')); this.button.addDropdown(button, dropdown); }, show: function() { this.modal.addTemplate('table', this.table.getTemplate()); this.modal.load('table', this.lang.get('insert_table'), 300); this.modal.createCancelButton(); var button = this.modal.createActionButton(this.lang.get('insert')); button.on('click', this.table.insert); this.selection.save(); this.modal.show(); $('#redactor-table-rows').focus(); }, insert: function() { var rows = $('#redactor-table-rows').val(), columns = $('#redactor-table-columns').val(), $tableBox = $('<div>'), tableId = Math.floor(Math.random() * 99999), $table = $('<table id="table' + tableId + '"><tbody></tbody></table>'), i, $row, z, $column; for (i = 0; i < rows; i++) { $row = $('<tr>'); for (z = 0; z < columns; z++) { $column = $('<td>' + this.opts.invisibleSpace + '</td>'); // set the focus to the first td if (i === 0 && z === 0) { $column.append(this.selection.getMarker()); } $($row).append($column); } $table.append($row); } $tableBox.append($table); var html = $tableBox.html(); this.modal.close(); this.selection.restore(); if (this.table.getTable()) return; this.buffer.set(); var current = this.selection.getBlock() || this.selection.getCurrent(); if (current && current.tagName != 'BODY') { if (current.tagName == 'LI') current = $(current).closest('ul, ol'); $(current).after(html); } else { this.insert.html(html); } this.selection.restore(); var table = this.$editor.find('#table' + tableId); if (!this.opts.linebreaks && (this.utils.browser('mozilla') || this.utils.browser('msie'))) { var $next = table.next(); if ($next.length === 0) { table.after(this.opts.emptyHtml); } } this.observe.buttons(); table.find('span.redactor-selection-marker').remove(); table.removeAttr('id'); this.code.sync(); this.core.setCallback('insertedTable', table); }, getTable: function() { var $table = $(this.selection.getParent()).closest('table'); if (!this.utils.isRedactorParent($table)) return false; if ($table.size() === 0) return false; return $table; }, restoreAfterDelete: function($table) { this.selection.restore(); $table.find('span.redactor-selection-marker').remove(); this.code.sync(); }, deleteTable: function() { var $table = this.table.getTable(); if (!$table) return; this.buffer.set(); var $next = $table.next(); if (!this.opts.linebreaks && $next.length !== 0) { this.caret.setStart($next); } else { this.caret.setAfter($table); } $table.remove(); this.code.sync(); }, deleteRow: function() { var $table = this.table.getTable(); if (!$table) return; var $current = $(this.selection.getCurrent()); this.buffer.set(); var $current_tr = $current.closest('tr'); var $focus_tr = $current_tr.prev().length ? $current_tr.prev() : $current_tr.next(); if ($focus_tr.length) { var $focus_td = $focus_tr.children('td, th').first(); if ($focus_td.length) $focus_td.prepend(this.selection.getMarker()); } $current_tr.remove(); this.table.restoreAfterDelete($table); }, deleteColumn: function() { var $table = this.table.getTable(); if (!$table) return; this.buffer.set(); var $current = $(this.selection.getCurrent()); var $current_td = $current.closest('td, th'); var index = $current_td[0].cellIndex; $table.find('tr').each($.proxy(function(i, elem) { var $elem = $(elem); var focusIndex = index - 1 < 0 ? index + 1 : index - 1; if (i === 0) $elem.find('td, th').eq(focusIndex).prepend(this.selection.getMarker()); $elem.find('td, th').eq(index).remove(); }, this)); this.table.restoreAfterDelete($table); }, addHead: function() { var $table = this.table.getTable(); if (!$table) return; this.buffer.set(); if ($table.find('thead').size() !== 0) { this.table.deleteHead(); return; } var tr = $table.find('tr').first().clone(); tr.find('td').html(this.opts.invisibleSpace); $thead = $('<thead></thead>').append(tr); $table.prepend($thead); this.code.sync(); }, deleteHead: function() { var $table = this.table.getTable(); if (!$table) return; var $thead = $table.find('thead'); if ($thead.size() === 0) return; this.buffer.set(); $thead.remove(); this.code.sync(); }, addRowAbove: function() { this.table.addRow('before'); }, addRowBelow: function() { this.table.addRow('after'); }, addColumnLeft: function() { this.table.addColumn('before'); }, addColumnRight: function() { this.table.addColumn('after'); }, addRow: function(type) { var $table = this.table.getTable(); if (!$table) return; this.buffer.set(); var $current = $(this.selection.getCurrent()); var $current_tr = $current.closest('tr'); var new_tr = $current_tr.clone(); new_tr.find('th').replaceWith(function() { var $td = $('<td>'); $td[0].attributes = this.attributes; return $td.append($(this).contents()); }); new_tr.find('td').html(this.opts.invisibleSpace); if (type == 'after') { $current_tr.after(new_tr); } else { $current_tr.before(new_tr); } this.code.sync(); }, addColumn: function (type) { var $table = this.table.getTable(); if (!$table) return; var index = 0; var current = $(this.selection.getCurrent()); this.buffer.set(); var $current_tr = current.closest('tr'); var $current_td = current.closest('td, th'); $current_tr.find('td, th').each($.proxy(function(i, elem) { if ($(elem)[0] === $current_td[0]) index = i; }, this)); $table.find('tr').each($.proxy(function(i, elem) { var $current = $(elem).find('td, th').eq(index); var td = $current.clone(); td.html(this.opts.invisibleSpace); if (type == 'after') { $current.after(td); } else { $current.before(td); } }, this)); this.code.sync(); } }; }; RedactorPlugins.textdirection = function() { return { init: function() { var that = this; var dropdown = {}; dropdown.ltr = { title: __('Left to Right'), callback: this.setLtr }; dropdown.rtl = { title: __('Right to Left'), callback: this.setRtl }; var button = this.button.add('textdirection', __('Change Text Direction'), false, dropdown); if (this.opts.direction == 'rtl') this.setRtl(); }, setRtl: function() { var c = this.getCurrent(), s = this.getSelection(); this.bufferSet(); if (s.type == 'Range' && s.focusNode.nodeName != 'div') { this.linebreakHack(s); } else if (!c) { var repl = '<div dir="rtl">' + this.get() + '</div>'; this.set(repl, false); } $(this.getCurrent()).attr('dir', 'rtl'); this.sync(); }, setLtr: function() { var c = this.getCurrent(), s = this.getSelection(); this.bufferSet(); if (s.type == 'Range' && s.focusNode.nodeName != 'div') { this.linebreakHack(s); } else if (!c) { var repl = '<div dir="ltr">' + this.get() + '</div>'; this.set(repl, false); } $(this.getCurrent()).attr('dir', 'ltr'); this.sync(); }, linebreakHack: function(sel) { var range = sel.getRangeAt(0); var wrapper = document.createElement('div'); wrapper.appendChild(range.extractContents()); range.insertNode(wrapper); this.selectionElement(wrapper); } }; }; RedactorPlugins.video = function() { return { reUrlYoutube: /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig, reUrlVimeo: /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/, getTemplate: function() { return String() + '<section id="redactor-modal-video-insert">' + '<label>' + this.lang.get('video_html_code') + '</label>' + '<textarea id="redactor-insert-video-area" style="height: 160px;"></textarea>' + '</section>'; }, init: function() { var button = this.button.addAfter('image', 'video', this.lang.get('video')); this.button.addCallback(button, this.video.show); }, show: function() { this.modal.addTemplate('video', this.video.getTemplate()); this.modal.load('video', this.lang.get('video'), 700); this.modal.createCancelButton(); var button = this.modal.createActionButton(this.lang.get('insert')); button.on('click', this.video.insert); this.selection.save(); this.modal.show(); $('#redactor-insert-video-area').focus(); }, insert: function() { var data = $('#redactor-insert-video-area').val(); data = this.clean.stripTags(data); // parse if it is link on youtube & vimeo var iframeStart = '<iframe style="width: 500px; height: 281px;" src="', iframeEnd = '" frameborder="0" allowfullscreen></iframe>'; if (data.match(this.video.reUrlYoutube)) { data = data.replace(this.video.reUrlYoutube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd); } else if (data.match(this.video.reUrlVimeo)) { data = data.replace(this.video.reUrlVimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd); } this.selection.restore(); this.modal.close(); var current = this.selection.getBlock() || this.selection.getCurrent(); if (current) $(current).after(data); else { this.insert.html(data); } this.code.sync(); } }; }; RedactorPlugins.imagepaste = function() { return { init: function() { if (this.utils.browser('webkit') && navigator.userAgent.indexOf('Chrome') === -1) { var arr = this.utils.browser('version').split('.'); if (arr[0] < 536) return true; } // paste except opera (not webkit) if (this.utils.browser('opera')) return true; this.$editor.on('paste.imagepaste', $.proxy(this.imagepaste.buildEventPaste, this)); }, buildEventPaste: function(e) { var event = e.originalEvent || e, fileUpload = false, files = [], i, file, cd = event.clipboardData; if (typeof(cd) === 'undefined') return; if (cd.items && cd.items.length) { for (i = 0, k = cd.items.length; i < k; i++) { if (cd.kind == 'file' && cd.type.indexOf('image/') !== -1) { file = cd.items[i].getAsFile(); if (file !== null) files.push(file); } } } else if (cd.files && cd.files.length) { files = cd.files } else if (cd.types.length) { for (i = 0, k = cd.types.length; i < k; i++) { if (cd.types[i].indexOf('image/') != -1) { var data = cd.getData(cd.types[i]); if (data.length) files.push(new Blob([data], {type: cd.types[i]})); } } } if (files.length) { // clipboard upload this.selection.save(); this.buffer.set(); this.clean.singleLine = false; for (i = 0, k = files.length; i < k; i++) this.upload.directUpload(files[i], event); return false; } } }; }; var loadedFabric = false; RedactorPlugins.imageannotate = function() { return { annotateButton: false, init: function() { var redactor = this, self = this.imageannotate; if (typeof window.fabric === 'undefined' && !loadedFabric) { $.getScript('../js/fabric.min.js'); loadedFabric = true; } $(document).on('click', '.redactor-box img', function() { var $image = $(this), image_box = $('#redactor-image-box'); if (!image_box.length || !redactor.image.editter) return; var edit_size = redactor.image.editter.outerWidth(); self.annotateButton = redactor.image.editter .on('remove.annotate', function() { self.teardownAnnotate.call(redactor, image_box); }) .clone() .text(' '+__('Annotate')) .prepend('<i class="icon-pencil"></i>') .addClass('annotate-button') .insertAfter(redactor.image.editter) .data('image', this) .on('click', function() { self.startAnnotate.call(redactor, $image) }); var diff = (edit_size - self.annotateButton.outerWidth()) / 2; self.annotateButton.css('margin-left', (diff + 5) + 'px'); redactor.image.editter.css('margin-left', (-edit_size + diff - 5) + 'px'); }); }, startAnnotate: function(img) { canvas = this.imageannotate.initCanvas(img); this.imageannotate.buildToolbar(img); this.image.editter.hide(); this.imageannotate.annotateButton.hide(); }, teardownAnnotate: function(box) { this.image.editter.off('.annotate'); this.opts.keydownCallback = false; this.opts.keyupCallback = false; box.find('.annotate-toolbar').remove(); box.find('.annotate-button').remove(); var img = box.find('img')[0], $img = $(img), fcanvas = $img.data('canvas'), state = fcanvas.toObject(); // Capture current annotations delete state.backgroundImage; $img.attr('data-annotations', btoa(JSON.stringify(state))); // Drop the canvas fcanvas.dispose(); box.find('canvas').parent().remove(); $img.data('canvas', false); // Deselect the image this.image.hideResize(); // Show the original image $img.removeClass('hidden'); }, buildToolbar: function(img) { var box = img.parent(), redactor = this, plugin = this.imageannotate, shapes = $('<span>') .attr('data-redactor', 'verified') .attr('contenteditable', 'false') .css({'display': 'inline-block', 'vertical-align': 'top'}), swatches = shapes.clone(), actions = shapes.clone(), container = $('<div></div>') .addClass('annotate-toolbar') .attr('data-redactor', 'verified') .attr('contenteditable', 'false') .css({position: 'absolute', bottom: 0, 'min-height': '28px', width: '100%', 'background-color': 'rgba(0,0,0,0.5)', margin: 0, 'padding-top': '4px' }) .appendTo(box) .append(shapes) .append(swatches) .append(actions); var button = $('<a></a>') .attr('href', '#') .attr('data-redactor', 'verified') .attr('contenteditable', 'false') .css({color: 'white', padding: '0 7px 1px', margin: '1px 3px', 'text-decoration': 'none', 'vertical-align': 'top'}); shapes .append(button.clone() .append($('<i class="icon-arrow-right icon-large"></i>') .on('click', plugin.drawArrow.bind(redactor)) .attr('title', __('Add Arrow'))) ) .append(button.clone() .append($('<i class="icon-check-empty icon-large"></i>') .on('click', plugin.drawBox.bind(redactor)) .attr('title', __('Add Rectangle'))) ) .append(button.clone() .append($('<i class="icon-circle-blank icon-large"></i>') .on('click', plugin.drawEllipse.bind(redactor)) .attr('title', __('Add Ellipse'))) ) .append(button.clone() .append($('<i class="icon-text-height icon-large"></i>') .on('click', plugin.drawText.bind(redactor)) .attr('title', __('Add Text'))) ); var colors = [ '#ffffff', '#888888', '#000000', 'fuchsia', 'blue', 'red', 'lime', 'blueviolet', 'cyan', '#f4a63b', 'yellow'] len = colors.length; swatches.append( $('<span><i class="icon-ellipsis-vertical icon-large"></i></span>') .css({color: 'white', padding: '0 3px 1px', margin: '1px 3px', height: '21px', position: 'relative', bottom: '8px'} ) ); for (var z = 0; z < len; z++) { var color = colors[z]; var $swatch = $('<a rel="' + color + '" href="#" style="font-size: 0; padding: 0; margin: 2px; width: 22px; height: 22px;"></a>'); $swatch.css({'background-color': color, 'border': '1px dotted rgba(255,255,255,0.4)'}); $swatch.attr('data-redactor', 'verified'); $swatch.attr('contenteditable', 'false'); $swatch.on('click', plugin.setColor.bind(redactor)); swatches.append($swatch); } actions .append( $('<span><i class="icon-ellipsis-vertical icon-large"></i></span>') .css({color: 'white', padding: '0 3px 1px', margin: '1px 3px', height: '21px'} ) ) .append(button.clone() .css('padding-left', '1px') .append($('<span></span>').css('position','relative') .append($('<i class="icon-font"></i>')) .append($('<i class="icon-minus"></i>') .css({position: 'absolute', right: '-4px', top: '5px', 'text-shadow': '0 0 2px black', 'font-size':'80%'}) ) ) .on('click', plugin.smallerFont.bind(redactor)) .attr('title', __('Decrease Font Size')) ) .append(button.clone() .css('padding-left', '1px') .append($('<span></span>').css('position','relative') .append($('<i class="icon-font icon-large"></i>')) .append($('<i class="icon-plus"></i>') .css({position: 'absolute', right: '-8px', top: '4px', 'text-shadow': '0 0 2px black'}) ) ) .on('click', plugin.biggerFont.bind(redactor)) .attr('title', __('Increase Font Size')) ) .append(button.clone() .attr('id', 'annotate-set-stroke') .append($('<span></span>').css({'position': 'relative', 'top': '2px'}) .append($('<i class="icon-check-empty icon-large"></i>') .css('font-size', '120%') ).append($('<i class="icon-tint"></i>') .css({position: 'absolute', left: '4.5px', top: 0}) ) ) .on('click', plugin.paintStroke.bind(redactor)) .attr('title', __('Set Stroke')) ) .append(button.clone() .attr('id', 'annotate-set-fill') .append($('<span></span>').css('position','relative') .append($('<i class="icon-sign-blank icon-large"></i>')) .append($('<i class="icon-tint icon-dark"></i>') .css({position: 'absolute', left: '4px', top: '2px'}) ) ) .on('click', plugin.paintFill.bind(redactor)) .attr('title', __('Set Fill')) ) .append(button.clone() .append($('<i class="icon-eye-close icon-large"></i>')) .on('click', plugin.setOpacity.bind(redactor)) .attr('title', __('Toggle Opacity')) ) .append(button.clone() .append($('<i class="icon-double-angle-up icon-large"></i>')) .on('click', plugin.bringForward.bind(redactor)) .attr('title', __('Bring Forward')) ) .append(button.clone() .append($('<i class="icon-trash icon-large"></i>')) .on('click', plugin.discard.bind(redactor)) .attr('title', __('Delete Object')) ); container.append(button.clone() .append($('<i class="icon-save icon-large"></i>')) .on('click', plugin.commit.bind(redactor)) .addClass('pull-right') .attr('title', __('Commit Annotations')) ); plugin.paintStroke(); }, setColor: function(e) { e.preventDefault(); var plugin = this.imageannotate, redactor = this, swatch = e.target, image_box = $('#redactor-image-box'), img = image_box.find('img')[0], fcanvas = $(img).data('canvas'); $.each(fcanvas.getObjects(), function() { if (this.get('active')) { if (plugin.paintMode == 'fill') this.setFill($(e.target).attr('rel')); else this.setStroke($(e.target).attr('rel')); } }); fcanvas.renderAll(); }, // Shapes drawShape: function(ondown, onmove, onup, cursor) { // @see http://jsfiddle.net/URWru/ var plugin = this.imageannotate, redactor = this, image_box = $('#redactor-image-box'), img = image_box.find('img')[0], fcanvas = $(img).data('canvas'), isDown, shape, mousedown = function(o) { isDown = true; plugin.setBuffer(); var pointer = fcanvas.getPointer(o.e); shape = ondown(pointer, o.e); fcanvas.add(shape); }, mousemove = function(o) { if (!isDown) return; var pointer = fcanvas.getPointer(o.e); onmove(shape, pointer, o.e); fcanvas.renderAll(); }, mouseup = function(o) { isDown = false; if (onup) { if (shape2 = onup(shape, fcanvas.getPointer(o.e))) { shape.remove(); fcanvas.add(shape2); shape = shape2; } } shape.setCoords() .set({ transparentCorners: false, borderColor: 'rgba(102,153,255,0.9)', cornerColor: 'rgba(102,153,255,0.5)', cornerSize: 10 }); fcanvas.calcOffset() .off('mouse:down', mousedown) .off('mouse:up', mouseup) .off('mouse:move', mousemove) .deactivateAll() .setActiveObject(shape) .renderAll(); fcanvas.selection = true; fcanvas.defaultCursor = 'default'; }; fcanvas.selection = false; fcanvas.defaultCursor = cursor || 'crosshair'; // Ensure double presses of same button are squelched fcanvas.off('mouse:down'); fcanvas.off('mouse:up'); fcanvas.off('mouse:move'); fcanvas.on('mouse:down', mousedown); fcanvas.on('mouse:up', mouseup); fcanvas.on('mouse:move', mousemove); return false; }, drawArrow: function(e) { e.preventDefault(); var top, left; return this.imageannotate.drawShape( function(pointer) { top = pointer.y; left = pointer.x; return new fabric.Group([ new fabric.Line([0, 5, 0, 5], { strokeWidth: 5, fill: 'red', stroke: 'red', originX: 'center', originY: 'center', selectable: false, hasBorders: false }), new fabric.Polygon([ {x: 20, y: 0}, {x: 0, y: -5}, {x: 0, y: 5} ], { strokeWidth: 0, fill: 'red', originX: 'center', originY: 'center', selectable: false, hasBorders: false }) ], { left: pointer.x, top: pointer.y, originX: 'center', originY: 'center' }); }, function(group, pointer) { var dx = pointer.x - left, dy = pointer.y - top, angle = Math.atan(dy / dx), d = Math.sqrt(dx * dx + dy * dy) - 10, sign = dx < 0 ? -1 : 1, dy2 = Math.sin(angle) * d * sign; dx2 = Math.cos(angle) * d * sign, group.item(0) .set({ x2: dx2, y2: dy2 }); group.item(1) .set({ angle: angle * 180 / Math.PI, flipX: dx < 0, flipY: dy < 0 }) .setPositionByOrigin(new fabric.Point(dx, dy), 'center', 'center'); }, function(shape, pointer) { var dx = pointer.x - left, dy = pointer.y - top, angle = Math.atan(dy / dx), d = Math.sqrt(dx * dx + dy * dy); // Mess with the next two lines and you *will* be sorry! shape.forEachObject(function(e) { shape.removeWithUpdate(e); }); return new fabric.Path( 'M '+left+' '+top+' l '+(d-20)+' 0 0 -3 15 3 -15 3 0 -3 z', { angle: angle * 180 / Math.PI + (dx < 0 ? 180 : 0), strokeWidth: 5, fill: 'red', stroke: 'red' }); } ); }, drawEllipse: function(e) { e.preventDefault(); return this.imageannotate.drawShape( function(pointer) { return new fabric.Ellipse({ top: pointer.y, left: pointer.x, strokeWidth: 5, fill: 'transparent', stroke: 'red', originX: 'left', originY: 'top' }); }, function(circle, pointer, event) { var x = circle.get('left'), y = circle.get('top'), dx = pointer.x - x, dy = pointer.y - y, sw = circle.getStrokeWidth()/2; // Use SHIFT to draw circles if (event.shiftKey) { dy = dx = Math.max(dx, dy); } circle.set({ rx: Math.max(0, Math.abs(dx/2) - sw), ry: Math.max(0, Math.abs(dy/2) - sw), originX: dx < 0 ? 'right' : 'left', originY: dy < 0 ? 'bottom' : 'top'}); } ); }, drawBox: function(e) { e.preventDefault(); return this.imageannotate.drawShape( function(pointer) { return new fabric.Rect({ top: pointer.y, left: pointer.x, strokeWidth: 5, fill: 'transparent', stroke: 'red', originX: 'left', originY: 'top' }); }, function(rect, pointer, event) { var x = rect.get('left'), y = rect.get('top'), dx = pointer.x - x, dy = pointer.y - y; // Use SHIFT to draw squares if (event.shiftKey) { dy = dx = Math.max(dx, dy); } rect.set({ width: Math.abs(dx), height: Math.abs(dy), originX: dx < 0 ? 'right' : 'left', originY: dy < 0 ? 'bottom' : 'top'}); } ); }, drawText: function(e) { e.preventDefault(); return this.imageannotate.drawShape( function(pointer) { return new fabric.IText(__('Text'), { top: pointer.y, left: pointer.x, fill: 'red', originX: 'left', originY: 'top', fontFamily: 'sans-serif', fontSize: 30 }); }, function(rect, pointer, event) { var x = rect.get('left'), y = rect.get('top'), dx = pointer.x - x, dy = pointer.y - y; // Use SHIFT to draw squares if (event.shiftKey) { dy = dx = Math.max(dx, dy); } rect.set({ width: Math.abs(dx), height: Math.abs(dy), originX: dx < 0 ? 'right' : 'left', originY: dy < 0 ? 'bottom' : 'top'}); }, function(shape) { shape.on('editing:exited', function() { if (!shape.getText()) shape.remove(); }); }, 'text' ); }, // Action buttons biggerFont: function(e) { e.preventDefault(); var image_box = $('#redactor-image-box'), img = image_box.find('img')[0], fcanvas = $(img).data('canvas'); $.each(fcanvas.getObjects(), function() { if (this.get('active') && this instanceof fabric.IText) { if (this.getSelectedText()) { this.setSelectionStyles({ fontSize: (this.getSelectionStyles().fontSize || this.getFontSize()) + 5 }); } else { this.setFontSize(this.getFontSize() + 5); } } }); fcanvas.renderAll(); return false; }, smallerFont: function(e) { e.preventDefault(); var image_box = $('#redactor-image-box'), img = image_box.find('img')[0], fcanvas = $(img).data('canvas'); $.each(fcanvas.getObjects(), function() { if (this.get('active') && this instanceof fabric.IText) { if (this.getSelectedText()) { this.setSelectionStyles({ fontSize: (this.getSelectionStyles().fontSize || this.getFontSize()) - 5 }); } else { this.setFontSize(this.getFontSize() - 5); } } }); fcanvas.renderAll(); return false; }, paintStroke: function(e) { $('#annotate-set-stroke').css({'background-color': 'rgba(255,255,255,0.3)'}); $('#annotate-set-fill').css({'background-color': 'transparent'}); this.imageannotate.paintMode = 'stroke'; return false; }, paintFill: function(e) { $('#annotate-set-fill').css({'background-color': 'rgba(255,255,255,0.3)'}); $('#annotate-set-stroke').css({'background-color': 'transparent'}); this.imageannotate.paintMode = 'fill'; return false; }, setOpacity: function(e) { e.preventDefault(); var image_box = $('#redactor-image-box'), img = image_box.find('img')[0], fcanvas = $(img).data('canvas'); $.each(fcanvas.getObjects(), function() { if (this.get('active')) { if (this.getOpacity() != 1) this.setOpacity(1); else this.setOpacity(0.6); } }); fcanvas.renderAll(); return false; }, bringForward: function(e) { e.preventDefault(); var image_box = $('#redactor-image-box'), img = image_box.find('img')[0], fcanvas = $(img).data('canvas'); $.each(fcanvas.getObjects(), function() { if (this.get('active')) { this.bringForward(); } }); }, keydown: function(e) { var image_box = $('#redactor-image-box'), img = image_box.find('img')[0], fcanvas = $(img).data('canvas'); if (!fcanvas) return; var active = fcanvas.getActiveObject(); // Check if editing a text element if (active instanceof fabric.IText && active.get('isEditing')) { // This keystroke is not for redactor var ss = active.get('selectionStart'), se = active.get('selectionEnd'); active.exitEditing(); active.enterEditing(); active.set({ 'selectionStart': ss, 'selectionEnd': se }); if (e.type == 'keydown') active.onKeyDown(e); else active.onKeyPress(e); return false; } // Check if [delete] was pressed with selected objects if (e.keyCode == 8 || e.keyCode == 46) return this.imageannotate.discard(e); else if (e.keyCode == 90 && (e.metaKey || e.ctrlKey)) { fcanvas.loadFromJSON(atob($(img).attr('data-annotations'))); return false; } }, discard: function(e) { var image_box = $('#redactor-image-box', this.$editor), img = image_box && image_box.find('img')[0], fcanvas = img && $(img).data('canvas'); if (!fcanvas) // Not annotating return; e.preventDefault(); this.imageannotate.setBuffer(); $.each(fcanvas.getObjects(), function() { if (this.get('active')) this.remove(); }); fcanvas.renderAll(); return false; }, commit: function(e) { e.preventDefault(); var redactor = this, image_box = $('#redactor-image-box'), img = image_box.find('img')[0], $img = $(img), fcanvas = $(img).data('canvas'); fcanvas.deactivateAll(); // Upload to server redactor.buffer.set(); var annotated = fcanvas.toDataURL({ format: 'jpg', quality: 4, multiplier: 1/fcanvas.getZoom() }), file = new Blob([annotated], {type: 'image/jpeg'}); // Fallback to the data URL — show while the image is being uploaded var origSrc = $img.attr('src'); $img.attr('src', annotated); var origCallback = redactor.opts.imageUploadCallback, origErrorCbk = redactor.opts.imageUploadErrorCallback; // After successful upload, replace the old image with the new one. // Transfer the annotation state to the new image for replay. redactor.opts.imageUploadCallback = function(image, json) { redactor.opts.imageUploadCallback = origCallback; redactor.opts.imageUploadErrorCallback = origErrorCbk; // Transfer the annotation JSON data and drop the original image. image.attr('data-annotations', $img.attr('data-annotations')); // Record the image that was originally annotated. If the committed // image is annotated again, it should be the original image with // the annotations placed live on the original image. The image // being committed here will be discarded. image.attr('data-orig-annotated-image-src', $img.attr('data-orig-annotated-image-src') || origSrc ); $img.remove(); // Redactor will add <br> before and after the image in linebreaks // mode var N = image.next(); if (N.is('br')) N.remove(); var P = image.prev(); if (N.is('br')) P.remove(); }; // Handle upload issues redactor.opts.imageUploadErrorCallback = function(json) { redactor.opts.imageUploadCallback = origCallback; redactor.opts.imageUploadErrorCallback = origErrorCbk; $img.show(); }; redactor.imageannotate.teardownAnnotate(image_box); $img.css({opacity: 0.5}); redactor.upload.directUpload(file, e); return false; }, // Utils resizeShape: function(o) { var shape = o.target; if (shape instanceof fabric.Ellipse) { shape.set({ rx: shape.get('rx') * shape.get('scaleX'), ry: shape.get('ry') * shape.get('scaleY'), scaleX: 1, scaleY: 1 }); } else if (shape instanceof fabric.Rect) { shape.set({ width: shape.get('width') * shape.get('scaleX'), height: shape.get('height') * shape.get('scaleY'), scaleX: 1, scaleY: 1 }); } }, setBuffer: function() { var image_box = $('#redactor-image-box'), img = image_box.find('img')[0], $img = $(img), fcanvas = $img.data('canvas'), state = fcanvas.toObject(); // Capture current annotations delete state.backgroundImage; $img.attr('data-annotations', btoa(JSON.stringify(state))); }, // Startup initCanvas: function(img) { var self = this, plugin = this.imageannotate, $img = $(img); if ($img.data('canvas')) return; var box = $img.parent(), canvas = $('<canvas>').css({ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, width: '100%', height: '100%' }).appendTo(box), fcanvas = new fabric.Canvas(canvas[0], { backgroundColor: 'rgba(0,0,0,0,0)', containerClass: 'no-margin', includeDefaultValues: false, }), previous = $(img).attr('data-annotations'); // Catch [delete] key and map to delete object self.opts.keydownCallback = plugin.keydown.bind(self); self.opts.keyupCallback = plugin.keydown.bind(self); var I = new Image(), scale; I.src = $img.attr('src'); // Use a maximum zoom-out of 0.7, so that very large pictures do not // result in unusually small annotations (esp. stroke widths which are // not adjustable). scale = Math.max(0.7, $img.width() / I.width); var scaleWidth = $img.width() / scale, scaleHeight = $img.height() / scale; console.log(I.width, scaleWidth, $img.width(), scale); fcanvas .setDimensions({width: $img.width(), height: $img.height()}) .setZoom(scale) .setBackgroundImage( $img.attr('data-orig-annotated-image-src') || $img.attr('src'), fcanvas.renderAll.bind(fcanvas), { width: scaleWidth, height: scaleHeight, // Needed to position overlayImage at 0/0 originX: 'left', originY: 'top' }) .on('object:scaling', plugin.resizeShape.bind(self)); if (previous) { fcanvas.loadFromJSON(atob(previous)); fcanvas.forEachObject(function(o) { o.set({ transparentCorners: false, borderColor: 'rgba(102,153,255,0.9)', cornerColor: 'rgba(102,153,255,0.5)', cornerSize: 10 }); }); } $img.data('canvas', fcanvas).addClass('hidden'); return fcanvas; } }; };