diff --git a/bennu-toolkit/pom.xml b/bennu-toolkit/pom.xml index f3c40311b..ec4885634 100644 --- a/bennu-toolkit/pom.xml +++ b/bennu-toolkit/pom.xml @@ -21,6 +21,9 @@ false META-INF/resources + + src/main/resources + src/main/webapp @@ -58,6 +61,7 @@ datetime.js codeEditor.js spinner.js + browser.js libs/MutationObserverPolyfill.js mutations.js @@ -93,6 +97,7 @@ datetime.js codeEditor.js spinner.js + browser.js bennu-angular.js ${project.build.outputDirectory}/META-INF/resources/ @@ -115,13 +120,33 @@ org.fenixedu bennu-core ${project.version} + + + org.jsoup + jsoup + 1.8.2 + + + javax.ws.rs + javax.ws.rs-api org.webjars ace 1.1.9 - + + com.googlecode.owasp-java-html-sanitizer + owasp-java-html-sanitizer + r239 + + + jsr305 + com.google.code.findbugs + + + + diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/Sanitization.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/Sanitization.java new file mode 100644 index 000000000..60396b90a --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/Sanitization.java @@ -0,0 +1,100 @@ +package org.fenixedu.bennu.toolkit; + +import java.util.Locale; +import java.util.function.Function; + +import org.fenixedu.commons.i18n.LocalizedString; +import org.owasp.html.HtmlPolicyBuilder; +import org.owasp.html.PolicyFactory; + +public class Sanitization { + + private static PolicyFactory TOOLKIT_SANITIZER = new HtmlPolicyBuilder() + .allowStyling() + .allowStandardUrlProtocols() + .allowElements("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", "div", "font", "span") + .allowAttributes("href") + .onElements("a") + .allowAttributes("title") + .onElements("a") + .allowAttributes("cite") + .onElements("blockquote") + .allowAttributes("span") + .onElements("col") + .allowAttributes("width") + .onElements("col") + .allowAttributes("span") + .onElements("colgroup") + .allowAttributes("width") + .onElements("colgroup") + .allowAttributes("align") + .onElements("img") + .allowAttributes("alt") + .onElements("img") + .allowAttributes("height") + .onElements("img") + .allowAttributes("src") + .onElements("img") + .allowAttributes("title") + .onElements("img") + .allowAttributes("width") + .onElements("img") + .allowAttributes("start") + .onElements("ol") + .allowAttributes("type") + .onElements("ol") + .allowAttributes("cite") + .onElements("q") + .allowAttributes("summary") + .onElements("table") + .allowAttributes("width") + .onElements("table") + .allowAttributes("abbr") + .onElements("td") + .allowAttributes("axis") + .onElements("td") + .allowAttributes("colspan") + .onElements("td") + .allowAttributes("rowspan") + .onElements("td") + .allowAttributes("width") + .onElements("td") + .allowAttributes("abbr") + .onElements("th") + .allowAttributes("axis") + .onElements("th") + .allowAttributes("colspan") + .onElements("th") + .allowAttributes("rowspan") + .onElements("th") + .allowAttributes("scope") + .onElements("th") + .allowAttributes("width") + .onElements("th") + .allowAttributes("type") + .onElements("ul") + .allowAttributes("class", "color") + .globally() + .allowAttributes("bennu-component", "component-resizable", "data-x", "data-y", "data-height", "data-width", + "data-source", "data-metadata") + .onElements("div", "span", "a", "img", "h1", "h2", "h3", "h4", "h5", "h6", "p", "table").allowElements().toFactory(); + + private static Function sanitizer = (origin) -> TOOLKIT_SANITIZER.sanitize(origin); + + public static LocalizedString sanitize(LocalizedString origin) { + LocalizedString result = new LocalizedString(); + + for (Locale l : origin.getLocales()) { + result = result.with(l, sanitize(origin.getContent(l))); + } + + return result; + } + + public static String sanitize(String original) { + return sanitizer.apply(original); + } + +} diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/ToolkitInitializer.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/ToolkitInitializer.java new file mode 100644 index 000000000..88258323b --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/ToolkitInitializer.java @@ -0,0 +1,26 @@ +package org.fenixedu.bennu.toolkit; + +import java.util.Set; + +import javax.servlet.ServletContainerInitializer; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.HandlesTypes; + +import org.fenixedu.bennu.toolkit.components.Component; +import org.fenixedu.bennu.toolkit.components.ToolkitComponent; + +@HandlesTypes({ ToolkitComponent.class }) +public class ToolkitInitializer implements ServletContainerInitializer { + + @Override + public void onStartup(Set> c, ServletContext ctx) throws ServletException { + if (c != null) { + for (Class type : c) { + if (type.isAnnotationPresent(ToolkitComponent.class)) { + Component.register(type); + } + } + } + } +} diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/Component.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/Component.java new file mode 100644 index 000000000..d041b5366 --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/Component.java @@ -0,0 +1,60 @@ +package org.fenixedu.bennu.toolkit.components; + +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import org.fenixedu.commons.i18n.LocalizedString; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Maps; + +public abstract class Component { + + private final static Logger LOGGER = LoggerFactory.getLogger(Component.class); + + private static Map COMPONENTS = Maps.newHashMap(); + + public abstract Element process(Element element); + + public static Component getComponent(String key) { + return Component.COMPONENTS.get(key); + } + + public static Collection getComponents() { + return COMPONENTS.values(); + } + + public static String process(String origin) { + Document doc = Jsoup.parse(origin); + Elements components = doc.select("[bennu-component]"); + + for (Element component : components) { + String key = component.attr("bennu-component"); + Optional.ofNullable(COMPONENTS.get(key)).ifPresent(x -> component.replaceWith(x.process(component))); + } + + return doc.toString(); + } + + public static void register(Class type) { + ToolkitComponent annotation = type.getAnnotation(ToolkitComponent.class); + + try { + COMPONENTS.put(annotation.key(), (Component) type.newInstance()); + } catch (InstantiationException | IllegalAccessException e) { + LOGGER.error("Error while instancing a toolkit component", e); + } + } + + public static LocalizedString process(LocalizedString origin) { + return origin.map(Component::process); + } + +} diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ImageComponent.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ImageComponent.java new file mode 100644 index 000000000..8d6533223 --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ImageComponent.java @@ -0,0 +1,34 @@ +package org.fenixedu.bennu.toolkit.components; + +import org.jsoup.nodes.Element; +import org.jsoup.parser.Tag; + +@ToolkitComponent(key = "image", name = "Image", description = "Adds images", + editorFiles = { "/bennu-toolkit/js/components/images.js" }) +public class ImageComponent extends Component { + + @Override + public Element process(Element element) { + + Element image = new Element(Tag.valueOf("img"), ""); + + String width = ""; + + if (element.hasAttr("data-width") && !element.attr("data-width").equals("")) { + width = element.attr("data-width"); + } + + String height = ""; + + if (element.hasAttr("data-height") && !element.attr("data-height").equals("")) { + height = element.attr("data-height"); + } + + image.attr("width", width); + image.attr("height", height); + image.attr("src", element.attr("data-source")); + + return image; + } + +} diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/LinkComponent.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/LinkComponent.java new file mode 100644 index 000000000..0a51aef65 --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/LinkComponent.java @@ -0,0 +1,15 @@ +package org.fenixedu.bennu.toolkit.components; + +import org.jsoup.nodes.Element; + +@ToolkitComponent(key = "link", name = "Link", description = "Add a link to some place", + editorFiles = { "/bennu-toolkit/js/components/link.js" }) +public class LinkComponent extends Component { + + @Override + public Element process(Element element) { + element.removeAttr("bennu-component"); + return element; + } + +} diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/TableComponent.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/TableComponent.java new file mode 100644 index 000000000..0997b2ae0 --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/TableComponent.java @@ -0,0 +1,14 @@ +package org.fenixedu.bennu.toolkit.components; + +import org.jsoup.nodes.Element; + +@ToolkitComponent(key = "table", name = "Table", description = "Adds a table", + editorFiles = { "/bennu-toolkit/js/components/table.js" }) +public class TableComponent extends Component { + + @Override + public Element process(Element element) { + return element; + } + +} diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ToolkitComponent.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ToolkitComponent.java new file mode 100644 index 000000000..fffb534ed --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ToolkitComponent.java @@ -0,0 +1,20 @@ +package org.fenixedu.bennu.toolkit.components; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ToolkitComponent { + String key(); + + String name(); + + String description(); + + String[] editorFiles(); + + String[] viewerFiles() default {}; +} diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ToolkitResources.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ToolkitResources.java new file mode 100644 index 000000000..eff3962e1 --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/ToolkitResources.java @@ -0,0 +1,51 @@ +package org.fenixedu.bennu.toolkit.components; + +import java.io.IOException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.fenixedu.bennu.core.rest.BennuRestResource; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +@Path("/bennu-toolkit/") +public class ToolkitResources extends BennuRestResource { + + private JsonArray cachedResponse; + + @GET + @Path("components") + @Produces({ MediaType.APPLICATION_JSON }) + public Response components() throws IOException { + if (cachedResponse == null) { + cachedResponse = new JsonArray(); + + for (Component component : Component.getComponents()) { + JsonObject obj = new JsonObject(); + + ToolkitComponent annotation = component.getClass().getAnnotation(ToolkitComponent.class); + + obj.addProperty("key", annotation.key()); + obj.addProperty("name", annotation.name()); + obj.addProperty("description", annotation.description()); + JsonArray array2 = new JsonArray(); + + for (String file : annotation.editorFiles()) { + JsonPrimitive element = new JsonPrimitive(file); + array2.add(element); + } + + obj.add("files", array2); + cachedResponse.add(obj); + } + + } + return Response.ok(toJson(cachedResponse)).build(); + } +} diff --git a/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/YouTubeComponent.java b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/YouTubeComponent.java new file mode 100644 index 000000000..68334ceac --- /dev/null +++ b/bennu-toolkit/src/main/java/org/fenixedu/bennu/toolkit/components/YouTubeComponent.java @@ -0,0 +1,53 @@ +package org.fenixedu.bennu.toolkit.components; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jsoup.nodes.Element; +import org.jsoup.parser.Tag; + +@ToolkitComponent(key = "youtube", name = "YouTube", description = "Embebed a YouTube Video", + editorFiles = { "/bennu-toolkit/js/components/youtube.js" }) +public class YouTubeComponent extends Component { + + private String getYouTubeId(String url) { + String pattern = "(?<=watch\\?v=|/videos/|embed\\/)[^#\\&\\?]*"; + + Pattern compiledPattern = Pattern.compile(pattern); + Matcher matcher = compiledPattern.matcher(url); + + if (matcher.find()) { + return matcher.group(); + } else { + return ""; + } + + } + + @Override + public Element process(Element element) { + + // + + Element iframe = new Element(Tag.valueOf("iframe"), ""); + + String width = "560"; + + if (!element.attr("data-width").equals("")) { + width = element.attr("data-width"); + } + + String height = "315"; + + if (!element.attr("data-height").equals("")) { + height = element.attr("data-height"); + } + + iframe.attr("width", width); + iframe.attr("height", height); + iframe.attr("src", "https://www.youtube.com/embed/" + getYouTubeId(element.attr("data-source"))); + iframe.attr("frameborder", "0"); + + return iframe; + } +} \ No newline at end of file diff --git a/bennu-toolkit/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/bennu-toolkit/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer new file mode 100644 index 000000000..b934a50b6 --- /dev/null +++ b/bennu-toolkit/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer @@ -0,0 +1 @@ +org.fenixedu.bennu.toolkit.ToolkitInitializer \ No newline at end of file diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/css/toolkit.css b/bennu-toolkit/src/main/webapp/bennu-toolkit/css/toolkit.css index 3922f5237..303705d11 100644 --- a/bennu-toolkit/src/main/webapp/bennu-toolkit/css/toolkit.css +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/css/toolkit.css @@ -657,40 +657,6 @@ ul.menu-paragraph li{ width: 200px !important; } -.btn-group .note-table { - min-width: 0; - padding: 5px; -} -.btn-group .note-table .note-dimension-picker { - font-size: 18px; -} -.btn-group .note-table .note-dimension-picker .note-dimension-picker-mousecatcher { - position: absolute !important; - z-index: 3; - width: 10em; - height: 10em; - cursor: pointer; -} -.btn-group .note-table .note-dimension-picker .note-dimension-picker-unhighlighted { - position: relative !important; - z-index: 1; - width: 5em; - height: 5em; - background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIj4+Pjp6ekKlAqjAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKhmnaJzPAAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat; -} -.btn-group .note-table .note-dimension-picker .note-dimension-picker-highlighted { - position: absolute !important; - z-index: 2; - width: 1em; - height: 1em; - background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIjd6vvD2f9LKLW+AAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKwNDEVT0AAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat; -} - -.note-dimension-display{ - text-align: center; - color:#888; -} - .bennu-html-code-editor-container{ margin-top: 15px; } diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/bennu.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/bennu.js index 526c59387..f46edce7a 100644 --- a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/bennu.js +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/bennu.js @@ -88,6 +88,18 @@ Bennu.utils = Bennu.utils || {}; + Bennu.utils.uniqueArray = function(arr){ + var u = {}, a = []; + for(var i = 0, l = arr.length; i < l; ++i){ + if(u.hasOwnProperty(arr[i])) { + continue; + } + a.push(arr[i]); + u[arr[i]] = 1; + } + return a; + }; + Bennu.utils.updateAttrs = function(input, widgetInput, allowedAttrs){ input = $(input); diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/browser.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/browser.js new file mode 100644 index 000000000..0d3072db6 --- /dev/null +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/browser.js @@ -0,0 +1,457 @@ +(function(){ + + var t = true; + Bennu.browser = Bennu.browser || {}; + function detect(ua) { + + function getFirstMatch(regex) { + var match = ua.match(regex); + return (match && match.length > 1 && match[1]) || ''; + } + + function getSecondMatch(regex) { + var match = ua.match(regex); + return (match && match.length > 1 && match[2]) || ''; + } + + var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase(); + var likeAndroid = /like android/i.test(ua); + var android = !likeAndroid && /android/i.test(ua); + var edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i); + var versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i); + var tablet = /tablet/i.test(ua); + var mobile = !tablet && /[^-]mobi/i.test(ua); + var result + + if (/opera|opr/i.test(ua)) { + result = { + name: 'Opera', + id:'opera', + opera: true, + version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i) + } + }else if (/yabrowser/i.test(ua)) { + result = { + name: 'Yandex Browser', + yandexbrowser: true, + id:"yandex", + version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i) + } + }else if (/windows phone/i.test(ua)) { + result = { + name: 'Windows Phone', + id:"windows_phone", + windowsphone: true + } + if (edgeVersion) { + result.msedge = t + result.version = edgeVersion + } + else { + result.msie = true + result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i) + } + }else if (/msie|trident/i.test(ua)) { + result = { + name: 'Internet Explorer', + msie: true, + id: "msie", + version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i), + } + }else if (/chrome.+? edge/i.test(ua)) { + result = { + name: 'Microsoft Edge', + id:"msedge", + msedge: true, + version: edgeVersion + } + }else if (/chrome|crios|crmo/i.test(ua)) { + result = { + name: 'Chrome', + id: "chrome", + chrome: true, + version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i) + } + }else if (/kindle/i.test(ua)) { + result = { + name: 'Kindle', + id: "kindle", + chrome: true, + version: getFirstMatch(/(?:kindle)\/(\d+(\.\d+)?)/i) + } + }else if (iosdevice) { + result = { + name : iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod' + } + result.id = result.name.toLowerCase(); + + // WTF: version is not part of user agent in web apps + if (versionIdentifier) { + result.version = versionIdentifier + } + }else if (/sailfish/i.test(ua)) { + result = { + name: 'Sailfish', + id:"sailfish", + sailfish: true, + version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i) + } + }else if (/seamonkey\//i.test(ua)) { + result = { + name: 'SeaMonkey', + id:"seamonkey", + seamonkey: true, + version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i) + } + }else if (/firefox|iceweasel/i.test(ua)) { + result = { + name: 'Firefox', + firefox: t, + id: "firefox", + version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i) + } + if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) { + result.firefoxos = true + } + }else if (/silk/i.test(ua)) { + result = { + name: 'Amazon Silk', + id:'silk', + silk: true, + version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i) + } + }else if (android) { + result = { + name: 'Android', + id: 'android', + version: versionIdentifier + } + }else if (/phantom/i.test(ua)) { + result = { + name: 'PhantomJS', + id: "phantomjs", + phantom: true, + version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i) + } + }else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) { + result = { + name: 'BlackBerry', + id: "blackberry", + blackberry: true, + version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i) + } + }else if (/(web|hpw)os/i.test(ua)) { + result = { + name: 'WebOS', + webos: true, + id: "webos", + version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i) + }; + /touchpad\//i.test(ua) && (result.touchpad = t) + }else if (/bada/i.test(ua)) { + result = { + name: 'Bada', + id: 'bada', + bada: true, + version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i) + }; + }else if (/tizen/i.test(ua)) { + result = { + name: 'Tizen', + tizen: true, + id: 'tizen', + version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier + }; + } + else if (/safari/i.test(ua)) { + result = { + name: 'Safari', + safari: true, + id: 'safari', + version: versionIdentifier + } + }else { + result = { + name: getFirstMatch(/^(.*)\/(.*) /), + id: getFirstMatch(/^(.*)\/(.*) /).toLowerCase(), + version: getSecondMatch(/^(.*)\/(.*) /) + }; + } + + // set webkit or gecko flag for browsers based on these engines + if (!result.msedge && /(apple)?webkit/i.test(ua)) { + result.name = result.name || "Webkit" + result.id = result.id || "webkit" + result.webkit = true; + if (!result.version && versionIdentifier) { + result.version = versionIdentifier + } + } else if (!result.opera && /gecko\//i.test(ua)) { + result.name = result.name || "Gecko" + result.id = result.id || "gecko" + result.gecko = true + result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i) + } + + // set OS flags for platforms that have multiple browsers + if (!result.msedge && (android || result.silk)) { + result.android = true; + } else if (iosdevice) { + result[iosdevice] = t + result.ios = true; + } + + // OS version extraction + var osVersion = ''; + if (result.windowsphone) { + osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i); + } else if (iosdevice) { + osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i); + osVersion = osVersion.replace(/[_\s]/g, '.'); + } else if (android) { + osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i); + } else if (result.webos) { + osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i); + } else if (result.blackberry) { + osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i); + } else if (result.bada) { + osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i); + } else if (result.tizen) { + osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i); + } + if (osVersion) { + result.osversion = osVersion; + } + + // device type extraction + var osMajorVersion = osVersion.split('.')[0]; + if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) { + result.tablet = true; + } else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) { + result.mobile = true; + } + + // Graded Browser Support + // http://developer.yahoo.com/yui/articles/gbs + if (result.msedge || + (result.msie && result.version >= 10) || + (result.yandexbrowser && result.version >= 15) || + (result.chrome && result.version >= 20) || + (result.firefox && result.version >= 20.0) || + (result.safari && result.version >= 6) || + (result.opera && result.version >= 10.0) || + (result.ios && result.osversion && result.osversion.split(".")[0] >= 6) || + (result.blackberry && result.version >= 10.1) + ) { + result.a = t; + } + else if ((result.msie && result.version < 10) || + (result.chrome && result.version < 20) || + (result.firefox && result.version < 20.0) || + (result.safari && result.version < 6) || + (result.opera && result.version < 10.0) || + (result.ios && result.osversion && result.osversion.split(".")[0] < 6) + ) { + result.c = true; + } else result.x = true; + + return result + } + + Bennu.browser = Bennu.browser || {}; + + Bennu.browser.ua = function(x){ + if(x === undefined){ + return Bennu.browser._ua; + }else{ + Bennu.browser._ua = x; + Bennu.browser._meta = detect(x); + return x; + } + }; + + Bennu.browser.ua(typeof navigator !== 'undefined' ? navigator.userAgent : ''); + + Bennu.browser.locale = function() { + return (navigator.userLanguage || navigator.language || 'en-US').toLowerCase(); + }; + + Bennu.browser.name = function() { + return this._meta.name; + }; + + Bennu.browser.id = function() { + return this._meta.id; + }; + + Bennu.browser.version = function() { + if(this._meta.version){ + var version = this._meta.version + "" || '0.0'; + + if (version.indexOf(".") == -1){ + version = version + ".0"; + } + return new Bennu.semanticVersion(version); + }else{ + return new Bennu.semanticVersion('0.0'); + } + }; + + + Bennu.browser.is_capable = function() { + return this.uses_webkit() || this.is_firefox() || this.is_opera() || (this.is_ie() && this.version().major >= 7); + }; + + Bennu.browser.is_supported = function() { + return this.uses_webkit() || this.is_firefox() || this.is_opera() || (this.is_ie() && this.version().major >= 9); + }; + + Bennu.browser.uses_webkit = function() { + return this._meta.webkit || false; + }; + + Bennu.browser.on_ios = function() { + return this.on_ipod() || this.on_ipad() || this.on_iphone(); + }; + + Bennu.browser.on_mobile = function() { + return this._meta.mobile; + }; + + Bennu.browser.on_blackberry = function() { + return this._meta.id == "blackberry"; + }; + + Bennu.browser.on_android = function() { + return this._meta.id == "android"; + }; + + Bennu.browser.on_iphone = function() { + return this._meta.id == "iphone"; + }; + + Bennu.browser.on_ipad = function() { + return this._meta.id == "ipad"; + } + + Bennu.browser.on_ipod = function() { + return this._meta.id == "ipod"; + }; + + Bennu.browser.is_safari = function() { + return this._meta.id == "safari"; + }; + + + Bennu.browser.is_firefox = function() { + return this._meta.id == "firefox"; + } + + Bennu.browser.is_chrome = function() { + return this._meta.id == "chrome" + }; + + Bennu.browser.is_ie = function() { + return this._meta.id == "msie" + }; + + Bennu.browser.is_ie6 = function() { + return this.is_ie() && this.version().major === '6'; + }; + + Bennu.browser.is_ie7 = function() { + return this.is_ie() && this.version().major === '7'; + }; + Bennu.browser.is_ie8 = function() { + return this.is_ie() && this.version().major === '8'; + }; + + Bennu.browser.is_ie9 = function() { + return this.is_ie() && this.version().major === '9'; + }; + + Bennu.browser.is_ie10 = function() { + return this.is_ie() && this.version().major === '10'; + }; + + Bennu.browser.is_ie11 = function() { + return this.is_ie() && this.version().major === '11'; + }; + + Bennu.browser.is_opera = function() { + return this._meta.id == "opera" + }; + + Bennu.browser.on_mac = function() { + return !!this._ua.match(/Mac OS X/); + }; + + Bennu.browser.on_windows = function() { + return !!this._ua.match(/Windows/); + }; + + Bennu.browser.on_linux = function() { + return !!this._ua.match(/Linux/); + }; + + Bennu.browser.on_tablet = function() { + return this._meta.tablet || false; + }; + + Bennu.browser.on_kindle = function() { + return !!this._ua.match(/Kindle/); + }; + + Bennu.browser.platform = function() { + if (this.on_linux()) { + return 'linux'; + } else if (this.on_mac()) { + return 'mac'; + } else if (this.on_windows()) { + return 'windows'; + } else { + return 'other'; + } + }; + + Bennu.browser.osVersion = function() { + if (this._meta.osVersion){ + if (this._meta.osVersion.indexOf(".") == -1){ + return this._meta.osVersion(this._meta.osVersion + ".0"); + }else{ + return this._meta.osVersion(this._meta.osVersion); + } + }else{ + return new Bennu.semanticVersion("0.0"); + } + }; + + Bennu.browser.meta = function() { + return this._meta; + }; + + Bennu.browser.a = function(cb) { + if (this._meta.a){ + cb && cb(); + } + return this; + }; + + Bennu.browser.c = function(cb) { + if (this._meta.c){ + cb && cb(); + } + return this; + }; + + Bennu.browser.x = function(cb) { + if (this._meta.c){ + cb && cb(); + } + return this; + }; + + Bennu.browser.toString = function() { + return this.name() + " " + this.version().toString(); + } +})() \ No newline at end of file diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/images.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/images.js new file mode 100644 index 000000000..b5c266f9b --- /dev/null +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/images.js @@ -0,0 +1,172 @@ +(function(){ + + function handleFiles(files,handler){ + handler.files(files, function(urls){ + var arr = []; + for (var i = 0; i < urls.length; i++) { + var o = urls[i]; + arr.push(Bennu.htmlEditor.components.callback.image.renderer(o)); + } + + if (arr.length == 1){ + Bennu.htmlEditor.components.callback.image.renderer(urls[0],function(e){ + editor(e,handler,true); + }); + }else{ + handler.text(arr); + Bennu.htmlEditor.components.hideModal(); + } + }); + } + + function uploader(element, handler){ + Bennu.htmlEditor.components.setTitle("Images"); + Bennu.htmlEditor.components.setSubtitle("Add a picture"); + + var uploader = '
'+ + '
'+ + '

Drag and Drop filesUpload files to this directory

'+ + '
' + + + '

Or just select a file

'+ + + '
'+ + '
'+ + ''+ + '
'+ + '
'; + + uploader = $('
' + uploader + '
'); + + Bennu.htmlEditor.components.setBody(uploader); + Bennu.htmlEditor.components.showPrimaryButton(); + Bennu.htmlEditor.components.setPrimaryButton("Make"); + Bennu.htmlEditor.components.hidePrimaryButton(); + + + // Drag and Drop box + $(".drop-box", uploader).on("dragenter dragover", function(evt){ + evt.stopPropagation(); + evt.preventDefault(); + $(".drop-box", uploader).addClass("dragover"); + }); + $(".drop-box", uploader).on("dragexit dragleave", function(evt){ + evt.stopPropagation(); + evt.preventDefault(); + $(".drop-box", uploader).removeClass("dragover"); + }); + + $(".drop-box", uploader).on("drop", function(evt){ + $(".drop-box", uploader).removeClass("dragover"); + + var dataTransfer = evt.originalEvent.dataTransfer; + evt.stopPropagation(); + evt.preventDefault(); + if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) { + handleFiles(dataTransfer.files,handler); + } + }); + + // Input file box + $("#file", uploader).on("click",function(){ + this.value = null; + }) + + $("#file", uploader).on("change",function(){ + handleFiles(this.files,handler); + }) + } + + function editor(element, handler,isnew){ + element = $(element); + Bennu.htmlEditor.components.setTitle("Images"); + Bennu.htmlEditor.components.setSubtitle((isnew?"Add":"Edit") + " a picture"); + + var editor = '
'+ + '

Preview

'+ + "
"+ + "'"+ + "
"+ + '
'+ + + '
'+ + ''+ + '
'+ + ''+ + '

'+ + '
'+ + ''+ + '
'+ + ''+ + '

'+ + '
'+ + '
'; + + editor = $('
' + editor + '
'); + var h = $("#height", editor) + var w = $("#width", editor); + + h.val(element.data("height")); + w.val(element.data("width")); + + Bennu.htmlEditor.components.setBody(editor); + Bennu.htmlEditor.components.showPrimaryButton(); + Bennu.htmlEditor.components.setPrimaryButton(isnew?"Add":"Edit"); + Bennu.htmlEditor.components.clickPrimaryButton(function(){ + handler.restore(); + + var height = parseInt(h.val()) || null; + var width = parseInt(w.val()) || null; + + + element.data("height",height); + element.attr("data-height", height); + + element.data("width",width); + element.attr("data-width", width); + + handler.text(element); + Bennu.htmlEditor.components.hideModal(); + }); + } + + Bennu.htmlEditor.components.callback.image = function(element,handler){ + if (element){ + editor(element, handler); + }else{ + uploader(element, handler); + } + }; + + Bennu.htmlEditor.components.callback.image.renderer = function(url,cb){ + var img = new Image(); + + img.onload = function() { + var e = $(img); + e.attr("data-height", this.height); + e.attr("data-width", this.width); + cb && cb(img); + }; + + img.setAttribute("bennu-component", "image"); + img.setAttribute("component-resizable", ""); + img.setAttribute("data-source", url); + img.src = url; + + return $(img); + } + + function preview(element){ + element.css({ + height:parseInt(element.data("height")), + width:parseInt(element.data("width")) + }); + } + + Bennu.htmlEditor.components.preview.image = preview + +})(); diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/link.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/link.js new file mode 100644 index 000000000..ab5b65b99 --- /dev/null +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/link.js @@ -0,0 +1,52 @@ +function editor(element, handler){ + Bennu.htmlEditor.components.setTitle("Link"); + Bennu.htmlEditor.components.setSubtitle((element?"Edit":"Add") + " a hyperlink"); + + var output = '
'+ + ''+ + '
'+ + ''+ + '

'+ + '
'+ + '
' + + + '
'+ + ''+ + '
'+ + ''+ + '

Use the full URL, including HTTP(S)

'+ + '
'+ + '
'; + + output = $('
' + output + '
'); + var z = $("#text", output); + var v = $("#url", output); + if (element){ + z.val(element.html()); + v.val(element.attr("href")); + }else if(handler.selection){ + z.val(handler.text()); + } + + Bennu.htmlEditor.components.setBody(output); + Bennu.htmlEditor.components.showPrimaryButton(); + Bennu.htmlEditor.components.setPrimaryButton(element?"Edit":"Add"); + Bennu.htmlEditor.components.clickPrimaryButton(function(){ + handler.restore(); + var url = $("#url",output).val(); + + if (Bennu.validation.isUrl(url)){ + var content = z.val(); + + content = content || url; + + handler.text('' + content + ''); + + Bennu.htmlEditor.components.hideModal(); + }else{ + Bennu.validation.addError($("#url",output).closest(".form-group")); + } + }); +} + +Bennu.htmlEditor.components.callback.link = editor; diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/table.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/table.js new file mode 100644 index 000000000..74bfebb82 --- /dev/null +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/table.js @@ -0,0 +1,324 @@ + +var tableMgmt = function(table,at){ + + return { + '_table':$(table)[0], + '_atRow':at.row, + '_atColumn':at.column, + findMaxColumns:function(){ + var lengths = $.map(this._table.rows,function(i,e){ + return i.children.length; + }); + return Math.max.apply(Math,lengths); + }, + 'addCellBefore':function(){ + var row = this._table.rows[this._atRow]; + var newCell = row.insertCell(this._atColumn); + var newText = document.createTextNode('\u00A0'); + newCell.appendChild(newText); + }, + 'addCellAfter':function(){ + var row = this._table.rows[this._atRow]; + var newCell = row.insertCell(this._atColumn + 1); + var newText = document.createTextNode('\u00A0'); + newCell.appendChild(newText); + }, + 'mergeCell':function(){ + + }, + 'horizontalSplit':function(){ + + }, + 'verticalSplit':function(){ + + }, + 'addRowBefore':function(){ + var row = this._table.insertRow(this._atRow); + + var max = this.findMaxColumns(); + + for (var i=0; iWidth'+ + '
'+ + ''+ + '

To use the full width keep this field empty

'+ + '
'+ + ''+ + '
'+ + ''+ + '
'+ + + '
'+ + ''+ + '
'+ + + '
'+ + ''+ + '
'; + + + if (!element){ + output = '
'+ + ''+ + '
'+ + ''+ + '

'+ + '
'+ + ''+ + '
'+ + ''+ + '

'+ + '
' + + '
' + output; + } + + output = $('
' + output + '
'); + + var columns = $("#columns", output); + var rows = $("#rows", output); + var width = $("#width", output); + var bordered = $("#bordered", output); + var header = $("#header", output); + var zebra = $("#zebra", output); + var hover = $("#hover", output); + + if (element){ + width.val(element.attr("data-width")); + var metadata = JSON.parse(element.attr("data-metadata")); + bordered.prop('checked',metadata.bordered && true || false); + header.prop('checked', $("thead",element).children().length > 0); + zebra.prop('checked',metadata.zebra && true || false); + hover.prop('checked',metadata.hover && true || false); + } + + Bennu.htmlEditor.components.setBody(output); + Bennu.htmlEditor.components.showPrimaryButton(); + Bennu.htmlEditor.components.setPrimaryButton(element?"Edit":"Add"); + Bennu.htmlEditor.components.clickPrimaryButton(function(){ + handler.restore(); + var prevent = false; + $("#width .help-block", output).html(""); + + if (width.val().trim() != ""){ + var w = parseInt(width.val()) + if (!w){ + Bennu.validation.addError($("#width",output).closest(".form-group")); + } + } + + w = w || null; + + if (!element){ + element = $(''+'
'); + var tobdy = $("tbody", element) + + for (var i = 0; i < parseInt(rows.val()); i++) { + var row = $("") + for (var j = 0; j < parseInt(columns.val()); j++) { + row.append(" "); + }; + tobdy.append(row) + }; + + if(header.prop("checked")){ + var thead = $("thead", element) + var row = $("") + for (var j = 0; j < parseInt(columns.val()); j++) { + row.append(" "); + }; + thead.append(row) + } + }else{ + var thead = $("thead", element); + var tr = $($("tbody tr", element)[0]); + var size = tr.children().length + + if (header.prop("checked") == true && thead.children().length == 0){ + var row = $("") + for (var j = 0; j < size; j++) { + row.append(" "); + }; + thead.append(row) + }else if(header.prop("checked") == false && thead.children().length > 0){ + thead.empty(); + } + } + + if (w){ + element.attr("data-width", w); + }else{ + element.attr("data-width",null); + element.removeAttr("data-width"); + } + + element.attr("data-metadata", JSON.stringify({ + zebra : zebra.prop('checked') && true || false, + hover : hover.prop('checked') && true || false, + })); + handler.text(element); + + Bennu.htmlEditor.components.hideModal(); + }); +} + +Bennu.htmlEditor.components.callback.table = editor; +Bennu.htmlEditor.components.preview.table = function(e){ + e.addClass("table"); + e.addClass("table-bordered"); + + if (e.attr("data-width") && parseInt(e.attr("data-width"))){ + e.css("width",e.attr("data-width") + "px"); + } + + if (e.attr("data-metadata")){ + var metadata = JSON.parse(e.attr("data-metadata")) + + if(metadata.zebra){ + e.addClass("table-striped"); + } + + if(metadata.hover){ + e.addClass("table-hover"); + } + } +}; + +Bennu.htmlEditor.components.menu.table = function(element, handler){ + var z = function(text,fn){ + text = $(text); + handler.add(text); + text.on("click",function(e){ + fn(); + }); + } + + var target = handler.target(); + var col = -1; + var row = -1; + + while(true){ + if (target.prop("tagName") == "TD"){ + col = target[0].cellIndex; + row = target.parent()[0].rowIndex; + break; + }if (target[0] == element || !target.length || !target){ + break; + }else{ + target = target.parent() + } + } + + var mgmt = new tableMgmt(element,{row:row, column:col}); + + z("
  • Insert cell before
  • ", function(){ + mgmt.addCellBefore(); + }); + z("
  • Insert cell after
  • ", function(){ + mgmt.addCellAfter(); + }); + + // z("
  • Merge cell
  • ", function(){ + + // }); + + // z("
  • Horizontal cell split
  • ", function(){ + + // }); + + // z("
  • Vertical cell split
  • ", function(){ + + // }); + + z("
  • Insert row before
  • ", function(){ + mgmt.addRowBefore(); + }); + + z("
  • Insert row after
  • ", function(){ + mgmt.addRowAfter(); + }); + + z("
  • Insert column before
  • ", function(){ + mgmt.addColumnBefore(); + }); + + z("
  • Insert column after
  • ", function(){ + mgmt.addColumnAfter(); + }); + + z("
  • Delete cell
  • ", function(){ + mgmt.deleteCell(); + }); + + z("
  • Delete row
  • ", function(){ + mgmt.deleteRow(); + }); + + z("
  • Delete column
  • ",function(){ + mgmt.deleteColumn(); + }); + +} diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/youtube.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/youtube.js new file mode 100644 index 000000000..16d2c1aad --- /dev/null +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/components/youtube.js @@ -0,0 +1,78 @@ +(function(){ + + function editor(element, handler){ + Bennu.htmlEditor.components.setTitle("YouTube"); + Bennu.htmlEditor.components.setSubtitle((element?"Edit":"Add") + " a Embebed YouTube Video"); + + var output = '
    '+ + ''+ + '
    '+ + ''+ + '

    Use the full URL, including HTTP(S)

    '+ + '
    '+ + '
    ' + + + '
    '+ + ''+ + '
    '+ + ''+ + '

    '+ + '
    '+ + ''+ + '
    '+ + ''+ + '

    '+ + '
    '+ + '
    ' + + ; + + output = $('
    ' + output + '
    '); + var v = $("#url", output); + var h = $("#height", output) + var w = $("#width", output); + + if (element){ + v.val(element.data("source")); + h.val(element.data("height")); + w.val(element.data("width")); + }else{ + w.val("560"); + h.val("315"); + } + + Bennu.htmlEditor.components.setBody(output); + Bennu.htmlEditor.components.showPrimaryButton(); + Bennu.htmlEditor.components.setPrimaryButton(element?"Edit":"Add"); + Bennu.htmlEditor.components.clickPrimaryButton(function(){ + handler.restore(); + var url = $("#url",output).val(); + + var height = parseInt(h.val()) || 315; + var weight = parseInt(w.val()) || 560; + + if (Bennu.validation.isUrl(url)){ + handler.text('
    '); + + Bennu.htmlEditor.components.hideModal(); + }else{ + Bennu.validation.addError($("#url",output).closest(".form-group")); + } + }); + } + + function preview(element){ + element.html('
    '); + var x = $(".drop-box",element); + x.css({ + display:"inline-block", + height:parseInt(element.data("height")), + width:parseInt(element.data("width")) + }); + element.attr("contenteditable","false"); + } + + Bennu.htmlEditor.components.callback.youtube = editor; + Bennu.htmlEditor.components.preview.youtube = preview; + +})(); diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/ensure.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/ensure.js index a4b356805..388c20dbf 100644 --- a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/ensure.js +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/ensure.js @@ -37,7 +37,6 @@ script.onload = function() { loaded[file] = true; - console.log("loaded " + file); for (var i = 0; i < cbs[file].length; i++) { cbs[file][i].call(); }; @@ -56,7 +55,16 @@ } }; - Bennu.ensure.isLoaded = function(x){ - return loaded[x] || false; + Bennu.ensure.isLoaded = function(files){ + if (typeof files == "string"){ + return loaded[files] || false; + }else{ + return Array.prototype.reduce.apply($.map(files, function(x){ + return loaded[files] || false;; + }), [function (x, y) { + return x && y; + }, true]); + } + }; })(); diff --git a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/htmlEditor.js b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/htmlEditor.js index 0963acbd1..2c50e55da 100644 --- a/bennu-toolkit/src/main/webapp/bennu-toolkit/js/htmlEditor.js +++ b/bennu-toolkit/src/main/webapp/bennu-toolkit/js/htmlEditor.js @@ -44,6 +44,341 @@ return null; } + Bennu.htmlEditor.components = Bennu.htmlEditor.components || {}; + + Bennu.htmlEditor.components.get = function(k){ + if(Bennu.htmlEditor.components.list){ + for (var i = 0; i < Bennu.htmlEditor.components.list.length; i++) { + if(Bennu.htmlEditor.components.list[i].key === k){ + return Bennu.htmlEditor.components.list[i]; + } + }; + } + } + + Bennu.htmlEditor.components.init = function(editor){ + Bennu.htmlEditor.components.list = []; + Bennu.htmlEditor.components.provided = {}; + $.get(Bennu.contextPath + "/api/bennu-toolkit/components",function(json){ + for (var i = 0; i < json.length; i++) { + var component = json[i]; + + if(!component.order){ + component.order = 0; + } + + Bennu.htmlEditor.components.list.push(component); + }; + Bennu.htmlEditor.components.reflow(editor); + }); + + Bennu.htmlEditor.components.attachModal(); + }; + + Bennu.htmlEditor.components.attachModal = function(){ + if(!$(".bennu-html-editor-component-modal").length){ + var template ='' + $(document.body).append(template); + } + } + + Bennu.htmlEditor.components.showModal = function(editor){ + $("#bennu-html-editor-component-modal").data("editor",editor) + $("#bennu-html-editor-component-modal").modal("show") + }; + + Bennu.htmlEditor.components.hideModal = function(){ + $("#bennu-html-editor-component-modal").modal("hide") + Bennu.htmlEditor.components.reflow($("#bennu-html-editor-component-modal").data("editor")); + $("#bennu-html-editor-component-modal").data("editor",null); + }; + + Bennu.htmlEditor.components.setTitle = function(t){ + $("#bennu-html-editor-component-modal .modal-header h3").html(t); + }; + + Bennu.htmlEditor.components.setSubtitle = function(t){ + $("#bennu-html-editor-component-modal .modal-header small").html(t); + }; + + Bennu.htmlEditor.components.setBody = function(t){ + $("#bennu-html-editor-component-modal .modal-body").html(t); + }; + + Bennu.htmlEditor.components.hidePrimaryButton = function(){ + $("#bennu-html-editor-component-modal .modal-footer button.btn-primary").hide(); + }; + + Bennu.htmlEditor.components.showPrimaryButton = function(){ + $("#bennu-html-editor-component-modal .modal-footer button.btn-primary").show(); + }; + + Bennu.htmlEditor.components.setPrimaryButton = function(t){ + $("#bennu-html-editor-component-modal .modal-footer button.btn-primary").html(t); + }; + + Bennu.htmlEditor.components.clickPrimaryButton = function(fn){ + $("#bennu-html-editor-component-modal .modal-footer button.btn-primary").off("click.components").on("click.components",fn); + } + + Bennu.htmlEditor.components.callback = {}; + Bennu.htmlEditor.components.preview = {}; + Bennu.htmlEditor.components.menu = {}; + + function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + + Bennu.htmlEditor.components.mkHandler = function(editor,element){ + return { + element:element, + editor : editor, + selection : Bennu.htmlEditor.saveSelection(), + files:function(files,cb){ + this.editor.closest(".bennu-html-editor-input").data("related").data("fileHandler")(files,cb); + }, + text: function(x){ + if (x === undefined){ + var html = ""; + if (this.element){ + return this.element.html(); + }else if (typeof window.getSelection != "undefined") { + var sel = this.selection; + if (sel.cloneContents) { + html = sel.cloneContents().textContent; + } + } else if (typeof document.selection != "undefined") { + if (document.selection.type == "Text") { + html = document.selection.createRange().htmlText; + } + } + return html; + }else{ + var sel, range; + if (this.element){ + var newEl = this.element.replaceWith(x); + + var r = document.createRange(); + r.selectNodeContents(newEl[0]); + + var sel=window.getSelection(); + sel.removeAllRanges(); + sel.addRange(r); + this.selection = sel; + + }else if (window.getSelection) { + if (this.selection && $(this.selection.commonAncestorContainer).closest(".bennu-html-editor-editor")[0] == this.editor[0]){ + this.selection.deleteContents(); + }else{ + this.editor.focus() + this.selection = document.createRange(); + this.selection.setStart(this.editor[0],0) + this.selection.setEnd(this.editor[0],0) + } + + var jq = $(x); + var that = this; + + // first asdd + jq = $.map(jq,function(e){ + var z = $(e); + that.selection.insertNode(z[0]); + + return z; + }); + + //r.selectNodeContents(); + var sel=window.getSelection(); + sel.removeAllRanges(); + + // then select + var selected = false + $.map(jq,function(e){ + var r = document.createRange(); + r.selectNode(e[0]); + sel.addRange(r); + }); + + this.selection = r; + } else if (document.selection && document.selection.createRange) { + range = document.selection.createRange(); + range.text = x; + } + return x; + } + }, + restore : function () { + $(editor).focus(); + Bennu.htmlEditor.restoreSelection(this.selection); + } + } + } + + Bennu.htmlEditor.components.firstStep = function(editor){ + var handler = Bennu.htmlEditor.components.mkHandler(editor); + Bennu.htmlEditor.components.clearModal(); + Bennu.htmlEditor.components.setTitle("Add New Component"); + Bennu.htmlEditor.components.setSubtitle("Choose something to add to your content"); + + var output = ""; + output += '
    '+ + '
      '; + + for (var i = 0; i < Bennu.htmlEditor.components.list.length; i++) { + var c = Bennu.htmlEditor.components.list[i] + output += '
    • ' + + '

      ' + c.name + '

      '+ + '

      ' + c.description + '

      '+ + '
    • ' + }; + + output += '
    '+ + '
    '; + + output = $(output); + + $("a", output).on("click",function(e){ + var key = $(e.target).data("type"); + + Bennu.htmlEditor.components.showEditorFor(key,null,handler); + }); + + Bennu.htmlEditor.components.setBody(output); + Bennu.htmlEditor.components.hidePrimaryButton(); + Bennu.htmlEditor.components.showModal(editor); + }; + + Bennu.htmlEditor.components.showEditorFor = function(key,element,handler){ + var c = Bennu.htmlEditor.components.get(key); + + var files = $.map(c.files,function(e){ + return Bennu.contextPath + e; + }); + + if (Bennu.ensure.isLoaded(files)){ + Bennu.htmlEditor.components.callback[c.key](element,handler); + }else{ + Bennu.htmlEditor.components.clearModal(); + Bennu.htmlEditor.components.setTitle("Loading..."); + Bennu.htmlEditor.components.setSubtitle("We are calling home"); + Bennu.htmlEditor.components.setBody("
    " + + ""+ + "
    "); + Bennu.htmlEditor.components.hidePrimaryButton(); + + Bennu.ensure(files,function(){ + Bennu.htmlEditor.components.clearModal(); + Bennu.htmlEditor.components.callback[c.key](element,handler); + }); + } + } + + Bennu.htmlEditor.components.reflow = function(editor){ + $("[bennu-component]",editor) + .off("dblclick.bennu-component").each(function(i,e){ + e=$(e); + var context = e.data("context"); + if (context) { + context.destroy(); + }; + }).on("dblclick.bennu-component", function(e){ + e=$(e.target); + e=e.closest("[bennu-component]"); + Bennu.htmlEditor.components.showModal(editor); + var handler = Bennu.htmlEditor.components.mkHandler(editor,e); + Bennu.htmlEditor.components.showEditorFor(e.attr("bennu-component"),e, handler); + }).each(function(i,element){ + element = $(element) + var key = element.attr("bennu-component"); + + var c = Bennu.htmlEditor.components.get(key); + + var files = $.map(c.files,function(e){ + return Bennu.contextPath + e; + }); + + Bennu.ensure(files,function(){ + element.removeAttr("style"); + element.removeAttr("class"); + var x = Bennu.htmlEditor.components.preview[key]; + + x && x(element); + element.contextmenu({ + target: $(".bennu-html-editor-context-menu", editor.closest(".bennu-html-editor-input")), + before: function (e) { + // This function is optional. + // Here we use it to stop the event if the user clicks a span + this.getMenu().find("ul").empty(); + e.preventDefault(); + var menu = Bennu.htmlEditor.components.menu[key] + var that = this; + menu && menu(element, { + add:function(el){ + that.getMenu().find("ul").append(el); + }, + target:function(){ + return $(e.target) + }, + closeMenu:function(){ + that.closemenu(e); + } + }); + var edit = $("
  • Edit
  • ") + edit.on("click",function(){ + that.closemenu(e); + Bennu.htmlEditor.components.showModal(editor); + var handler = Bennu.htmlEditor.components.mkHandler(editor,element); + Bennu.htmlEditor.components.showEditorFor(element.attr("bennu-component"),element,handler); + }); + + that.getMenu().find("ul").append(edit); + var del = $("
  • Delete
  • ") + del.on("click",function(){ + that.closemenu(e); + element.remove() + }); + that.getMenu().find("ul").append(del); + return true; + } + }); + }); + }); + var components = $("[bennu-component]",editor) + if(components.length > 0){ + components.each(function(i,e){ + e=$(e); + e=e.closest("[bennu-component]"); + }); + } + }; + + + Bennu.htmlEditor.components.clearModal = function(){ + var modal = $("#bennu-html-editor-component-modal"); + $(".modal-body", modal).empty(); + $(".modal-header h3", modal).html(""); + $(".modal-header small", modal).html(""); + $(".modal-footer button.btn-primary", modal).html(""); + $("#bennu-html-editor-component-modal .modal-footer button.btn-primary").off("click") + }; + Bennu.htmlEditor.restoreSelection = function(range) { if (range) { if (window.getSelection) { @@ -138,7 +473,8 @@ Bennu.contextPath + "/bennu-toolkit/js/libs/bootstrap-wysiwyg.js", Bennu.contextPath + "/bennu-toolkit/js/libs/jquery.fullscreen.js", Bennu.contextPath + "/bennu-toolkit/js/libs/sanitize.js", - Bennu.contextPath + "/bennu-toolkit/js/libs/jquery.hotkeys.js" + Bennu.contextPath + "/bennu-toolkit/js/libs/jquery.hotkeys.js", + Bennu.contextPath + "/bennu-toolkit/js/libs/bootstrap-contextmenu.js" ], function(){ var dom = $('
    ' + '
    ' + @@ -146,8 +482,12 @@ '
    ' + '
    ' + '' + + '
    '+ + ''+ + '
    '+ '
    '); - var toolbarReqs = "size,style,lists,align,links,table,image,undo,fullscreen,source"; + var toolbarReqs = "size,style,lists,align,colors,links,table,image,components,undo,fullscreen,source"; if (Bennu.utils.hasAttr(e,"toolbar")) { toolbarReqs = e.attr("toobar"); } @@ -204,29 +544,21 @@ ''); } else if (c === "links") { $(".btn-toolbar", dom).append('
    ' + - '' + - '' + + '' + '' + '
    '); } else if (c === "table"){ $(".btn-toolbar", dom).append('
    ' + - ''+ - - ''+ + ''+ + '
    '); + } else if (c === "components"){ + $(".btn-toolbar", dom).append('
    '+ + ''+ '
    '); } else if (c === "colors"){ - $(".btn-toolbar", dom).append('
    '+ + $(".btn-toolbar", dom).append('
    '+ ''+ @@ -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 '' + trHTML + '
    '; - }; - - 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)); + } + +}