gosora/public/trumbowyg/trumbowyg.js
Azareal 14a14b7e80 More work on Cosora, we have a screenshot of it up now, although it's super experimental at the moment.
This commit might be a little broken, another is coming to fix things up!

The topics list is now paginated.
Refactored the error handling system.
Added the Trumboyg WYSIWYG editor for Cosora.
Moved Delete() out of the TopicStore and into *Topic
You can now bulk delete and bulk lock topics on Cosora.
h1s are now formatted properly on Firefox.
Added more ARIA Labels.
SuperModOnly is now a piece of middleware for the Control Panel routes.
Refactored and extended the router generator.
Improved the SEO for the paginators.
Added bits of Microdata to improve SEO further.
Wrote benchmarks for users.Get() and users.BypassGet()
More errors are caught now.
You can now attach pcss files to posts.
Improved the error logging for JavaScript.
Topic list avatars now link to the associated profiles.
Added last poster avatars to the forum list.
2017-10-30 09:57:08 +00:00

1689 lines
56 KiB
JavaScript

/**
* Trumbowyg v2.8.1 - A lightweight WYSIWYG editor
* Trumbowyg core file
* ------------------------
* @link http://alex-d.github.io/Trumbowyg
* @license MIT
* @author Alexandre Demode (Alex-D)
* Twitter : @AlexandreDemode
* Website : alex-d.fr
*/
jQuery.trumbowyg = {
langs: {
en: {
viewHTML: 'View HTML',
undo: 'Undo',
redo: 'Redo',
formatting: 'Formatting',
p: 'Paragraph',
blockquote: 'Quote',
code: 'Code',
header: 'Header',
bold: 'Bold',
italic: 'Italic',
strikethrough: 'Stroke',
underline: 'Underline',
strong: 'Strong',
em: 'Emphasis',
del: 'Deleted',
superscript: 'Superscript',
subscript: 'Subscript',
unorderedList: 'Unordered list',
orderedList: 'Ordered list',
insertImage: 'Insert Image',
link: 'Link',
createLink: 'Insert link',
unlink: 'Remove link',
justifyLeft: 'Align Left',
justifyCenter: 'Align Center',
justifyRight: 'Align Right',
justifyFull: 'Align Justify',
horizontalRule: 'Insert horizontal rule',
removeformat: 'Remove format',
fullscreen: 'Fullscreen',
close: 'Close',
submit: 'Confirm',
reset: 'Cancel',
required: 'Required',
description: 'Description',
title: 'Title',
text: 'Text',
target: 'Target'
}
},
// Plugins
plugins: {},
// SVG Path globally
svgPath: null,
hideButtonTexts: null
};
// Makes default options read-only
Object.defineProperty(jQuery.trumbowyg, 'defaultOptions', {
value: {
lang: 'en',
fixedBtnPane: false,
fixedFullWidth: false,
autogrow: false,
autogrowOnEnter: false,
prefix: 'trumbowyg-',
semantic: true,
resetCss: false,
removeformatPasted: false,
tagsToRemove: [],
btns: [
['viewHTML'],
['undo', 'redo'], // Only supported in Blink browsers
['formatting'],
['strong', 'em', 'del'],
['superscript', 'subscript'],
['link'],
['insertImage'],
['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
['unorderedList', 'orderedList'],
['horizontalRule'],
['removeformat'],
['fullscreen']
],
// For custom button definitions
btnsDef: {},
inlineElementsSelector: 'a,abbr,acronym,b,caption,cite,code,col,dfn,dir,dt,dd,em,font,hr,i,kbd,li,q,span,strikeout,strong,sub,sup,u',
pasteHandlers: [],
// imgDblClickHandler: default is defined in constructor
plugins: {}
},
writable: false,
enumerable: true,
configurable: false
});
(function (navigator, window, document, $) {
'use strict';
var CONFIRM_EVENT = 'tbwconfirm',
CANCEL_EVENT = 'tbwcancel';
$.fn.trumbowyg = function (options, params) {
var trumbowygDataName = 'trumbowyg';
if (options === Object(options) || !options) {
return this.each(function () {
if (!$(this).data(trumbowygDataName)) {
$(this).data(trumbowygDataName, new Trumbowyg(this, options));
}
});
}
if (this.length === 1) {
try {
var t = $(this).data(trumbowygDataName);
switch (options) {
// Exec command
case 'execCmd':
return t.execCmd(params.cmd, params.param, params.forceCss);
// Modal box
case 'openModal':
return t.openModal(params.title, params.content);
case 'closeModal':
return t.closeModal();
case 'openModalInsert':
return t.openModalInsert(params.title, params.fields, params.callback);
// Range
case 'saveRange':
return t.saveRange();
case 'getRange':
return t.range;
case 'getRangeText':
return t.getRangeText();
case 'restoreRange':
return t.restoreRange();
// Enable/disable
case 'enable':
return t.setDisabled(false);
case 'disable':
return t.setDisabled(true);
// Destroy
case 'destroy':
return t.destroy();
// Empty
case 'empty':
return t.empty();
// HTML
case 'html':
return t.html(params);
}
} catch (c) {
}
}
return false;
};
// @param: editorElem is the DOM element
var Trumbowyg = function (editorElem, options) {
var t = this,
trumbowygIconsId = 'trumbowyg-icons',
$trumbowyg = $.trumbowyg;
// Get the document of the element. It use to makes the plugin
// compatible on iframes.
t.doc = editorElem.ownerDocument || document;
// jQuery object of the editor
t.$ta = $(editorElem); // $ta : Textarea
t.$c = $(editorElem); // $c : creator
options = options || {};
// Localization management
if (options.lang != null || $trumbowyg.langs[options.lang] != null) {
t.lang = $.extend(true, {}, $trumbowyg.langs.en, $trumbowyg.langs[options.lang]);
} else {
t.lang = $trumbowyg.langs.en;
}
t.hideButtonTexts = $trumbowyg.hideButtonTexts != null ? $trumbowyg.hideButtonTexts : options.hideButtonTexts;
// SVG path
var svgPathOption = $trumbowyg.svgPath != null ? $trumbowyg.svgPath : options.svgPath;
t.hasSvg = svgPathOption !== false;
t.svgPath = !!t.doc.querySelector('base') ? window.location.href.split('#')[0] : '';
if ($('#' + trumbowygIconsId, t.doc).length === 0 && svgPathOption !== false) {
if (svgPathOption == null) {
// Hack to get svgPathOption based on trumbowyg.js path
try {
throw new Error();
} catch (e) {
if (!e.hasOwnProperty('stack')) {
console.warn('You must define svgPath: https://goo.gl/CfTY9U'); // jshint ignore:line
} else {
var stackLines = e.stack.split('\n');
for (var i in stackLines) {
if (!stackLines[i].match(/https?:\/\//)) {
continue;
}
svgPathOption = stackLines[Number(i)].match(/((https?:\/\/.+\/)([^\/]+\.js))(\?.*)?:/)[1].split('/');
svgPathOption.pop();
svgPathOption = svgPathOption.join('/') + '/ui/icons.svg';
break;
}
}
}
}
var div = t.doc.createElement('div');
div.id = trumbowygIconsId;
t.doc.body.insertBefore(div, t.doc.body.childNodes[0]);
$.ajax({
async: true,
type: 'GET',
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
dataType: 'xml',
crossDomain: true,
url: svgPathOption,
data: null,
beforeSend: null,
complete: null,
success: function (data) {
div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
}
});
}
/**
* When the button is associated to a empty object
* fn and title attributs are defined from the button key value
*
* For example
* foo: {}
* is equivalent to :
* foo: {
* fn: 'foo',
* title: this.lang.foo
* }
*/
var h = t.lang.header, // Header translation
isBlinkFunction = function () {
return (window.chrome || (window.Intl && Intl.v8BreakIterator)) && 'CSS' in window;
};
t.btnsDef = {
viewHTML: {
fn: 'toggle'
},
undo: {
isSupported: isBlinkFunction,
key: 'Z'
},
redo: {
isSupported: isBlinkFunction,
key: 'Y'
},
p: {
fn: 'formatBlock'
},
blockquote: {
fn: 'formatBlock'
},
h1: {
fn: 'formatBlock',
title: h + ' 1'
},
h2: {
fn: 'formatBlock',
title: h + ' 2'
},
h3: {
fn: 'formatBlock',
title: h + ' 3'
},
h4: {
fn: 'formatBlock',
title: h + ' 4'
},
subscript: {
tag: 'sub'
},
superscript: {
tag: 'sup'
},
bold: {
key: 'B',
tag: 'b'
},
italic: {
key: 'I',
tag: 'i'
},
underline: {
tag: 'u'
},
strikethrough: {
tag: 'strike'
},
strong: {
fn: 'bold',
key: 'B'
},
em: {
fn: 'italic',
key: 'I'
},
del: {
fn: 'strikethrough'
},
createLink: {
key: 'K',
tag: 'a'
},
unlink: {},
insertImage: {},
justifyLeft: {
tag: 'left',
forceCss: true
},
justifyCenter: {
tag: 'center',
forceCss: true
},
justifyRight: {
tag: 'right',
forceCss: true
},
justifyFull: {
tag: 'justify',
forceCss: true
},
unorderedList: {
fn: 'insertUnorderedList',
tag: 'ul'
},
orderedList: {
fn: 'insertOrderedList',
tag: 'ol'
},
horizontalRule: {
fn: 'insertHorizontalRule'
},
removeformat: {},
fullscreen: {
class: 'trumbowyg-not-disable'
},
close: {
fn: 'destroy',
class: 'trumbowyg-not-disable'
},
// Dropdowns
formatting: {
dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4'],
ico: 'p'
},
link: {
dropdown: ['createLink', 'unlink']
}
};
// Defaults Options
t.o = $.extend(true, {}, $trumbowyg.defaultOptions, options);
if (!t.o.hasOwnProperty('imgDblClickHandler')) {
t.o.imgDblClickHandler = t.getDefaultImgDblClickHandler();
}
t.disabled = t.o.disabled || (editorElem.nodeName === 'TEXTAREA' && editorElem.disabled);
if (options.btns) {
t.o.btns = options.btns;
} else if (!t.o.semantic) {
t.o.btns[3] = ['bold', 'italic', 'underline', 'strikethrough'];
}
$.each(t.o.btnsDef, function (btnName, btnDef) {
t.addBtnDef(btnName, btnDef);
});
// put this here in the event it would be merged in with options
t.eventNamespace = 'trumbowyg-event';
// Keyboard shortcuts are load in this array
t.keys = [];
// Tag to button dynamically hydrated
t.tagToButton = {};
t.tagHandlers = [];
// Admit multiple paste handlers
t.pasteHandlers = [].concat(t.o.pasteHandlers);
// Check if browser is IE
t.isIE = (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') !== -1);
t.init();
};
Trumbowyg.prototype = {
init: function () {
var t = this;
t.height = t.$ta.height();
t.initPlugins();
try {
// Disable image resize, try-catch for old IE
t.doc.execCommand('enableObjectResizing', false, false);
t.doc.execCommand('defaultParagraphSeparator', false, 'p');
} catch (e) {
}
t.buildEditor();
t.buildBtnPane();
t.fixedBtnPaneEvents();
t.buildOverlay();
setTimeout(function () {
if (t.disabled) {
t.setDisabled(true);
}
t.$c.trigger('tbwinit');
});
},
addBtnDef: function (btnName, btnDef) {
this.btnsDef[btnName] = btnDef;
},
buildEditor: function () {
var t = this,
prefix = t.o.prefix,
html = '';
t.$box = $('<div/>', {
class: prefix + 'box ' + prefix + 'editor-visible ' + prefix + t.o.lang + ' trumbowyg'
});
// $ta = Textarea
// $ed = Editor
t.isTextarea = t.$ta.is('textarea');
if (t.isTextarea) {
html = t.$ta.val();
t.$ed = $('<div/>');
t.$box
.insertAfter(t.$ta)
.append(t.$ed, t.$ta);
} else {
t.$ed = t.$ta;
html = t.$ed.html();
t.$ta = $('<textarea/>', {
name: t.$ta.attr('id'),
height: t.height
}).val(html);
t.$box
.insertAfter(t.$ed)
.append(t.$ta, t.$ed);
t.syncCode();
}
t.$ta
.addClass(prefix + 'textarea')
.attr('tabindex', -1)
;
t.$ed
.addClass(prefix + 'editor')
.attr({
contenteditable: true,
dir: t.lang._dir || 'ltr'
})
.html(html)
;
if (t.o.tabindex) {
t.$ed.attr('tabindex', t.o.tabindex);
}
if (t.$c.is('[placeholder]')) {
t.$ed.attr('placeholder', t.$c.attr('placeholder'));
}
if (t.$c.is('[spellcheck]')) {
t.$ed.attr('spellcheck', t.$c.attr('spellcheck'));
}
if (t.o.resetCss) {
t.$ed.addClass(prefix + 'reset-css');
}
if (!t.o.autogrow) {
t.$ta.add(t.$ed).css({
height: t.height
});
}
t.semanticCode();
if (t.o.autogrowOnEnter) {
t.$ed.addClass(prefix + 'autogrow-on-enter');
}
var ctrl = false,
composition = false,
debounceButtonPaneStatus,
updateEventName = t.isIE ? 'keyup' : 'input';
t.$ed
.on('dblclick', 'img', t.o.imgDblClickHandler)
.on('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && !e.altKey) {
ctrl = true;
var key = t.keys[String.fromCharCode(e.which).toUpperCase()];
try {
t.execCmd(key.fn, key.param);
return false;
} catch (c) {
}
}
})
.on('compositionstart compositionupdate', function () {
composition = true;
})
.on(updateEventName + ' compositionend', function (e) {
if (e.type === 'compositionend') {
composition = false;
} else if (composition) {
return;
}
var keyCode = e.which;
if (keyCode >= 37 && keyCode <= 40) {
return;
}
if ((e.ctrlKey || e.metaKey) && (keyCode === 89 || keyCode === 90)) {
t.$c.trigger('tbwchange');
} else if (!ctrl && keyCode !== 17) {
t.semanticCode(false, e.type === 'compositionend' && keyCode === 13);
t.$c.trigger('tbwchange');
} else if (typeof e.which === 'undefined') {
t.semanticCode(false, false, true);
}
setTimeout(function () {
ctrl = false;
}, 200);
})
.on('mouseup keydown keyup', function () {
clearTimeout(debounceButtonPaneStatus);
debounceButtonPaneStatus = setTimeout(function () {
t.updateButtonPaneStatus();
}, 50);
})
.on('focus blur', function (e) {
t.$c.trigger('tbw' + e.type);
if (e.type === 'blur') {
$('.' + prefix + 'active-button', t.$btnPane).removeClass(prefix + 'active-button ' + prefix + 'active');
}
if (t.o.autogrowOnEnter) {
if (t.autogrowOnEnterDontClose) {
return;
}
if (e.type === 'focus') {
t.autogrowOnEnterWasFocused = true;
t.autogrowEditorOnEnter();
}
else if (!t.o.autogrow) {
t.$ed.css({height: t.$ed.css('min-height')});
t.$c.trigger('tbwresize');
}
}
})
.on('cut', function () {
setTimeout(function () {
t.semanticCode(false, true);
t.$c.trigger('tbwchange');
}, 0);
})
.on('paste', function (e) {
if (t.o.removeformatPasted) {
e.preventDefault();
if (window.getSelection && window.getSelection().deleteFromDocument) {
window.getSelection().deleteFromDocument();
}
try {
// IE
var text = window.clipboardData.getData('Text');
try {
// <= IE10
t.doc.selection.createRange().pasteHTML(text);
} catch (c) {
// IE 11
t.doc.getSelection().getRangeAt(0).insertNode(t.doc.createTextNode(text));
}
t.$c.trigger('tbwchange', e);
} catch (d) {
// Not IE
t.execCmd('insertText', (e.originalEvent || e).clipboardData.getData('text/plain'));
}
}
// Call pasteHandlers
$.each(t.pasteHandlers, function (i, pasteHandler) {
pasteHandler(e);
});
setTimeout(function () {
t.semanticCode(false, true);
t.$c.trigger('tbwpaste', e);
}, 0);
});
t.$ta
.on('keyup', function () {
t.$c.trigger('tbwchange');
})
.on('paste', function () {
setTimeout(function () {
t.$c.trigger('tbwchange');
}, 0);
});
t.$box.on('keydown', function (e) {
if (e.which === 27 && $('.' + prefix + 'modal-box', t.$box).length === 1) {
t.closeModal();
return false;
}
});
},
//autogrow when entering logic
autogrowEditorOnEnter: function () {
var t = this;
t.$ed.removeClass('autogrow-on-enter');
var oldHeight = t.$ed[0].clientHeight;
t.$ed.height('auto');
var totalHeight = t.$ed[0].scrollHeight;
t.$ed.addClass('autogrow-on-enter');
if (oldHeight !== totalHeight) {
t.$ed.height(oldHeight);
setTimeout(function () {
t.$ed.css({height: totalHeight});
t.$c.trigger('tbwresize');
}, 0);
}
},
// Build button pane, use o.btns option
buildBtnPane: function () {
var t = this,
prefix = t.o.prefix;
var $btnPane = t.$btnPane = $('<div/>', {
class: prefix + 'button-pane'
});
$.each(t.o.btns, function (i, btnGrp) {
if (!$.isArray(btnGrp)) {
btnGrp = [btnGrp];
}
var $btnGroup = $('<div/>', {
class: prefix + 'button-group ' + ((btnGrp.indexOf('fullscreen') >= 0) ? prefix + 'right' : '')
});
$.each(btnGrp, function (i, btn) {
try { // Prevent buildBtn error
if (t.isSupportedBtn(btn)) { // It's a supported button
$btnGroup.append(t.buildBtn(btn));
}
} catch (c) {
}
});
$btnPane.append($btnGroup);
});
t.$box.prepend($btnPane);
},
// Build a button and his action
buildBtn: function (btnName) { // btnName is name of the button
var t = this,
prefix = t.o.prefix,
btn = t.btnsDef[btnName],
isDropdown = btn.dropdown,
hasIcon = btn.hasIcon != null ? btn.hasIcon : true,
textDef = t.lang[btnName] || btnName,
$btn = $('<button/>', {
type: 'button',
class: prefix + btnName + '-button ' + (btn.class || '') + (!hasIcon ? ' ' + prefix + 'textual-button' : ''),
html: t.hasSvg && hasIcon ?
'<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' :
t.hideButtonTexts ? '' : (btn.text || btn.title || t.lang[btnName] || btnName),
title: (btn.title || btn.text || textDef) + ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : ''),
tabindex: -1,
mousedown: function () {
if (!isDropdown || $('.' + btnName + '-' + prefix + 'dropdown', t.$box).is(':hidden')) {
$('body', t.doc).trigger('mousedown');
}
if (t.$btnPane.hasClass(prefix + 'disable') && !$(this).hasClass(prefix + 'active') && !$(this).hasClass(prefix + 'not-disable')) {
return false;
}
t.execCmd((isDropdown ? 'dropdown' : false) || btn.fn || btnName, btn.param || btnName, btn.forceCss);
return false;
}
});
if (isDropdown) {
$btn.addClass(prefix + 'open-dropdown');
var dropdownPrefix = prefix + 'dropdown',
$dropdown = $('<div/>', { // the dropdown
class: dropdownPrefix + '-' + btnName + ' ' + dropdownPrefix + ' ' + prefix + 'fixed-top',
'data-dropdown': btnName
});
$.each(isDropdown, function (i, def) {
if (t.btnsDef[def] && t.isSupportedBtn(def)) {
$dropdown.append(t.buildSubBtn(def));
}
});
t.$box.append($dropdown.hide());
} else if (btn.key) {
t.keys[btn.key] = {
fn: btn.fn || btnName,
param: btn.param || btnName
};
}
if (!isDropdown) {
t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName;
}
return $btn;
},
// Build a button for dropdown menu
// @param n : name of the subbutton
buildSubBtn: function (btnName) {
var t = this,
prefix = t.o.prefix,
btn = t.btnsDef[btnName],
hasIcon = btn.hasIcon != null ? btn.hasIcon : true;
if (btn.key) {
t.keys[btn.key] = {
fn: btn.fn || btnName,
param: btn.param || btnName
};
}
t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName;
return $('<button/>', {
type: 'button',
class: prefix + btnName + '-dropdown-button' + (btn.ico ? ' ' + prefix + btn.ico + '-button' : ''),
html: t.hasSvg && hasIcon ? '<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' + (btn.text || btn.title || t.lang[btnName] || btnName) : (btn.text || btn.title || t.lang[btnName] || btnName),
title: ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : null),
style: btn.style || null,
mousedown: function () {
$('body', t.doc).trigger('mousedown');
t.execCmd(btn.fn || btnName, btn.param || btnName, btn.forceCss);
return false;
}
});
},
// Check if button is supported
isSupportedBtn: function (b) {
try {
return this.btnsDef[b].isSupported();
} catch (c) {
}
return true;
},
// Build overlay for modal box
buildOverlay: function () {
var t = this;
t.$overlay = $('<div/>', {
class: t.o.prefix + 'overlay'
}).appendTo(t.$box);
return t.$overlay;
},
showOverlay: function () {
var t = this;
$(window).trigger('scroll');
t.$overlay.fadeIn(200);
t.$box.addClass(t.o.prefix + 'box-blur');
},
hideOverlay: function () {
var t = this;
t.$overlay.fadeOut(50);
t.$box.removeClass(t.o.prefix + 'box-blur');
},
// Management of fixed button pane
fixedBtnPaneEvents: function () {
var t = this,
fixedFullWidth = t.o.fixedFullWidth,
$box = t.$box;
if (!t.o.fixedBtnPane) {
return;
}
t.isFixed = false;
$(window)
.on('scroll.' + t.eventNamespace + ' resize.' + t.eventNamespace, function () {
if (!$box) {
return;
}
t.syncCode();
var scrollTop = $(window).scrollTop(),
offset = $box.offset().top + 1,
bp = t.$btnPane,
oh = bp.outerHeight() - 2;
if ((scrollTop - offset > 0) && ((scrollTop - offset - t.height) < 0)) {
if (!t.isFixed) {
t.isFixed = true;
bp.css({
position: 'fixed',
top: 0,
left: fixedFullWidth ? '0' : 'auto',
zIndex: 7
});
$([t.$ta, t.$ed]).css({marginTop: bp.height()});
}
bp.css({
width: fixedFullWidth ? '100%' : (($box.width() - 1) + 'px')
});
$('.' + t.o.prefix + 'fixed-top', $box).css({
position: fixedFullWidth ? 'fixed' : 'absolute',
top: fixedFullWidth ? oh : oh + (scrollTop - offset) + 'px',
zIndex: 15
});
} else if (t.isFixed) {
t.isFixed = false;
bp.removeAttr('style');
$([t.$ta, t.$ed]).css({marginTop: 0});
$('.' + t.o.prefix + 'fixed-top', $box).css({
position: 'absolute',
top: oh
});
}
});
},
// Disable editor
setDisabled: function (disable) {
var t = this,
prefix = t.o.prefix;
t.disabled = disable;
if (disable) {
t.$ta.attr('disabled', true);
} else {
t.$ta.removeAttr('disabled');
}
t.$box.toggleClass(prefix + 'disabled', disable);
t.$ed.attr('contenteditable', !disable);
},
// Destroy the editor
destroy: function () {
var t = this,
prefix = t.o.prefix;
if (t.isTextarea) {
t.$box.after(
t.$ta
.css({height: ''})
.val(t.html())
.removeClass(prefix + 'textarea')
.show()
);
} else {
t.$box.after(
t.$ed
.css({height: ''})
.removeClass(prefix + 'editor')
.removeAttr('contenteditable')
.removeAttr('dir')
.html(t.html())
.show()
);
}
t.$ed.off('dblclick', 'img');
t.destroyPlugins();
t.$box.remove();
t.$c.removeData('trumbowyg');
$('body').removeClass(prefix + 'body-fullscreen');
t.$c.trigger('tbwclose');
$(window).off('scroll.' + t.eventNamespace + ' resize.' + t.eventNamespace);
},
// Empty the editor
empty: function () {
this.$ta.val('');
this.syncCode(true);
},
// Function call when click on viewHTML button
toggle: function () {
var t = this,
prefix = t.o.prefix;
if (t.o.autogrowOnEnter) {
t.autogrowOnEnterDontClose = !t.$box.hasClass(prefix + 'editor-hidden');
}
t.semanticCode(false, true);
setTimeout(function () {
t.doc.activeElement.blur();
t.$box.toggleClass(prefix + 'editor-hidden ' + prefix + 'editor-visible');
t.$btnPane.toggleClass(prefix + 'disable');
$('.' + prefix + 'viewHTML-button', t.$btnPane).toggleClass(prefix + 'active');
if (t.$box.hasClass(prefix + 'editor-visible')) {
t.$ta.attr('tabindex', -1);
} else {
t.$ta.removeAttr('tabindex');
}
if (t.o.autogrowOnEnter && !t.autogrowOnEnterDontClose) {
t.autogrowEditorOnEnter();
}
}, 0);
},
// Open dropdown when click on a button which open that
dropdown: function (name) {
var t = this,
d = t.doc,
prefix = t.o.prefix,
$dropdown = $('[data-dropdown=' + name + ']', t.$box),
$btn = $('.' + prefix + name + '-button', t.$btnPane),
show = $dropdown.is(':hidden');
$('body', d).trigger('mousedown');
if (show) {
var o = $btn.offset().left;
$btn.addClass(prefix + 'active');
$dropdown.css({
position: 'absolute',
top: $btn.offset().top - t.$btnPane.offset().top + $btn.outerHeight(),
left: (t.o.fixedFullWidth && t.isFixed) ? o + 'px' : (o - t.$btnPane.offset().left) + 'px'
}).show();
$(window).trigger('scroll');
$('body', d).on('mousedown.' + t.eventNamespace, function (e) {
if (!$dropdown.is(e.target)) {
$('.' + prefix + 'dropdown', d).hide();
$('.' + prefix + 'active', d).removeClass(prefix + 'active');
$('body', d).off('mousedown.' + t.eventNamespace);
}
});
}
},
// HTML Code management
html: function (html) {
var t = this;
if (html != null) {
t.$ta.val(html);
t.syncCode(true);
return t;
}
return t.$ta.val();
},
syncTextarea: function () {
var t = this;
t.$ta.val(t.$ed.text().trim().length > 0 || t.$ed.find('hr,img,embed,iframe,input').length > 0 ? t.$ed.html() : '');
},
syncCode: function (force) {
var t = this;
if (!force && t.$ed.is(':visible')) {
t.syncTextarea();
} else {
// wrap the content in a div it's easier to get the innerhtml
var html = $('<div>').html(t.$ta.val());
//scrub the html before loading into the doc
var safe = $('<div>').append(html);
$(t.o.tagsToRemove.join(','), safe).remove();
t.$ed.html(safe.contents().html());
}
if (t.o.autogrow) {
t.height = t.$ed.height();
if (t.height !== t.$ta.css('height')) {
t.$ta.css({height: t.height});
t.$c.trigger('tbwresize');
}
}
if (t.o.autogrowOnEnter) {
// t.autogrowEditorOnEnter();
t.$ed.height('auto');
var totalheight = t.autogrowOnEnterWasFocused ? t.$ed[0].scrollHeight : t.$ed.css('min-height');
if (totalheight !== t.$ta.css('height')) {
t.$ed.css({height: totalheight});
t.$c.trigger('tbwresize');
}
}
},
// Analyse and update to semantic code
// @param force : force to sync code from textarea
// @param full : wrap text nodes in <p>
// @param keepRange : leave selection range as it is
semanticCode: function (force, full, keepRange) {
var t = this;
t.saveRange();
t.syncCode(force);
if (t.o.semantic) {
t.semanticTag('b', 'strong');
t.semanticTag('i', 'em');
t.semanticTag('strike', 'del');
if (full) {
var inlineElementsSelector = t.o.inlineElementsSelector,
blockElementsSelector = ':not(' + inlineElementsSelector + ')';
// Wrap text nodes in span for easier processing
t.$ed.contents().filter(function () {
return this.nodeType === 3 && this.nodeValue.trim().length > 0;
}).wrap('<span data-tbw/>');
// Wrap groups of inline elements in paragraphs (recursive)
var wrapInlinesInParagraphsFrom = function ($from) {
if ($from.length !== 0) {
var $finalParagraph = $from.nextUntil(blockElementsSelector).addBack().wrapAll('<p/>').parent(),
$nextElement = $finalParagraph.nextAll(inlineElementsSelector).first();
$finalParagraph.next('br').remove();
wrapInlinesInParagraphsFrom($nextElement);
}
};
wrapInlinesInParagraphsFrom(t.$ed.children(inlineElementsSelector).first());
t.semanticTag('div', 'p', true);
// Unwrap paragraphs content, containing nothing usefull
t.$ed.find('p').filter(function () {
// Don't remove currently being edited element
if (t.range && this === t.range.startContainer) {
return false;
}
return $(this).text().trim().length === 0 && $(this).children().not('br,span').length === 0;
}).contents().unwrap();
// Get rid of temporial span's
$('[data-tbw]', t.$ed).contents().unwrap();
// Remove empty <p>
t.$ed.find('p:empty').remove();
}
if (!keepRange) {
t.restoreRange();
}
t.syncTextarea();
}
},
semanticTag: function (oldTag, newTag, copyAttributes) {
$(oldTag, this.$ed).each(function () {
var $oldTag = $(this);
$oldTag.wrap('<' + newTag + '/>');
if (copyAttributes) {
$.each($oldTag.prop('attributes'), function () {
$oldTag.parent().attr(this.name, this.value);
});
}
$oldTag.contents().unwrap();
});
},
// Function call when user click on "Insert Link"
createLink: function () {
var t = this,
documentSelection = t.doc.getSelection(),
node = documentSelection.focusNode,
url,
title,
target;
while (['A', 'DIV'].indexOf(node.nodeName) < 0) {
node = node.parentNode;
}
if (node && node.nodeName === 'A') {
var $a = $(node);
url = $a.attr('href');
title = $a.attr('title');
target = $a.attr('target');
var range = t.doc.createRange();
range.selectNode(node);
documentSelection.removeAllRanges();
documentSelection.addRange(range);
}
t.saveRange();
t.openModalInsert(t.lang.createLink, {
url: {
label: 'URL',
required: true,
value: url
},
title: {
label: t.lang.title,
value: title
},
text: {
label: t.lang.text,
value: t.getRangeText()
},
target: {
label: t.lang.target,
value: target
}
}, function (v) { // v is value
var link = $(['<a href="', v.url, '">', v.text, '</a>'].join(''));
if (v.title.length > 0) {
link.attr('title', v.title);
}
if (v.target.length > 0) {
link.attr('target', v.target);
}
t.range.deleteContents();
t.range.insertNode(link[0]);
return true;
});
},
unlink: function () {
var t = this,
documentSelection = t.doc.getSelection(),
node = documentSelection.focusNode;
if (documentSelection.isCollapsed) {
while (['A', 'DIV'].indexOf(node.nodeName) < 0) {
node = node.parentNode;
}
if (node && node.nodeName === 'A') {
var range = t.doc.createRange();
range.selectNode(node);
documentSelection.removeAllRanges();
documentSelection.addRange(range);
}
}
t.execCmd('unlink', undefined, undefined, true);
},
insertImage: function () {
var t = this;
t.saveRange();
t.openModalInsert(t.lang.insertImage, {
url: {
label: 'URL',
required: true
},
alt: {
label: t.lang.description,
value: t.getRangeText()
}
}, function (v) { // v are values
t.execCmd('insertImage', v.url);
$('img[src="' + v.url + '"]:not([alt])', t.$box).attr('alt', v.alt);
return true;
});
},
fullscreen: function () {
var t = this,
prefix = t.o.prefix,
fullscreenCssClass = prefix + 'fullscreen',
isFullscreen;
t.$box.toggleClass(fullscreenCssClass);
isFullscreen = t.$box.hasClass(fullscreenCssClass);
$('body').toggleClass(prefix + 'body-fullscreen', isFullscreen);
$(window).trigger('scroll');
t.$c.trigger('tbw' + (isFullscreen ? 'open' : 'close') + 'fullscreen');
},
/*
* Call method of trumbowyg if exist
* else try to call anonymous function
* and finaly native execCommand
*/
execCmd: function (cmd, param, forceCss, skipTrumbowyg) {
var t = this;
skipTrumbowyg = !!skipTrumbowyg || '';
if (cmd !== 'dropdown') {
t.$ed.focus();
}
try {
t.doc.execCommand('styleWithCSS', false, forceCss || false);
} catch (c) {
}
try {
t[cmd + skipTrumbowyg](param);
} catch (c) {
try {
cmd(param);
} catch (e2) {
if (cmd === 'insertHorizontalRule') {
param = undefined;
} else if (cmd === 'formatBlock' && t.isIE) {
param = '<' + param + '>';
}
t.doc.execCommand(cmd, false, param);
t.syncCode();
t.semanticCode(false, true);
}
if (cmd !== 'dropdown') {
t.updateButtonPaneStatus();
t.$c.trigger('tbwchange');
}
}
},
// Open a modal box
openModal: function (title, content) {
var t = this,
prefix = t.o.prefix;
// No open a modal box when exist other modal box
if ($('.' + prefix + 'modal-box', t.$box).length > 0) {
return false;
}
if (t.o.autogrowOnEnter) {
t.autogrowOnEnterDontClose = true;
}
t.saveRange();
t.showOverlay();
// Disable all btnPane btns
t.$btnPane.addClass(prefix + 'disable');
// Build out of ModalBox, it's the mask for animations
var $modal = $('<div/>', {
class: prefix + 'modal ' + prefix + 'fixed-top'
}).css({
top: t.$btnPane.height()
}).appendTo(t.$box);
// Click on overlay close modal by cancelling them
t.$overlay.one('click', function () {
$modal.trigger(CANCEL_EVENT);
return false;
});
// Build the form
var $form = $('<form/>', {
action: '',
html: content
})
.on('submit', function () {
$modal.trigger(CONFIRM_EVENT);
return false;
})
.on('reset', function () {
$modal.trigger(CANCEL_EVENT);
return false;
})
.on('submit reset', function () {
if (t.o.autogrowOnEnter) {
t.autogrowOnEnterDontClose = false;
}
});
// Build ModalBox and animate to show them
var $box = $('<div/>', {
class: prefix + 'modal-box',
html: $form
})
.css({
top: '-' + t.$btnPane.outerHeight() + 'px',
opacity: 0
})
.appendTo($modal)
.animate({
top: 0,
opacity: 1
}, 100);
// Append title
$('<span/>', {
text: title,
class: prefix + 'modal-title'
}).prependTo($box);
$modal.height($box.outerHeight() + 10);
// Focus in modal box
$('input:first', $box).focus();
// Append Confirm and Cancel buttons
t.buildModalBtn('submit', $box);
t.buildModalBtn('reset', $box);
$(window).trigger('scroll');
return $modal;
},
// @param n is name of modal
buildModalBtn: function (n, $modal) {
var t = this,
prefix = t.o.prefix;
return $('<button/>', {
class: prefix + 'modal-button ' + prefix + 'modal-' + n,
type: n,
text: t.lang[n] || n
}).appendTo($('form', $modal));
},
// close current modal box
closeModal: function () {
var t = this,
prefix = t.o.prefix;
t.$btnPane.removeClass(prefix + 'disable');
t.$overlay.off();
// Find the modal box
var $modalBox = $('.' + prefix + 'modal-box', t.$box);
$modalBox.animate({
top: '-' + $modalBox.height()
}, 100, function () {
$modalBox.parent().remove();
t.hideOverlay();
});
t.restoreRange();
},
// Preformated build and management modal
openModalInsert: function (title, fields, cmd) {
var t = this,
prefix = t.o.prefix,
lg = t.lang,
html = '';
$.each(fields, function (fieldName, field) {
var l = field.label,
n = field.name || fieldName,
a = field.attributes || {};
var attr = Object.keys(a).map(function (prop) {
return prop + '="' + a[prop] + '"';
}).join(' ');
html += '<label><input type="' + (field.type || 'text') + '" name="' + n + '" value="' + (field.value || '').replace(/"/g, '&quot;') + '"' + attr + '><span class="' + prefix + 'input-infos"><span>' +
((!l) ? (lg[fieldName] ? lg[fieldName] : fieldName) : (lg[l] ? lg[l] : l)) +
'</span></span></label>';
});
return t.openModal(title, html)
.on(CONFIRM_EVENT, function () {
var $form = $('form', $(this)),
valid = true,
values = {};
$.each(fields, function (fieldName, field) {
var $field = $('input[name="' + fieldName + '"]', $form),
inputType = $field.attr('type');
if (inputType.toLowerCase() === 'checkbox') {
values[fieldName] = $field.is(':checked');
} else {
values[fieldName] = $.trim($field.val());
}
// Validate value
if (field.required && values[fieldName] === '') {
valid = false;
t.addErrorOnModalField($field, t.lang.required);
} else if (field.pattern && !field.pattern.test(values[fieldName])) {
valid = false;
t.addErrorOnModalField($field, field.patternError);
}
});
if (valid) {
t.restoreRange();
if (cmd(values, fields)) {
t.syncCode();
t.$c.trigger('tbwchange');
t.closeModal();
$(this).off(CONFIRM_EVENT);
}
}
})
.one(CANCEL_EVENT, function () {
$(this).off(CONFIRM_EVENT);
t.closeModal();
});
},
addErrorOnModalField: function ($field, err) {
var prefix = this.o.prefix,
$label = $field.parent();
$field
.on('change keyup', function () {
$label.removeClass(prefix + 'input-error');
});
$label
.addClass(prefix + 'input-error')
.find('input+span')
.append(
$('<span/>', {
class: prefix + 'msg-error',
text: err
})
);
},
getDefaultImgDblClickHandler: function () {
var t = this;
return function () {
var $img = $(this),
src = $img.attr('src'),
base64 = '(Base64)';
if (src.indexOf('data:image') === 0) {
src = base64;
}
t.openModalInsert(t.lang.insertImage, {
url: {
label: 'URL',
value: src,
required: true
},
alt: {
label: t.lang.description,
value: $img.attr('alt')
}
}, function (v) {
if (v.src !== base64) {
$img.attr({
src: v.src
});
}
$img.attr({
alt: v.alt
});
return true;
});
return false;
};
},
// Range management
saveRange: function () {
var t = this,
documentSelection = t.doc.getSelection();
t.range = null;
if (documentSelection.rangeCount) {
var savedRange = t.range = documentSelection.getRangeAt(0),
range = t.doc.createRange(),
rangeStart;
range.selectNodeContents(t.$ed[0]);
range.setEnd(savedRange.startContainer, savedRange.startOffset);
rangeStart = (range + '').length;
t.metaRange = {
start: rangeStart,
end: rangeStart + (savedRange + '').length
};
}
},
restoreRange: function () {
var t = this,
metaRange = t.metaRange,
savedRange = t.range,
documentSelection = t.doc.getSelection(),
range;
if (!savedRange) {
return;
}
if (metaRange && metaRange.start !== metaRange.end) { // Algorithm from http://jsfiddle.net/WeWy7/3/
var charIndex = 0,
nodeStack = [t.$ed[0]],
node,
foundStart = false,
stop = false;
range = t.doc.createRange();
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType === 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && metaRange.start >= charIndex && metaRange.start <= nextCharIndex) {
range.setStart(node, metaRange.start - charIndex);
foundStart = true;
}
if (foundStart && metaRange.end >= charIndex && metaRange.end <= nextCharIndex) {
range.setEnd(node, metaRange.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var cn = node.childNodes,
i = cn.length;
while (i > 0) {
i -= 1;
nodeStack.push(cn[i]);
}
}
}
}
documentSelection.removeAllRanges();
documentSelection.addRange(range || savedRange);
},
getRangeText: function () {
return this.range + '';
},
updateButtonPaneStatus: function () {
var t = this,
prefix = t.o.prefix,
tags = t.getTagsRecursive(t.doc.getSelection().focusNode),
activeClasses = prefix + 'active-button ' + prefix + 'active';
$('.' + prefix + 'active-button', t.$btnPane).removeClass(activeClasses);
$.each(tags, function (i, tag) {
var btnName = t.tagToButton[tag.toLowerCase()],
$btn = $('.' + prefix + btnName + '-button', t.$btnPane);
if ($btn.length > 0) {
$btn.addClass(activeClasses);
} else {
try {
$btn = $('.' + prefix + 'dropdown .' + prefix + btnName + '-dropdown-button', t.$box);
var dropdownBtnName = $btn.parent().data('dropdown');
$('.' + prefix + dropdownBtnName + '-button', t.$box).addClass(activeClasses);
} catch (e) {
}
}
});
},
getTagsRecursive: function (element, tags) {
var t = this;
tags = tags || (element && element.tagName ? [element.tagName] : []);
if (element && element.parentNode) {
element = element.parentNode;
} else {
return tags;
}
var tag = element.tagName;
if (tag === 'DIV') {
return tags;
}
if (tag === 'P' && element.style.textAlign !== '') {
tags.push(element.style.textAlign);
}
$.each(t.tagHandlers, function (i, tagHandler) {
tags = tags.concat(tagHandler(element, t));
});
tags.push(tag);
return t.getTagsRecursive(element, tags).filter(function(tag) { return tag != null; });
},
// Plugins
initPlugins: function () {
var t = this;
t.loadedPlugins = [];
$.each($.trumbowyg.plugins, function (name, plugin) {
if (!plugin.shouldInit || plugin.shouldInit(t)) {
plugin.init(t);
if (plugin.tagHandler) {
t.tagHandlers.push(plugin.tagHandler);
}
t.loadedPlugins.push(plugin);
}
});
},
destroyPlugins: function () {
$.each(this.loadedPlugins, function (i, plugin) {
if (plugin.destroy) {
plugin.destroy();
}
});
}
};
})(navigator, window, document, jQuery);