'+
@@ -259,7 +591,6 @@
} else if (c === "image") {
$(".btn-toolbar", dom).append('
');
} else if (c === "undo") {
$(".btn-toolbar", dom).append('
' +
@@ -312,11 +643,45 @@
else {
console.log("error uploading file", reason, detail);
}
+
+ // TODO: attach this to the warning system
$('
' +
'File upload error ' + msg + '
').prependTo('#alerts');
}
- var s = new Sanitize(Sanitize.Config.RELAXED);
+ // HTML Sanitization Rules
+ var s = new Sanitize({
+ elements: [
+ 'a', 'b', 'blockquote', 'br', 'caption', 'cite', 'code', 'col',
+ 'colgroup', 'dd', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+ 'i', 'img', 'li', 'ol', 'p', 'pre', 'q', 'small', 'strike', 'strong',
+ 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'u',
+ 'ul'],
+
+ attributes: {
+ 'a' : ['href', 'title'],
+ 'blockquote': ['cite'],
+ 'col' : ['span', 'width'],
+ 'colgroup' : ['span', 'width'],
+ 'img' : ['align', 'alt', 'height', 'src', 'title', 'width'],
+ 'ol' : ['start', 'type'],
+ 'q' : ['cite'],
+ 'table' : ['summary', 'width'],
+ 'td' : ['abbr', 'axis', 'colspan', 'rowspan', 'width'],
+ 'th' : ['abbr', 'axis', 'colspan', 'rowspan', 'scope',
+ 'width'],
+ 'ul' : ['type'],
+ '__ALL__' : ["bennu-component", "data-height","data-x","data-y", "data-width", "data-source", "data-metadata",]
+ },
+
+ protocols: {
+ 'a' : {'href': ['ftp', 'http', 'https', 'mailto',
+ Sanitize.RELATIVE]},
+ 'blockquote': {'cite': ['http', 'https', Sanitize.RELATIVE]},
+ 'img' : {'src' : ['http', 'https', Sanitize.RELATIVE]},
+ 'q' : {'cite': ['http', 'https', Sanitize.RELATIVE]}
+ }
+ });
$('.bennu-html-editor-editor', dom).on("paste", function () {
setTimeout(function () {
@@ -360,6 +725,7 @@
Bennu.localizedString.makeLocaleList($(".bennu-localized-string-menu", dom), dom, function (e) {
Bennu.localizedString.changeData($(e.target).parent().data("locale"), $(".bennu-localized-string-language", dom), widgetInput, dom);
+ Bennu.htmlEditor.components.reflow($(".bennu-html-editor-editor", dom));
});
Bennu.localizedString.changeData(Bennu.locale, $(".bennu-localized-string-language", dom), widgetInput, dom);
@@ -374,7 +740,15 @@
}else{
$(".bennu-html-editor-editor", dom).html("");
}
-
+
+ }
+
+ if ($(".bennu-html-editor-components-button", dom).length){
+ var editor = $(".bennu-html-editor-editor",dom);
+ Bennu.htmlEditor.components.init(editor);
+ $(".bennu-html-editor-components-button", dom).on("click",function(){
+ Bennu.htmlEditor.components.firstStep(editor);
+ });
}
$(".bennu-html-editor-editor", dom).on('change', function () {
@@ -406,26 +780,54 @@
});
$(".btn-toolbar .pictureBtn", dom).on("click", function () {
- $(".btn-toolbar input[name='pictureTlb']", dom).click();
+ var editor = $(".bennu-html-editor-editor", dom);
+ var handler = Bennu.htmlEditor.components.mkHandler(editor);
+ Bennu.htmlEditor.components.showModal(editor);
+ Bennu.htmlEditor.components.showEditorFor("image",null,handler);
});
var attachPicture = function(urls){
+ var c = Bennu.htmlEditor.components.get("image");
+
+ var files = $.map(c.files,function(e){
+ return Bennu.contextPath + e;
+ });
+
+ Bennu.ensure(files, function(){
+ var editor = $(".bennu-html-editor-editor", dom);
- $(".bennu-html-editor-editor", dom).focus()
+ var handler = editor.data("handlerTMP");
+ editor.data("handlerTMP",null);
+ handler.restore();
+
+ if (urls.length == 1){
+ Bennu.htmlEditor.components.callback.image.renderer(urls[0],function(e){
+ Bennu.htmlEditor.components.showModal(editor);
+ Bennu.htmlEditor.components.showEditorFor("image",e,handler);
+ });
+ }else{
+ var arr = [];
for (var i = 0; i < urls.length; i++) {
var o = urls[i];
- document.execCommand('insertimage', 0, o);
+ arr.push(Bennu.htmlEditor.components.callback.image.renderer(o));
}
- };
+ handler.text(arr);
+ Bennu.htmlEditor.components.reflow(editor);
+ }
+ });
+
+ };
- $(".btn-toolbar input[name='pictureTlb']", dom).on("change", function (evt) {
- var z = e.data("fileHandler");
- z && z($(".btn-toolbar input[name='pictureTlb']", dom)[0].files, attachPicture);
- });
dom.on('dragenter dragover', false)
.on('drop', function (evt) {
+ var editor = $(".bennu-html-editor-editor", dom);
+ editor.focus();
+ var handler = Bennu.htmlEditor.components.mkHandler(editor);
+
+ editor.data("handlerTMP",handler);
+
var dataTransfer = evt.originalEvent.dataTransfer;
evt.stopPropagation();
evt.preventDefault();
@@ -441,112 +843,28 @@
e.after(dom);
Bennu.validation.attachToForm(dom);
Bennu.utils.replaceRequired(e);
-
- // table shit
-
- var PX_PER_EM = 18;
- var hDimensionPickerMove = function (event, options) {
- var $picker = $(event.target.parentNode); // target is mousecatcher
- var $dimensionDisplay = $picker.next();
- var $catcher = $picker.find('.note-dimension-picker-mousecatcher');
- var $highlighted = $picker.find('.note-dimension-picker-highlighted');
- var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted');
-
- var posOffset;
- // HTML5 with jQuery - e.offsetX is undefined in Firefox
- if (event.offsetX === undefined) {
- var posCatcher = $(event.target).offset();
- posOffset = {
- x: event.pageX - posCatcher.left,
- y: event.pageY - posCatcher.top
- };
- } else {
- posOffset = {
- x: event.offsetX,
- y: event.offsetY
- };
- }
-
- var dim = {
- c: Math.ceil(posOffset.x / PX_PER_EM) || 1,
- r: Math.ceil(posOffset.y / PX_PER_EM) || 1
- };
-
- $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' });
- $catcher.attr('data-value', dim.c + 'x' + dim.r);
-
- if (3 < dim.c && dim.c < 10) {
- $unhighlighted.css({ width: dim.c + 1 + 'em'});
- }
-
- if (3 < dim.r && dim.r < 10) {
- $unhighlighted.css({ height: dim.r + 1 + 'em'});
- }
-
- $dimensionDisplay.html(dim.c + ' x ' + dim.r);
- };
-
- var createTable = function (colCount, rowCount) {
- var tds = [], tdHTML;
- for (var idxCol = 0; idxCol < colCount; idxCol++) {
- tds.push('
' + " " + ' | ');
- }
- tdHTML = tds.join('');
-
- var trs = [], trHTML;
- for (var idxRow = 0; idxRow < rowCount; idxRow++) {
- trs.push('
' + tdHTML + '
');
- }
- trHTML = trs.join('');
- return '
';
- };
-
- var $catcher = $(".btn-toolbar", dom).find('.note-dimension-picker-mousecatcher');
- $catcher.css({
- width: 10 + 'em',
- height: 10 + 'em'
- }).on('mousemove', function (event) {
- hDimensionPickerMove(event, {});
- });
-
- $(".note-dimension-picker-mousecatcher", dom).on("click",function(){
- var val = $(this).attr("data-value").split("x");
- $(".bennu-html-editor-editor", dom).focus();
- document.execCommand("insertHTML", false,createTable(parseInt(val[0]), parseInt(val[1])));
- });
-
- // end table shit
-
-
- $(".link-to-add-ui-btn",dom).on("click",function(){
- var select = Bennu.htmlEditor.saveSelection();
- $(".bennu-html-editor-editor", dom).data("save-selection",select);
- setTimeout(function(){
- $(".link-to-add",dom).focus();
- },100);
- });
+ $(".table-btn",dom).on("click",function(){
+ var editor = $(".bennu-html-editor-editor", dom);
+ var handler = Bennu.htmlEditor.components.mkHandler(editor);
+ Bennu.htmlEditor.components.showModal(editor);
+ Bennu.htmlEditor.components.showEditorFor("table",null,handler);
+ })
$(".link-to-add-btn", dom).on("click",function(){
- var val = $(".bennu-html-editor-editor", dom).data("save-selection");
- $(".bennu-html-editor-editor",dom).focus();
- setTimeout(function(){
- if (val){
- Bennu.htmlEditor.restoreSelection(val);
- }
-
- document.execCommand("CreateLink", false, $(".link-to-add").val());
- $(".link-to-add").val("")
- });
+ var editor = $(".bennu-html-editor-editor", dom);
+ var handler = Bennu.htmlEditor.components.mkHandler(editor);
+ Bennu.htmlEditor.components.showModal(editor);
+ Bennu.htmlEditor.components.showEditorFor("link",null,handler);
});
e.on("change.bennu", function (ev) {
if (Bennu.utils.hasAttr(e,"bennu-localized-string")) {
- var value = $(e).val();
- if (value === null || value === undefined || value === "") {
- value = "{}";
- $(e).val(value);
- }
+ var value = $(e).val();
+ if (value === null || value === undefined || value === "") {
+ value = "{}";
+ $(e).val(value);
+ }
var data = JSON.parse(value);
var locale = $(".bennu-localized-string-language", dom).data("locale");
var tag = locale.tag
@@ -571,10 +889,28 @@
$(".bennu-html-editor-editor", dom)[0].innerHTML = t;
}
}
- e.data("handler").trigger();
+ e.data("handler").trigger();
});
-
+ if (!e.data("fileHandler")){
+ e.data("fileHandler",function(files,cb){
+ var result = []
+ var total = files.length;
+ var proc = 0;
+
+ $.map(files, function(f,i){
+ var fileReader = new FileReader(f);
+ fileReader.onload = function (e) {
+ result[i] = e.target.result;
+ proc++;
+ if (proc == total){
+ cb(result);
+ }
+ }
+ fileReader.readAsDataURL(f);
+ });
+ });
+ }
var setupEditor = function(dom, cb) {
var editor = $(".bennu-html-code-editor", dom).data("editor");
diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/libs/bootstrap-contextmenu.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/libs/bootstrap-contextmenu.js
new file mode 100644
index 000000000..da688171e
--- /dev/null
+++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/libs/bootstrap-contextmenu.js
@@ -0,0 +1,200 @@
+/*!
+ * Bootstrap Context Menu
+ * Author: @sydcanem
+ * https://github.com/sydcanem/bootstrap-contextmenu
+ *
+ * Inspired by Bootstrap's dropdown plugin.
+ * Bootstrap (http://getbootstrap.com).
+ *
+ * Licensed under MIT
+ * ========================================================= */
+
+;(function($) {
+
+ 'use strict';
+
+ /* CONTEXTMENU CLASS DEFINITION
+ * ============================ */
+ var toggle = '[data-toggle="context"]';
+
+ var ContextMenu = function (element, options) {
+ this.$element = $(element);
+
+ this.before = options.before || this.before;
+ this.onItem = options.onItem || this.onItem;
+ this.scopes = options.scopes || null;
+
+ if (options.target) {
+ this.$element.data('target', options.target);
+ }
+
+ this.listen();
+ };
+
+ ContextMenu.prototype = {
+
+ constructor: ContextMenu
+ ,show: function(e) {
+
+ var $menu
+ , evt
+ , tp
+ , items
+ , relatedTarget = { relatedTarget: this, target: e.currentTarget };
+
+ if (this.isDisabled()) return;
+
+ this.closemenu();
+
+ if (this.before.call(this,e,$(e.currentTarget)) === false) return;
+
+ $menu = this.getMenu();
+ $menu.trigger(evt = $.Event('show.bs.context', relatedTarget));
+
+ tp = this.getPosition(e, $menu);
+ items = 'li:not(.divider)';
+ $menu.attr('style', '')
+ .css(tp)
+ .addClass('open')
+ .on('click.context.data-api', items, $.proxy(this.onItem, this, $(e.currentTarget)))
+ .trigger('shown.bs.context', relatedTarget);
+
+ // Delegating the `closemenu` only on the currently opened menu.
+ // This prevents other opened menus from closing.
+ $('html')
+ .on('click.context.data-api', $menu.selector, $.proxy(this.closemenu, this));
+
+ return false;
+ }
+
+ ,closemenu: function(e) {
+ var $menu
+ , evt
+ , items
+ , relatedTarget;
+
+ $menu = this.getMenu();
+
+ if(!$menu.hasClass('open')) return;
+
+ relatedTarget = { relatedTarget: this };
+ $menu.trigger(evt = $.Event('hide.bs.context', relatedTarget));
+
+ items = 'li:not(.divider)';
+ $menu.removeClass('open')
+ .off('click.context.data-api', items)
+ .trigger('hidden.bs.context', relatedTarget);
+
+ $('html')
+ .off('click.context.data-api', $menu.selector);
+ // Don't propagate click event so other currently
+ // opened menus won't close.
+ e && e.stopPropagation();
+ }
+
+ ,keydown: function(e) {
+ if (e.which == 27) this.closemenu(e);
+ }
+
+ ,before: function(e) {
+ return true;
+ }
+
+ ,onItem: function(e) {
+ return true;
+ }
+
+ ,listen: function () {
+ this.$element.on('contextmenu.context.data-api', this.scopes, $.proxy(this.show, this));
+ $('html').on('click.context.data-api', $.proxy(this.closemenu, this));
+ $('html').on('keydown.context.data-api', $.proxy(this.keydown, this));
+ }
+
+ ,destroy: function() {
+ this.$element.off('.context.data-api').removeData('context');
+ $('html').off('.context.data-api');
+ }
+
+ ,isDisabled: function() {
+ return this.$element.hasClass('disabled') ||
+ this.$element.attr('disabled');
+ }
+
+ ,getMenu: function () {
+ var selector = this.$element.data('target')
+ , $menu;
+
+ $menu = $(selector);
+
+ return $menu && $menu.length ? $menu : this.$element.find(selector);
+ }
+
+ ,getPosition: function(e, $menu) {
+ var mouseX = e.clientX
+ , mouseY = e.clientY
+ , boundsX = $(window).width()
+ , boundsY = $(window).height()
+ , menuWidth = $menu.find('.dropdown-menu').outerWidth()
+ , menuHeight = $menu.find('.dropdown-menu').outerHeight()
+ , tp = {"position":"absolute","z-index":9999}
+ , Y, X, parentOffset;
+
+ if (mouseY + menuHeight > boundsY) {
+ Y = {"top": mouseY - menuHeight + $(window).scrollTop()};
+ } else {
+ Y = {"top": mouseY + $(window).scrollTop()};
+ }
+
+ if ((mouseX + menuWidth > boundsX) && ((mouseX - menuWidth) > 0)) {
+ X = {"left": mouseX - menuWidth + $(window).scrollLeft()};
+ } else {
+ X = {"left": mouseX + $(window).scrollLeft()};
+ }
+
+ // If context-menu's parent is positioned using absolute or relative positioning,
+ // the calculated mouse position will be incorrect.
+ // Adjust the position of the menu by its offset parent position.
+ parentOffset = $menu.offsetParent().offset();
+ X.left = X.left - parentOffset.left;
+ Y.top = Y.top - parentOffset.top;
+
+ return $.extend(tp, Y, X);
+ }
+
+ };
+
+ /* CONTEXT MENU PLUGIN DEFINITION
+ * ========================== */
+
+ $.fn.contextmenu = function (option,e) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('context')
+ , options = (typeof option == 'object') && option;
+
+ if (!data) $this.data('context', (data = new ContextMenu($this, options)));
+ if (typeof option == 'string') data[option].call(data, e);
+ });
+ };
+
+ $.fn.contextmenu.Constructor = ContextMenu;
+
+ /* APPLY TO STANDARD CONTEXT MENU ELEMENTS
+ * =================================== */
+
+ $(document)
+ .on('contextmenu.context.data-api', function() {
+ $(toggle).each(function () {
+ var data = $(this).data('context');
+ if (!data) return;
+ data.closemenu();
+ });
+ })
+ .on('contextmenu.context.data-api', toggle, function(e) {
+ $(this).contextmenu('show', e);
+
+ e.preventDefault();
+ e.stopPropagation();
+ });
+
+}(jQuery));
\ No newline at end of file
diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/semver.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/semver.js
index f55c53ab8..b47b35734 100644
--- a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/semver.js
+++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/semver.js
@@ -17,7 +17,7 @@
(function(){
Bennu.semanticVersion = Bennu.semanticVersion || function(version){
- var match = /^[^\d]*(\d+)\.(\d+)\.(\d+)(.*)?/g.exec(version);
+ var match = /^[^\d]*(\d+)\.(\d+)(\.(\d+))?(.*)?/g.exec(version);
if (!match){
return null;
@@ -25,7 +25,7 @@
this.major = parseInt(match[1]);
this.minor = parseInt(match[2]);
- this.revision = parseInt(match[3]);
+ this.revision = parseInt(match[4] || 0);
var rest = match[4];
if (rest){
diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/validation.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/validation.js
index b8060c315..86f890516 100644
--- a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/validation.js
+++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/validation.js
@@ -19,6 +19,20 @@
var POST_ON_ERROR = false;
Bennu.validation = {};
+ Bennu.validation.addError = function(el,errorMessage){
+ el.addClass("has-error");
+ if(errorMessage){
+ $(".help-block", el).html(errorMessage);
+ }
+ };
+
+ Bennu.validation.resetError = function(el,errorMessage){
+ el.removeClass("has-error");
+ if(errorMessage){
+ $(".help-block", el).html(errorMessage);
+ }
+ };
+
Bennu.validation.attachToForm = function (widget) {
var form = widget.closest("form");
if (!form.data("bennu-validator")) {
@@ -27,16 +41,29 @@
var val = Array.prototype.reduce.apply([
$("[bennu-localized-string]", form).map(function (i, xx) {
xx = $(xx);
- xx.data("input").removeClass("has-error");
+ Bennu.validation.resetError(xx.data("input"))
return Bennu.validation.validateLocalizedInput(xx);
}),
$("[bennu-time],[bennu-date],[bennu-datetime]", form).map(function (i, xx) {
xx = $(xx);
- xx.data("input").removeClass("has-error");
+ Bennu.validation.resetError(xx.data("input"));
return Bennu.validation.validateDateTime(xx);
}),
+ $("[requires-url]", form).map(function (i, xx) {
+ xx = $(xx);
+ Bennu.validation.resetError(xx.data("input"));
+
+ if(Bennu.validation.isUrl(xx.val())){
+ return true;
+ }else{
+ Bennu.validation.addError(xx.data("input"),"Requires a valid URL");
+ return false;
+ }
+
+ }),
+
$("[bennu-html-editor]", form).map(function (i, xx) {
xx = $(xx);
@@ -45,7 +72,7 @@
return true;
}
- xx.data("input").removeClass("has-error");
+ Bennu.validation.resetError(xx.data("input"));
return Bennu.validation.validateInput(xx);
})], [$.merge, []]);
@@ -76,19 +103,20 @@
var val = true;
if (Bennu.utils.hasAttr(xx, "bennu-required")) {
if (!value) {
- inputObject.data("input").addClass("has-error");
-
+ var errorMessage;
if (Bennu.utils.hasAttr(inputObject, "bennu-time")) {
- $(".help-block", inputObject.data("input")).html(messages['bennu-time']);
+ errorMessage = messages['bennu-time'];
}
if (Bennu.utils.hasAttr(inputObject, "bennu-date")) {
- $(".help-block", inputObject.data("input")).html(messages['bennu-date']);
+ errorMessage = messages['bennu-date'];
}
if (Bennu.utils.hasAttr(inputObject, "bennu-datetime")) {
- $(".help-block", inputObject.data("input")).html(messages['bennu-date-time']);
+ errorMessage = messages['bennu-date-time'];
}
+
+ Bennu.validation.addError(inputObject.data("input"), errorMessage);
val = false;
}
}
@@ -101,8 +129,7 @@
var val = true;
if (Bennu.utils.hasAttr(inputObject, "bennu-required")) {
if (!value) {
- inputObject.data("input").addClass("has-error");
- $(".help-block", inputObject.data("input")).html('This field is required');
+ Bennu.validation.addError(inputObject.data("input"), 'This field is required');
val = false;
}
}
@@ -119,8 +146,7 @@
}, true);
if (!val) {
- inputObject.data("input").addClass("has-error");
- $(".help-block", inputObject.data("input")).html('You need to to insert text in all languages');
+ Bennu.validation.addError(inputObject.data("input"), 'You need to to insert text in all languages');
}
return val;
@@ -131,12 +157,15 @@
return x || y;
}, false);
if (!val) {
- inputObject.data("input").addClass("has-error");
- $(".help-block", inputObject.data("input")).html('You need to to insert text in at least one language');
+ Bennu.validation.addError(inputObject.data("input"), 'You need to to insert text in at least one language');
}
return val;
}
};
+ Bennu.validation.isUrl = function(url){
+ return url.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/) && true || false;
+ }
+
})();
diff --git a/bennu-toolkit/src/test/java/org/fenixedu/bennu/toolkit/SanitizationTest.java b/bennu-toolkit/src/test/java/org/fenixedu/bennu/toolkit/SanitizationTest.java
new file mode 100644
index 000000000..82c88876d
--- /dev/null
+++ b/bennu-toolkit/src/test/java/org/fenixedu/bennu/toolkit/SanitizationTest.java
@@ -0,0 +1,38 @@
+package org.fenixedu.bennu.toolkit;
+
+import static org.junit.Assert.assertEquals;
+
+import org.fenixedu.bennu.toolkit.Sanitization;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@RunWith(JUnit4.class)
+public class SanitizationTest {
+
+ @Test
+ public void testBasicHtml() {
+ checkNoChange("Hello world!");
+ }
+
+ @Test
+ public void testLinks() {
+ checkNoChange("
Hello");
+ }
+
+ @Test
+ public void testStyles() {
+ checkNoChange("
Hello
");
+ checkNoChange("
");
+ }
+
+ public void testEmail() {
+ checkNoChange("
hello@fenixedu.org");
+ }
+
+ private void checkNoChange(String str) {
+ assertEquals(str, Sanitization.sanitize(str));
+ }
+
+}