diff --git a/css/blueimp-gallery.min.css b/css/blueimp-gallery.min.css
new file mode 100644
index 0000000000..42a66d53ee
--- /dev/null
+++ b/css/blueimp-gallery.min.css
@@ -0,0 +1 @@
+@charset "UTF-8";.blueimp-gallery,.blueimp-gallery>.slides>.slide>.slide-content{position:absolute;top:0;right:0;bottom:0;left:0;-moz-backface-visibility:hidden}.blueimp-gallery>.slides>.slide>.slide-content{margin:auto;width:auto;height:auto;max-width:100%;max-height:100%;opacity:1}.blueimp-gallery{position:fixed;z-index:999999;overflow:hidden;background:#000;background:rgba(0,0,0,.9);opacity:0;display:none;direction:ltr;-ms-touch-action:none;touch-action:none}.blueimp-gallery-carousel{position:relative;z-index:auto;margin:1em auto;padding-bottom:56.25%;box-shadow:0 0 10px #000;-ms-touch-action:pan-y;touch-action:pan-y}.blueimp-gallery-display{display:block;opacity:1}.blueimp-gallery>.slides{position:relative;height:100%;overflow:hidden}.blueimp-gallery-carousel>.slides{position:absolute}.blueimp-gallery>.slides>.slide{position:relative;float:left;height:100%;text-align:center;-webkit-transition-timing-function:cubic-bezier(.645,.045,.355,1);-moz-transition-timing-function:cubic-bezier(.645,.045,.355,1);-ms-transition-timing-function:cubic-bezier(.645,.045,.355,1);-o-transition-timing-function:cubic-bezier(.645,.045,.355,1);transition-timing-function:cubic-bezier(.645,.045,.355,1)}.blueimp-gallery,.blueimp-gallery>.slides>.slide>.slide-content{-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;-ms-transition:opacity .2s linear;-o-transition:opacity .2s linear;transition:opacity .2s linear}.blueimp-gallery>.slides>.slide-loading{background:url(../img/loading.gif) center no-repeat;background-size:64px 64px}.blueimp-gallery>.slides>.slide-loading>.slide-content{opacity:0}.blueimp-gallery>.slides>.slide-error{background:url(../img/error.png) center no-repeat}.blueimp-gallery>.slides>.slide-error>.slide-content{display:none}.blueimp-gallery>.next,.blueimp-gallery>.prev{position:absolute;top:50%;left:15px;width:40px;height:40px;margin-top:-23px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-decoration:none;text-shadow:0 0 2px #000;text-align:center;background:#222;background:rgba(0,0,0,.5);-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;cursor:pointer;display:none}.blueimp-gallery>.next{left:auto;right:15px}.blueimp-gallery>.close,.blueimp-gallery>.title{position:absolute;top:15px;left:15px;margin:0 40px 0 0;font-size:20px;line-height:30px;color:#fff;text-shadow:0 0 2px #000;opacity:.8;display:none}.blueimp-gallery>.close{padding:15px;right:15px;left:auto;margin:-15px;font-size:30px;text-decoration:none;cursor:pointer}.blueimp-gallery>.play-pause{position:absolute;right:15px;bottom:15px;width:15px;height:15px;background:url(../img/play-pause.png) 0 0 no-repeat;cursor:pointer;opacity:.5;display:none}.blueimp-gallery-playing>.play-pause{background-position:-15px 0}.blueimp-gallery>.close:hover,.blueimp-gallery>.next:hover,.blueimp-gallery>.play-pause:hover,.blueimp-gallery>.prev:hover,.blueimp-gallery>.title:hover{color:#fff;opacity:1}.blueimp-gallery-controls>.close,.blueimp-gallery-controls>.next,.blueimp-gallery-controls>.play-pause,.blueimp-gallery-controls>.prev,.blueimp-gallery-controls>.title{display:block;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.blueimp-gallery-left>.prev,.blueimp-gallery-right>.next,.blueimp-gallery-single>.next,.blueimp-gallery-single>.play-pause,.blueimp-gallery-single>.prev{display:none}.blueimp-gallery>.close,.blueimp-gallery>.next,.blueimp-gallery>.play-pause,.blueimp-gallery>.prev,.blueimp-gallery>.slides>.slide>.slide-content{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}body:last-child .blueimp-gallery>.slides>.slide-error{background-image:url(../img/error.svg)}body:last-child .blueimp-gallery>.play-pause{width:20px;height:20px;background-size:40px 20px;background-image:url(../img/play-pause.svg)}body:last-child .blueimp-gallery-playing>.play-pause{background-position:-20px 0}.blueimp-gallery>.indicator{position:absolute;top:auto;right:15px;bottom:15px;left:15px;margin:0 40px;padding:0;list-style:none;text-align:center;line-height:10px;display:none}.blueimp-gallery>.indicator>li{display:inline-block;width:9px;height:9px;margin:6px 3px 0 3px;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;border:1px solid transparent;background:#ccc;background:rgba(255,255,255,.25) center no-repeat;border-radius:5px;box-shadow:0 0 2px #000;opacity:.5;cursor:pointer}.blueimp-gallery>.indicator>.active,.blueimp-gallery>.indicator>li:hover{background-color:#fff;border-color:#fff;opacity:1}.blueimp-gallery-controls>.indicator{display:block;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.blueimp-gallery-single>.indicator{display:none}.blueimp-gallery>.indicator{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.blueimp-gallery>.slides>.slide>.video-content>img{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto;width:auto;height:auto;max-width:100%;max-height:100%;-moz-backface-visibility:hidden}.blueimp-gallery>.slides>.slide>.video-content>video{position:absolute;top:0;left:0;width:100%;height:100%}.blueimp-gallery>.slides>.slide>.video-content>iframe{position:absolute;top:100%;left:0;width:100%;height:100%;border:none}.blueimp-gallery>.slides>.slide>.video-playing>iframe{top:0}.blueimp-gallery>.slides>.slide>.video-content>a{position:absolute;top:50%;right:0;left:0;margin:-64px auto 0;width:128px;height:128px;background:url(../img/video-play.png) center no-repeat;opacity:.8;cursor:pointer}.blueimp-gallery>.slides>.slide>.video-content>a:hover{opacity:1}.blueimp-gallery>.slides>.slide>.video-playing>a,.blueimp-gallery>.slides>.slide>.video-playing>img{display:none}.blueimp-gallery>.slides>.slide>.video-content>video{display:none}.blueimp-gallery>.slides>.slide>.video-playing>video{display:block}.blueimp-gallery>.slides>.slide>.video-loading>a{background:url(../img/loading.gif) center no-repeat;background-size:64px 64px}body:last-child .blueimp-gallery>.slides>.slide>.video-content:not(.video-loading)>a{background-image:url(../img/video-play.svg)}/*# sourceMappingURL=blueimp-gallery.min.css.map */
\ No newline at end of file
diff --git a/img/error.png b/img/error.png
new file mode 100644
index 0000000000..a5577c33ab
Binary files /dev/null and b/img/error.png differ
diff --git a/img/error.svg b/img/error.svg
new file mode 100644
index 0000000000..184206a144
--- /dev/null
+++ b/img/error.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/img/loading.gif b/img/loading.gif
new file mode 100644
index 0000000000..90f28cbdbb
Binary files /dev/null and b/img/loading.gif differ
diff --git a/img/play-pause.png b/img/play-pause.png
new file mode 100644
index 0000000000..ece6cfb9b7
Binary files /dev/null and b/img/play-pause.png differ
diff --git a/img/play-pause.svg b/img/play-pause.svg
new file mode 100644
index 0000000000..a7f1f50cd3
--- /dev/null
+++ b/img/play-pause.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/img/video-play.png b/img/video-play.png
new file mode 100644
index 0000000000..353e3a592d
Binary files /dev/null and b/img/video-play.png differ
diff --git a/img/video-play.svg b/img/video-play.svg
new file mode 100644
index 0000000000..b5ea206dde
--- /dev/null
+++ b/img/video-play.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/js/blueimp-gallery.min.js b/js/blueimp-gallery.min.js
new file mode 100644
index 0000000000..8146cd06fe
--- /dev/null
+++ b/js/blueimp-gallery.min.js
@@ -0,0 +1,3 @@
+!function(){"use strict";function t(t,e){var i;for(i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function e(t){if(!this||this.find!==e.prototype.find)return new e(t);if(this.length=0,t)if("string"==typeof t&&(t=this.find(t)),t.nodeType||t===t.window)this.length=1,this[0]=t;else{var i=t.length;for(this.length=i;i;)i-=1,this[i]=t[i]}}e.extend=t,e.contains=function(t,e){do if(e=e.parentNode,e===t)return!0;while(e);return!1},e.parseJSON=function(t){return window.JSON&&JSON.parse(t)},t(e.prototype,{find:function(t){var i=this[0]||document;return"string"==typeof t&&(t=i.querySelectorAll?i.querySelectorAll(t):"#"===t.charAt(0)?i.getElementById(t.slice(1)):i.getElementsByTagName(t)),new e(t)},hasClass:function(t){return this[0]?new RegExp("(^|\\s+)"+t+"(\\s+|$)").test(this[0].className):!1},addClass:function(t){for(var e,i=this.length;i;){if(i-=1,e=this[i],!e.className)return e.className=t,this;if(this.hasClass(t))return this;e.className+=" "+t}return this},removeClass:function(t){for(var e,i=new RegExp("(^|\\s+)"+t+"(\\s+|$)"),s=this.length;s;)s-=1,e=this[s],e.className=e.className.replace(i," ");return this},on:function(t,e){for(var i,s,n=t.split(/\s+/);n.length;)for(t=n.shift(),i=this.length;i;)i-=1,s=this[i],s.addEventListener?s.addEventListener(t,e,!1):s.attachEvent&&s.attachEvent("on"+t,e);return this},off:function(t,e){for(var i,s,n=t.split(/\s+/);n.length;)for(t=n.shift(),i=this.length;i;)i-=1,s=this[i],s.removeEventListener?s.removeEventListener(t,e,!1):s.detachEvent&&s.detachEvent("on"+t,e);return this},empty:function(){for(var t,e=this.length;e;)for(e-=1,t=this[e];t.hasChildNodes();)t.removeChild(t.lastChild);return this},first:function(){return new e(this[0])}}),"function"==typeof define&&define.amd?define(function(){return e}):(window.blueimp=window.blueimp||{},window.blueimp.helper=e)}(),function(t){"use strict";"function"==typeof define&&define.amd?define(["./blueimp-helper"],t):(window.blueimp=window.blueimp||{},window.blueimp.Gallery=t(window.blueimp.helper||window.jQuery))}(function(t){"use strict";function e(t,i){return void 0===document.body.style.maxHeight?null:this&&this.options===e.prototype.options?t&&t.length?(this.list=t,this.num=t.length,this.initOptions(i),void this.initialize()):void this.console.log("blueimp Gallery: No or empty list provided as first argument.",t):new e(t,i)}return t.extend(e.prototype,{options:{container:"#blueimp-gallery",slidesContainer:"div",titleElement:"h3",displayClass:"blueimp-gallery-display",controlsClass:"blueimp-gallery-controls",singleClass:"blueimp-gallery-single",leftEdgeClass:"blueimp-gallery-left",rightEdgeClass:"blueimp-gallery-right",playingClass:"blueimp-gallery-playing",slideClass:"slide",slideLoadingClass:"slide-loading",slideErrorClass:"slide-error",slideContentClass:"slide-content",toggleClass:"toggle",prevClass:"prev",nextClass:"next",closeClass:"close",playPauseClass:"play-pause",typeProperty:"type",titleProperty:"title",urlProperty:"href",srcsetProperty:"urlset",displayTransition:!0,clearSlides:!0,stretchImages:!1,toggleControlsOnReturn:!0,toggleControlsOnSlideClick:!0,toggleSlideshowOnSpace:!0,enableKeyboardNavigation:!0,closeOnEscape:!0,closeOnSlideClick:!0,closeOnSwipeUpOrDown:!0,emulateTouchEvents:!0,stopTouchEventsPropagation:!1,hidePageScrollbars:!0,disableScroll:!0,carousel:!1,continuous:!0,unloadElements:!0,startSlideshow:!1,slideshowInterval:5e3,index:0,preloadRange:2,transitionSpeed:400,slideshowTransitionSpeed:void 0,event:void 0,onopen:void 0,onopened:void 0,onslide:void 0,onslideend:void 0,onslidecomplete:void 0,onclose:void 0,onclosed:void 0},carouselOptions:{hidePageScrollbars:!1,toggleControlsOnReturn:!1,toggleSlideshowOnSpace:!1,enableKeyboardNavigation:!1,closeOnEscape:!1,closeOnSlideClick:!1,closeOnSwipeUpOrDown:!1,disableScroll:!1,startSlideshow:!0},console:window.console&&"function"==typeof window.console.log?window.console:{log:function(){}},support:function(e){function i(){var t,i,s=n.transition;document.body.appendChild(e),s&&(t=s.name.slice(0,-9)+"ransform",void 0!==e.style[t]&&(e.style[t]="translateZ(0)",i=window.getComputedStyle(e).getPropertyValue(s.prefix+"transform"),n.transform={prefix:s.prefix,name:t,translate:!0,translateZ:!!i&&"none"!==i})),void 0!==e.style.backgroundSize&&(n.backgroundSize={},e.style.backgroundSize="contain",n.backgroundSize.contain="contain"===window.getComputedStyle(e).getPropertyValue("background-size"),e.style.backgroundSize="cover",n.backgroundSize.cover="cover"===window.getComputedStyle(e).getPropertyValue("background-size")),document.body.removeChild(e)}var s,n={touch:void 0!==window.ontouchstart||window.DocumentTouch&&document instanceof DocumentTouch},o={webkitTransition:{end:"webkitTransitionEnd",prefix:"-webkit-"},MozTransition:{end:"transitionend",prefix:"-moz-"},OTransition:{end:"otransitionend",prefix:"-o-"},transition:{end:"transitionend",prefix:""}};for(s in o)if(o.hasOwnProperty(s)&&void 0!==e.style[s]){n.transition=o[s],n.transition.name=s;break}return document.body?i():t(document).on("DOMContentLoaded",i),n}(document.createElement("div")),requestAnimationFrame:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame,initialize:function(){return this.initStartIndex(),this.initWidget()===!1?!1:(this.initEventListeners(),this.onslide(this.index),this.ontransitionend(),void(this.options.startSlideshow&&this.play()))},slide:function(t,e){window.clearTimeout(this.timeout);var i,s,n,o=this.index;if(o!==t&&1!==this.num){if(e||(e=this.options.transitionSpeed),this.support.transform){for(this.options.continuous||(t=this.circle(t)),i=Math.abs(o-t)/(o-t),this.options.continuous&&(s=i,i=-this.positions[this.circle(t)]/this.slideWidth,i!==s&&(t=-i*this.num+t)),n=Math.abs(o-t)-1;n;)n-=1,this.move(this.circle((t>o?t:o)-n-1),this.slideWidth*i,0);t=this.circle(t),this.move(o,this.slideWidth*i,e),this.move(t,0,e),this.options.continuous&&this.move(this.circle(t-i),-(this.slideWidth*i),0)}else t=this.circle(t),this.animate(o*-this.slideWidth,t*-this.slideWidth,e);this.onslide(t)}},getIndex:function(){return this.index},getNumber:function(){return this.num},prev:function(){(this.options.continuous||this.index)&&this.slide(this.index-1)},next:function(){(this.options.continuous||this.index1&&(this.timeout=this.setTimeout(!this.requestAnimationFrame&&this.slide||function(t,i){e.animationFrameId=e.requestAnimationFrame.call(window,function(){e.slide(t,i)})},[this.index+1,this.options.slideshowTransitionSpeed],this.interval)),this.container.addClass(this.options.playingClass)},pause:function(){window.clearTimeout(this.timeout),this.interval=null,this.container.removeClass(this.options.playingClass)},add:function(t){var e;for(t.concat||(t=Array.prototype.slice.call(t)),this.list.concat||(this.list=Array.prototype.slice.call(this.list)),this.list=this.list.concat(t),this.num=this.list.length,this.num>2&&null===this.options.continuous&&(this.options.continuous=!0,this.container.removeClass(this.options.leftEdgeClass)),this.container.removeClass(this.options.rightEdgeClass).removeClass(this.options.singleClass),e=this.num-t.length;ei?(s.slidesContainer[0].style.left=e+"px",s.ontransitionend(),void window.clearInterval(o)):void(s.slidesContainer[0].style.left=(e-t)*(Math.floor(r/i*100)/100)+t+"px")},4)},preventDefault:function(t){t.preventDefault?t.preventDefault():t.returnValue=!1},stopPropagation:function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},onresize:function(){this.initSlides(!0)},onmousedown:function(t){t.which&&1===t.which&&"VIDEO"!==t.target.nodeName&&(t.preventDefault(),(t.originalEvent||t).touches=[{pageX:t.pageX,pageY:t.pageY}],this.ontouchstart(t))},onmousemove:function(t){this.touchStart&&((t.originalEvent||t).touches=[{pageX:t.pageX,pageY:t.pageY}],this.ontouchmove(t))},onmouseup:function(t){this.touchStart&&(this.ontouchend(t),delete this.touchStart)},onmouseout:function(e){if(this.touchStart){var i=e.target,s=e.relatedTarget;(!s||s!==i&&!t.contains(i,s))&&this.onmouseup(e)}},ontouchstart:function(t){this.options.stopTouchEventsPropagation&&this.stopPropagation(t);var e=(t.originalEvent||t).touches[0];this.touchStart={x:e.pageX,y:e.pageY,time:Date.now()},this.isScrolling=void 0,this.touchDelta={}},ontouchmove:function(t){this.options.stopTouchEventsPropagation&&this.stopPropagation(t);var e,i,s=(t.originalEvent||t).touches[0],n=(t.originalEvent||t).scale,o=this.index;if(!(s.length>1||n&&1!==n))if(this.options.disableScroll&&t.preventDefault(),this.touchDelta={x:s.pageX-this.touchStart.x,y:s.pageY-this.touchStart.y},e=this.touchDelta.x,void 0===this.isScrolling&&(this.isScrolling=this.isScrolling||Math.abs(e)0||o===this.num-1&&0>e?Math.abs(e)/this.slideWidth+1:1,i=[o],o&&i.push(o-1),o20||Math.abs(this.touchDelta.x)>a/2,c=!r&&this.touchDelta.x>0||r===this.num-1&&this.touchDelta.x<0,u=!d&&this.options.closeOnSwipeUpOrDown&&(h&&Math.abs(this.touchDelta.y)>20||Math.abs(this.touchDelta.y)>this.slideHeight/2);this.options.continuous&&(c=!1),e=this.touchDelta.x<0?-1:1,this.isScrolling?u?this.close():this.translateY(r,0,l):d&&!c?(i=r+e,s=r-e,n=a*e,o=-a*e,this.options.continuous?(this.move(this.circle(i),n,0),this.move(this.circle(r-2*e),o,0)):i>=0&&ithis.container[0].clientHeight&&(s.style.maxHeight=this.container[0].clientHeight),this.interval&&this.slides[this.index]===n&&this.play(),this.setTimeout(this.options.onslidecomplete,[i,n]))},onload:function(t){this.oncomplete(t)},onerror:function(t){this.oncomplete(t)},onkeydown:function(t){switch(t.which||t.keyCode){case 13:this.options.toggleControlsOnReturn&&(this.preventDefault(t),this.toggleControls());break;case 27:this.options.closeOnEscape&&(this.close(),t.stopImmediatePropagation());break;case 32:this.options.toggleSlideshowOnSpace&&(this.preventDefault(t),this.toggleSlideshow());break;case 37:this.options.enableKeyboardNavigation&&(this.preventDefault(t),this.prev());break;case 39:this.options.enableKeyboardNavigation&&(this.preventDefault(t),this.next())}},handleClick:function(e){function i(e){return t(n).hasClass(e)||t(o).hasClass(e)}var s=this.options,n=e.target||e.srcElement,o=n.parentNode;i(s.toggleClass)?(this.preventDefault(e),this.toggleControls()):i(s.prevClass)?(this.preventDefault(e),this.prev()):i(s.nextClass)?(this.preventDefault(e),this.next()):i(s.closeClass)?(this.preventDefault(e),this.close()):i(s.playPauseClass)?(this.preventDefault(e),this.toggleSlideshow()):o===this.slidesContainer[0]?s.closeOnSlideClick?(this.preventDefault(e),this.close()):s.toggleControlsOnSlideClick&&(this.preventDefault(e),this.toggleControls()):o.parentNode&&o.parentNode===this.slidesContainer[0]&&s.toggleControlsOnSlideClick&&(this.preventDefault(e),this.toggleControls())},onclick:function(t){return this.options.emulateTouchEvents&&this.touchDelta&&(Math.abs(this.touchDelta.x)>20||Math.abs(this.touchDelta.y)>20)?void delete this.touchDelta:this.handleClick(t)},updateEdgeClasses:function(t){t?this.container.removeClass(this.options.leftEdgeClass):this.container.addClass(this.options.leftEdgeClass),t===this.num-1?this.container.addClass(this.options.rightEdgeClass):this.container.removeClass(this.options.rightEdgeClass)},handleSlide:function(t){this.options.continuous||this.updateEdgeClasses(t),this.loadElements(t),this.options.unloadElements&&this.unloadElements(t),this.setTitle(t)},onslide:function(t){this.index=t,this.handleSlide(t),this.setTimeout(this.options.onslide,[t,this.slides[t]])},setTitle:function(t){var e=this.slides[t].firstChild.title,i=this.titleElement;i.length&&(this.titleElement.empty(),e&&i[0].appendChild(document.createTextNode(e)))},setTimeout:function(t,e,i){var s=this;return t&&window.setTimeout(function(){t.apply(s,e||[])},i||0)},imageFactory:function(e,i){function s(e){if(!n){if(e={type:e.type,target:o},!o.parentNode)return l.setTimeout(s,[e]);n=!0,t(a).off("load error",s),d&&"load"===e.type&&(o.style.background='url("'+h+'") center no-repeat',o.style.backgroundSize=d),i(e)}}var n,o,r,l=this,a=this.imagePrototype.cloneNode(!1),h=e,d=this.options.stretchImages;return"string"!=typeof h&&(h=this.getItemProperty(e,this.options.urlProperty),r=this.getItemProperty(e,this.options.titleProperty)),d===!0&&(d="contain"),d=this.support.backgroundSize&&this.support.backgroundSize[d]&&d,d?o=this.elementPrototype.cloneNode(!1):(o=a,a.draggable=!1),r&&(o.title=r),t(a).on("load error",s),a.src=h,o},createElement:function(e,i){var s=e&&this.getItemProperty(e,this.options.typeProperty),n=s&&this[s.split("/")[0]+"Factory"]||this.imageFactory,o=e&&n.call(this,e,i),r=this.getItemProperty(e,this.options.srcsetProperty);return o||(o=this.elementPrototype.cloneNode(!1),this.setTimeout(i,[{type:"error",target:o}])),r&&o.setAttribute("srcset",r),t(o).addClass(this.options.slideContentClass),o},loadElement:function(e){this.elements[e]||(this.slides[e].firstChild?this.elements[e]=t(this.slides[e]).hasClass(this.options.slideErrorClass)?3:2:(this.elements[e]=1,t(this.slides[e]).addClass(this.options.slideLoadingClass),this.slides[e].appendChild(this.createElement(this.list[e],this.proxyListener))))},loadElements:function(t){var e,i=Math.min(this.num,2*this.options.preloadRange+1),s=t;for(e=0;i>e;e+=1)s+=e*(e%2===0?-1:1),s=this.circle(s),this.loadElement(s)},unloadElements:function(t){var e,i;for(e in this.elements)this.elements.hasOwnProperty(e)&&(i=Math.abs(t-e),i>this.options.preloadRange&&i+this.options.preloadRanget?-this.slideWidth:this.indext;t++)this.unloadSlide(t)},toggleControls:function(){var t=this.options.controlsClass;this.container.hasClass(t)?this.container.removeClass(t):this.container.addClass(t)},toggleSlideshow:function(){this.interval?this.pause():this.play()},getNodeIndex:function(t){return parseInt(t.getAttribute("data-index"),10)},getNestedProperty:function(t,e){return e.replace(/\[(?:'([^']+)'|"([^"]+)"|(\d+))\]|(?:(?:^|\.)([^\.\[]+))/g,function(e,i,s,n,o){var r=o||i||s||n&&parseInt(n,10);e&&t&&(t=t[r])}),t},getDataProperty:function(e,i){if(e.getAttribute){var s=e.getAttribute("data-"+i.replace(/([A-Z])/g,"-$1").toLowerCase());if("string"==typeof s){if(/^(true|false|null|-?\d+(\.\d+)?|\{[\s\S]*\}|\[[\s\S]*\])$/.test(s))try{return t.parseJSON(s)}catch(n){}return s}}},getItemProperty:function(t,e){var i=t[e];return void 0===i&&(i=this.getDataProperty(t,e),void 0===i&&(i=this.getNestedProperty(t,e))),i},initStartIndex:function(){var t,e=this.options.index,i=this.options.urlProperty;if(e&&"number"!=typeof e)for(t=0;t= 301 && xhr.status <= 303)
- if (redirect && xhr.getResponseHeader('X-PJAX-REDIRECT-URL') != "" && xhr.getResponseHeader('X-PJAX-REDIRECT-URL') !== null) {
+ if (isPjaxRedirect(xhr)) {
options.url = xhr.getResponseHeader('X-PJAX-REDIRECT-URL');
- console.log('Handled redirect to: ' + options.url);
+ options.replace = true;
+ module.log.info('Handled redirect to: ' + options.url);
$.pjax(options);
} else {
orgErrorHandler(xhr, textStatus, errorThrown);
}
- }
+ };
});
};
+ var isPjaxRedirect = function (xhr) {
+ if (!xhr) {
+ return false;
+ }
+
+ var redirect = (xhr.status >= 301 && xhr.status <= 303);
+ return redirect && xhr.getResponseHeader('X-PJAX-REDIRECT-URL') != "" && xhr.getResponseHeader('X-PJAX-REDIRECT-URL') !== null;
+ };
+
var installLoader = function () {
NProgress.configure({showSpinner: false});
NProgress.configure({template: ''});
- $(document).on('pjax:start', function () {
+ $(document).on('pjax:start', function (evt, xhr, options) {
NProgress.start();
});
-
- $(document).on('pjax:end', function () {
- NProgress.done();
+
+ $(document).on('pjax:end', function (evt, xhr, options) {
+ if (!isPjaxRedirect(xhr)) {
+ NProgress.done();
+ }
});
};
diff --git a/js/humhub/humhub.core.js b/js/humhub/humhub.core.js
index a2a4e84f71..ecb8ae65a6 100644
--- a/js/humhub/humhub.core.js
+++ b/js/humhub/humhub.core.js
@@ -142,14 +142,15 @@ var humhub = humhub || (function($) {
* require('humhub.modules.ui.modal');
*
* @param {type} moduleId
+ * @param {boolean} lazy - can be set to require modules which are not yet created.
* @returns object - the module instance if already initialized else undefined
*
* */
- var require = function(moduleNS) {
- var module = resolveNameSpace(moduleNS);
+ var require = function(moduleNS, lazy) {
+ var module = resolveNameSpace(moduleNS, lazy);
if(!module) {
//TODO: load remote module dependencies
- console.warn('No module found for id: '+moduleNS);
+ console.error('No module found for namespace: '+moduleNS);
}
return module;
};
@@ -181,7 +182,7 @@ var humhub = humhub || (function($) {
return result;
} catch(e) {
var log = require('log') || console;
- log.error('Error while resolving namespace: '+typePathe, e);
+ log.error('Error while resolving namespace: '+typePath, e);
}
};
@@ -324,7 +325,7 @@ var humhub = humhub || (function($) {
var addModuleLogger = function(module, log) {
log = log || require('log');
module.log = log.module(module);
- }
+ };
//Initialize all initial modules
$(document).ready(function() {
@@ -356,13 +357,11 @@ var humhub = humhub || (function($) {
event.on('humhub:modules:client:pjax:afterPageLoad', function (evt) {
$.each(pjaxInitModules, function(i, module) {
if(module.initOnPjaxLoad) {
- module.init();
+ module.init(true);
}
});
});
-
-
return {
initModule: initModule,
modules: modules,
diff --git a/js/humhub/humhub.log.js b/js/humhub/humhub.log.js
index 6706230f68..e8d2e54fc5 100644
--- a/js/humhub/humhub.log.js
+++ b/js/humhub/humhub.log.js
@@ -80,6 +80,10 @@ humhub.initModule('log', function (module, require, $) {
Logger.prototype._log = function (msg, details, setStatus, level) {
try {
+ if (this.traceLevel > level) {
+ return;
+ }
+
if (object.isBoolean(details)) {
setStatus = details;
details = undefined;
@@ -95,10 +99,6 @@ humhub.initModule('log', function (module, require, $) {
msg = this.getMessage(msg, level, (!object.isDefined(msg) && level >= TRACE_WARN));
}
- if (this.traceLevel > level) {
- return;
- }
-
this._consoleLog(msg, level, details);
if (setStatus) {
@@ -133,7 +133,7 @@ humhub.initModule('log', function (module, require, $) {
if (window.console) {
var consoleMsg = traceLevels[level] + ' - ';
consoleMsg += this.moduleId || 'root';
- consoleMsg += msg;
+ consoleMsg += ': '+msg;
switch (level) {
case TRACE_ERROR:
case TRACE_FATAL:
diff --git a/js/humhub/humhub.ui.additions.js b/js/humhub/humhub.ui.additions.js
index 0c089cdc29..3cafc9ee0e 100644
--- a/js/humhub/humhub.ui.additions.js
+++ b/js/humhub/humhub.ui.additions.js
@@ -33,7 +33,7 @@ humhub.initModule('ui.additions', function (module, require, $) {
* @returns {undefined}
*/
module.applyTo = function (element) {
- var $element = $(element);
+ var $element = (element instanceof $) ? element : $(element);
$.each(_additions, function (selector, additions) {
$.each(additions, function (i, addition) {
$.each($element.find(selector).addBack(selector), function () {
diff --git a/js/humhub/humhub.ui.gallery.js b/js/humhub/humhub.ui.gallery.js
new file mode 100644
index 0000000000..1ca2a5c959
--- /dev/null
+++ b/js/humhub/humhub.ui.gallery.js
@@ -0,0 +1,25 @@
+/**
+ *
+ * @param {type} param1
+ * @param {type} param2
+ */
+humhub.initModule('ui.gallery', function (module, require, $) {
+
+ module.initOnPjaxLoad = false;
+
+ var init = function () {
+ $(document).on('click', '[data-ui-gallery]', function (evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ var $this = $(this);
+ var gallery = $this.data('ui-gallery');
+ var $links = (gallery) ? $('[data-ui-gallery="' + gallery + '"]') : $this.parent().find('[data-ui-gallery]');
+ var options = {index: $this[0], event: evt.originalEvent};
+ blueimp.Gallery($links.get(), options);
+ });
+ };
+
+ module.export({
+ init: init
+ });
+});
\ No newline at end of file
diff --git a/js/humhub/humhub.ui.loader.js b/js/humhub/humhub.ui.loader.js
index e202c8e058..52e182f0a8 100644
--- a/js/humhub/humhub.ui.loader.js
+++ b/js/humhub/humhub.ui.loader.js
@@ -28,7 +28,7 @@ humhub.initModule('ui.loader', function (module, require, $) {
module.initOnPjaxLoad = false;
var set = function (node, cfg) {
- var $node = $(node);
+ var $node = (node instanceof $) ? node : $(node);
if ($node.length) {
$node.each(function () {
var $this = $(this);
@@ -40,14 +40,14 @@ humhub.initModule('ui.loader', function (module, require, $) {
};
var append = function (node, cfg) {
- var $node = $(node);
+ var $node = (node instanceof $) ? node : $(node);
if ($node.length) {
$node.append(getInstance(cfg));
}
};
var prepend = function (node, cfg) {
- var $node = $(node);
+ var $node = (node instanceof $) ? node : $(node);
if ($node.length) {
$node.prepend(getInstance(cfg));
}
@@ -58,7 +58,7 @@ humhub.initModule('ui.loader', function (module, require, $) {
};
var reset = function (node) {
- var $node = $(node);
+ var $node = (node instanceof $) ? node : $(node);
var $loader = $node.find('.loader').length;
if (!$loader) {
return;
@@ -112,7 +112,7 @@ humhub.initModule('ui.loader', function (module, require, $) {
};
var init = function (cfg) {
- $(document).on('click.humhub:modules:ui:loader', 'a[data-ui-loader], button[data-ui-loader]', function (evt) {
+ $(document).on('click.humhub:modules:ui:loader', '[data-ui-loader]', function (evt) {
return module.initLoaderButton(this, evt);
});
@@ -126,7 +126,7 @@ humhub.initModule('ui.loader', function (module, require, $) {
};
var initLoaderButton = function (node, evt) {
- var $node = $(node);
+ var $node = (node instanceof $) ? node : $(node);
var loader = $node.find('.loader').length > 0;
/**
diff --git a/js/humhub/humhub.ui.navigation.js b/js/humhub/humhub.ui.navigation.js
new file mode 100644
index 0000000000..fa4c770ace
--- /dev/null
+++ b/js/humhub/humhub.ui.navigation.js
@@ -0,0 +1,43 @@
+/**
+ *
+ * @param {type} param1
+ * @param {type} param2
+ */
+humhub.initModule('ui.navigation', function (module, require, $) {
+
+ var init = function () {
+ // Default implementation for topbar. Activate li on click.
+ $('#top-menu-nav a').on('click', function () {
+ var $this = $(this);
+ if (!$this.is('#space-menu')) {
+ setActiveItem($this);
+ }
+ });
+
+ // Activate by config
+ $.each(module.config['active'], function (id, url) {
+ setActive(id, url);
+ });
+
+ // Reset active config.
+ module.config['active'] = undefined;
+ };
+
+ var setActive = function (id, url) {
+ setActiveItem($('#' + id).find('[href="' + url + '"]'));
+ };
+
+ var setActiveItem = function ($item) {
+ if (!$item.length) {
+ module.log.warn('Could not activate navigation item', $item);
+ }
+ $item.closest('ul').find('li').removeClass('active');
+ $item.closest('li').addClass('active');
+ $item.trigger('blur');
+ };
+
+ module.export({
+ init: init,
+ setActive: setActive
+ });
+});
\ No newline at end of file
diff --git a/js/humhub/humhub.ui.status.js b/js/humhub/humhub.ui.status.js
index 7040b65b35..6cf43b2b27 100644
--- a/js/humhub/humhub.ui.status.js
+++ b/js/humhub/humhub.ui.status.js
@@ -127,7 +127,7 @@ humhub.initModule('ui.status', function (module, require, $) {
} catch (e) {
log.error(e);
}
- }
+ };
StatusBar.prototype.show = function (callback) {
// Make the container transparent for beeing able to measure the body height
diff --git a/js/select2-extension.js b/js/select2-extension.js
index 9673b25fe9..11eafc5abf 100644
--- a/js/select2-extension.js
+++ b/js/select2-extension.js
@@ -33,7 +33,7 @@ var checkForMultiSelectDropDowns = function() {
$('.multiselect_dropdown').trigger('update');
}
-$(document).ready(function () {
+$(document).on('ready pjax:success', function () {
$.fn.select2.defaults = {};
checkForMultiSelectDropDowns();
});
\ No newline at end of file
diff --git a/protected/humhub/assets/AppAsset.php b/protected/humhub/assets/AppAsset.php
index f33cfda1e8..b2645f6135 100755
--- a/protected/humhub/assets/AppAsset.php
+++ b/protected/humhub/assets/AppAsset.php
@@ -34,6 +34,7 @@ class AppAsset extends AssetBundle
'css/temp.css',
'css/bootstrap-wysihtml5.css',
'css/flatelements.css',
+ 'css/blueimp-gallery.min.css'
];
/**
@@ -45,7 +46,8 @@ class AppAsset extends AssetBundle
* @inheritdoc
*/
public $js = [
- 'js/ekko-lightbox-modified.js',
+ //'js/ekko-lightbox-modified.js',
+ 'js/blueimp-gallery.min.js',
//'js/modernizr.js', // In use???
'js/jquery.highlight.min.js',
//'js/wysihtml5-0.3.0.js',
diff --git a/protected/humhub/assets/CoreApiAsset.php b/protected/humhub/assets/CoreApiAsset.php
index 3e028553d3..670d9273f3 100755
--- a/protected/humhub/assets/CoreApiAsset.php
+++ b/protected/humhub/assets/CoreApiAsset.php
@@ -50,10 +50,13 @@ class CoreApiAsset extends AssetBundle
'js/humhub/humhub.ui.additions.js',
'js/humhub/humhub.ui.loader.js',
'js/humhub/humhub.ui.modal.js',
- 'js/humhub/humhub.client.js',
- 'js/humhub/humhub.client.pjax.js',
'js/humhub/humhub.action.js',
- 'js/humhub/humhub.ui.status.js'
+ 'js/humhub/humhub.client.js',
+ 'js/humhub/humhub.ui.status.js',
+ 'js/humhub/humhub.ui.navigation.js',
+ 'js/humhub/humhub.ui.gallery.js',
+ // Note this should stay at last for other click event listeners beeing able to prevent pjax handling (e.g gallery)
+ 'js/humhub/humhub.client.pjax.js',
];
/**
diff --git a/protected/humhub/components/Controller.php b/protected/humhub/components/Controller.php
index e5c1bf5e79..390a7f16f4 100644
--- a/protected/humhub/components/Controller.php
+++ b/protected/humhub/components/Controller.php
@@ -41,6 +41,14 @@ class Controller extends \yii\web\Controller
* @var boolean append page title
*/
public $prependActionTitles = true;
+
+ /**
+ * Can be used to set the active topmenu item.
+ *
+ * @see Controller::setActiveTopMenuItem
+ * @var type
+ */
+ public $topMenuRoute;
/**
* @inheritdoc
@@ -134,9 +142,15 @@ class Controller extends \yii\web\Controller
$this->appendPageTitle($this->actionTitlesMap[$this->action->id]);
}
}
+
if (!empty($this->pageTitle)) {
$this->getView()->pageTitle = $this->pageTitle;
}
+
+ if(!empty($this->topMenuRoute)) {
+ $this->setActiveTopMenuItem(Url::to([$this->topMenuRoute]));
+ }
+
return true;
}
return false;
@@ -199,5 +213,10 @@ class Controller extends \yii\web\Controller
return Yii::$app->getResponse()->redirect(Url::to($url), $statusCode);
}
+
+ public function setActiveTopMenuItem($url)
+ {
+ \humhub\widgets\TopMenu::markAsActive($url);
+ }
}
diff --git a/protected/humhub/components/View.php b/protected/humhub/components/View.php
index d65a886473..1c09b65a06 100644
--- a/protected/humhub/components/View.php
+++ b/protected/humhub/components/View.php
@@ -51,16 +51,17 @@ class View extends \yii\web\View
$jsCode = "var " . $name . " = '" . addslashes($value) . "';\n";
$this->registerJs($jsCode, View::POS_HEAD, $name);
}
-
- public function registerJsConfig($module, $params = null) {
- if(is_array($module)) {
- foreach($module as $moduleId => $value) {
+
+ public function registerJsConfig($module, $params = null)
+ {
+ if (is_array($module)) {
+ foreach ($module as $moduleId => $value) {
$this->registerJsConfig($moduleId, $value);
}
return;
}
-
- if(isset($this->jsConfig[$module])) {
+
+ if (isset($this->jsConfig[$module])) {
$this->jsConfig[$module] = yii\helpers\ArrayHelper::merge($this->jsConfig[$module], $params);
} else {
$this->jsConfig[$module] = $params;
@@ -140,16 +141,27 @@ class View extends \yii\web\View
*/
public function endBody()
{
- $this->registerJs("humhub.config.set(".json_encode($this->jsConfig).");", View::POS_BEGIN, 'jsConfig');
-
+ \humhub\widgets\CoreJsConfig::widget();
+
+ $this->flushJsConfig();
+
if (Yii::$app->request->isAjax) {
return parent::endBody();
}
-
+
echo \humhub\widgets\LayoutAddons::widget();
-
+
+ // Will add js configuraiton added by layoutaddons.
+ $this->flushJsConfig();
+
// Add Layout Addons
return parent::endBody();
}
+ protected function flushJsConfig($key = null)
+ {
+ $this->registerJs("humhub.config.set(" . json_encode($this->jsConfig) . ");", View::POS_BEGIN, $key);
+ $this->jsConfig = [];
+ }
+
}
diff --git a/protected/humhub/config/assets-prod.php b/protected/humhub/config/assets-prod.php
index 7d1b5a0069..fe86fdd8d5 100644
--- a/protected/humhub/config/assets-prod.php
+++ b/protected/humhub/config/assets-prod.php
@@ -2,7 +2,7 @@
/**
* This file is generated by the "yii asset" command.
* DO NOT MODIFY THIS FILE DIRECTLY.
- * @version 2016-10-21 15:35:07
+ * @version 2016-10-29 22:30:11
*/
return [
'all' => [
@@ -10,10 +10,10 @@ return [
'basePath' => '@webroot',
'baseUrl' => '@web',
'js' => [
- 'js/all-a366b3723e2189716ce68fbe333d59bd.js',
+ 'js/all-a0250144cce3bef41eb0dedbe8a451f9.js',
],
'css' => [
- 'css/all-2d605362857c9db6fdb10a1df599e902.css',
+ 'css/all-fe9e5fa22cdeaff0701299704a33505f.css',
],
'sourcePath' => null,
'depends' => [],
@@ -134,14 +134,6 @@ return [
'all',
],
],
- 'humhub\\assets\\JqueryPjaxAsset' => [
- 'sourcePath' => null,
- 'js' => [],
- 'css' => [],
- 'depends' => [
- 'all',
- ],
- ],
'humhub\\assets\\CaretJsAsset' => [
'sourcePath' => null,
'js' => [],
@@ -197,6 +189,39 @@ return [
'all',
],
],
+ 'humhub\\assets\\NProgressAsset' => [
+ 'sourcePath' => null,
+ 'js' => [],
+ 'css' => [],
+ 'depends' => [
+ 'all',
+ ],
+ ],
+ 'humhub\\assets\\IE9FixesAsset' => [
+ 'sourcePath' => null,
+ 'js' => [],
+ 'css' => [],
+ 'depends' => [
+ 'all',
+ ],
+ ],
+ 'humhub\\assets\\Html5shivAsset' => [
+ 'sourcePath' => null,
+ 'js' => [],
+ 'css' => [],
+ 'depends' => [
+ 'all',
+ ],
+ ],
+ 'humhub\\assets\\IEFixesAsset' => [
+ 'sourcePath' => null,
+ 'js' => [],
+ 'css' => [],
+ 'depends' => [
+ 'humhub\\assets\\Html5shivAsset',
+ 'all',
+ ],
+ ],
'humhub\\assets\\AppAsset' => [
'sourcePath' => null,
'js' => [],
@@ -215,11 +240,13 @@ return [
'humhub\\assets\\JqueryHighlightAsset',
'humhub\\assets\\JqueryCookieAsset',
'humhub\\assets\\JqueryAutosizeAsset',
- 'humhub\\assets\\JqueryPjaxAsset',
'humhub\\assets\\AtJsAsset',
'humhub\\assets\\AnimateCssAsset',
'humhub\\assets\\CoreApiAsset',
'humhub\\modules\\content\\assets\\ContentAsset',
+ 'humhub\\assets\\NProgressAsset',
+ 'humhub\\assets\\IE9FixesAsset',
+ 'humhub\\assets\\IEFixesAsset',
'all',
],
],
diff --git a/protected/humhub/docs/guide/dev-javascript.md b/protected/humhub/docs/guide/dev-javascript.md
index e53c9edbf6..0333dcbd3b 100644
--- a/protected/humhub/docs/guide/dev-javascript.md
+++ b/protected/humhub/docs/guide/dev-javascript.md
@@ -1,39 +1,64 @@
-Javascript frontend
+Javascript API
=======
-HumHub provides a simple Javascript module system, which enables a similar module structure as the backend.
-Instead of using inline code blocks in php, a module (especially complex modules) should add it's frontend logic
-as a javascript module to the HumHub module system. The module system provides, an event system for event driven
-communication, module configuration, server communication utilities and more under the global namespace `humhub`.
+Since version 1.2 HumHub provides a module based Javascript API within the `humhub` namespace.
+Instead of using inline script blocks in your views it's highly recommended using the new module system for your
+frontend scripts. The core components of this api are described in the following.
+
+
+## Modules
+
+### Module Asset
+
+Your Module script files should reside within the `asset/js` folder of your backend module and be appended at the bottom of your document by using yii's asset bundles.
+
+Example:
+
+```php
+namespace humhub\modules\example\assets;
+
+use yii\web\AssetBundle;
+
+class ExampleAsset extends AssetBundle
+{
+ public $jsOptions = ['position' => \yii\web\View::POS_END];
+ public $sourcePath = '@example/assets';
+ public $css = [];
+ public $js = [
+ 'js/humhub.example.js'
+ ];
+}
+```
-## Core
### Module Registration
-Modules can be registered by calling the `humhub.initModule` function. This function
-accepts an id and the actual module function and will add the given module to the namespace `humhub.modules`.
+Modules are registered by calling the `humhub.initModule`. This function
+requires an unique module id and your module function. The module function provides the following arumgents
-The module function is provided with three arguments:
+1. `module`: Your module instance, for exporting module functions and attributes.
+2. `require`: Method for injecting other modules.
+3. `$`: jQuery.
-- The __module__ object is used to export functions either by appending functions/properties directly to `module` or by calling `module.export`.
-- The __require__ function can be used to inject other modules.
-- __$__ a references jquery
-
-Modules can export a `init` function, which is called automatically after the document is ready.
-The following example shows the implementation of a dummy module `humhub.modules.myModule`.
+The following example shows the registraion of module with id 'example':
```javascript
-//Initialization of myModule
-humhub.initModule('myModule', function(module, require, $) {
- //Require at client module at startup
+// After registration, all exported functions will be available under the namespace humhub.modules.example
+humhub.initModule('example', function(module, require, $) {
+ // We require the client module
var client = require('client');
- //Private property
+ // Private property
var myProperty;
- //Definition of an exported object
+ // Definition of an exported object
module.myPublicObject = {};
- //Export some other functions
+ // export single function
+ module.myPublicFunction = function() {
+ // Some logic
+ }
+
+ // Export multiple values by calling module.export.
module.export({
myFunction: function() {
...
@@ -46,30 +71,129 @@ humhub.initModule('myModule', function(module, require, $) {
...
-//Calling myFunction within another module
-require('myModule').myFunction();
-
-//Calling myFunction within another module (full path)
-require('humhub.modules.myModule').myFunction();
-
-//Also a valid call
-require('modules.myModule').myFunction();
-
-//Calling myFunction outside of a module
-humhub.modules.myModule.myFunction();
+// Submodules can be registered as following
+humhub.initModule('example.mySubmodule', function(module, require, $) {
+...
+}
```
-> NOTE: If a module requires another module at startup the required module has to be initialized before. The init order of core modules is configured in the Gruntfile.js
+Accessing your example module:
-> TIP: You can require modules at runtime by calling `require` within exproted functions, this can solve potential startup dependency issues.
+```javascript
+//Calling myFunction within another module
+require('example').myFunction();
-> TIP: Code that requires the page to be loaded (dom access, dom event delegation) should be pushed to the `init` function
+//Calling myFunction within another module (full path)
+require('humhub.modules.example').myFunction();
-> NOTE: Module functions should only be called by other modules, since most of them require an initialization.
+//Also a valid call
+require('modules.example').myFunction();
-### Module Config
+//Calling myFunction outside of a module
+humhub.modules.example.myFunction();
+```
-The HumHub javascript core provides a mechanism to configure modules. A moduleconfig can be set by calling `humhub.config.set`,
+### Module Dependencies
+
+As described before the `require` function can be used to inject other modules into your own module.
+Note that you should only require modules at the beginning of your own module, if you are sure the required module is already
+registered.
+
+The registration order should be assured by using the Assetbundle's `$depends` mechanism:
+
+
+```php
+// Add this to your ExampleAsset.php file
+public $depends = [
+ 'humhub\modules\anotherModule\assets\AnotherModuleAsset'
+];
+```
+
+If you can't assure the module registration order, but need to require another module, you can either require it within your module function instead of the beginning
+of your module or using the `lazy` flag of the require function.
+The call to `require('anotherModule', true)` will return an empty namespace object, which will be filled after the required module is available.
+
+>Note: If you use the `lazy` flat to require another module, you can't assure the required module will be initialized within your own module's `init` function.
+
+>Info: All core modules are registrated at the beginning of the body, so they are available very early.
+
+### Module Initialisation
+
+Modules can export a `init` function, which is called automatically after the document is ready.
+
+```javascript
+humhub.initModule('example', function(module, require, $) {
+ ...
+
+ var init = function() {
+ // Dom will be ready here.
+ }
+
+ // Export multiple values by calling module.export.
+ module.export({
+ init: init,
+ ...
+ });
+});
+```
+
+Since HumHub can be operated in [[pjax]] mode as single page application.
+By default the `init` function of your module is automatically after a pjax load. This can be deactivated for modules
+which do not need to be reinitialized by setting
+
+```javascript
+module.initOnPjaxLoad = false
+```
+
+This mostly applies to modules which are not dependent on dynamic dom nodes.
+If you module needs to implement a special behaviour for pjax reloads, it can also listen to the following event
+
+```javascript
+var event = require('event');
+
+...
+
+event.on('humhub:modules:client:pjax:afterPageLoad', function() {
+...
+}
+```
+
+### Module Configuration
+
+If you need to transfer values as texts, flags or urls from your php backend to your frontend module, you can use the `module.config` array which is automatically available
+within your module function as in the following example:
+
+```javascript
+humhub.initModule('example', function(module, require, $) {
+...
+
+ var myAction = function() {
+ if(module.config['showMore']) {
+ // Do something
+ }
+
+ };
+});
+```
+
+In your php view you can set the module as follows
+
+```php
+// Single module
+$this->registerJsVar('example', ['showMore' => true]);
+
+// Multiple modules
+$this->registerJsVar([
+ 'example' => [
+ 'showMore' => true
+ ],
+ 'anotherModule' => [
+ ...
+ ]
+);
+```
+
+Setting configurations in javascript:
```javascript
//Set config values for multiple modules,
@@ -90,13 +214,93 @@ humhub.config.set('myModule', {
//You can also call
humhub.config.set('myModule', 'myKey', 'value');
```
+
+>Note: Since the configuration can easily be manipulated, you should not set values which can compromise the security of your application.
> TIP: Module setter are normally called within views or widgets to inject urls or translated text for errors or modals
-Modules can retrieve its config through `humhub.config.get`
+### Module Texts
+
+Beside the configuration addition, the module instance does furthermore provide a `module.text` function for easily accessing texts of your configuration.
+
+Example of an error text.
+
+```php
+//Configurate your text in your php view.
+$this->registerJsVar([
+ 'example' => [
+ 'showMore' => true,
+ 'text' => [
+ 'error.notallowed' => Yii::t('ExampleModule.views.example', 'You are not allowed to access example!');
+ ]
+ ]
+);
+```
+
+Access your text within your module function as this
```javascript
-//Retrieves the whole config object of 'myModule'
-var config = humhub.config.get('myModule');
+module.text('error.notallowed');
+
+// which is a short form of:
+module.config['text']['error.notallowed'];
+```
+
+### Module Log
+
+Your module is able to create module specific log entries by using the `module.log` object of your module instance.
+The log object supports the following log level functions:
+
+1. trace - For detailed trace output
+2. debug - For debug output
+3. info - Info messages
+4. success - Used for success info logs
+5. warn - Warnings
+6. error - For error messages
+7. fatal - Fatal errors
+
+All log functions accept up to three arguments:
+
+1. The actual message
+2. Details about the message (or errors in case of warn/error/fatal)
+3. A setStatus flag, which will trigger a global `humhub:modules:log:setStatus` event. This can be used to give user-feedback (status bar).
+
+Instead of an actual message, you can also just provide a text key as the first argument.
+The following calls are valid:
+
+```javascript
+// Log config text 'error.notallowed' and give user feedback.
+module.log.error('error.notallowed', true);
+
+// In the following example we received an error response by our humhub.modules.client. The response message will try to resolve a default
+// message for the status of your response. Those default messages are configured in the core configuration texts.
+module.log.error(response, true);
+
+// The error.default text message is available through the configuration of the log module see humhub\widgets\JSConfig
+module.log.error('error.default', new Error('xy'), true);
+```
+
+> Info: Your module logger will try resolving your message string to a module or global text.
+
+> Info: The core ui.status module is responsible for triggering the user result.
+
+> Note: The success log will by default trigger a status log event.
+```
+
+The trace level of your module can be configured by setting the `traceLevel` of your module configuration.
+If your module does not define an own trace level the log modules's traceLevel configuration will be used.
+
+> Info: In production mode the default log level is set to `INFO`, in dev mode its set to `DEBUG`.
+
+> Note: If you change the `traceLevel` of a module at runtime, you'll have to call `module.log.update()`.
+
+## Core Modules
+### Config Module
+
+Beside the `module.config` utility you can also use the global configuration as follows
+
+```javascript
+// Retrieves the whole config object of 'myModule'
+var moduleConfig = require('config').get('myModule');
var myValue = config['myKey'];
//Single value getter with default value
@@ -115,8 +319,10 @@ if(humhub.config.is('myModule', 'enabled', 'false')) {
## Client
The `humhub.modules.client` module provides some utilities for calling backend actions. The client is build on top of
-jquery and provides some additional functionality as a response wrapper and enhanced error handling. A backend action can
-be called by `client.ajax` or `client.post`. Both functions expect an url and jquery like ajax configuration.
+jquery's `$.ajax` function and provides some additional functionality as a response wrapper, promises and enhanced error handling.
+A backend action can be called by `client.ajax`, `client.get` or `client.post`.
+
+The client module can be used as follows
```javascript
var client = require('client');
@@ -128,9 +334,9 @@ client.ajax(url, {
type: 'POST'
}
}).then(function(response) {
- handle(response.content);
+ handleSuccess(response.content);
}).catch(function(errResponse) {
- handleError(errResponse.getErrors());
+ handleError(errResponse);
});
//The same call with forcing a post call
@@ -143,28 +349,148 @@ client.post(url, {
}).catch(function(errResponse) {
handleError(errResponse.getErrors());
});
-```
-> Note: Since Yii urls can't be created on the client side, you'll have to inject them through data attributes or the module config.
-> TIP: The action module provides an uniform way of registering ajax actions without the need of calling the client itself.
+// The status function can be used to react to specific response status codes
+client.post(url, cfg)
+.status({
+ 200: function(response) {
+ // Success handler with user feedback
+ $('container').html(response.output);
+ module.log.success('success.edit', true);
+ },
+ 400: function(response) {
+ // Validation error user feedback is given by validation errors
+ $('container').html(response.output);
+ }
+}).catch(function(e) {
+ // Unexpected error with user feedback
+ module.log.error(e, true);
+});
+```
+> Note: Since Yii urls can't be created on client side, you'll have to inject them through data attributes or the module config.
+
+> TIP: The `action` mechanism described later, facilitates the mapping of urls and action handlers.
### Response Wrapper
-(TBD)
+The response object returned by your client contains the following attributes:
-## Actions
+ - url: the url of your call
+ - status: the result status of the xhr object
+ - response: the server response, either a json object or html depending of the 'dataType' setting of your call.
+ - textStatus: In case of error: "timeout", "error", "abort", "parsererror", "application"
+ - dataType: the datatype of your call
+ - error: ajax error info
+ - validationError: flag which is set if status = 400
+ - If your response is of type 'json' all your json values will also be directly appended to the response object's root.
+
+## Actions (TBD)
The `humhub.modules.action` module can be used to define frontend actions, which are triggered by events like clicking a button or changing an input field.
+The action mechanism provides an unified way of binding actions to your ui components.
+
+The following example binds all click event of a button to the `myAction` function of your module.
+
+```html
+
+Call my action!
+```
+
+```javascript
+// within your module
+var myAction = function(evt) {
+
+ client.get(evt).then(function(response) {
+ ...
+ evt.$trigger.text(response.output);
+ module.log.success('success.');
+ }).catch(function(response) {
+ ...
+ module.log.error(response, true);
+ });
+}
+
+...
+
+module.export({
+ ...
+ myAction: myAction
+});
+```
+
+> Note: don't forget to export your action handler, otherwise they won't be accessible.
### Action Binding
-The `humhub.modules.action.bindAction` function can be used to bind an action-handler to a specific event type (e.g. click, change,...) of nodes.
+The `humhub.modules.action.bindAction` function is used to bind event types to nodes of a given selector.
+
+The following event type action bindings are available by default:
+
+```javascript
+// This line adds the action behavior for all elements with a data-action-click attribute for 'click' events.
+this.bindAction(document, 'click', '[data-action-click]');
+this.bindAction(document, 'dblclick', '[data-action-dblclick]');
+this.bindAction(document, 'change', '[data-action-change]');
+```
+
+You can extend the supported event types with custom types as in the following example:
+
+```javascript
+// This line adds the action behavior for all elements with a data-action-click attribute for 'click' events.
+this.bindAction(document, 'customevent', '[data-action-customevent]');
+```
+
+__How does it work:__
+
+In the previous examples the bindAction call will bind a delegate to the `document` e.g. `$(document).on('click', '[data-action-click]', function() {...});`
+If the delegate receives an unhandled action event, it will rebind all bindings directly to the trigger elements and run the action.
+All upcoming events will directly be handled by the trigger, which prevents the bubbling latency.
+
+### Action Event
+
+All action-handlers are provided with an action event which is a derivate of `$.Event` and provides, beside others, the following attributes:
+
+- `$trigger`: The jquery $trigger object, which was responsible for triggering the event e.g. a button.
+- `$target`: Can be set by the data-action-target attribute of your $trigger, by default the $trigger is also the event target. See the action component section for more details.
+- `$form`: In case your $trigger is of `type="submit"` or has a `data-action-submit` attribute, the action event will include a jquery object of the sorrounding form.
+
+```html
+
+```
+
+```javascript
+//The client knows how to handle action events, so you just have to call the following to submit evt.$form
+var submit = function(evt) {
+ client.submit(evt).then(...).catch(...);
+}
+```
+
+- `url`: Contains the `data-action-url` (used for all actions of $trigger) or `data-action-click-url` (will be prefered in case of click events)
+- `params`: Can be used to add additional action parameters by setting `data-action-params` or the more specific `data-action-click-params` for click events
+
+```html
+
+```
+
+```javascript
+var call = function(evt) {
+ alert(evt.params.type);
+}
+```
+
+ - `originalEvent`: The original event which triggered the action
+
+### Action Handlers
There are different types of action-handlers:
- __Direct__ action-handlers can be directly passed to the `bindAction` function.
-- __Registered__ action-handler are registered by the `registerHandler` or `registerAjaxHandler` and can be shared by modules.
-- __Content__ action-handlers are used to execute content related actions (see Content) .
+- __Registered__ action-handler are registered by the `registerHandler` and can be shared by modules.
+- __Component__ action-handlers are used to execute actions of a ui component (see Action Components) .
- __Namespace__ action-handlers will be searched within the humhub namespace if there is no other matching handler.
Example of a `direct-handler`:
@@ -172,7 +498,8 @@ Example of a `direct-handler`:
```html
-
+
+
```
@@ -181,59 +508,32 @@ Example of a `direct-handler`:
var action = require('action');
//Bind a click handler to all .mySpecialButtons within #myContainer
-action.bindAction('#myContainer', 'click', '.mySendButton', function(evt) {
+action.bindAction('#myContainer', 'click', '.sendButton', function(evt) {
//this within a handler function always points to the triggered jQuery node
- client.post(this.data('action-url').then(function(resp) {...});
+ client.post(evt).then(function(resp) {...});
});
```
> TIP: Since humhub action binding is based on jquerys event delegation, you can use all event types of jquery.
-> TIP: In case of direct action-handlers, there is no need to define a action-handler like data-action-click="myHandler" on the trigger element.
-
> NOTE: The first argument of the bindAction should be the first static (never removed from dom or lazy loaded) parent node of all nodes you wish to bind. Too many delegated events to the `document` is a performance antipattern.
-Example registered `ajax-handler`:
-
-```html
-
-
-```
-
-```javascript
-//Somewhere within myModule
-action.registerAjaxHandler('humhub.modules.myModule.sendAjax', {
- success: function(response) {
- //My success handler
- },
- error: function(response) {
- //My error handler
- }
-}
-
-//No need to call bindAction, since data-action-click nodes are bound automatically
-``
-
Example a `namepace-handler`:
```html
-
+
```
-
-> TIP: The action handler will determine the action url and execute the provides success/error handler automatically
-
-> TIP: If you have multiple actions with different action urls you can specify `data-action-url-click`, `data-action-url-change`,...
-data-action-url is always used as fallback
-
-> TIP: The action module binds some default actions like click, dbclick and change to nodes with a data-action- attribute, so these event types do not have to be bound manually.
+> TIP: If you have multiple actions with different action urls you can specify `data-action-click-url`, `data-action-change-url`.
### Components
-Action components can be used to connect specific dom sections to a javascript action component class. The root of a component is marked with a ´data-action-component´ assignment. This data attribute
-contains the component type e.g `humhub.modules.tasks.Task` or short `tasks.Task`. The component class must be dereived from ´humhub.modules.action.Component´.
-Action components can be cascaded for to share data between a container and entry components e.g. a `tasks.TaskList` contains multiple `tasks.Task` entries.
+Action components can be used to connect specific dom sections to a action component class. The root of a component is marked with a ´data-action-component´ attribute.
+This data attribute contains the component type e.g. `tasks.Task`. The component class must be dereived from ´humhub.modules.action.Component´.
+Action components can be cascaded to share data between a container and entry components e.g. a `tasks.TaskList` contains multiple `tasks.Task` entries.
The TaskList can provide action urls for all its Task entries and provide additional actions.
-For this purpose the components `data` function can be used to search for data values which are either set on the component root itself or a parent component root.
+For this purpose the components `data` function can be used to search for data values which are either directly set on the component itself or a parent component.
+
+Example:
```html
@@ -252,14 +552,19 @@ For this purpose the components `data` function can be used to search for data v
...
+
+
+
```
+(TBD: component class example)
+
> TIP: If you want to handle content models as posts which are extending [[humhub\modules\content\components\ContentActiveRecord]] you should extend the content-component described in the next section!
-### Content
+### Content Components
-One of the main tasks of HumHub is the manipulation (create/edit/delete) of content entries as posts, wikis and polls. The `humhub.modules.content` module provides a
-interface for representing and handling content entries on the frontend. The following module implements a task module with an Task content component and a Tasklist content component.
+One of the main tasks of HumHub is the manipulation (create/edit/delete) of content entries as posts, wikis and polls. The `humhub.modules.content` module provides an
+interface for representing and handling content entries in the frontend. The following module implements a task module with an Task content component and a Tasklist content component.
If your content class supports the actions edit and delete, it will have to set a data-content-key attribute with the given content id. This is not necessary if your
implementation does not support these functions as in the TaskList example.
@@ -309,8 +614,8 @@ humhub.initModule('tasks', function(module, require, $) {
__How does it work:__
1. An action-event-handler is bound to all dom nodes with a `data-action-click` on startup.
-2. When triggered the action-event-handler does check if a direct handler was provided
-2. If not it will try to call `Component.handleAction`
+2. When triggered, the action event handler does check if a direct handler was provided.
+2. If not it will try to call `Component.handleAction`.
3. If this handler does find a sorrounding component it will instantiate the component and try to execute the given handler.
4. If no other handler was found, the handler will try to find a handler in the humhub namespace.
@@ -324,6 +629,8 @@ which will overwrite the setting of a parent data-content-base.
## Additions
+## Stream
+
## Modal
## Util
\ No newline at end of file
diff --git a/protected/humhub/docs/guide/theming-migrate.md b/protected/humhub/docs/guide/theming-migrate.md
index 558d02f17f..fe2ff4df95 100644
--- a/protected/humhub/docs/guide/theming-migrate.md
+++ b/protected/humhub/docs/guide/theming-migrate.md
@@ -6,10 +6,37 @@ Here you will learn how you can adapt existing themes to working fine with actua
### Stream
-Set data-stream attributes for stream
+The new stream js rewrite requires some additional data-* attributes, which have to be added in case your theme overwrites either the stream or
+streamentry view/layout. Furthermore the frontend modules configuration was added to the `stream.php` file.
+Please see the following files for changes:
+
+`protected/humhub/modules/stream/widget/views/stream.php`
+
+`protected/humhub/modules/content/views/layouts/wallEntry.php`
+
+The same applies to the activity stream:
+
+`protected/humhub/modules/activity/widget/views/activityStream.php`
+
+`protected/humhub/modules/activity/views/layouts/web.php`
+
### Status Bar
+We added a new status bar and a loader for pjax loading to the theme.less.
+Please see the following file for 1.2 changes (at the buttom):
+
+`themes/HumHub/css/theme.less`
+
+### Layout
+
+// Pjax changes
+- Add 'top-menu-nav' to main.php layout.
+
+### Gallery
+
+- Use data-ui-gallery instead of old data-toggle and data-gallery
+
## Migrate to 1.1
- Make sure to update your themed less file with the latest version.
diff --git a/protected/humhub/modules/activity/assets/js/humhub.activity.js b/protected/humhub/modules/activity/assets/js/humhub.activity.js
index cf8c564741..b280e3997d 100644
--- a/protected/humhub/modules/activity/assets/js/humhub.activity.js
+++ b/protected/humhub/modules/activity/assets/js/humhub.activity.js
@@ -73,9 +73,20 @@ humhub.initModule('activity', function (module, require, $) {
ActivityStream.prototype.hideLoader = function() {
this.$content.find('#activityLoader').remove();
};
+
+ ActivityStream.prototype.onChange = function () {
+ if(!this.hasEntries()) {
+ this.$.html('