gijgo
Version:
Gijgo is a set of free open source javascript controls distributed under MIT License. All widgets are high performance, built on top of the jQuery JavaScript Library with built-in support for Bootstrap, Material Design and Font Awesome. They are designed
657 lines (586 loc) • 25.1 kB
JavaScript
/*
* Gijgo Editor v1.4.0
* http://gijgo.com/editor
*
* Copyright 2014, 2017 gijgo.com
* Released under the MIT license
*/
if (typeof (gj) === 'undefined') {
gj = {};
}
gj.widget = function () {
var self = this;
self.xhr = null;
self.generateGUID = function () {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
};
self.mouseX = function (e) {
if (e) {
if (e.pageX) {
return e.pageX;
} else if (e.clientX) {
return e.clientX + (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft);
} else if (e.touches && e.touches.length) {
return e.touches[0].pageX;
} else if (e.changedTouches && e.changedTouches.length) {
return e.changedTouches[0].pageX;
} else if (e.originalEvent && e.originalEvent.touches && e.originalEvent.touches.length) {
return e.originalEvent.touches[0].pageX;
} else if (e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches.length) {
return e.originalEvent.touches[0].pageX;
}
}
return null;
};
self.mouseY = function (e) {
if (e) {
if (e.pageY) {
return e.pageY;
} else if (e.clientY) {
return e.clientY + (document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop);
} else if (e.touches && e.touches.length) {
return e.touches[0].pageY;
} else if (e.changedTouches && e.changedTouches.length) {
return e.changedTouches[0].pageY;
} else if (e.originalEvent && e.originalEvent.touches && e.originalEvent.touches.length) {
return e.originalEvent.touches[0].pageY;
} else if (e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches.length) {
return e.originalEvent.touches[0].pageY;
}
}
return null;
};
};
gj.widget.prototype.init = function (jsConfig, type) {
var option, clientConfig, fullConfig;
clientConfig = $.extend(true, {}, this.getHTMLConfig() || {});
$.extend(true, clientConfig, jsConfig || {});
fullConfig = this.getConfig(clientConfig, type);
this.attr('data-guid', fullConfig.guid);
this.data(fullConfig);
// Initialize events configured as options
for (option in fullConfig) {
if (gj[type].events.hasOwnProperty(option)) {
this.on(option, fullConfig[option]);
delete fullConfig[option];
}
}
// Initialize all plugins
for (plugin in gj[type].plugins) {
if (gj[type].plugins.hasOwnProperty(plugin)) {
gj[type].plugins[plugin].configure(this, fullConfig, clientConfig);
}
}
return this;
};
gj.widget.prototype.getConfig = function (clientConfig, type) {
var config, uiLibrary, iconsLibrary, plugin;
config = $.extend(true, {}, gj[type].config.base);
uiLibrary = clientConfig.hasOwnProperty('uiLibrary') ? clientConfig.uiLibrary : config.uiLibrary;
if (gj[type].config[uiLibrary]) {
$.extend(true, config, gj[type].config[uiLibrary]);
}
iconsLibrary = clientConfig.hasOwnProperty('iconsLibrary') ? clientConfig.iconsLibrary : config.iconsLibrary;
if (gj[type].config[iconsLibrary]) {
$.extend(true, config, gj[type].config[iconsLibrary]);
}
for (plugin in gj[type].plugins) {
if (gj[type].plugins.hasOwnProperty(plugin)) {
$.extend(true, config, gj[type].plugins[plugin].config.base);
if (gj[type].plugins[plugin].config[uiLibrary]) {
$.extend(true, config, gj[type].plugins[plugin].config[uiLibrary]);
}
if (gj[type].plugins[plugin].config[iconsLibrary]) {
$.extend(true, config, gj[type].plugins[plugin].config[iconsLibrary]);
}
}
}
$.extend(true, config, clientConfig);
if (!config.guid) {
config.guid = this.generateGUID();
}
return config;
}
gj.widget.prototype.getHTMLConfig = function () {
var result = this.data(),
attrs = this[0].attributes;
if (attrs['width']) {
result.width = attrs['width'].nodeValue;
}
if (attrs['height']) {
result.height = attrs['height'].nodeValue;
}
if (attrs['align']) {
result.align = attrs['align'].nodeValue;
}
if (result && result.source) {
result.dataSource = result.source;
delete result.source;
}
return result;
};
gj.widget.prototype.createDoneHandler = function () {
var $widget = this;
return function (response) {
if (typeof (response) === 'string' && JSON) {
response = JSON.parse(response);
}
gj[$widget.data('type')].methods.render($widget, response);
};
};
gj.widget.prototype.createErrorHandler = function () {
var $widget = this;
return function (response) {
if (response && response.statusText && response.statusText !== 'abort') {
alert(response.statusText);
}
};
};
gj.widget.prototype.reload = function (params) {
var ajaxOptions, result, data = this.data(), type = this.data('type');
if (data.dataSource === undefined) {
gj[type].methods.useHtmlDataSource(this, data);
}
$.extend(data.params, params);
if ($.isArray(data.dataSource)) {
result = gj[type].methods.filter(this);
gj[type].methods.render(this, result);
} else if (typeof(data.dataSource) === 'string') {
ajaxOptions = { url: data.dataSource, data: data.params };
if (this.xhr) {
this.xhr.abort();
}
this.xhr = $.ajax(ajaxOptions).done(this.createDoneHandler()).fail(this.createErrorHandler());
} else if (typeof (data.dataSource) === 'object') {
if (!data.dataSource.data) {
data.dataSource.data = {};
}
$.extend(data.dataSource.data, data.params);
ajaxOptions = $.extend(true, {}, data.dataSource); //clone dataSource object
if (ajaxOptions.dataType === 'json' && typeof(ajaxOptions.data) === 'object') {
ajaxOptions.data = JSON.stringify(ajaxOptions.data);
}
if (!ajaxOptions.success) {
ajaxOptions.success = this.createDoneHandler();
}
if (!ajaxOptions.error) {
ajaxOptions.error = this.createErrorHandler();
}
if (this.xhr) {
this.xhr.abort();
}
this.xhr = $.ajax(ajaxOptions);
}
return this;
}
gj.documentManager = {
events: {},
subscribeForEvent: function (eventName, widgetId, callback) {
if (!gj.documentManager.events[eventName] || gj.documentManager.events[eventName].length === 0) {
gj.documentManager.events[eventName] = [{ widgetId: widgetId, callback: callback }];
$(document).on(eventName, gj.documentManager.executeCallbacks);
} else if (!gj.documentManager.events[eventName][widgetId]) {
gj.documentManager.events[eventName].push({ widgetId: widgetId, callback: callback });
} else {
throw "Event " + eventName + " for widget with guid='" + widgetId + "' is already attached.";
}
},
executeCallbacks: function (e) {
var callbacks = gj.documentManager.events[e.type];
if (callbacks) {
for (var i = 0; i < callbacks.length; i++) {
callbacks[i].callback(e);
}
}
},
unsubscribeForEvent: function (eventName, widgetId) {
var success = false,
events = gj.documentManager.events[eventName];
if (events) {
for (var i = 0; i < events.length; i++) {
if (events[i].widgetId === widgetId) {
events.splice(i, 1);
success = true;
if (events.length === 0) {
$(document).off(eventName);
delete gj.documentManager.events[eventName];
}
}
}
}
if (!success) {
throw 'The "' + eventName + '" for widget with guid="' + widgetId + '" can\'t be removed.';
}
}
};
if (typeof (gj.editor) === 'undefined') {
gj.editor = {
plugins: {},
messages: []
};
}
gj.editor.messages['en-us'] = {
bold: 'Bold',
italic: 'Italic',
strikethrough: 'Strikethrough',
underline: 'Underline',
listBulleted: 'List Bulleted',
listNumbered: 'List Numbered',
indentDecrease: 'Indent Decrease',
indentIncrease: 'Indent Increase',
alignLeft: 'Align Left',
alignCenter: 'Align Center',
alignRight: 'Align Right',
alignJustify: 'Align Justify',
undo: 'Undo',
redo: 'Redo'
};
/* global window alert jQuery */
/**
* @widget Editor
* @plugin Base
*/
gj.editor.config = {
base: {
/** The height of the editor. Numeric values are treated as pixels.
* @type number|string
* @default 300
* @example sample <!-- editor, materialicons -->
* <div id="editor"></div>
* <script>
* $('#editor').editor({
* height: 500
* });
* </script>
*/
height: 300,
/** The width of the editor. Numeric values are treated as pixels.
* @type number|string
* @default undefined
* @example sample <!-- editor, materialicons -->
* <div id="editor"></div>
* <script>
* $('#editor').editor({
* width: 900
* });
* </script>
*/
width: undefined,
/** The name of the UI library that is going to be in use. Currently we support only Material Design and Bootstrap.
* @additionalinfo The css files for Bootstrap should be manually included to the page if you use bootstrap as uiLibrary.
* @type string (materialdesign|bootstrap|bootstrap4)
* @default 'materialdesign'
* @example Material.Design <!-- editor, materialicons -->
* <div id="editor"></div>
* <script>
* $('#editor').editor({ uiLibrary: 'materialdesign' });
* </script>
* @example Bootstrap.3 <!-- fontawesome, bootstrap, editor -->
* <div class="container"><div id="editor"></div></div>
* <script>
* $('#editor').editor({
* uiLibrary: 'bootstrap'
* });
* </script>
* @example Bootstrap.4 <!-- fontawesome, bootstrap4, editor -->
* <div class="container"><div id="editor"></div></div>
* <script>
* $('#editor').editor({
* uiLibrary: 'bootstrap4'
* });
* </script>
*/
uiLibrary: 'materialdesign',
/** The name of the icons library that is going to be in use. Currently we support Material Icons and Font Awesome.
* @additionalinfo If you use Bootstrap as uiLibrary, then the iconsLibrary is set to font awesome by default.<br/>
* If you use Material Design as uiLibrary, then the iconsLibrary is set to Material Icons by default.<br/>
* The css files for Material Icons or Font Awesome should be manually included to the page where the grid is in use.
* @type (materialicons|fontawesome)
* @default 'materialicons'
* @example Base.Theme.Material.Icons <!-- materialicons, bootstrap, editor -->
* <div id="editor"></div>
* <script>
* $('#editor').editor({
* uiLibrary: 'bootstrap',
* iconsLibrary: 'materialicons'
* });
* </script>
*/
iconsLibrary: 'materialicons',
/** The language that needs to be in use.
* @type string
* @default 'en-us'
* @example French <!-- materialicons, editor -->
* <script src="../../dist/modular/editor/js/messages/messages.fr-fr.js"></script>
* <div id="editor">Hover buttons in the toolbar in order to see localized tooltips</div>
* <script>
* $("#editor").editor({
* locale: 'fr-fr'
* });
* </script>
* @example German <!-- materialicons, editor -->
* <script src="../../dist/modular/editor/js/messages/messages.de-de.js"></script>
* <div id="editor">Hover <b><u>buttons</u></b> in the toolbar in order to see localized tooltips</div>
* <script>
* $("#editor").editor({
* locale: 'de-de'
* });
* </script>
*/
locale: 'en-us',
buttons: undefined,
style: {
wrapper: 'gj-editor-md',
buttonsGroup: 'gj-button-md-group',
button: 'gj-button-md',
buttonActive: 'active'
}
},
bootstrap: {
style: {
wrapper: 'gj-editor-bootstrap',
buttonsGroup: 'btn-group',
button: 'btn btn-default gj-cursor-pointer',
buttonActive: 'active'
},
iconsLibrary: 'fontawesome'
},
bootstrap4: {
style: {
wrapper: 'gj-editor-bootstrap',
buttonsGroup: 'btn-group',
button: 'btn btn-secondary gj-cursor-pointer',
buttonActive: 'active'
},
iconsLibrary: 'fontawesome'
},
materialicons: {
icons: {
bold: '<i class="material-icons">format_bold</i>',
italic: '<i class="material-icons">format_italic</i>',
strikethrough: '<i class="material-icons">strikethrough_s</i>',
underline: '<i class="material-icons">format_underlined</i>',
listBulleted: '<i class="material-icons">format_list_bulleted</i>',
listNumbered: '<i class="material-icons">format_list_numbered</i>',
indentDecrease: '<i class="material-icons">format_indent_decrease</i>',
indentIncrease: '<i class="material-icons">format_indent_increase</i>',
alignLeft: '<i class="material-icons">format_align_left</i>',
alignCenter: '<i class="material-icons">format_align_center</i>',
alignRight: '<i class="material-icons">format_align_right</i>',
alignJustify: '<i class="material-icons">format_align_justify</i>',
undo: '<i class="material-icons">undo</i>',
redo: '<i class="material-icons">redo</i>'
}
},
fontawesome: {
icons: {
bold: '<i class="fa fa-bold" aria-hidden="true"></i>',
italic: '<i class="fa fa-italic" aria-hidden="true"></i>',
strikethrough: '<i class="fa fa-strikethrough" aria-hidden="true"></i>',
underline: '<i class="fa fa-underline" aria-hidden="true"></i>',
listBulleted: '<i class="fa fa-list-ul" aria-hidden="true"></i>',
listNumbered: '<i class="fa fa-list-ol" aria-hidden="true"></i>',
indentDecrease: '<i class="fa fa-indent" aria-hidden="true"></i>',
indentIncrease: '<i class="fa fa-outdent" aria-hidden="true"></i>',
alignLeft: '<i class="fa fa-align-left" aria-hidden="true"></i>',
alignCenter: '<i class="fa fa-align-center" aria-hidden="true"></i>',
alignRight: '<i class="fa fa-align-right" aria-hidden="true"></i>',
alignJustify: '<i class="fa fa-align-justify" aria-hidden="true"></i>',
undo: '<i class="fa fa-undo" aria-hidden="true"></i>',
redo: '<i class="fa fa-repeat" aria-hidden="true"></i>'
}
}
};
gj.editor.methods = {
init: function (jsConfig) {
gj.widget.prototype.init.call(this, jsConfig, 'editor');
this.attr('data-editor', 'true');
gj.editor.methods.initialize(this);
return this;
},
initialize: function ($editor) {
var self = this, data = $editor.data(), $group, $btn,
$body = $editor.children('div[data-role="body"]'),
$toolbar = $editor.children('div[data-role="toolbar"]');
gj.editor.methods.localization(data);
$editor.addClass(data.style.wrapper);
if (data.width) {
$editor.width(data.width);
}
if ($body.length === 0) {
$editor.wrapInner('<div data-role="body"></div>');
$body = $editor.children('div[data-role="body"]');
}
$body.attr('contenteditable', true);
$body.on('mouseup keyup mouseout', function () {
self.updateToolbar($editor, $toolbar);
});
if ($toolbar.length === 0) {
$toolbar = $('<div data-role="toolbar"></div>');
$body.before($toolbar);
for (var group in data.buttons) {
$group = $('<div />').addClass(data.style.buttonsGroup);
for (var btn in data.buttons[group]) {
$btn = $(data.buttons[group][btn]);
$btn.on('click', function () {
gj.editor.methods.executeCmd($editor, $body, $toolbar, $(this));
});
$group.append($btn);
}
$toolbar.append($group);
}
}
$body.height(data.height - $toolbar.outerHeight());
},
localization: function (data) {
var msg = gj.editor.messages[data.locale];
if (typeof (data.buttons) === 'undefined') {
data.buttons = [
[
'<button type="button" class="' + data.style.button + '" title="' + msg.bold + '" data-role="bold">' + data.icons.bold + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.italic + '" data-role="italic">' + data.icons.italic + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.strikethrough + '" data-role="strikethrough">' + data.icons.strikethrough + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.underline + '" data-role="underline">' + data.icons.underline + '</button>'
],
[
'<button type="button" class="' + data.style.button + '" title="' + msg.listBulleted + '" data-role="insertunorderedlist">' + data.icons.listBulleted + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.listNumbered + '" data-role="insertorderedlist">' + data.icons.listNumbered + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.indentDecrease + '" data-role="outdent">' + data.icons.indentDecrease + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.indentIncrease + '" data-role="indent">' + data.icons.indentIncrease + '</button>'
],
[
'<button type="button" class="' + data.style.button + '" title="' + msg.alignLeft + '" data-role="justifyleft">' + data.icons.alignLeft + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.alignCenter + '" data-role="justifycenter">' + data.icons.alignCenter + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.alignRight + '" data-role="justifyright">' + data.icons.alignRight + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.alignJustify + '" data-role="justifyfull">' + data.icons.alignJustify + '</button>'
],
[
'<button type="button" class="' + data.style.button + '" title="' + msg.undo + '" data-role="undo">' + data.icons.undo + '</button>',
'<button type="button" class="' + data.style.button + '" title="' + msg.redo + '" data-role="redo">' + data.icons.redo + '</button>'
]
];
}
},
updateToolbar: function ($editor, $toolbar) {
var data = $editor.data();
$buttons = $toolbar.find('[data-role]').each(function() {
var $btn = $(this),
cmd = $btn.attr('data-role');
if (cmd && document.queryCommandEnabled(cmd) && document.queryCommandValue(cmd) === "true") {
$btn.addClass(data.style.buttonActive);
} else {
$btn.removeClass(data.style.buttonActive);
}
});
gj.editor.events.change($editor);
},
executeCmd: function ($editor, $body, $toolbar, $btn) {
$body.focus();
document.execCommand($btn.attr('data-role'), false);
gj.editor.methods.updateToolbar($editor, $toolbar);
},
content: function ($editor, html) {
var $body = $editor.children('div[data-role="body"]');
if (typeof (html) === "undefined") {
return $body.html();
} else {
return $body.html(html);
}
},
destroy: function ($editor) {
if ($editor.attr('data-editor') === 'true') {
$editor.removeClass($editor.data().style.wrapper);
$editor.removeData();
$editor.removeAttr('data-guid');
$editor.removeAttr('data-editor');
$editor.off();
$editor.empty();
}
return $editor;
}
};
gj.editor.events = {
/**
* Triggered when the editor text is changed.
*
* @event change
* @param {object} e - event data
* @example sample <!-- editor, materialicons -->
* <div id="editor"></div>
* <script>
* $('#editor').editor({
* change: function (e) {
* alert('Change is fired');
* }
* });
* </script>
*/
change: function ($editor) {
return $editor.triggerHandler('change');
}
};
gj.editor.widget = function ($element, jsConfig) {
var self = this,
methods = gj.editor.methods;
/** Get or set html content in the body.
* @method
* @return string
* @example Get <!-- editor, materialicons -->
* <button class="gj-button-md" onclick="alert($editor.content())">Get Content</button>
* <hr/>
* <div id="editor"></div>
* <script>
* var $editor = $('#editor').editor();
* </script>
* @example Set <!-- editor, materialicons -->
* <button class="gj-button-md" onclick="$editor.content('<h1>new value</h1>')">Set Content</button>
* <hr/>
* <div id="editor"></div>
* <script>
* var $editor = $('#editor').editor();
* </script>
*/
self.content = function (html) {
return methods.content(this, html);
};
/** Remove editor functionality from the element.
* @method
* @return jquery element
* @example sample <!-- editor, materialicons -->
* <button class="gj-button-md" onclick="editor.destroy()">Destroy</button>
* <div id="editor"></div>
* <script>
* var editor = $('#editor').editor();
* </script>
*/
self.destroy = function () {
return methods.destroy(this);
};
$.extend($element, self);
if ('true' !== $element.attr('data-editor')) {
methods.init.call($element, jsConfig);
}
return $element;
};
gj.editor.widget.prototype = new gj.widget();
gj.editor.widget.constructor = gj.editor.widget;
(function ($) {
$.fn.editor = function (method) {
var $widget;
if (this && this.length) {
if (typeof method === 'object' || !method) {
return new gj.editor.widget(this, method);
} else {
$widget = new gj.editor.widget(this, null);
if ($widget[method]) {
return $widget[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else {
throw 'Method ' + method + ' does not exist.';
}
}
}
};
})(jQuery);