You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2254 lines
71 KiB
2254 lines
71 KiB
if (!RedactorPlugins) var RedactorPlugins = {};
|
|
|
|
(function ($R) {
|
|
$R.add('plugin', 'definedlinks', {
|
|
init: function (app) {
|
|
this.app = app;
|
|
this.opts = app.opts;
|
|
this.component = app.component;
|
|
// local
|
|
this.links = [];
|
|
},
|
|
// messages
|
|
onmodal: {
|
|
link: {
|
|
open: function ($modal, $form) {
|
|
if (!this.opts.definedlinks) return;
|
|
this.$modal = $modal;
|
|
this.$form = $form;
|
|
this._load();
|
|
}
|
|
}
|
|
},
|
|
// private
|
|
_load: function () {
|
|
if (typeof this.opts.definedlinks === 'object') {
|
|
this._build(this.opts.definedlinks);
|
|
} else {
|
|
$R.ajax.get({
|
|
url: this.opts.definedlinks,
|
|
success: this._build.bind(this)
|
|
});
|
|
}
|
|
},
|
|
_build: function (data) {
|
|
var $selector = this.$modal.find('#redactor-defined-links');
|
|
if ($selector.length === 0) {
|
|
var $body = this.$modal.getBody();
|
|
var $item = $R.dom('<div class="form-item" />');
|
|
var $selector = $R.dom('<select id="redactor-defined-links" />');
|
|
$item.append($selector);
|
|
$body.prepend($item);
|
|
}
|
|
this.links = [];
|
|
$selector.html('');
|
|
$selector.off('change');
|
|
for (var key in data) {
|
|
if (!data.hasOwnProperty(key) || typeof data[key] !== 'object') {
|
|
continue;
|
|
}
|
|
this.links[key] = data[key];
|
|
var $option = $R.dom('<option>');
|
|
$option.val(key);
|
|
$option.html(data[key].name);
|
|
$selector.append($option);
|
|
}
|
|
$selector.on('change', this._select.bind(this));
|
|
},
|
|
_select: function (e) {
|
|
var formData = this.$form.getData();
|
|
var key = $R.dom(e.target)
|
|
.val();
|
|
var data = {
|
|
text: '',
|
|
url: ''
|
|
};
|
|
if (key !== '0') {
|
|
data.text = this.links[key].name;
|
|
data.url = this.links[key].url;
|
|
}
|
|
if (formData.text !== '') {
|
|
data = {
|
|
url: data.url
|
|
};
|
|
}
|
|
this.$form.setData(data);
|
|
}
|
|
});
|
|
|
|
$R.add('plugin', 'fontcolor', {
|
|
translations: {
|
|
en: {
|
|
"fontcolor": "Text Color",
|
|
"text": "Text",
|
|
"highlight": "Highlight"
|
|
}
|
|
},
|
|
init: function (app) {
|
|
this.app = app;
|
|
this.opts = app.opts;
|
|
this.lang = app.lang;
|
|
this.inline = app.inline;
|
|
this.toolbar = app.toolbar;
|
|
this.selection = app.selection;
|
|
// local
|
|
this.colors = (this.opts.fontcolors) ? this.opts.fontcolors : ['#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'];
|
|
},
|
|
// messages
|
|
onfontcolor: {
|
|
set: function (rule, value) {
|
|
this._set(rule, value);
|
|
},
|
|
remove: function (rule) {
|
|
this._remove(rule);
|
|
}
|
|
},
|
|
// public
|
|
start: function () {
|
|
var btnObj = {
|
|
title: this.lang.get('fontcolor')
|
|
};
|
|
var $dropdown = this._buildDropdown();
|
|
this.$button = this.toolbar.addButtonAuto('fontcolor', btnObj);
|
|
this.$button.setIcon('<i class="re-icon-fontcolor"></i>');
|
|
this.$button.setDropdown($dropdown);
|
|
},
|
|
// private
|
|
_buildDropdown: function () {
|
|
var $dropdown = $R.dom('<div class="redactor-dropdown-cells">');
|
|
this.$selector = this._buildSelector();
|
|
this.$selectorText = this._buildSelectorItem('text', this.lang.get('text'));
|
|
this.$selectorText.addClass('active');
|
|
this.$selectorBack = this._buildSelectorItem('back', this.lang.get('highlight'));
|
|
this.$selector.append(this.$selectorText);
|
|
this.$selector.append(this.$selectorBack);
|
|
this.$pickerText = this._buildPicker('textcolor');
|
|
this.$pickerBack = this._buildPicker('backcolor');
|
|
$dropdown.append(this.$selector);
|
|
$dropdown.append(this.$pickerText);
|
|
$dropdown.append(this.$pickerBack);
|
|
this._buildSelectorEvents();
|
|
$dropdown.width(242);
|
|
return $dropdown;
|
|
},
|
|
_buildSelector: function () {
|
|
var $selector = $R.dom('<div>');
|
|
$selector.addClass('redactor-dropdown-selector');
|
|
return $selector;
|
|
},
|
|
_buildSelectorItem: function (name, title) {
|
|
var $item = $R.dom('<span>');
|
|
$item.attr('rel', name)
|
|
.html(title);
|
|
$item.addClass('redactor-dropdown-not-close');
|
|
return $item;
|
|
},
|
|
_buildSelectorEvents: function () {
|
|
this.$selectorText.on('mousedown', function (e) {
|
|
e.preventDefault();
|
|
this.$selector.find('span')
|
|
.removeClass('active');
|
|
this.$pickerBack.hide();
|
|
this.$pickerText.show();
|
|
this.$selectorText.addClass('active');
|
|
}.bind(this));
|
|
this.$selectorBack.on('mousedown', function (e) {
|
|
e.preventDefault();
|
|
this.$selector.find('span')
|
|
.removeClass('active');
|
|
this.$pickerText.hide();
|
|
this.$pickerBack.show();
|
|
this.$selectorBack.addClass('active');
|
|
}.bind(this));
|
|
},
|
|
_buildPicker: function (name) {
|
|
var $box = $R.dom('<div class="re-dropdown-box-' + name + '">');
|
|
var rule = (name == 'backcolor') ? 'background-color' : 'color';
|
|
var len = this.colors.length;
|
|
var self = this;
|
|
var func = function (e) {
|
|
e.preventDefault();
|
|
var $el = $R.dom(e.target);
|
|
self._set($el.data('rule'), $el.attr('rel'));
|
|
};
|
|
for (var z = 0; z < len; z++) {
|
|
var color = this.colors[z];
|
|
var $swatch = $R.dom('<span>');
|
|
$swatch.attr({
|
|
'rel': color,
|
|
'data-rule': rule
|
|
});
|
|
$swatch.css({
|
|
'background-color': color,
|
|
'font-size': 0,
|
|
'border': '2px solid #fff',
|
|
'width': '22px',
|
|
'height': '22px'
|
|
});
|
|
$swatch.on('mousedown', func);
|
|
$box.append($swatch);
|
|
}
|
|
var $el = $R.dom('<a>');
|
|
$el.attr({
|
|
'href': '#'
|
|
});
|
|
$el.css({
|
|
'display': 'block',
|
|
'clear': 'both',
|
|
'padding': '8px 5px',
|
|
'font-size': '12px',
|
|
'line-height': 1
|
|
});
|
|
$el.html(this.lang.get('none'));
|
|
$el.on('click', function (e) {
|
|
e.preventDefault();
|
|
self._remove(rule);
|
|
});
|
|
$box.append($el);
|
|
if (name == 'backcolor') $box.hide();
|
|
return $box;
|
|
},
|
|
_set: function (rule, value) {
|
|
var style = {};
|
|
style[rule] = value;
|
|
var args = {
|
|
tag: 'span',
|
|
style: style,
|
|
type: 'toggle'
|
|
};
|
|
this.inline.format(args);
|
|
},
|
|
_remove: function (rule) {
|
|
this.inline.remove({
|
|
style: rule
|
|
});
|
|
}
|
|
});
|
|
|
|
$R.add('plugin', 'fontfamily', {
|
|
translations: {
|
|
en: {
|
|
"fontfamily": "Font",
|
|
"remove-font-family": "Remove Font Family"
|
|
}
|
|
},
|
|
init: function (app) {
|
|
this.app = app;
|
|
this.opts = app.opts;
|
|
this.lang = app.lang;
|
|
this.inline = app.inline;
|
|
this.toolbar = app.toolbar;
|
|
// local
|
|
this.fonts = (this.opts.fontfamily) ? this.opts.fontfamily : ['Arial', 'Helvetica', 'Georgia', 'Times New Roman', 'Monospace'];
|
|
},
|
|
// public
|
|
start: function () {
|
|
var dropdown = {};
|
|
for (var i = 0; i < this.fonts.length; i++) {
|
|
var font = this.fonts[i];
|
|
dropdown[i] = {
|
|
title: font.replace(/'/g, ''),
|
|
api: 'plugin.fontfamily.set',
|
|
args: font
|
|
};
|
|
}
|
|
dropdown.remove = {
|
|
title: this.lang.get('remove-font-family'),
|
|
api: 'plugin.fontfamily.remove'
|
|
};
|
|
var $button = this.toolbar.addButtonAuto('fontfamily', {
|
|
title: this.lang.get('fontfamily')
|
|
});
|
|
$button.setIcon('<i class="re-icon-fontfamily"></i>');
|
|
$button.setDropdown(dropdown);
|
|
},
|
|
set: function (value) {
|
|
var args = {
|
|
tag: 'span',
|
|
style: {
|
|
'font-family': value
|
|
},
|
|
type: 'toggle'
|
|
};
|
|
this.inline.format(args);
|
|
},
|
|
remove: function () {
|
|
this.inline.remove({
|
|
style: 'font-family'
|
|
});
|
|
}
|
|
});
|
|
|
|
$R.add('plugin', 'table', {
|
|
translations: {
|
|
en: {
|
|
"table": "Table",
|
|
"insert-table": "Insert table",
|
|
"insert-row-above": "Insert row above",
|
|
"insert-row-below": "Insert row below",
|
|
"insert-column-left": "Insert column left",
|
|
"insert-column-right": "Insert column right",
|
|
"add-head": "Add head",
|
|
"delete-head": "Delete head",
|
|
"delete-column": "Delete column",
|
|
"delete-row": "Delete row",
|
|
"delete-table": "Delete table"
|
|
}
|
|
},
|
|
init: function (app) {
|
|
this.app = app;
|
|
this.lang = app.lang;
|
|
this.opts = app.opts;
|
|
this.caret = app.caret;
|
|
this.editor = app.editor;
|
|
this.toolbar = app.toolbar;
|
|
this.component = app.component;
|
|
this.inspector = app.inspector;
|
|
this.insertion = app.insertion;
|
|
this.selection = app.selection;
|
|
},
|
|
// messages
|
|
ondropdown: {
|
|
table: {
|
|
observe: function (dropdown) {
|
|
this._observeDropdown(dropdown);
|
|
}
|
|
}
|
|
},
|
|
onbottomclick: function () {
|
|
this.insertion.insertToEnd(this.editor.getLastNode(), 'table');
|
|
},
|
|
// public
|
|
start: function () {
|
|
var dropdown = {
|
|
observe: 'table',
|
|
'insert-table': {
|
|
title: this.lang.get('insert-table'),
|
|
api: 'plugin.table.insert'
|
|
},
|
|
'insert-row-above': {
|
|
title: this.lang.get('insert-row-above'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.addRowAbove'
|
|
},
|
|
'insert-row-below': {
|
|
title: this.lang.get('insert-row-below'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.addRowBelow'
|
|
},
|
|
'insert-column-left': {
|
|
title: this.lang.get('insert-column-left'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.addColumnLeft'
|
|
},
|
|
'insert-column-right': {
|
|
title: this.lang.get('insert-column-right'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.addColumnRight'
|
|
},
|
|
'add-head': {
|
|
title: this.lang.get('add-head'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.addHead'
|
|
},
|
|
'delete-head': {
|
|
title: this.lang.get('delete-head'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.deleteHead'
|
|
},
|
|
'delete-column': {
|
|
title: this.lang.get('delete-column'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.deleteColumn'
|
|
},
|
|
'delete-row': {
|
|
title: this.lang.get('delete-row'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.deleteRow'
|
|
},
|
|
'delete-table': {
|
|
title: this.lang.get('delete-table'),
|
|
classname: 'redactor-table-item-observable',
|
|
api: 'plugin.table.deleteTable'
|
|
}
|
|
};
|
|
var obj = {
|
|
title: this.lang.get('table')
|
|
};
|
|
var $button = this.toolbar.addButtonBefore('link', 'table', obj);
|
|
$button.setIcon('<i class="re-icon-table"></i>');
|
|
$button.setDropdown(dropdown);
|
|
},
|
|
insert: function () {
|
|
var rows = 2;
|
|
var columns = 3;
|
|
var $component = this.component.create('table');
|
|
for (var i = 0; i < rows; i++) {
|
|
$component.addRow(columns);
|
|
}
|
|
$component = this.insertion.insertHtml($component);
|
|
this.caret.setStart($component);
|
|
},
|
|
addRowAbove: function () {
|
|
var $component = this._getComponent();
|
|
if ($component) {
|
|
var current = this.selection.getCurrent();
|
|
var $row = $component.addRowTo(current, 'before');
|
|
this.caret.setStart($row);
|
|
}
|
|
},
|
|
addRowBelow: function () {
|
|
var $component = this._getComponent();
|
|
if ($component) {
|
|
var current = this.selection.getCurrent();
|
|
var $row = $component.addRowTo(current, 'after');
|
|
this.caret.setStart($row);
|
|
}
|
|
},
|
|
addColumnLeft: function () {
|
|
var $component = this._getComponent();
|
|
if ($component) {
|
|
var current = this.selection.getCurrent();
|
|
this.selection.save();
|
|
$component.addColumnTo(current, 'left');
|
|
this.selection.restore();
|
|
}
|
|
},
|
|
addColumnRight: function () {
|
|
var $component = this._getComponent();
|
|
if ($component) {
|
|
var current = this.selection.getCurrent();
|
|
this.selection.save();
|
|
$component.addColumnTo(current, 'right');
|
|
this.selection.restore();
|
|
}
|
|
},
|
|
addHead: function () {
|
|
var $component = this._getComponent();
|
|
if ($component) {
|
|
this.selection.save();
|
|
$component.addHead();
|
|
this.selection.restore();
|
|
}
|
|
},
|
|
deleteHead: function () {
|
|
var $component = this._getComponent();
|
|
if ($component) {
|
|
var current = this.selection.getCurrent();
|
|
var $head = $R.dom(current)
|
|
.closest('thead');
|
|
if ($head.length !== 0) {
|
|
$component.removeHead();
|
|
this.caret.setStart($component);
|
|
} else {
|
|
this.selection.save();
|
|
$component.removeHead();
|
|
this.selection.restore();
|
|
}
|
|
}
|
|
},
|
|
deleteColumn: function () {
|
|
var $component = this._getComponent();
|
|
if ($component) {
|
|
var current = this.selection.getCurrent();
|
|
var $currentCell = $R.dom(current)
|
|
.closest('td, th');
|
|
var nextCell = $currentCell.nextElement()
|
|
.get();
|
|
var prevCell = $currentCell.prevElement()
|
|
.get();
|
|
$component.removeColumn(current);
|
|
if (nextCell) this.caret.setStart(nextCell);
|
|
else if (prevCell) this.caret.setEnd(prevCell);
|
|
else this.deleteTable();
|
|
}
|
|
},
|
|
deleteRow: function () {
|
|
var $component = this._getComponent();
|
|
if ($component) {
|
|
var current = this.selection.getCurrent();
|
|
var $currentRow = $R.dom(current)
|
|
.closest('tr');
|
|
var nextRow = $currentRow.nextElement()
|
|
.get();
|
|
var prevRow = $currentRow.prevElement()
|
|
.get();
|
|
var $head = $R.dom(current).closest('thead');
|
|
$component.removeRow(current);
|
|
if (nextRow) this.caret.setStart(nextRow);
|
|
else if (prevRow) this.caret.setEnd(prevRow);
|
|
else if ($head.length !== 0) {
|
|
$component.removeHead();
|
|
this.caret.setStart($component);
|
|
}
|
|
else this.deleteTable();
|
|
}
|
|
},
|
|
deleteTable: function () {
|
|
var table = this._getTable();
|
|
if (table) {
|
|
this.component.remove(table);
|
|
}
|
|
},
|
|
// private
|
|
_getTable: function () {
|
|
var current = this.selection.getCurrent();
|
|
var data = this.inspector.parse(current);
|
|
if (data.isTable()) {
|
|
return data.getTable();
|
|
}
|
|
},
|
|
_getComponent: function () {
|
|
var current = this.selection.getCurrent();
|
|
var data = this.inspector.parse(current);
|
|
if (data.isTable()) {
|
|
var table = data.getTable();
|
|
return this.component.create('table', table);
|
|
}
|
|
},
|
|
_observeDropdown: function (dropdown) {
|
|
var table = this._getTable();
|
|
var items = dropdown.getItemsByClass('redactor-table-item-observable');
|
|
var tableItem = dropdown.getItem('insert-table');
|
|
if (table) {
|
|
this._observeItems(items, 'enable');
|
|
tableItem.disable();
|
|
} else {
|
|
this._observeItems(items, 'disable');
|
|
tableItem.enable();
|
|
}
|
|
},
|
|
_observeItems: function (items, type) {
|
|
for (var i = 0; i < items.length; i++) {
|
|
items[i][type]();
|
|
}
|
|
}
|
|
});
|
|
|
|
$R.add('class', 'table.component', {
|
|
mixins: ['dom', 'component'],
|
|
init: function (app, el) {
|
|
this.app = app;
|
|
// init
|
|
return (el && el.cmnt !== undefined) ? el : this._init(el);
|
|
},
|
|
// public
|
|
addHead: function () {
|
|
this.removeHead();
|
|
var columns = this.$element.find('tr')
|
|
.first()
|
|
.children('td, th')
|
|
.length;
|
|
var $head = $R.dom('<thead>');
|
|
var $row = this._buildRow(columns, '<th>');
|
|
$head.append($row);
|
|
this.$element.prepend($head);
|
|
},
|
|
addRow: function (columns) {
|
|
var $row = this._buildRow(columns);
|
|
this.$element.append($row);
|
|
return $row;
|
|
},
|
|
addRowTo: function (current, type) {
|
|
return this._addRowTo(current, type);
|
|
},
|
|
addColumnTo: function (current, type) {
|
|
var $current = $R.dom(current);
|
|
var $currentRow = $current.closest('tr');
|
|
var $currentCell = $current.closest('td, th');
|
|
var index = 0;
|
|
$currentRow.find('td, th')
|
|
.each(function (node, i) {
|
|
if (node === $currentCell.get()) index = i;
|
|
});
|
|
this.$element.find('tr')
|
|
.each(function (node) {
|
|
var $node = $R.dom(node);
|
|
var origCell = $node.find('td, th')
|
|
.get(index);
|
|
var $origCell = $R.dom(origCell);
|
|
var $td = $origCell.clone();
|
|
$td.html('<div data-redactor-tag="tbr"></div>');
|
|
if (type === 'right') $origCell.after($td);
|
|
else $origCell.before($td);
|
|
});
|
|
},
|
|
removeHead: function () {
|
|
var $head = this.$element.find('thead');
|
|
if ($head.length !== 0) $head.remove();
|
|
},
|
|
removeRow: function (current) {
|
|
var $current = $R.dom(current);
|
|
var $currentRow = $current.closest('tr');
|
|
$currentRow.remove();
|
|
},
|
|
removeColumn: function (current) {
|
|
var $current = $R.dom(current);
|
|
var $currentRow = $current.closest('tr');
|
|
var $currentCell = $current.closest('td, th');
|
|
var index = 0;
|
|
$currentRow.find('td, th')
|
|
.each(function (node, i) {
|
|
if (node === $currentCell.get()) index = i;
|
|
});
|
|
this.$element.find('tr')
|
|
.each(function (node) {
|
|
var $node = $R.dom(node);
|
|
var origCell = $node.find('td, th')
|
|
.get(index);
|
|
var $origCell = $R.dom(origCell);
|
|
$origCell.remove();
|
|
});
|
|
},
|
|
// private
|
|
_init: function (el) {
|
|
var wrapper, element;
|
|
if (typeof el !== 'undefined') {
|
|
var $node = $R.dom(el);
|
|
var node = $node.get();
|
|
var $figure = $node.closest('figure');
|
|
if ($figure.length !== 0) {
|
|
wrapper = $figure;
|
|
element = $figure.find('table')
|
|
.get();
|
|
} else if (node.tagName === 'TABLE') {
|
|
element = node;
|
|
}
|
|
}
|
|
this._buildWrapper(wrapper);
|
|
this._buildElement(element);
|
|
this._initWrapper();
|
|
},
|
|
_addRowTo: function (current, position) {
|
|
var $current = $R.dom(current);
|
|
var $currentRow = $current.closest('tr');
|
|
if ($currentRow.length !== 0) {
|
|
var columns = $currentRow.children('td, th')
|
|
.length;
|
|
var $newRow = this._buildRow(columns);
|
|
$currentRow[position]($newRow);
|
|
return $newRow;
|
|
}
|
|
},
|
|
_buildRow: function (columns, tag) {
|
|
tag = tag || '<td>';
|
|
var $row = $R.dom('<tr>');
|
|
for (var i = 0; i < columns; i++) {
|
|
var $cell = $R.dom(tag);
|
|
$cell.attr('contenteditable', true);
|
|
$cell.html('<div data-redactor-tag="tbr"></div>');
|
|
$row.append($cell);
|
|
}
|
|
return $row;
|
|
},
|
|
_buildElement: function (node) {
|
|
if (node) {
|
|
this.$element = $R.dom(node);
|
|
} else {
|
|
this.$element = $R.dom('<table>');
|
|
this.append(this.$element);
|
|
}
|
|
},
|
|
_buildWrapper: function (node) {
|
|
node = node || '<figure>';
|
|
this.parse(node);
|
|
},
|
|
_initWrapper: function () {
|
|
this.addClass('redactor-component');
|
|
this.attr({
|
|
'data-redactor-type': 'table',
|
|
'tabindex': '-1',
|
|
'contenteditable': false
|
|
});
|
|
if (this.app.detector.isIe()) {
|
|
this.removeAttr('contenteditable');
|
|
}
|
|
}
|
|
});
|
|
|
|
$R.add('plugin', 'imagemanager', {
|
|
translations: {
|
|
en: {
|
|
"choose": "Choose"
|
|
}
|
|
},
|
|
init: function (app) {
|
|
this.app = app;
|
|
this.lang = app.lang;
|
|
this.opts = app.opts;
|
|
},
|
|
// messages
|
|
onmodal: {
|
|
image: {
|
|
open: function ($modal, $form) {
|
|
if (!this.opts.imageManagerJson) return;
|
|
this._load($modal)
|
|
}
|
|
}
|
|
},
|
|
// private
|
|
_load: function ($modal) {
|
|
var $body = $modal.getBody();
|
|
this.$box = $R.dom('<div>');
|
|
this.$box.attr('data-title', this.lang.get('choose'));
|
|
this.$box.addClass('redactor-modal-tab');
|
|
this.$box.hide();
|
|
this.$box.css({
|
|
overflow: 'auto',
|
|
height: '300px',
|
|
'line-height': 1
|
|
});
|
|
$body.append(this.$box);
|
|
$R.ajax.get({
|
|
url: this.opts.imageManagerJson,
|
|
success: this._parse.bind(this)
|
|
});
|
|
},
|
|
_parse: function (data) {
|
|
for (var key in data) {
|
|
var obj = data[key];
|
|
if (typeof obj !== 'object') continue;
|
|
var $img = $R.dom('<img>');
|
|
var url = (obj.thumb) ? obj.thumb : obj.url;
|
|
$img.attr('src', url);
|
|
$img.attr('data-params', encodeURI(JSON.stringify(obj)));
|
|
$img.css({
|
|
width: '96px',
|
|
height: '72px',
|
|
margin: '0 4px 2px 0',
|
|
cursor: 'pointer'
|
|
});
|
|
$img.on('click', this._insert.bind(this));
|
|
this.$box.append($img);
|
|
}
|
|
},
|
|
_insert: function (e) {
|
|
e.preventDefault();
|
|
var $el = $R.dom(e.target);
|
|
var data = JSON.parse(decodeURI($el.attr('data-params')));
|
|
this.app.api('module.image.insert', {
|
|
image: data
|
|
});
|
|
}
|
|
});
|
|
|
|
$R.add('plugin', 'fullscreen', {
|
|
translations: {
|
|
en: {
|
|
"fullscreen": "Fullscreen"
|
|
}
|
|
},
|
|
init: function (app) {
|
|
this.app = app;
|
|
this.opts = app.opts;
|
|
this.lang = app.lang;
|
|
this.$win = app.$win;
|
|
this.$doc = app.$doc;
|
|
this.$body = app.$body;
|
|
this.editor = app.editor;
|
|
this.toolbar = app.toolbar;
|
|
this.container = app.container;
|
|
this.selection = app.selection;
|
|
// local
|
|
this.isOpen = false;
|
|
this.docScroll = 0;
|
|
},
|
|
// public
|
|
start: function () {
|
|
var data = {
|
|
title: this.lang.get('fullscreen'),
|
|
api: 'plugin.fullscreen.toggle'
|
|
};
|
|
var button = this.toolbar.addButtonAuto('fullscreen', data);
|
|
button.setIcon('<i class="re-icon-expand"></i>');
|
|
this.$target = (this.toolbar.isTarget()) ? this.toolbar.getTargetElement() : this.$body;
|
|
if (this.opts.fullscreen) this.toggle();
|
|
},
|
|
toggle: function () {
|
|
return (this.isOpen) ? this.close() : this.open();
|
|
},
|
|
open: function () {
|
|
this.docScroll = this.$doc.scrollTop();
|
|
this._createPlacemarker();
|
|
this.selection.save();
|
|
var $container = this.container.getElement();
|
|
var $editor = this.editor.getElement();
|
|
var $html = (this.toolbar.isTarget()) ? $R.dom('body, html') : this.$target;
|
|
if (this.opts.toolbarExternal) this._buildInternalToolbar();
|
|
this.$target.prepend($container);
|
|
this.$target.addClass('redactor-body-fullscreen');
|
|
$container.addClass('redactor-box-fullscreen');
|
|
if (this.isTarget) $container.addClass('redactor-box-fullscreen-target');
|
|
$html.css('overflow', 'hidden');
|
|
if (this.opts.maxHeight) $editor.css('max-height', '');
|
|
if (this.opts.minHeight) $editor.css('min-height', '');
|
|
this._resize();
|
|
this.$win.on('resize.redactor-plugin-fullscreen', this._resize.bind(this));
|
|
this.$doc.scrollTop(0);
|
|
var button = this.toolbar.getButton('fullscreen');
|
|
button.setIcon('<i class="re-icon-retract"></i>');
|
|
this.selection.restore();
|
|
this.isOpen = true;
|
|
this.opts.zindex = 1051;
|
|
// fix bootstrap modal focus
|
|
if (window.jQuery) jQuery(document).off('focusin.modal');
|
|
},
|
|
close: function () {
|
|
this.isOpen = false;
|
|
this.opts.zindex = false;
|
|
this.selection.save();
|
|
var $container = this.container.getElement();
|
|
var $editor = this.editor.getElement();
|
|
var $html = $R.dom('body, html');
|
|
if (this.opts.toolbarExternal) this._buildExternalToolbar();
|
|
this.$target.removeClass('redactor-body-fullscreen');
|
|
this.$win.off('resize.redactor-plugin-fullscreen');
|
|
$html.css('overflow', '');
|
|
$container.removeClass('redactor-box-fullscreen redactor-box-fullscreen-target');
|
|
$editor.css('height', 'auto');
|
|
if (this.opts.minHeight) $editor.css('minHeight', this.opts.minHeight);
|
|
if (this.opts.maxHeight) $editor.css('maxHeight', this.opts.maxHeight);
|
|
var button = this.toolbar.getButton('fullscreen');
|
|
button.setIcon('<i class="re-icon-expand"></i>');
|
|
this._removePlacemarker($container);
|
|
this.selection.restore();
|
|
this.$doc.scrollTop(this.docScroll);
|
|
},
|
|
// private
|
|
_resize: function () {
|
|
var $toolbar = this.toolbar.getElement();
|
|
var $editor = this.editor.getElement();
|
|
var height = this.$win.height() - $toolbar.height();
|
|
$editor.height(height);
|
|
},
|
|
_buildInternalToolbar: function () {
|
|
var $wrapper = this.toolbar.getWrapper();
|
|
var $toolbar = this.toolbar.getElement();
|
|
$wrapper.addClass('redactor-toolbar-wrapper');
|
|
$wrapper.append($toolbar);
|
|
$toolbar.removeClass('redactor-toolbar-external');
|
|
$container.prepend($wrapper);
|
|
},
|
|
_buildExternalToolbar: function () {
|
|
var $wrapper = this.toolbar.getWrapper();
|
|
var $toolbar = this.toolbar.getElement();
|
|
this.$external = $R.dom(this.opts.toolbarExternal);
|
|
$toolbar.addClass('redactor-toolbar-external');
|
|
this.$external.append($toolbar);
|
|
$wrapper.remove();
|
|
},
|
|
_createPlacemarker: function () {
|
|
var $container = this.container.getElement();
|
|
this.$placemarker = $R.dom('<span />');
|
|
$container.after(this.$placemarker);
|
|
},
|
|
_removePlacemarker: function ($container) {
|
|
this.$placemarker.before($container);
|
|
this.$placemarker.remove();
|
|
}
|
|
});
|
|
|
|
$R.add('plugin', 'video', {
|
|
translations: {
|
|
en: {
|
|
"video": "Video",
|
|
"video-html-code": "Video Embed Code or Youtube/Vimeo Link"
|
|
}
|
|
},
|
|
modals: {
|
|
'video':
|
|
'<form action=""> \
|
|
<div class="form-item"> \
|
|
<label for="modal-video-input">## video-html-code ##</label> \
|
|
<textarea id="modal-video-input" name="video" style="height: 160px;"></textarea> \
|
|
</div> \
|
|
</form>'
|
|
},
|
|
init: function (app) {
|
|
this.app = app;
|
|
this.lang = app.lang;
|
|
this.opts = app.opts;
|
|
this.toolbar = app.toolbar;
|
|
this.component = app.component;
|
|
this.insertion = app.insertion;
|
|
this.inspector = app.inspector;
|
|
this.selection = app.selection;
|
|
},
|
|
// messages
|
|
onmodal: {
|
|
video: {
|
|
opened: function ($modal, $form) {
|
|
$video = $form.getField('video');
|
|
$video.focus();
|
|
},
|
|
insert: function ($modal, $form) {
|
|
var data = $form.getData();
|
|
this._insert(data);
|
|
}
|
|
}
|
|
},
|
|
oncontextbar: function (e, contextbar) {
|
|
var data = this.inspector.parse(e.target)
|
|
if (data.isComponentType('video')) {
|
|
var node = data.getComponent();
|
|
var buttons = {
|
|
"remove": {
|
|
title: this.lang.get('delete'),
|
|
api: 'plugin.video.remove',
|
|
args: node
|
|
}
|
|
};
|
|
contextbar.set(e, node, buttons, 'bottom');
|
|
}
|
|
},
|
|
// public
|
|
start: function () {
|
|
var obj = {
|
|
title: this.lang.get('video'),
|
|
api: 'plugin.video.open'
|
|
};
|
|
var $button = this.toolbar.addButtonAfter('image', 'video', obj);
|
|
$button.setIcon('<i class="re-icon-video"></i>');
|
|
},
|
|
open: function () {
|
|
var options = {
|
|
title: this.lang.get('video'),
|
|
width: '600px',
|
|
name: 'video',
|
|
handle: 'insert',
|
|
commands: {
|
|
insert: {
|
|
title: this.lang.get('insert')
|
|
},
|
|
cancel: {
|
|
title: this.lang.get('cancel')
|
|
}
|
|
}
|
|
};
|
|
this.app.api('module.modal.build', options);
|
|
},
|
|
remove: function (node) {
|
|
this.component.remove(node);
|
|
},
|
|
// private
|
|
_insert: function (data) {
|
|
this.app.api('module.modal.close');
|
|
if (data.video.trim() === '') {
|
|
return;
|
|
}
|
|
// parsing
|
|
data.video = this._matchData(data.video);
|
|
// inserting
|
|
if (this._isVideoIframe(data.video)) {
|
|
var $video = this.component.create('video', data.video);
|
|
this.insertion.insertHtml($video);
|
|
}
|
|
},
|
|
_isVideoIframe: function (data) {
|
|
return (data.match(/<iframe|<video/gi) !== null);
|
|
},
|
|
_matchData: function (data) {
|
|
var iframeStart = '<iframe style="width: 500px; height: 281px;" src="';
|
|
var iframeEnd = '" frameborder="0" allowfullscreen></iframe>';
|
|
if (this._isVideoIframe(data)) {
|
|
var allowed = ['iframe', 'video', 'source'];
|
|
var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
|
|
data = data.replace(/<p(.*?[^>]?)>([\w\W]*?)<\/p>/gi, '');
|
|
data = data.replace(tags, function ($0, $1) {
|
|
return (allowed.indexOf($1.toLowerCase()) === -1) ? '' : $0;
|
|
});
|
|
} else {
|
|
if (data.match(this.opts.regex.youtube)) {
|
|
data = data.replace(this.opts.regex.youtube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd);
|
|
} else if (data.match(this.opts.regex.vimeo)) {
|
|
data = data.replace(this.opts.regex.vimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd);
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
});
|
|
$R.add('class', 'video.component', {
|
|
mixins: ['dom', 'component'],
|
|
init: function (app, el) {
|
|
this.app = app;
|
|
// init
|
|
return (el && el.cmnt !== undefined) ? el : this._init(el);
|
|
},
|
|
// private
|
|
_init: function (el) {
|
|
if (typeof el !== 'undefined') {
|
|
var $node = $R.dom(el);
|
|
var $wrapper = $node.closest('figure');
|
|
if ($wrapper.length !== 0) {
|
|
this.parse($wrapper);
|
|
} else {
|
|
this.parse('<figure>');
|
|
this.append(el);
|
|
}
|
|
} else {
|
|
this.parse('<figure>');
|
|
}
|
|
this._initWrapper();
|
|
},
|
|
_initWrapper: function () {
|
|
this.addClass('redactor-component');
|
|
this.attr({
|
|
'data-redactor-type': 'video',
|
|
'tabindex': '-1',
|
|
'contenteditable': false
|
|
});
|
|
}
|
|
});
|
|
$R.add('plugin', 'textdirection', {
|
|
translations: {
|
|
en: {
|
|
"change-text-direction": "RTL-LTR",
|
|
"left-to-right": "Left to Right",
|
|
"right-to-left": "Right to Left"
|
|
}
|
|
},
|
|
init: function(app) {
|
|
this.app = app;
|
|
this.lang = app.lang;
|
|
this.block = app.block;
|
|
this.editor = app.editor;
|
|
this.toolbar = app.toolbar;
|
|
this.selection = app.selection;
|
|
},
|
|
// public
|
|
start: function()
|
|
{
|
|
var dropdown = {};
|
|
|
|
dropdown.ltr = { title: this.lang.get('left-to-right'), api: 'plugin.textdirection.set', args: 'ltr' };
|
|
dropdown.rtl = { title: this.lang.get('right-to-left'), api: 'plugin.textdirection.set', args: 'rtl' };
|
|
|
|
var $button = this.toolbar.addButton('textdirection', { title: this.lang.get('change-text-direction') });
|
|
$button.setIcon('<i class="re-icon-textdirection"></i>');
|
|
$button.setDropdown(dropdown);
|
|
},
|
|
set: function(type) {
|
|
var block = this.selection.getBlock();
|
|
if (block && block.tagName === 'LI') {
|
|
var list = $R.dom(block).parents('ul, ol', this.editor.getElement()).last();
|
|
this.block.add({ attr: { dir: type }}, false, list);
|
|
}
|
|
else {
|
|
this.block.add({ attr: { dir: type }});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Monkey patch context bar to have ability to add a button
|
|
var contextbar = $R[$R.env['module']]['contextbar'];
|
|
$R.add('module', 'contextbar', $R.extend(contextbar.prototype, {
|
|
append: function(e, button) {
|
|
var $btn = $R.create('contextbar.button', this.app, button);
|
|
if ($btn.html() !== '')
|
|
{
|
|
this.$contextbar.append($btn);
|
|
}
|
|
var pos = this._buildPosition(e, this.$el);
|
|
this.$contextbar.css(pos);
|
|
}
|
|
}));
|
|
|
|
$R.add('plugin', 'imageannotate', {
|
|
init: function(app) {
|
|
this.app = app;
|
|
this.lang = app.lang;
|
|
this.colors = [
|
|
'#ffffff', '#888888', '#000000', 'fuchsia', 'blue', 'red',
|
|
'lime', 'blueviolet', 'cyan', '#f4a63b', 'yellow']
|
|
.concat(app.instances.fontcolor.colors.slice(11));
|
|
this.loadedFabric = false;
|
|
},
|
|
modals: {
|
|
'annotate':
|
|
'<div class="toolbar" style="margin-top:-24px;margin-bottom:10px"></div>'
|
|
+'<div class="canvas" style="position:relative"></div>',
|
|
},
|
|
onmodal: {
|
|
annotate: {
|
|
open: function($modal, $form) {
|
|
this._build($modal);
|
|
},
|
|
commit: function ($modal, $form) {
|
|
this.commit();
|
|
}
|
|
},
|
|
},
|
|
oncontextbar: function(e, contextbar) {
|
|
var current = this.app.selection.getCurrent();
|
|
var data = this.app.inspector.parse(current);
|
|
|
|
if (!data.isFigcaption() && data.isComponentType('image')) {
|
|
contextbar.append(e, {
|
|
title: __('Annotate'),
|
|
api: 'plugin.imageannotate.startAnnotate',
|
|
args: [data.getComponent()]
|
|
});
|
|
}
|
|
},
|
|
_build: function($modal) {
|
|
var $body = $modal.getBody();
|
|
this._buildToolbar($body.find('.toolbar'));
|
|
|
|
this.$image = $R.dom(this.image).find('img');
|
|
canvas = this.initCanvas(this.$image, $body);
|
|
},
|
|
startAnnotate: function(img) {
|
|
this.image = img;
|
|
var that=this;
|
|
if (!this.loadedFabric) {
|
|
getConfig().then(function(c) {
|
|
$.getScript(c.path + 'js/fabric.min.js', function() {
|
|
that.loadedFabric = true;
|
|
});
|
|
});
|
|
}
|
|
var options = {
|
|
title: __('Annotate Image'),
|
|
width: '850px',
|
|
name: 'annotate',
|
|
handle: 'commit',
|
|
commands: {
|
|
commit: {
|
|
title: __('Commit')
|
|
},
|
|
cancel: {
|
|
title: this.lang.get('cancel')
|
|
}
|
|
}
|
|
};
|
|
|
|
// Await loading of Fabric framework
|
|
var that = this, I = setInterval(function() {
|
|
if (that.loadedFabric) {
|
|
clearInterval(I);
|
|
that.app.api('module.modal.build', options);
|
|
}
|
|
}, 25);
|
|
},
|
|
teardownAnnotate: function() {
|
|
var state = this.canvas.toObject(), places = 2;
|
|
// Capture current annotations
|
|
delete state.backgroundImage;
|
|
this.$image.attr('data-annotations',
|
|
btoa(JSON.stringify(state, function(key, value) {
|
|
// limit precision of floats
|
|
if (typeof value === 'number') {
|
|
return parseFloat(value.toFixed(places));
|
|
}
|
|
return value;
|
|
}))
|
|
);
|
|
this.app.api('module.modal.close');
|
|
},
|
|
_buildToolbar: function($body) {
|
|
var T = this.toolbar = $R.create('toolbar', this.app);
|
|
T.getWrapper().addClass('redactor-toolbar-wrapper');
|
|
T.getElement().addClass('redactor-toolbar');
|
|
$body.append(T.getWrapper());
|
|
|
|
var $button = T.addButton('drawshape', {
|
|
title: __('Add Shape'),
|
|
dropdown: {
|
|
arrow: {
|
|
title: '<span><i class="icon-long-arrow-right icon-fixed-width"></i> {}</span>'
|
|
.replace('{}', __('Add Arrow')),
|
|
api: 'plugin.imageannotate.drawArrow'
|
|
},
|
|
box: {
|
|
title: '<span><i class="icon-check-empty icon-fixed-width"></i> {}</span>'
|
|
.replace('{}', __('Add Box')),
|
|
api: 'plugin.imageannotate.drawBox'
|
|
},
|
|
ellipse: {
|
|
title: '<span><i class="icon-circle-blank icon-fixed-width"></i> {}</span>'
|
|
.replace('{}', __('Add Ellipse')),
|
|
api: 'plugin.imageannotate.drawEllipse'
|
|
},
|
|
text: {
|
|
title: '<span><i class="icon-font icon-fixed-width"></i> {}</span>'
|
|
.replace('{}', __('Add Text')),
|
|
api: 'plugin.imageannotate.drawText'
|
|
},
|
|
scribble: {
|
|
title: '<span><i class="icon-pencil icon-fixed-width"></i> {}</span>'
|
|
.replace('{}', __('Add Scribble')),
|
|
api: 'plugin.imageannotate.drawFree'
|
|
},
|
|
},
|
|
icon: '<i class="icon-plus"></i>',
|
|
}, 'first');
|
|
|
|
T.addButton('sel_color', {
|
|
title: __('Shape Color'),
|
|
icon: '<i class="icon-tint"></i>',
|
|
dropdown: this._buildDropdown(),
|
|
});
|
|
|
|
var dropdown = {}, sizes = [15,20,25,30,40,50];
|
|
sizes.forEach(function(i) {
|
|
var text = '' + i + 'px';
|
|
dropdown['size' + i] = {
|
|
title: $R.dom('<span>').css('font-size', '' + i + 'px').text(text).get().outerHTML,
|
|
api: 'plugin.imageannotate.setFontSize',
|
|
args: i,
|
|
};
|
|
});
|
|
var $button = T.addButton('sel_textsize', {
|
|
title: __('Font Size'),
|
|
icon: '<i class="icon-text-height"></i>',
|
|
dropdown: dropdown,
|
|
});
|
|
|
|
var dropdown = {};
|
|
var fonts = ['Arial', 'Times New Roman', 'Monospace', 'Fantasy', 'Cursive'];
|
|
fonts.forEach(function(i) {
|
|
dropdown[i] = {
|
|
title: $R.dom('<span>').css('font-family', i).text(i).get().outerHTML,
|
|
api: 'plugin.imageannotate.setFontFamily',
|
|
args: i,
|
|
};
|
|
});
|
|
$button = T.addButton('sel_fontfamily', {
|
|
title: __('Font Family'),
|
|
icon: '<i class="icon-font"></i>',
|
|
dropdown: dropdown,
|
|
});
|
|
|
|
var $button = T.addButton('sel_strokewidth', {
|
|
title: __('Outline'),
|
|
icon: '<i class="icon-check-empty"></i>',
|
|
});
|
|
var dropdown = {}, sizes = [0,2.5,5,7.5,10,15];
|
|
sizes.forEach(function(i) {
|
|
var text = '' + i + 'px';
|
|
dropdown['size' + i] = {
|
|
title: $R.dom('<span>').css('border-left', '' + i + 'px solid black')
|
|
.append($R.dom('<span>').text(text))
|
|
.get().outerHTML,
|
|
api: 'plugin.imageannotate.setStrokeWidth',
|
|
args: i,
|
|
};
|
|
});
|
|
$button.setDropdown(dropdown);
|
|
|
|
T.addButton('sel_layerup', {
|
|
title: __('Bring Forward'),
|
|
icon: '<i class="icon-double-angle-up"></i>',
|
|
api: 'plugin.imageannotate.bringForward',
|
|
});
|
|
T.addButton('sel_opacity', {
|
|
title: __('Toggle Opacity'),
|
|
icon: '<i class="icon-eye-close"></i>',
|
|
api: 'plugin.imageannotate.setOpacity',
|
|
});
|
|
|
|
T.addButton('sel_trash', {
|
|
title: __('Delete Shape'),
|
|
icon: '<i class="icon-trash"></i>',
|
|
api: 'plugin.imageannotate.discard',
|
|
});
|
|
|
|
this.disableContextualButtons();
|
|
},
|
|
|
|
updateSelection: function() {
|
|
if (this.canvas.getActiveObjects().length > 0)
|
|
this.enableContextualButtons();
|
|
else
|
|
this.disableContextualButtons();
|
|
},
|
|
|
|
enableContextualButtons: function() {
|
|
this.toolbar.getButtons().forEach(function(b) {
|
|
if (b.name.indexOf('sel_') == 0)
|
|
b.enable();
|
|
});
|
|
},
|
|
|
|
disableContextualButtons: function() {
|
|
this.toolbar.getButtons().forEach(function(b) {
|
|
if (b.name.indexOf('sel_') == 0)
|
|
b.disable();
|
|
});
|
|
},
|
|
|
|
_setColor: function(attr, color) {
|
|
$.each(this.canvas.getActiveObjects(), function() {
|
|
this.set(attr, color);
|
|
});
|
|
this.canvas.renderAll();
|
|
},
|
|
|
|
_removeColor: function(attr) {
|
|
$.each(this.canvas.getActiveObjects(), function() {
|
|
this.set(attr, 'rgba(0, 0, 0, 0)');
|
|
});
|
|
this.canvas.renderAll();
|
|
},
|
|
|
|
// Color picker -- copied from `fontcolor` plugin
|
|
_buildDropdown: function () {
|
|
var $dropdown = $R.dom('<div class="redactor-dropdown-cells">');
|
|
this.$selector = this._buildSelector();
|
|
this.$selectorFill = this._buildSelectorItem('fill', __('Fill'));
|
|
this.$selectorFill.addClass('active');
|
|
this.$selectorStroke = this._buildSelectorItem('stroke', __('Outline'));
|
|
this.$selector.append(this.$selectorFill);
|
|
this.$selector.append(this.$selectorStroke);
|
|
this.$pickerFill = this._buildPicker('fill');
|
|
this.$pickerStroke = this._buildPicker('stroke');
|
|
$dropdown.append(this.$selector);
|
|
$dropdown.append(this.$pickerFill);
|
|
$dropdown.append(this.$pickerStroke);
|
|
this._buildSelectorEvents();
|
|
$dropdown.width(242);
|
|
return $dropdown;
|
|
},
|
|
_buildSelector: function () {
|
|
var $selector = $R.dom('<div>');
|
|
$selector.addClass('redactor-dropdown-selector');
|
|
return $selector;
|
|
},
|
|
_buildSelectorItem: function (name, title) {
|
|
var $item = $R.dom('<span>');
|
|
$item.attr('rel', name)
|
|
.html(title);
|
|
$item.addClass('redactor-dropdown-not-close');
|
|
return $item;
|
|
},
|
|
_buildSelectorEvents: function () {
|
|
this.$selectorFill.on('mousedown', function (e) {
|
|
e.preventDefault();
|
|
this.$selector.find('span')
|
|
.removeClass('active');
|
|
this.$pickerStroke.hide();
|
|
this.$pickerFill.show();
|
|
this.$selectorFill.addClass('active');
|
|
}.bind(this));
|
|
this.$selectorStroke.on('mousedown', function (e) {
|
|
e.preventDefault();
|
|
this.$selector.find('span')
|
|
.removeClass('active');
|
|
this.$pickerFill.hide();
|
|
this.$pickerStroke.show();
|
|
this.$selectorStroke.addClass('active');
|
|
}.bind(this));
|
|
},
|
|
_buildPicker: function (name) {
|
|
var $box = $R.dom('<div class="re-dropdown-box-' + name + '">');
|
|
var len = this.colors.length;
|
|
var self = this;
|
|
var func = function (e) {
|
|
e.preventDefault();
|
|
var $el = $R.dom(e.target);
|
|
self._setColor($el.data('rule'), $el.attr('rel'));
|
|
};
|
|
for (var z = 0; z < len; z++) {
|
|
var color = this.colors[z];
|
|
var $swatch = $R.dom('<span>');
|
|
$swatch.attr({
|
|
'rel': color,
|
|
'data-rule': name
|
|
});
|
|
$swatch.css({
|
|
'background-color': color,
|
|
'font-size': 0,
|
|
'border': '2px solid #fff',
|
|
'width': '22px',
|
|
'height': '22px'
|
|
});
|
|
$swatch.on('mousedown', func);
|
|
$box.append($swatch);
|
|
}
|
|
var $el = $R.dom('<a>');
|
|
$el.attr({
|
|
'href': '#'
|
|
});
|
|
$el.css({
|
|
'display': 'block',
|
|
'clear': 'both',
|
|
'padding': '8px 5px',
|
|
'font-size': '12px',
|
|
'line-height': 1
|
|
});
|
|
$el.html(this.lang.get('none'));
|
|
$el.on('click', function (e) {
|
|
e.preventDefault();
|
|
self._removeColor(name);
|
|
});
|
|
$box.append($el);
|
|
if (name == 'stroke') $box.hide();
|
|
return $box;
|
|
},
|
|
|
|
// Shapes
|
|
drawShape: function(ondown, onmove, onup, cursor) {
|
|
// @see http://jsfiddle.net/URWru/
|
|
var fcanvas = this.canvas,
|
|
scale = this.scale,
|
|
isDown, shape,
|
|
that = this,
|
|
mousedown = function(o) {
|
|
isDown = true;
|
|
that.app.api('module.buffer.trigger');
|
|
var pointer = fcanvas.getPointer(o.e);
|
|
shape = ondown(pointer, o.e);
|
|
if (shape) fcanvas.add(shape);
|
|
},
|
|
mousemove = function(o) {
|
|
if (!isDown) return;
|
|
onmove(shape, fcanvas.getPointer(o.e), o.e);
|
|
fcanvas.requestRenderAll();
|
|
},
|
|
mouseup = function(o) {
|
|
isDown = false;
|
|
if (onup) {
|
|
if (shape2 = onup(shape, fcanvas.getPointer(o.e))) {
|
|
shape.remove();
|
|
fcanvas.add(shape2);
|
|
shape = shape2;
|
|
}
|
|
}
|
|
if (shape) shape.setCoords()
|
|
fcanvas.calcOffset()
|
|
.off('mouse:down', mousedown)
|
|
.off('mouse:up', mouseup)
|
|
.off('mouse:move', mousemove)
|
|
.discardActiveObject()
|
|
.renderAll();
|
|
|
|
if (shape) fcanvas.setActiveObject(shape);
|
|
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;
|
|
},
|
|
|
|
drawFree: function() {
|
|
var scale = this.scale, fcanvas = this.canvas;
|
|
fcanvas.isDrawingMode = true;
|
|
fcanvas.freeDrawingBrush = new fabric.PencilBrush(fcanvas)
|
|
fcanvas.freeDrawingBrush.width = 5 * scale;
|
|
fcanvas.freeDrawingBrush.color = 'red';
|
|
return this.drawShape(
|
|
function() {},
|
|
function() {},
|
|
function(shape, pointer, event) {
|
|
fcanvas.isDrawingMode = false;
|
|
}
|
|
);
|
|
},
|
|
|
|
drawArrow: function() {
|
|
var top, left, scale = this.scale
|
|
return this.drawShape(
|
|
function(pointer) {
|
|
top = pointer.y;
|
|
left = pointer.x;
|
|
return new fabric.Group([
|
|
new fabric.Line([0, 5 * scale, 0, 5 * scale], {
|
|
strokeWidth: 5 * scale,
|
|
fill: 'red',
|
|
stroke: 'red',
|
|
originX: 'center',
|
|
originY: 'center',
|
|
selectable: false,
|
|
hasBorders: false
|
|
}),
|
|
new fabric.Polygon([
|
|
{x: 20 * scale, y: 0},
|
|
{x: 0, y: -5 * scale},
|
|
{x: 0, y: 5 * scale}
|
|
], {
|
|
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-15*scale)+' 0 0 -a b a -b a 0 -a z'
|
|
.replace(/a/g, 3 * scale).replace(/b/g, 15 * scale),
|
|
{
|
|
angle: angle * 180 / Math.PI + (dx < 0 ? 180 : 0),
|
|
strokeWidth: 5 * scale,
|
|
fill: 'red',
|
|
stroke: 'red'
|
|
});
|
|
}
|
|
);
|
|
},
|
|
|
|
drawEllipse: function() {
|
|
var scale = this.scale
|
|
return this.drawShape(
|
|
function(pointer) {
|
|
return new fabric.Ellipse({
|
|
top: pointer.y,
|
|
left: pointer.x,
|
|
strokeWidth: 5 * scale,
|
|
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.strokeWidth / 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() {
|
|
var scale = this.scale
|
|
return this.drawShape(
|
|
function(pointer) {
|
|
return new fabric.Rect({
|
|
top: pointer.y,
|
|
left: pointer.x,
|
|
strokeWidth: 5 * scale,
|
|
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() {
|
|
var fcanvas = this.canvas, scale = this.scale;
|
|
return this.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 * scale
|
|
});
|
|
},
|
|
function(rect, pointer, event) {
|
|
rect.set({width: 75 * scale, height: 30 * scale,
|
|
originX: 'left', originY: 'top'});
|
|
},
|
|
function(shape) {
|
|
shape.on('editing:exited', function() {
|
|
if (!shape.get('text'))
|
|
fcanvas.remove(shape);
|
|
});
|
|
},
|
|
'text'
|
|
);
|
|
},
|
|
|
|
// Action buttons
|
|
_setFont: function(attr, value) {
|
|
var obj = {};
|
|
obj[attr] = value;
|
|
$.each(this.canvas.getActiveObjects(), function(element) {
|
|
if (this instanceof fabric.IText) {
|
|
if (this.getSelectedText()) {
|
|
this.setSelectionStyles(obj);
|
|
}
|
|
else {
|
|
this.set(attr, value);
|
|
}
|
|
}
|
|
});
|
|
this.canvas.renderAll();
|
|
},
|
|
|
|
setFontSize: function(pixels) {
|
|
var scale = this.scale;
|
|
this._setFont('fontSize', pixels * scale);
|
|
},
|
|
|
|
setFontFamily: function(name) {
|
|
this._setFont('fontFamily', name);
|
|
},
|
|
|
|
setStrokeWidth: function(pixels) {
|
|
var scale = this.scale;
|
|
$.each(this.canvas.getActiveObjects(), function() {
|
|
this.set('strokeWidth', pixels * scale);
|
|
});
|
|
this.canvas.renderAll();
|
|
},
|
|
|
|
setOpacity: function() {
|
|
$.each(this.canvas.getActiveObjects(), function() {
|
|
if (this.get('opacity') != 1)
|
|
this.set('opacity', 1);
|
|
else
|
|
this.set('opacity', 0.6);
|
|
});
|
|
this.canvas.renderAll();
|
|
},
|
|
|
|
bringForward: function(e) {
|
|
$.each(this.canvas.getActiveObjects(), function() {
|
|
this.bringForward();
|
|
});
|
|
},
|
|
|
|
keydown: function(e) {
|
|
// Check if [delete] was pressed with selected objects
|
|
if (e.keyCode == 8 || e.keyCode == 46)
|
|
return this.discard(e);
|
|
// Revert to previous state for CTRL+Z or CMD+Z
|
|
else if (e.keyCode == 90 && (e.metaKey || e.ctrlKey)) {
|
|
this.canvas.loadFromJSON(atob(this.$image.attr('data-annotations')));
|
|
return false;
|
|
}
|
|
},
|
|
|
|
discard: function(e) {
|
|
this.setBuffer();
|
|
var that = this;
|
|
$.each(this.canvas.getActiveObjects(), function() {
|
|
that.canvas.remove(this); }
|
|
);
|
|
this.canvas.renderAll();
|
|
},
|
|
|
|
commit: function() {
|
|
this.canvas.discardActiveObject();
|
|
|
|
// Upload the annotated image to server
|
|
this.app.api('module.buffer.trigger');
|
|
this.setBuffer();
|
|
var annotated = this.canvas.toDataURL({
|
|
format: 'jpg', quality: 4,
|
|
multiplier: 1 / this.canvas.getZoom()
|
|
}),
|
|
file = new Blob([annotated], {type: 'image/jpeg'});
|
|
|
|
// Fallback to the data URL — show while the image is being uploaded
|
|
this.origSrc = this.$image.attr('src');
|
|
this.$image.attr('src', annotated);
|
|
|
|
this.teardownAnnotate();
|
|
this.app.api('module.upload.send', {
|
|
url: this.app.opts.imageUpload,
|
|
data: this.app.opts.imageData,
|
|
paramName: this.app.opts.imageUploadParam,
|
|
name: 'imageannotate',
|
|
files: [file],
|
|
});
|
|
|
|
this.toolbar.destroy();
|
|
return false;
|
|
},
|
|
|
|
// Fired from module.upload.send() after completion
|
|
onupload: {
|
|
imageannotate: {
|
|
complete: function(response) {
|
|
response.imageannotate = true;
|
|
this.app.api('module.image.insert', response);
|
|
},
|
|
},
|
|
},
|
|
|
|
onimage: {
|
|
uploaded: function(image, response) {
|
|
// This is called for all uploads, but we only care about the
|
|
// ones initiated from this plugin
|
|
if (!this.$image || !response.imageannotate)
|
|
return;
|
|
|
|
// After successful upload, replace the old image with the new one.
|
|
// Transfer the annotation state to the new image for replay.
|
|
var $image = $R.dom(image).find('img');
|
|
// Transfer the annotation JSON data and drop the original image.
|
|
$image.attr('data-annotations', this.$image.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',
|
|
this.$image.attr('data-orig-annotated-image-src') || this.origSrc
|
|
);
|
|
// Out with the old
|
|
this.$image.remove();
|
|
},
|
|
},
|
|
|
|
// Keep the scale at 1.0 so that the stroke size is not stretched when
|
|
// the size and shape of the object is stretched.
|
|
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 state = this.canvas.toObject(), places = 2;
|
|
// Capture current annotations
|
|
delete state.backgroundImage;
|
|
this.$image.attr('data-annotations',
|
|
btoa(JSON.stringify(state, function(key, value) {
|
|
// limit precision of floats
|
|
if (typeof value === 'number') {
|
|
return parseFloat(value.toFixed(places));
|
|
}
|
|
return value;
|
|
}))
|
|
);
|
|
},
|
|
|
|
// Startup
|
|
|
|
initCanvas: function($img, $body) {
|
|
var canvas = $R.dom('<canvas>').css({
|
|
width: '100%', height: '100%'
|
|
});
|
|
$body.find('.canvas').append(canvas);
|
|
var fcanvas = new fabric.Canvas(canvas.get(), {
|
|
backgroundColor: 'rgba(0,0,0,0)',
|
|
includeDefaultValues: false,
|
|
width: canvas.width(),
|
|
height: canvas.height(),
|
|
}),
|
|
previous = $img.attr('data-annotations');
|
|
|
|
// Catch [delete] key and map to delete object
|
|
//self.opts.keydownCallback = this.keydown.bind(self);
|
|
//self.opts.keyupCallback = this.keydown.bind(self);
|
|
|
|
var I = new Image();
|
|
I.src = $img.attr('src');
|
|
// Determine the scaling adjustment to fit the image in the modal
|
|
// dialog. Also note, that both the width and height should be
|
|
// considered to ensure very tall images do not float off the
|
|
// screen.
|
|
var width = Math.min(canvas.width(), $img.width()),
|
|
viewscale = width / $img.width(),
|
|
height = $img.height() * viewscale,
|
|
maxheight = $(window).height() - 300;
|
|
|
|
if (height > maxheight) {
|
|
viewscale *= maxheight / height;
|
|
height = maxheight;
|
|
width = $img.width() * viewscale;
|
|
}
|
|
|
|
var drawscale = width / I.width,
|
|
scaleWidth = width / drawscale,
|
|
scaleHeight = height / drawscale;
|
|
this.scale = 1 / drawscale;
|
|
|
|
// Set default control appearance for all objects
|
|
fabric.Object.prototype.set({
|
|
transparentCorners: false,
|
|
borderColor: 'rgba(102,153,255,0.9)',
|
|
cornerColor: 'rgba(102,153,255,0.5)',
|
|
cornerSize: 8,
|
|
});
|
|
|
|
fcanvas
|
|
.setDimensions({width: width, height: height})
|
|
.setZoom(drawscale)
|
|
.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', this.resizeShape.bind(this))
|
|
.on('selection:updated', this.updateSelection.bind(this))
|
|
.on('selection:created', this.updateSelection.bind(this))
|
|
.on('selection:cleared', this.updateSelection.bind(this));
|
|
|
|
if (previous) {
|
|
fcanvas.loadFromJSON(atob(previous));
|
|
}
|
|
|
|
this.canvas = fcanvas;
|
|
return fcanvas;
|
|
}
|
|
});
|
|
|
|
$R.add('plugin', 'contexttypeahead', {
|
|
typeahead: false,
|
|
context: false,
|
|
variables: false,
|
|
|
|
init: function(app) {
|
|
this.app = app;
|
|
},
|
|
|
|
start: function() {
|
|
this.$editor = this.app.editor.getElement();
|
|
this.$element = $(this.app.rootElement);
|
|
if (!this.$element.data('rootContext'))
|
|
return;
|
|
|
|
this.$editor.on('keyup', this.watch.bind(this));
|
|
this.$editor.on('keydown', this.watch.bind(this));
|
|
this.$editor.on('click', this.watch.bind(this));
|
|
},
|
|
|
|
watch: function(e) {
|
|
var current = this.app.api('selection.getCurrent'),
|
|
allText = this.$editor.text(),
|
|
offset = this.app.api('offset.get', this.app.editor.$editor),
|
|
lhs = allText.substring(0, offset.start),
|
|
search = new RegExp(/%\{([^}]*)$/),
|
|
match,
|
|
e = $.Event(e);
|
|
|
|
if (!lhs) {
|
|
return !e.isDefaultPrevented();
|
|
}
|
|
|
|
if (e.which == 27 || !(match = search.exec(lhs)))
|
|
// No longer in a element — close typeahead
|
|
return this.destroy();
|
|
|
|
if (e.type == 'click')
|
|
return;
|
|
|
|
// Locate the position of the cursor and the number of characters back
|
|
// to the `%{` symbols
|
|
var sel = this.app.api('selection.get'),
|
|
range = sel.getRangeAt(0),
|
|
content = current.textContent,
|
|
clientRects = range.getClientRects(),
|
|
position = clientRects[0],
|
|
backText = match[1],
|
|
parent = this.app.api('selection.getParent') || this.$element,
|
|
that = this;
|
|
|
|
// Insert a hidden text input to receive the typed text and add a
|
|
// typeahead widget
|
|
if (!this.typeahead) {
|
|
this.typeahead = $('<input type="text">')
|
|
.css({position: 'absolute', visibility: 'hidden'})
|
|
.width(0).height(position.height - 4)
|
|
.appendTo(document.body)
|
|
.typeahead({
|
|
property: 'variable',
|
|
minLength: 0,
|
|
arrow: $('<span class="pull-right"><i class="icon-muted icon-chevron-right"></i></span>')
|
|
.css('padding', '0 0 0 6px'),
|
|
highlighter: function(variable, item) {
|
|
var base = $.fn.typeahead.Constructor.prototype.highlighter
|
|
.call(this, variable),
|
|
further = new RegExp(variable + '\\.'),
|
|
extendable = Object.keys(that.variables).some(function(v) {
|
|
return v.match(further);
|
|
}),
|
|
arrow = extendable ? this.options.arrow.clone() : '';
|
|
|
|
return $('<div/>').html(base).prepend(arrow).html()
|
|
+ $('<span class="faded">')
|
|
.text(' — ' + item.desc)
|
|
.wrap('<div>').parent().html();
|
|
},
|
|
item: '<li><a href="#" style="display:block"></a></li>',
|
|
source: this.getContext.bind(this),
|
|
sorter: function(items) {
|
|
items.sort(
|
|
function(a,b) {return a.variable > b.variable ? 1 : -1;}
|
|
);
|
|
return items;
|
|
},
|
|
matcher: function(item) {
|
|
if (item.toLowerCase().indexOf(this.query.toLowerCase()) !== 0)
|
|
return false;
|
|
|
|
return (this.query.match(/\./g) || []).length == (item.match(/\./g) || []).length;
|
|
},
|
|
onselect: this.select.bind(this),
|
|
scroll: true,
|
|
items: 100
|
|
});
|
|
}
|
|
|
|
if (position) {
|
|
var width = this.textWidth(
|
|
backText,
|
|
this.app.api('selection.getParent') || $('<div class="redactor-editor">')
|
|
),
|
|
pleft = $(parent).offset().left,
|
|
left = position.left - width;
|
|
|
|
if (left < pleft)
|
|
// This is a bug in chrome, but I'm not sure how to adjust it
|
|
left += pleft;
|
|
|
|
this.typeahead
|
|
.css({top: position.top + this.app.$win.scrollTop(), left: left});
|
|
}
|
|
|
|
this.typeahead
|
|
.val(match[1])
|
|
.triggerHandler(e);
|
|
|
|
return !e.isDefaultPrevented();
|
|
},
|
|
|
|
getContext: function(typeahead, query) {
|
|
var dfd, that=this,
|
|
root = this.$element.data('rootContext');
|
|
if (!this.context) {
|
|
dfd = $.Deferred();
|
|
$.ajax('ajax.php/content/context', {
|
|
data: {root: root},
|
|
success: function(json) {
|
|
var items = $.map(json, function(v,k) {
|
|
return {variable: k, desc: v};
|
|
});
|
|
that.variables = json;
|
|
dfd.resolve(items);
|
|
}
|
|
});
|
|
this.context = dfd;
|
|
}
|
|
// Only fetch the context once for this redactor box
|
|
this.context.then(function(items) {
|
|
typeahead.process(items);
|
|
});
|
|
},
|
|
|
|
textWidth: function(text, clone) {
|
|
var c = $(clone),
|
|
o = c.clone().text(text)
|
|
.css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden'})
|
|
.css({'font-family': c.css('font-family'), 'font-weight': c.css('font-weight'),
|
|
'font-size': c.css('font-size')})
|
|
.appendTo($('body')),
|
|
w = o.width();
|
|
|
|
o.remove();
|
|
|
|
return w;
|
|
},
|
|
|
|
destroy: function() {
|
|
if (this.typeahead) {
|
|
this.typeahead.typeahead('hide');
|
|
this.typeahead.remove();
|
|
this.typeahead = false;
|
|
}
|
|
},
|
|
|
|
select: function(item, event) {
|
|
// Collapse multiple textNodes together
|
|
(this.app.api('selection.getBlock') || this.$element.get(0)).normalize();
|
|
var current = this.app.api('selection.getCurrent'),
|
|
sel = this.app.api('selection.get'),
|
|
range = sel.getRangeAt(0),
|
|
cursorAt = range.endOffset,
|
|
// TODO: Consume immediately following `}` symbols
|
|
search = new RegExp(/%\{([^}]*)(\}?)$/);
|
|
|
|
// FIXME: ENTER will end up here, but current will be empty
|
|
|
|
if (!current)
|
|
return;
|
|
|
|
// Set cursor at the end of the expanded text
|
|
var left = current.textContent.substring(0, cursorAt),
|
|
right = current.textContent.substring(cursorAt),
|
|
autoExpand = event.target.nodeName == 'I',
|
|
selected = item.variable + (autoExpand ? '.' : '')
|
|
newLeft = left.replace(search, '%{' + selected + '}');
|
|
|
|
current.textContent = newLeft
|
|
// Drop the remaining part of a variable block, if any
|
|
+ right.replace(/[^%}]*?[%}]/, '');
|
|
|
|
range.setStart(current, newLeft.length - 1);
|
|
range.setEnd(current, newLeft.length - 1);
|
|
this.app.api('selection.setRange', range);
|
|
if (!autoExpand)
|
|
return this.destroy();
|
|
|
|
this.typeahead.val(selected);
|
|
this.typeahead.typeahead('lookup');
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Make the toolbar a class rather than a service, so it can be reused in
|
|
// a dialog
|
|
var ToolbarService = $R[$R.env['service']]['toolbar'];
|
|
$R.add('class', 'toolbar', $R.extend(ToolbarService.prototype, {
|
|
init: function(app) {
|
|
this.app = app
|
|
this._oldtoolbar = app.toolbar;
|
|
this.app.toolbar = this;
|
|
// Connect what's normally available in a service module
|
|
this.opts = app.opts;
|
|
this.detector = app.detector;
|
|
|
|
this.buttons = [];
|
|
this.dropdownOpened = false;
|
|
this.buttonsObservers = {};
|
|
|
|
// Start immediately
|
|
this.create();
|
|
this.$wrapper.append(this.$toolbar);
|
|
},
|
|
is: function() {
|
|
return true;
|
|
},
|
|
destroy: function() {
|
|
this.app.toolbar = this._oldtoolbar;
|
|
}
|
|
}));
|
|
|
|
$R.add('plugin', 'translatable', {
|
|
langs: undefined,
|
|
config: undefined,
|
|
current: undefined,
|
|
primary: undefined,
|
|
changed: {},
|
|
|
|
init: function(app) {
|
|
this.app = app;
|
|
this.statusbar = app.statusbar;
|
|
this.$textarea = $R.dom(this.app.rootElement);
|
|
this.$editor = app.editor.getElement();
|
|
},
|
|
|
|
start: function() {
|
|
this.fetch('ajax.php/i18n/langs/all')
|
|
.then(this.setLangs.bind(this));
|
|
getConfig().then(this.setConfig.bind(this));
|
|
$editor = this.app.editor.getElement();
|
|
this.translateTag = this.$textarea.data()['translateTag'];
|
|
},
|
|
|
|
setLangs: function(langs) {
|
|
if (Object.keys(langs).length < 2)
|
|
return;
|
|
this.langs = langs;
|
|
this.buildDropdown();
|
|
},
|
|
|
|
setConfig: function(config) {
|
|
this.config = config;
|
|
this.buildDropdown();
|
|
},
|
|
|
|
buildDropdown: function() {
|
|
if (!this.config || !this.langs)
|
|
return;
|
|
|
|
var primary = this.$textarea,
|
|
primary_lang = this.config.primary_language.replace('-','_'),
|
|
primary_info = this.langs[primary_lang],
|
|
items = {},
|
|
dropdown = {
|
|
primary_lang: {
|
|
title: '<i class="flag flag-'+primary_info.flag+'"></i> '+primary_info.name,
|
|
api: 'plugin.translatable.switchTo',
|
|
args: primary_lang,
|
|
},
|
|
},
|
|
button = this.app.toolbar.addButton('flag', {
|
|
title: __('Translate'),
|
|
});
|
|
|
|
this.primary = this.current = primary_lang;
|
|
this.button = button;
|
|
|
|
$.each(this.langs, function(lang, info) {
|
|
if (lang == primary_lang)
|
|
return;
|
|
dropdown[lang] = {
|
|
title: '<i class="flag flag-'+info.flag+'"></i> '+info.name,
|
|
api: 'plugin.translatable.switchTo',
|
|
args: lang,
|
|
};
|
|
});
|
|
|
|
// Add the button to the toolbar
|
|
button.setDropdown(dropdown);
|
|
|
|
// Flip back to primary language before submitting
|
|
var that=this;
|
|
this.app.editor.getElement().closest('form').on('submit', function() {
|
|
that.switchTo(primary_lang);
|
|
});
|
|
|
|
this.showStatus(this.primary);
|
|
},
|
|
|
|
showStatus: function(lang) {
|
|
var tstatus = $R.dom('<span>').text('lang: ')
|
|
tstatus.append($R.dom('<i class="flag flag-'+this.langs[this.current].flag+'"></i>'))
|
|
tstatus.append(document.createTextNode(' ' + this.current))
|
|
this.statusbar.add('translatable', tstatus);
|
|
|
|
this.button.setIcon('<i class="flag flag-'+this.langs[lang].flag+'"></i>');
|
|
},
|
|
|
|
switchTo: function(lang) {
|
|
if (lang == this.current)
|
|
return;
|
|
|
|
var that = this;
|
|
this.fetch('ajax.php/i18n/translate/' + this.translateTag)
|
|
.then(function(json) {
|
|
// Preserve current text
|
|
json[that.current] = that.app.source.getCode();
|
|
that.current = lang;
|
|
that.app.insertion.set(json[lang] || '', false, true);
|
|
that.app.api('module.source.sync');
|
|
|
|
that.app.editor.getElement()
|
|
.attr({lang: lang, dir: that.langs[lang].direction});
|
|
|
|
that.showStatus(lang);
|
|
that.showCommit();
|
|
});
|
|
},
|
|
|
|
onchanged: function() {
|
|
this.showCommit();
|
|
this.changed[this.current] = true;
|
|
},
|
|
|
|
showCommit: function() {
|
|
if (this.current == this.primary) {
|
|
this.statusbar.remove('translatable:commit');
|
|
return true;
|
|
}
|
|
|
|
if (!this.changed[this.current])
|
|
return true;
|
|
|
|
var tstatus = $R.dom('<a href="#"></a>')
|
|
.text(__('save translation'))
|
|
.on('click', this.commit.bind(this))
|
|
this.statusbar.add('translatable:commit', tstatus);
|
|
},
|
|
|
|
commit: function() {
|
|
if (!this.changed[this.current])
|
|
return this.app.statusbar.remove('translatable:commit');
|
|
|
|
var changes = {}, self = this;
|
|
|
|
this.app.statusbar.add('translatable:commit', __('saving...'))
|
|
changes[this.current] = this.app.source.getCode();
|
|
|
|
$.ajax('ajax.php/i18n/translate/' + this.translateTag, {
|
|
type: 'post',
|
|
data: changes,
|
|
success: function() {
|
|
self.changed[self.current] = false;
|
|
self.app.statusbar.remove('translatable:commit');
|
|
}
|
|
});
|
|
// Don't bubble the click event
|
|
return false;
|
|
},
|
|
|
|
urlcache: {},
|
|
fetch: function( url, data, callback ) {
|
|
var urlcache = this.urlcache;
|
|
if ( !urlcache[ url ] ) {
|
|
urlcache[ url ] = $.Deferred(function( defer ) {
|
|
$.ajax( url, { data: data, dataType: 'json' } )
|
|
.then( defer.resolve, defer.reject );
|
|
}).promise();
|
|
}
|
|
return urlcache[ url ].done( callback );
|
|
},
|
|
});
|
|
})(Redactor);
|