diff --git a/.gitignore b/.gitignore index 57f9e1cb88..378ffc49d4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,7 @@ assets/* protected/runtime/* !protected/runtime/.gitignore -node_modules/* -!node_modules/.gitignore +node_modules protected/config/local/* !protected/config/local/.gitignore diff --git a/Gruntfile.js b/Gruntfile.js index b6bf03953f..596010fb9f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -7,8 +7,8 @@ module.exports = function (grunt) { }, dist: { src: ['js/humhub.core.js', 'js/humhub.util.js' ,'js/humhub.additions.js', - 'js/humhub.client.js', 'js/humhub.ui.js', 'js/humhub.actions.js', 'js/humhub.content.js', - 'js/humhub.stream.js', 'js/humhub.ui.modal.js'], + 'js/humhub.client.js', 'js/humhub.ui.js', 'js/humhub.ui.modal.js', 'js/humhub.actions.js', + 'js/humhub.content.js', 'js/humhub.stream.js'], dest: 'js/dist/humhub.all.js' } }, diff --git a/composer.json b/composer.json index d5f542fb66..e9078d1870 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "bower-asset/fontawesome": "^4.3.0", "bower-asset/bootstrap-markdown": "2.10.*", "bower-asset/select2" : "^4.0.2", + "bower-asset/bluebird" : "^3.3.5", "bower-asset/select2-bootstrap-theme" : "0.1.0-beta.4" }, "require-dev": { diff --git a/js/dist/humhub.all.js b/js/dist/humhub.all.js index 8ad2c03710..302f848fd9 100644 --- a/js/dist/humhub.all.js +++ b/js/dist/humhub.all.js @@ -149,6 +149,38 @@ var humhub = humhub || (function($) { } }; + /** + * Config implementation + */ + + var config = { + get : function(module, key, defaultVal) { + if(_isDefined(key)) { + var result = this.getModuleConfig(module)[key]; + return (_isDefined(result)) ? result : defaultVal; + } + }, + getModuleConfig: function(module) { + if(!this.module) { + this.module = {}; + } + return this.module; + }, + + is : function(module, key, defaultVal) { + return this.get(module, key,defaultVal) === true; + }, + + set : function(module, key, value) { + //Moduleid with multiple values + if(arguments.length === 2) { + $.extend(this.getModuleConfig(module), key); + } else if(arguments.length === 3) { + this.getModuleConfig(module)[key] = value; + } + } + }; + /** * Cuts the prefix humub.modules or modules. from the given value. * @param {type} value @@ -186,6 +218,10 @@ var humhub = humhub || (function($) { return val.indexOf(prefix) === 0; }; + var _isDefined = function(obj) { + return typeof obj !== 'undefined'; + }; + //Initialize all initial modules $(document).ready(function() { $.each(initialModules, function(i, module) { @@ -197,8 +233,12 @@ var humhub = humhub || (function($) { }); }); + + return { - initModule: initModule + initModule: initModule, + modules: modules, + config: config }; })($);;/** * Util module with sub module for object and string utility functions @@ -217,6 +257,9 @@ humhub.initModule('util', function(module, require, $) { isArray: function(obj) { return $.isArray(obj); }, + isEmpty: function(obj) { + return $.isEmptyObject(obj); + }, isString: function (obj) { return typeof obj === 'string'; }, @@ -248,7 +291,7 @@ humhub.initModule('util', function(module, require, $) { var string = { cutprefix : function(val, prefix) { - if(!this.startsWith(prefix)) { + if(!this.startsWith(val, prefix)) { return val; } return val.substring(prefix.length, val.length); @@ -359,6 +402,7 @@ humhub.initModule('client', function (module, require, $) { */ var Response = function (data) { this.data = data; + $.extend(this, data); }; /** @@ -378,14 +422,10 @@ humhub.initModule('client', function (module, require, $) { Response.prototype.isError = function () { return this.getStatus() > 0 || this.getErrors().length; }; - - Response.prototype.getStatus = function () { - return (this.data && object.isDefined(this.data.status)) ? this.data.status : -1; - }; - Response.prototype.getErrorTitle = function() { - return (this.data) ? this.data.errorTitle : undefined; - }; + Response.prototype.getStatus = function () { + return (this.status) ? this.status : -1; + }; Response.prototype.getFirstError = function() { var errors = this.getErrors(); @@ -397,9 +437,8 @@ humhub.initModule('client', function (module, require, $) { Response.prototype.setAjaxError = function(xhr, errorThrown, textStatus,data , status) { this.xhr = xhr; this.textStatus = textStatus; - this.data = data || {}; - this.data.status = status || xhr.status; - this.data.errors = [errorThrown]; + this.status = status || xhr.status; + this.errors = [errorThrown]; }; /** @@ -408,40 +447,8 @@ humhub.initModule('client', function (module, require, $) { * @returns {array} error array or empty array */ Response.prototype.getErrors = function () { - if (this.data) { - var errors = this.data.errors || []; - return (object.isString(errors)) ? [errors] : errors; - } - return []; - }; - - /** - * Returns the raw content object. The content object can either be an - * object with multiple partials {partialId: content string} or a single content string. - * @param {type} id - * @returns {undefined|humhub.client_L5.Response.data.content}1 - */ - Response.prototype.getContent = function () { - return this.data.content; - }; - - /** - * Returns the response partial. If no id is given we return the first partial - * we find. - * @returns {humhub.client_L5.Response.data.content} - */ - Response.prototype.getPartial = function (id) { - if (!this.data) { - return; - } - //TODO: handleResponse filter... - if (object.isObject(this.data.content)) { - return (id) ? this.data.content[id] : this.data.content; - } else if (!id) { - return this.data.content; - } - - return; + var errors = this.errors || []; + return (object.isString(errors)) ? [errors] : errors; }; Response.prototype.toString = function () { @@ -452,53 +459,62 @@ humhub.initModule('client', function (module, require, $) { var cfg = cfg || {}; $form = object.isString($form) ? $($form) : $form; cfg.type = $form.attr('method') || 'post'; - cfg.data = $form.serialize() + cfg.data = $form.serialize(); ajax($form.attr('action'), cfg); }; - var ajax = function (path, cfg) { + var post = function(path, cfg) { var cfg = cfg || {}; - var async = cfg.async || true; - var dataType = cfg.dataType || "json"; + cfg.type = 'POST'; + return ajax(path, cfg); + }; - var error = function (xhr, textStatus, errorThrown, data, status) { - //Textstatus = "timeout", "error", "abort", "parsererror", "application" - if (cfg.error && object.isFunction(cfg.error)) { - var response = new Response(); - response.setAjaxError(xhr, errorThrown, textStatus, data, status); - cfg.error(response); - } else { - console.warn('Unhandled ajax error: ' + path + " type" + type + " error: " + errorThrown); - } - }; + var ajax = function (path, cfg) { + return new Promise(function(resolve, reject) { + var cfg = cfg || {}; + var async = cfg.async || true; + var dataType = cfg.dataType || "json"; - var success = function (json, textStatus, xhr) { - var response = new Response(json); - if (response.isError()) { //Application errors - return error(xhr, "application", response.getErrors(), json, response.getStatus() ); - } else if (cfg.success) { - response.textStatus = textStatus; - response.xhr = xhr; - cfg.success(response); - } - }; + var error = function (xhr, textStatus, errorThrown, data, status) { + //Textstatus = "timeout", "error", "abort", "parsererror", "application" + if (cfg.error && object.isFunction(cfg.error)) { + var response = new Response(); + response.setAjaxError(xhr, errorThrown, textStatus, data, status); + cfg.error(response); + } + reject(xhr, textStatus, errorThrown, data, status); + }; - $.ajax({ - url: path, - data: cfg.data, - type: cfg.type, - beforeSend: cfg.beforeSend, - processData: cfg.processData, - contentType: cfg.contentType, - async: async, - dataType: dataType, - success: success, - error: error + var success = function (json, textStatus, xhr) { + var response = new Response(json); + if (response.isError()) { //Application errors + return error(xhr, "application", response.getErrors(), json, response.getStatus() ); + } else if (cfg.success) { + response.textStatus = textStatus; + response.xhr = xhr; + cfg.success(response); + } + resolve(response); + }; + + $.ajax({ + url: path, + data: cfg.data, + type: cfg.type, + beforeSend: cfg.beforeSend, + processData: cfg.processData, + contentType: cfg.contentType, + async: async, + dataType: dataType, + success: success, + error: error + }); }); }; module.export({ ajax: ajax, + post: post, submit: submit, init: init }); @@ -532,515 +548,7 @@ humhub.initModule('client', function (module, require, $) { additions.registerAddition('.autosize', function($match) { $match.autosize(); }); - } -});;/** - * Thid module can be used by humhub sub modules for registering handlers and serves as core module for executing actions triggered in the gui. - * A module can either register global handler by using the registerHandler and registerAjaxHandler functions or use the content mechanism. - */ -humhub.initModule('actions', function (module, require, $) { - var _handler = {}; - var object = require('util').object; - var string = require('util').string; - var client = require('client'); - - /** - * Constructor for initializing the module. - */ - module.init = function () { - //Binding default action types - this.bindAction(document, 'click', '[data-action-click]'); - this.bindAction(document, 'dblclick', '[data-action-dblclick]'); - this.bindAction(document, 'change', '[data-action-mouseout]'); - - //Add addition for loader buttons - require('additions').registerAddition('[data-action-load-button]', function () { - var that = this; - this.on('click.humhub-action-load-button', function (evt) { - if (!that.find('.action-loader').length) { - that.append(''); - } - }); - }); }; - - /** - * Registers a given handler with the given id. - * - * This handler will be called e.g. after clicking a button with the handler id as - * data-action-click attribute. - * - * The handler can access additional event information through the argument event. - * The this object within the handler will be the trigger of the event. - * - * @param {string} id handler id should contain the module namespace - * @param {function} handler function with one event argument - * @returns {undefined} - */ - module.registerHandler = function (id, handler) { - if (!id) { - return; - } - - if (handler) { - _handler[id] = handler; - } - }; - - /** - * Registers an ajax eventhandler. - * The function can either be called with four arguments (id, successhandler, errorhandler, additional config) - * or with two (id, cfg) where tha handlers are contained in the config object itself. - * - * The successhandler will be called only if the response does not contain any errors or errormessages. - * So the errorhandler is called for application and http errors. - * - * The config can contain additional ajax settings. - * - * @param {type} id - * @param {type} success - * @param {type} error - * @param {type} cfg - * @returns {undefined} - */ - module.registerAjaxHandler = function (id, success, error, cfg) { - cfg = cfg || {}; - if (!id) { - return; - } - - if (object.isFunction(success)) { - cfg.success = success; - cfg.error = error; - } else { - cfg = success; - } - - if (success) { - _handler[id] = function (event) { - var path = $(this).data('url-' + event.type) || $(this).data('url'); - client.ajax(path, cfg, event); - }; - } - }; - - /** - * Binds an delegate wrapper event handler to the parent node. This is used to detect action handlers like - * data-action-click events and map the call to either a stand alone handler or a content - * action handler. The trigger of a contentAction has to be contained in a data-content-base node. - * - * This function uses the jQuery event delegation: - * - * $(parent).on(type, selector, function(){...}); - * - * This assures the event binding for dynamic content (ajax content etc..) - * - * @see {@link humhub.modules.content.handleAction} - * @param {Node|jQuery} parent - the event target - * @param {string} type - event type e.g. click, change,... - * @param {string} selector - jQuery selector - * @param {string} selector - jQuery selector - */ - module.bindAction = function (parent, type, selector, directHandler) { - parent = parent || document; - var $parent = parent.jquery ? parent : $(parent); - $parent.on(type+'.humhub-action', selector, function (evt) { - evt.preventDefault(); - //The element which triggered the action e.g. a button or link - $trigger = $(this); - - //Get the handler id, either a stand alone handler or a content handler function e.g: 'edit' - var handlerId = $trigger.data('action' + '-' + type); - var event = {type: type, $trigger: $trigger, handler: handlerId}; - - event.finish = function() { - _removeLoaderFromEventTarget(evt); - }; - - //TODO: handle with $.Event - //var event = $.Event(type, {$trigger: $trigger}); - //event.originalEvent = evt; - - //Search and execute a stand alone handler or try to call the content action handler - try { - if(object.isFunction(directHandler)) { - //Direct action handler - directHandler.apply($trigger, [event]); - } else if (_handler[handlerId]) { - //Registered action handler - var handler = _handler[handlerId]; - handler.apply($trigger, [event]); - } else if (!_handler['humhub.modules.content.actiontHandler'](event)) { //Content action handler - //If the content handler did not accept this event we try to find a handler by namespace - var splittedNS = handlerId.split('.'); - var handler = splittedNS[splittedNS.length - 1]; - var target = require(string.cutsuffix(handlerId, '.' + handler)); - target[handler]({type: type, $trigger: $trigger}); - } - } catch (e) { - //TODO: handle error ! - console.error('Error while handling action event for handler "' + handlerId+'"', e); - _removeLoaderFromEventTarget(evt); - } - }); - }; - - var _removeLoaderFromEventTarget = function (evt) { - if (evt.target) { - $target = $(evt.target); - $loader = $target.find('.action-loader'); - - if ($loader.length) { - $loader.remove(); - } - } - }; -});;/** - * This module provides an api for handling content objects e.g. Posts, Polls... - * - * @type undefined|Function - */ - -humhub.initModule('content', function(module, require, $) { - var client = require('client'); - var object = require('util').object; - var actions = require('actions'); - - module.init = function() { - actions.registerHandler('humhub.modules.content.actiontHandler', function(event) { - return module.handleAction(event); - }); - }; - - /** - * Handles the given contentAction event. The event should provide the following properties: - * - * $trigger (required) : the trigger node of the event - * handler (required) : the handler functionn name to be executed on the content - * type (optoinal) : the event type 'click', 'change',... - * - * @param {object} event - event object - * @returns {Boolean} true if the contentAction could be executed else false - */ - module.handleAction = function(event) { - var $contentBase = this.getContentBase(event.$trigger); - if($contentBase.length) { - //Initialize a content instance by means of the content-base type and execute the handler - var ContentType = require($contentBase.data('content-base')); - if(ContentType) { - var content = new ContentType($contentBase); - if(event.handler && content[event.handler]) { - content[event.handler](event); - return true; - } - } else { - console.error('No ContentType found for '+$contentBase.data('content-base')); - } - } - return false; - }; - - module.getContentBase = function($element) { - return $element.closest('[data-content-base]'); - }; - - var Content = function(id) { - if(!id) { //Create content - return; - } - if (typeof id === 'string') { - this.id = id; - this.$ = $('#' + id); - } else if (id.jquery) { - this.$ = id; - this.id = this.$.attr('id'); - } - }; - - Content.prototype.getKey = function () { - return this.$.data('content-key'); - }; - - Content.prototype.getEditUrl = function () { - var result = this.$.data('content-edit-url'); - if(!result) { - var parentContent = this.getParentContentBase('[data-content-base]'); - if(parentContent) { - return parentContent.getEditUrl(); - } - } - return result; - }; - - Content.prototype.getParentContentBase = function() { - var $parent = this.$.parent().closest('[data-content-base]'); - if($parent.length) { - try { - var ParentType = require($parent.data('content-base')); - return new ParentType($parent); - } catch(err) { - console.error('Could not instantiate parent content base: '+$parent.data('content-base')); - } - } - }; - - Content.prototype.create = function (addContentHandler) { - //Note that this Content won't have an id, so the backend will create an instance - this.edit(addContentHandler); - }; - - Content.prototype.edit = function (successHandler) { - var editUrl = this.getEditUrl(); - var contentId = this.getKey(); - var modal = require('ui.modal').global; - - if(!editUrl) { - //Todo: handle error - console.error('No editUrl found for edit content action editUrl: '+editUrl+ ' contentId '+contentId); - return; - } - - var that = this; - - client.ajax(editUrl, { - data: { - 'id' : contentId - }, - beforeSend: function() { - modal.loader(); - }, - success: function(response) { - //Successfully retrieved the edit form, now show it within a modal - modal.content(response.getContent(), function() { - //Bind direct action handler we could use a global registeredHandler but this is more efficient - actions.bindAction(modal.getBody(), 'click', '[data-content-save]', function(event) { - client.submit(modal.getForm(), { - success : function(response) { - if(object.isFunction(successHandler)) { - if(successHandler(response, modal)) {modal.close();}; - } else { - that.replaceContent(response.getContent()); - //TODO: check for content.highlight - modal.close(); - } - event.finish(); - }, - error : function(error) { - //TODO: handle error - modal.error(error); - console.error('Error while submitting form :'+error); - event.finish(); - } - }); - }); - }); - }, - error: function(errResponse) { - modal.error(errResponse); - console.log('Error occured while editing content: '+errResponse.getFirstError()); - //Todo: handle error - } - }); - }; - - Content.prototype.replaceContent = function(content) { - try { - this.$.html($(content).children()); - } catch(e) { - console.error('Error occured while replacing content: '+this.id , e); - } - }; - - Content.prototype.delete = function () { - //Search for data-content-delte-url on root. - //if(this.deleteModal) {open modal bla} - //Call this url with data-content-pk - //Trigger delete event - }; - - module.Content = Content; -});;/** - * Core module for managing Streams and StreamItems - * @type Function - */ -humhub.initModule('stream', function(module, require, $) { - - var ENTRY_ID_SELECTOR_PREFIX = '#wallEntry_'; - var WALLSTREAM_ID = 'wallStream'; - - /** - * Base class for all StreamItems - * @param {type} id - * @returns {undefined} - */ - var StreamItem = function (id) { - if (typeof id === 'string') { - this.id = id; - this.$ = $('#' + id); - } else if (id.jquery) { - this.$ = id; - this.id = this.$.attr('id'); - } - }; - - /** - * Removes the stream item from stream - */ - StreamItem.prototype.remove = function () { - this.$.remove(); - }; - - StreamItem.prototype.getContentKey = function () {} - - StreamItem.prototype.edit = function () { - //Search for data-content-edit-url on root. - //Call this url with data-content-pk - //Trigger delete event - }; - - StreamItem.prototype.delete = function () { - //Search for data-content-delte-url on root. - //Call this url with data-content-pk - //Trigger delete event - }; - - StreamItem.prototype.getContent = function () { - return this.$.find('.content'); - }; - -/* - module.StreamItem.prototype.highlightContent = function () { - var $content = this.getContent(); - $content.addClass('highlight'); - $content.delay(200).animate({backgroundColor: 'transparent'}, 1000, function () { - $content.removeClass('highlight'); - $content.css('backgroundColor', ''); - }); - }; -*/ - /** - * Stream implementation - * @param {type} id - * @returns {undefined} - */ - var Stream = function (id) { - this.id = id; - this.$ = $('#' + id); - }; - - Stream.prototype.getEntry = function (id) { - //Search for data-content-base and try to initiate the Item class - - return new module.Entry(this.$.find(ENTRY_ID_SELECTOR_PREFIX + id)); - }; - - Stream.prototype.wallStick = function (url) { - $.ajax({ - dataType: "json", - type: 'post', - url: url - }).done(function (data) { - if (data.success) { - if (currentStream) { - $.each(data.wallEntryIds, function (k, wallEntryId) { - currentStream.deleteEntry(wallEntryId); - currentStream.prependEntry(wallEntryId); - }); - $('html, body').animate({scrollTop: 0}, 'slow'); - } - } else { - alert(data.errorMessage); - } - }); - }; - - Stream.prototype.wallUnstick = function (url) { - $.ajax({ - dataType: "json", - type: 'post', - url: url - }).done(function (data) { - if (data.success) { - //Reload the whole stream, since we have to reorder the entries - currentStream.showStream(); - } - }); - }; - - /** - * Click Handler for Archive Link of Wall Posts - * (archiveLink.php) - * - * @param {type} className - * @param {type} id - */ - Stream.prototype.wallArchive = function (id) { - - url = wallArchiveLinkUrl.replace('-id-', id); - - $.ajax({ - dataType: "json", - type: 'post', - url: url - }).done(function (data) { - if (data.success) { - if (currentStream) { - $.each(data.wallEntryIds, function (k, wallEntryId) { - //currentStream.reloadWallEntry(wallEntryId); - // fade out post - setInterval(fadeOut(), 1000); - - function fadeOut() { - // fade out current archived post - $('#wallEntry_' + wallEntryId).fadeOut('slow'); - } - }); - } - } - }); - }; - - - /** - * Click Handler for Un Archive Link of Wall Posts - * (archiveLink.php) - * - * @param {type} className - * @param {type} id - */ - Stream.prototype.wallUnarchive = function (id) { - url = wallUnarchiveLinkUrl.replace('-id-', id); - - $.ajax({ - dataType: "json", - type: 'post', - url: url - }).done(function (data) { - if (data.success) { - if (currentStream) { - $.each(data.wallEntryIds, function (k, wallEntryId) { - currentStream.reloadWallEntry(wallEntryId); - }); - - } - } - }); - }; - - var getStream = function () { - if (!module.mainStream) { - module.mainStream = new module.Stream(WALLSTREAM_ID); - } - return module.mainStream; - }; - - var getEntry = function (id) { - return module.getStream().getEntry(id); - }; - - module.export({ - getStream : getStream, - getEntry : getEntry - }); });;/** * Module for creating an manipulating modal dialoges. * Normal layout of a dialog: @@ -1059,8 +567,8 @@ humhub.initModule('stream', function(module, require, $) { * @param {type} param2 */ humhub.initModule('ui.modal', function (module, require, $) { + var object = require('util').object; var additions = require('additions'); - var actions = require('actions'); //Keeps track of all initialized modals var modals = []; @@ -1328,10 +836,879 @@ humhub.initModule('ui.modal', function (module, require, $) { return this.$modal.find('.modal-body'); }; + var ConfirmModal = function(id, confirmHandler) { + Modal.call(this, id); + this.initButtons(); + }; + + ConfirmModal.prototype.initButtons = function(confirmHandler) { + this.$confirm = this.$modal.find('[data-modal-submit]'); + this.$confirm.on('click', confirmHandler); + }; + + object.inherits(ConfirmModal, Modal); + module.export({ init: function () { module.global = new Modal('global-modal'); }, Modal: Modal }); -}); \ No newline at end of file +});;/** + * Thid module can be used by humhub sub modules for registering handlers and serves as core module for executing actions triggered in the gui. + * A module can either register global handler by using the registerHandler and registerAjaxHandler functions or use the content mechanism. + */ +humhub.initModule('actions', function (module, require, $) { + var _handler = {}; + var object = require('util').object; + var string = require('util').string; + var client = require('client'); + + /** + * Constructor for initializing the module. + */ + module.init = function () { + //Binding default action types + this.bindAction(document, 'click', '[data-action-click]'); + this.bindAction(document, 'dblclick', '[data-action-dblclick]'); + this.bindAction(document, 'change', '[data-action-mouseout]'); + + //Add addition for loader buttons + require('additions').registerAddition('[data-action-load-button]', function () { + var that = this; + this.on('click.humhub-action-load-button', function (evt) { + if (!that.find('.action-loader').length) { + that.append(''); + } + }); + }); + }; + + /** + * Registers a given handler with the given id. + * + * This handler will be called e.g. after clicking a button with the handler id as + * data-action-click attribute. + * + * The handler can access additional event information through the argument event. + * The this object within the handler will be the trigger of the event. + * + * @param {string} id handler id should contain the module namespace + * @param {function} handler function with one event argument + * @returns {undefined} + */ + module.registerHandler = function (id, handler) { + if (!id) { + return; + } + + if (handler) { + _handler[id] = handler; + } + }; + + /** + * Registers an ajax eventhandler. + * The function can either be called with four arguments (id, successhandler, errorhandler, additional config) + * or with two (id, cfg) where tha handlers are contained in the config object itself. + * + * The successhandler will be called only if the response does not contain any errors or errormessages. + * So the errorhandler is called for application and http errors. + * + * The config can contain additional ajax settings. + * + * @param {type} id + * @param {type} success + * @param {type} error + * @param {type} cfg + * @returns {undefined} + */ + module.registerAjaxHandler = function (id, success, error, cfg) { + cfg = cfg || {}; + if (!id) { + return; + } + + if (object.isFunction(success)) { + cfg.success = success; + cfg.error = error; + } else { + cfg = success; + } + + if (success) { + _handler[id] = function (event) { + var path = $(this).data('url-' + event.type) || $(this).data('url'); + client.ajax(path, cfg, event); + }; + } + }; + + /** + * Binds an delegate wrapper event handler to the parent node. This is used to detect action handlers like + * data-action-click events and map the call to either a stand alone handler or a content + * action handler. The trigger of a contentAction has to be contained in a data-content-base node. + * + * This function uses the jQuery event delegation: + * + * $(parent).on(type, selector, function(){...}); + * + * This assures the event binding for dynamic content (ajax content etc..) + * + * @see {@link humhub.modules.content.handleAction} + * @param {Node|jQuery} parent - the event target + * @param {string} type - event type e.g. click, change,... + * @param {string} selector - jQuery selector + * @param {string} selector - jQuery selector + */ + module.bindAction = function (parent, type, selector, directHandler) { + parent = parent || document; + var $parent = parent.jquery ? parent : $(parent); + $parent.on(type+'.humhub-action', selector, function (evt) { + evt.preventDefault(); + //The element which triggered the action e.g. a button or link + $trigger = $(this); + + //Get the handler id, either a stand alone handler or a content handler function e.g: 'edit' + var handlerId = $trigger.data('action' + '-' + type); + var event = {type: type, $trigger: $trigger, handler: handlerId}; + + event.finish = function() { + _removeLoaderFromEventTarget(evt); + }; + + //TODO: handle with $.Event + //var event = $.Event(type, {$trigger: $trigger}); + //event.originalEvent = evt; + + //Search and execute a stand alone handler or try to call the content action handler + try { + if(object.isFunction(directHandler)) { + //Direct action handler + directHandler.apply($trigger, [event]); + } else if (_handler[handlerId]) { + //Registered action handler + var handler = _handler[handlerId]; + handler.apply($trigger, [event]); + } else if (!_handler['humhub.modules.content.actiontHandler'](event)) { //Content action handler + //If the content handler did not accept this event we try to find a handler by namespace + var splittedNS = handlerId.split('.'); + var handler = splittedNS[splittedNS.length - 1]; + var target = require(string.cutsuffix(handlerId, '.' + handler)); + target[handler]({type: type, $trigger: $trigger}); + } + } catch (e) { + //TODO: handle error ! + console.error('Error while handling action event for handler "' + handlerId+'"', e); + _removeLoaderFromEventTarget(evt); + } + }); + }; + + var _removeLoaderFromEventTarget = function (evt) { + if (evt.target) { + $target = $(evt.target); + $loader = $target.find('.action-loader'); + + if ($loader.length) { + $loader.remove(); + } + } + }; +});;/** + * This module provides an api for handling content objects e.g. Posts, Polls... + * + * @type undefined|Function + */ + +humhub.initModule('content', function(module, require, $) { + var client = require('client'); + var object = require('util').object; + var actions = require('actions'); + + var Content = function(container) { + if(!container) { //Create content + return; + } + this.$ = (object.isString(container)) ? $('#' + container) : container; + this.contentBase = this.$.data('content-base'); + }; + + Content.prototype.getContentActions = function() { + return ['create','edit','delete']; + }; + + Content.prototype.getKey = function () { + return this.$.data('content-pk'); + }; + + Content.prototype.data = function(dataSuffix) { + var result = this.$.data(dataSuffix); + if(!result) { + var parentContent = this.getParentContentBase(); + if(parentContent) { + return parentContent.data(dataSuffix); + } + } + return result; + }; + + Content.prototype.getParentContentBase = function() { + var $parent = this.$.parent().closest('[data-content-base]'); + if($parent.length) { + try { + var ParentType = require($parent.data('content-base')); + return new ParentType($parent); + } catch(err) { + console.error('Could not instantiate parent content base: '+$parent.data('content-base')); + } + } + }; + + Content.prototype.create = function (addContentHandler) { + //Note that this Content won't have an id, so the backend will create an instance + if(indexOf(this.getContentActions(), 'create') < 0) { + return; + } + + this.edit(addContentHandler); + }; + + Content.prototype.edit = function (successHandler) { + if(indexOf(this.getContentActions(), 'edit') < 0) { + return; + } + + var editUrl = this.data('content-edit-url'); + var contentId = this.getKey(); + var modal = require('ui.modal').global; + + if(!editUrl) { + //Todo: handle error + console.error('No editUrl found for edit content action editUrl: '+editUrl+ ' contentId '+contentId); + return; + } + + var that = this; + + client.ajax(editUrl, { + data: { + 'id' : contentId + }, + beforeSend: function() { + modal.loader(); + }, + success: function(response) { + //Successfully retrieved the edit form, now show it within a modal + modal.content(response.getContent(), function() { + //Bind direct action handler we could use a global registeredHandler but this is more efficient + actions.bindAction(modal.getBody(), 'click', '[data-content-save]', function(event) { + client.submit(modal.getForm(), { + success : function(response) { + if(object.isFunction(successHandler)) { + if(successHandler(response, modal)) {modal.close();}; + } else { + that.replaceContent(response.getContent()); + //TODO: check for content.highlight + modal.close(); + } + event.finish(); + }, + error : function(error) { + //TODO: handle error + modal.error(error); + console.error('Error while submitting form :'+error); + event.finish(); + } + }); + }); + }); + }, + error: function(errResponse) { + modal.error(errResponse); + console.log('Error occured while editing content: '+errResponse.getFirstError()); + //Todo: handle error + } + }); + }; + + Content.prototype.delete = function () { + if(this.getContentActions().indexOf('delete') < 0) { + return; + } + + var that = this; + var url = this.data('content-delete-url'); + if(url) { + client.post(url, { + data: { + id: that.getKey() + }, + success: function(json) { + json.success; + that.remove(); + }, + error: function(json) { + console.error(json); + } + }) + } else { + console.error('Content delete was called, but no url could be determined for '+this.contentBase); + } + }; + + Content.prototype.replaceContent = function(content) { + try { + var that = this; + this.$.animate({ opacity: 0 }, 'fast', function() { + that.$.html($(content).children()); + that.$.stop().animate({ opacity: 1 }, 'fast'); + if(that.highlight) { + that.highlight(); + } + }); + } catch(e) { + console.error('Error occured while replacing content: '+this.$.attr('id') , e); + } + }; + + Content.prototype.remove = function() { + var that = this; + this.$.animate({ height: 'toggle', opacity: 'toggle' }, 'fast', function() { + that.$.remove(); + //TODO: fire global event + }); + }; + + Content.getContentBase = function($element) { + return $element.closest('[data-content-base]'); + }; + + Content.getInstance = function($contentBase) { + $contentBase = (object.isString($contentBase)) ? $('#'+$contentBase) : $contentBase; + var ContentType = require($contentBase.data('content-base')); + if(ContentType) { + return new ContentType($contentBase); + } + }; + + var init = function() { + actions.registerHandler('humhub.modules.content.actiontHandler', function(event) { + return module.handleAction(event); + }); + }; + + /** + * Handles the given contentAction event. The event should provide the following properties: + * + * $trigger (required) : the trigger node of the event + * handler (required) : the handler functionn name to be executed on the content + * type (optoinal) : the event type 'click', 'change',... + * + * @param {object} event - event object + * @returns {Boolean} true if the contentAction could be executed else false + */ + handleAction = function(event) { + var $contentBase = Content.getContentBase(event.$trigger); + if($contentBase.length) { + //Initialize a content instance by means of the content-base type and execute the handler + var content = Content.getInstance($contentBase); + if(content) { + //Check if the content instance provides this actionhandler + if(event.handler && content[event.handler]) { + content[event.handler](event); + return true; + } + } else { + console.error('No ContentType found for '+$contentBase.data('content-base')); + } + } + return false; + }; + + module.export({ + Content : Content, + init : init, + handleAction: handleAction + }); +});;/** + * Core module for managing Streams and StreamItems + * @type Function + */ +humhub.initModule('stream', function (module, require, $) { + + var util = require('util'); + var object = util.object; + var string = util.string; + var client = require('client'); + var modal = require('modal'); + var Content = require('content').Content; + + var STREAM_INIT_COUNT = 8; + var STREAM_LOAD_COUNT = 4; + + //TODO: load streamUrl from config + //TODO: readonly + + /** + * Base class for all StreamContent + * @param {type} id + * @returns {undefined} + */ + var StreamEntry = function (id) { + this.$ = object.isString(id) ? this.$ = $('#' + id) : id; + Content.call(this); + }; + + object.inherits(StreamEntry, Content); + + StreamEntry.prototype.getContentActions = function() { + return ['delete', 'edit']; + }; + + StreamEntry.prototype.delete = function () { + var content = this.getContentInstance(); + if(content && content.delete) { + //TODO: modalconfirm + content.delete(); + } else { + StreamEntry._super.delete.call(this); + } + }; + + StreamEntry.prototype.reload = function () { + getStream().reload(this); + }; + + StreamEntry.prototype.edit = function () { + //Search for data-content-edit-url on root. + //Call this url with data-content-pk + //Trigger delete event + }; + + StreamEntry.prototype.getContentInstance = function () { + return Content.getInstance(this.$.find('[data-content-base]')); + }; + + /** + * Stream implementation. + * + * @param {type} container id or jQuery object of the stream container + * @returns {undefined} + */ + var Stream = function (container) { + this.$ = (object.isString(container)) ? $('#' + container) : container; + + if (!this.$.length) { + console.error('Could not initialize stream, invalid container given'+ container); + return; + } + + //If a contentId is set on the stream root we will only show the single content + if(this.$.data('stream-contentid')) { + this.contentId = parseInt(this.$.data('stream-contentid')); + } + + this.$stream = this.$.find(".s2_stream"); + + //Cache some stream relevant data/nodes + this.url = this.$.data('stream'); //TODO: set this in config instead of data field + this.$loader = this.$stream.find(".streamLoader"); + this.$content = this.$stream.find('.s2_streamContent'); + this.$filter = $('.wallFilterPanel'); + + //TODO: make this configurable + this.filters = []; + this.sort = "c"; + + Content.call(this); + }; + + object.inherits(Stream, Content); + + Stream.prototype.getContentActions = function() { + return []; + }; + + /** + * Initializes the stream, by clearing the stream and reloading initial stream entries, + * this should be called if any filter/sort settings are changed or the stream + * needs an reload. + * + * @returns {humhub.stream_L5.Stream.prototype} + */ + Stream.prototype.init = function () { + this.clear(); + this.$stream.show(); + if (this.isShowSingleEntry()) { + this.loadSingleEntry(this.contentId); + } else { + this.loadEntries(STREAM_INIT_COUNT); + } + return this; + }; + + Stream.prototype.clear = function() { + this.lastEntryLoaded = false; + this.readOnly = false; + this.loading = false; + this.$.find(".s2_streamContent").empty(); + this.$.find(".s2_stream").hide(); + this.$.find(".s2_single").hide(); + this.$.find(".streamLoader").hide(); + this.$.find(".emptyStreamMessage").hide(); + this.$.find(".emptyFilterStreamMessage").hide(); + this.$.find('.back_button_holder').hide(); + this.$filter.hide(); + }; + + Stream.prototype.loadSingleEntry = function(contentId) { + this.$.find('.back_button_holder').show(); + this.loadEntries(1, (contentId + 1), ''); + }; + + Stream.prototype.reloadEntry = function(entry) { + var that = this; + return new Promise(function(resolve, reject) { + entry = (entry instanceof StreamEntry) ? entry : that.getEntry(entry); + + if(!entry) { + console.warn('Attempt to reload of non existent entry: '+entry); + reject(); + return; + } + + var contentId = entry.getKey(); + return that._load(1, (contentId + 1), '').then(function(response) { + if(response.content[contentId]) { + entry.replaceContent(response.content[contentId].output); + resolve(entry); + } else { + console.warn('Reload failed: ContentId not found in response: '+contentId); + reject(); + } + }, reject); + }); + }; + + Stream.prototype.loadEntries = function (limit, from, filter, sort) { + if (this.loading || this.lastEntryLoaded) { + return; + } + + //Initialize loading process + this.$loader.show(); + this.loading = true; + + //Overwrite the stream settings if provided + limit = limit || STREAM_LOAD_COUNT; + from = from || this.getLastContentId(); + filter = filter || this.getFilterString(); + sort = sort || this.sort; + + var that = this; + return new Promise(function(resolve, reject) { + that._load(limit, from, filter,sort).then(function(response) { + that.$loader.hide(); + if (object.isEmpty(response.content)) { + that.lastEntryLoaded = true; + $('#btn-load-more').hide(); + } else { + that.lastEntryLoaded = response.is_last; + that.appendEntries(response); + } + + that.loading = false; + that.onChange(); + resolve(); + }).catch(function(err) { + //TODO: handle error + that.loading = false; + that.$loader.hide(); + reject(); + }); + }); + }; + + Stream.prototype._load = function (limit, from, filter, sort) { + return client.ajax(this.url, { + data: { + filters: filter, + sort: sort, + from: from, + limit: limit + } + }); + }; + + Stream.prototype.getLastContentId = function () { + var $lastEntry = this.$stream.find('[data-content-pk]').last(); + if ($lastEntry.length) { + return $lastEntry.data('stream-contentid'); + } + }; + + Stream.prototype.appendEntries = function (response) { + var that = this; + var result = ''; + $.each(response.contentIds, function (i, key) { + var $entry = that.$.find('[data-content-pk="' + key + '"]'); + if ($entry.length) { + $entry.remove(); + } + result += response.content[key].output; + }); + return this.$content.append(result); + }; + + /** + * Fired when new entries are shown + */ + Stream.prototype.onChange = function () { + if (this.readOnly) { + $('.wallReadOnlyHide').hide(); + $('.wallReadOnlyShow').show(); + } else { + $('.wallReadOnlyShow').hide(); + } + + var hasEntries = this.hasEntries(); + if (!hasEntries && !this.hasFilter()) { + this.$.find('.emptyStreamMessage').show(); + this.$filter.hide(); + } else if (!hasEntries) { + this.$.find('.emptyFilterStreamMessage').hide(); + } else if(!this.isShowSingleEntry()) { + this.$filter.show(); + this.$.find('.emptyStreamMessage').hide(); + this.$.find('.emptyFilterStreamMessage').hide(); + } + + //TODO: fire global event + }; + + Stream.prototype.isShowSingleEntry = function () { + return object.isDefined(this.contentId); + }; + + Stream.prototype.hasEntries = function () { + return this.getEntryCount() > 0; + }; + + Stream.prototype.getEntryCount = function () { + return this.$.find('[data-content-pk]').length; + }; + + Stream.prototype.hasFilter = function () { + return this.filters.length > 0; + }; + + Stream.prototype.getFilterString = function () { + var result = ''; + $.each(this.filters, function(i, filter) { + result += filter+','; + }); + + return string.cutsuffix(result, ','); + }; + + Stream.prototype.setFilter = function (filterId) { + if(this.filters.indexOf(filterId) < 0) { + this.filters.push(filterId); + } + }; + + Stream.prototype.unsetFilter = function (filterId) { + var index = this.filters.indexOf(filterId); + if(index > -1) { + this.filters.splice(index, 1); + } + }; + + Stream.prototype.getEntry = function(key) { + return new StreamEntry(this.$.find('[data-content-pk="' + key + '"]')); + }; + + Stream.prototype.getEntryByNode = function($childNode) { + return new StreamEntry($childNode.closest('[data-content-pk]')); + }; + + var getStream = function () { + if (!module.instance) { + module.instance = new Stream($('[data-stream]')); + } + return module.instance; + }; + + var getEntry = function (id) { + return module.getStream().getEntry(id); + }; + + var init = function () { + var stream = getStream().init(); + $(window).scroll(function () { + if ($(window).scrollTop() == $(document).height() - $(window).height()) { + if (stream && !stream.loading && !stream.isShowSingleEntry() && !stream.lastEntryLoaded) { + stream.loadEntries(); + } + } + }); + + stream.$.on('click', '.singleBackLink', function() { + stream.contentId = undefined; + stream.init(); + $(this).hide(); + }); + + initFilterNav(); + }; + + var initFilterNav = function() { + $(".wallFilter").click(function () { + var $filter = $(this); + var checkboxi = $filter.children("i"); + checkboxi.toggleClass('fa-square-o').toggleClass('fa-check-square-o'); + if(checkboxi.hasClass('fa-check-square-o')) { + getStream().setFilter($filter.attr('id').replace('filter_', '')); + } else { + getStream().unsetFilter($filter.attr('id').replace('filter_', '')); + } + getStream().init(); + }); + + $(".wallSorting").click(function () { + var newSortingMode = $(this).attr('id'); + + // uncheck all sorting + $(".wallSorting").find('i') + .removeClass('fa-check-square-o') + .addClass('fa-square-o'); + + // check current sorting mode + $("#" + newSortingMode).children("i") + .removeClass('fa-square-o') + .addClass('fa-check-square-o'); + + // remove sorting id append + newSortingMode = newSortingMode.replace('sorting_', ''); + + // Switch sorting mode and reload stream + getStream().sort = newSortingMode; + getStream().init(); + }); + }; + + module.export({ + StreamEntry: StreamEntry, + Stream: Stream, + getStream: getStream, + getEntry: getEntry, + init: init + }); +}); + +/* TODO: + Stream.prototype.wallStick = function (url) { + $.ajax({ + dataType: "json", + type: 'post', + url: url + }).done(function (data) { + if (data.success) { + if (currentStream) { + $.each(data.wallEntryIds, function (k, wallEntryId) { + currentStream.deleteEntry(wallEntryId); + currentStream.prependEntry(wallEntryId); + }); + $('html, body').animate({scrollTop: 0}, 'slow'); + } + } else { + alert(data.errorMessage); + } + }); + }; + + Stream.prototype.wallUnstick = function (url) { + $.ajax({ + dataType: "json", + type: 'post', + url: url + }).done(function (data) { + if (data.success) { + //Reload the whole stream, since we have to reorder the entries + currentStream.showStream(); + } + }); + }; + + /** + * Click Handler for Archive Link of Wall Posts + * (archiveLink.php) + * + * @param {type} className + * @param {type} id + + Stream.prototype.wallArchive = function (id) { + + url = wallArchiveLinkUrl.replace('-id-', id); + + $.ajax({ + dataType: "json", + type: 'post', + url: url + }).done(function (data) { + if (data.success) { + if (currentStream) { + $.each(data.wallEntryIds, function (k, wallEntryId) { + //currentStream.reloadWallEntry(wallEntryId); + // fade out post + setInterval(fadeOut(), 1000); + + function fadeOut() { + // fade out current archived post + $('#wallEntry_' + wallEntryId).fadeOut('slow'); + } + }); + } + } + }); + }; + + + /** + * Click Handler for Un Archive Link of Wall Posts + * (archiveLink.php) + * + * @param {type} className + * @param {type} id + + Stream.prototype.wallUnarchive = function (id) { + url = wallUnarchiveLinkUrl.replace('-id-', id); + + $.ajax({ + dataType: "json", + type: 'post', + url: url + }).done(function (data) { + if (data.success) { + if (currentStream) { + $.each(data.wallEntryIds, function (k, wallEntryId) { + currentStream.reloadWallEntry(wallEntryId); + }); + + } + } + }); + }; + + + /* + module.StreamItem.prototype.highlightContent = function () { + var $content = this.getContent(); + $content.addClass('highlight'); + $content.delay(200).animate({backgroundColor: 'transparent'}, 1000, function () { + $content.removeClass('highlight'); + $content.css('backgroundColor', ''); + }); + }; + */ \ No newline at end of file diff --git a/js/dist/humhub.all.min.js b/js/dist/humhub.all.min.js index f5b7b64553..9df265fbbf 100644 --- a/js/dist/humhub.all.min.js +++ b/js/dist/humhub.all.min.js @@ -1 +1 @@ -var humhub=humhub||function(a){var b={},c=[],d=!1,e=function(b,e){var i=g(b,!0);i.id="humhub.modules."+h(b),i.require=f,i["export"]=function(b){a.extend(i,b)},e(i,f,a),d?i.init():c.push(i)},f=function(a){var b=g(a);return b||console.warn("No module found for id: "+a),b},g=function(c,d){try{var e=h(c),f=b;return a.each(e.split("."),function(a,b){if(b in f)f=f[b];else{if(!d)return f=void 0,!1;f=f[b]={}}}),f}catch(g){return}},h=function(a){return i(i(a,"humhub."),"modules.")},i=function(a,b){return j(a,b)?a.substring(b.length,a.length):a},j=function(a,b){return a&&b?0===a.indexOf(b):!1};return a(document).ready(function(){a.each(c,function(a,b){b.init&&b.init(),d=!0,console.log("Module initialized: "+b.id)})}),{initModule:e}}($);humhub.initModule("util",function(a,b,c){var d={isFunction:function(a){return c.isFunction(a)},isObject:function(a){return c.isPlainObject(a)},isJQuery:function(a){return this.isDefined(a)&&a.jquery},isArray:function(a){return c.isArray(a)},isString:function(a){return"string"==typeof a},isNumber:function(a){return this.isDefined(a)&&!isNaN(parseFloat(a))&&isFinite(a)},isBoolean:function(a){return"boolean"==typeof a},isDefined:function(a){if(arguments.length>1){var b=!0,c=this;return this.each(arguments,function(a,b){return c.isDefined(b)?void 0:!1}),b}return"undefined"!=typeof a},inherits:function(a,b){a.prototype=Object.create(b.prototype),a._super=b.prototype}},e={cutprefix:function(a,b){return this.startsWith(b)?a.substring(b.length,a.length):a},cutsuffix:function(a,b){return a.slice(0,-1*b.length)},startsWith:function(a,b){return d.isDefined(a)&&d.isDefined(b)?0===a.indexOf(b):!1},endsWith:function(a,b){return d.isDefined(a)&&d.isDefined(b)?-1!==a.indexOf(b,a.length-b.length):!1}};a["export"]({object:d,string:e})}),humhub.initModule("additions",function(a,b,c){var d={};a.registerAddition=function(a,b){d[a]||(d[a]=[]),d[a].push(b)},a.applyTo=function(a){var b=c(a);c.each(d,function(a,d){c.each(d,function(d,e){c.each(b.find(a).addBack(a),function(){try{var d=c(this);e.apply(d,[d,b])}catch(f){console.error("Error while applying addition "+e+" on selector "+a)}})})})},a.init=function(){}}),humhub.initModule("client",function(a,b,c){var d=b("util").object,e=(b("scripts"),function(){}),f=function(a){this.data=a};f.prototype.isConfirmation=function(){return 0===this.getStatus()},f.prototype.isError=function(){return this.getStatus()>0||this.getErrors().length},f.prototype.getStatus=function(){return this.data&&d.isDefined(this.data.status)?this.data.status:-1},f.prototype.getErrorTitle=function(){return this.data?this.data.errorTitle:void 0},f.prototype.getFirstError=function(){var a=this.getErrors();return a.length?a[0]:void 0},f.prototype.setAjaxError=function(a,b,c,d,e){this.xhr=a,this.textStatus=c,this.data=d||{},this.data.status=e||a.status,this.data.errors=[b]},f.prototype.getErrors=function(){if(this.data){var a=this.data.errors||[];return d.isString(a)?[a]:a}return[]},f.prototype.getContent=function(){return this.data.content},f.prototype.getPartial=function(a){return this.data?d.isObject(this.data.content)?a?this.data.content[a]:this.data.content:a?void 0:this.data.content:void 0},f.prototype.toString=function(){return"{ status: "+this.getStatus()+" error: "+this.getErrors()+" data: "+this.getContent()+" }"};var g=function(a,b){var b=b||{};a=d.isString(a)?c(a):a,b.type=a.attr("method")||"post",b.data=a.serialize(),h(a.attr("action"),b)},h=function(a,b){var b=b||{},e=b.async||!0,g=b.dataType||"json",h=function(c,e,g,h,i){if(b.error&&d.isFunction(b.error)){var j=new f;j.setAjaxError(c,g,e,h,i),b.error(j)}else console.warn("Unhandled ajax error: "+a+" type"+type+" error: "+g)},i=function(a,c,d){var e=new f(a);return e.isError()?h(d,"application",e.getErrors(),a,e.getStatus()):void(b.success&&(e.textStatus=c,e.xhr=d,b.success(e)))};c.ajax({url:a,data:b.data,type:b.type,beforeSend:b.beforeSend,processData:b.processData,contentType:b.contentType,async:e,dataType:g,success:i,error:h})};a["export"]({ajax:h,submit:g,init:e})}),humhub.initModule("ui",function(a,b,c){var d=b("additions");a.init=function(){d.registerAddition(".autosize",function(a){a.autosize()})}}),humhub.initModule("actions",function(a,b,c){var d={},e=b("util").object,f=b("util").string,g=b("client");a.init=function(){this.bindAction(document,"click","[data-action-click]"),this.bindAction(document,"dblclick","[data-action-dblclick]"),this.bindAction(document,"change","[data-action-mouseout]"),b("additions").registerAddition("[data-action-load-button]",function(){var a=this;this.on("click.humhub-action-load-button",function(b){a.find(".action-loader").length||a.append('')})})},a.registerHandler=function(a,b){a&&b&&(d[a]=b)},a.registerAjaxHandler=function(a,b,f,h){h=h||{},a&&(e.isFunction(b)?(h.success=b,h.error=f):h=b,b&&(d[a]=function(a){var b=c(this).data("url-"+a.type)||c(this).data("url");g.ajax(b,h,a)}))},a.bindAction=function(a,g,i,j){a=a||document;var k=a.jquery?a:c(a);k.on(g+".humhub-action",i,function(a){a.preventDefault(),$trigger=c(this);var i=$trigger.data("action-"+g),k={type:g,$trigger:$trigger,handler:i};k.finish=function(){h(a)};try{if(e.isFunction(j))j.apply($trigger,[k]);else if(d[i]){var l=d[i];l.apply($trigger,[k])}else if(!d["humhub.modules.content.actiontHandler"](k)){var m=i.split("."),l=m[m.length-1],n=b(f.cutsuffix(i,"."+l));n[l]({type:g,$trigger:$trigger})}}catch(o){console.error('Error while handling action event for handler "'+i+'"',o),h(a)}})};var h=function(a){a.target&&($target=c(a.target),$loader=$target.find(".action-loader"),$loader.length&&$loader.remove())}}),humhub.initModule("content",function(a,b,c){var d=b("client"),e=b("util").object,f=b("actions");a.init=function(){f.registerHandler("humhub.modules.content.actiontHandler",function(b){return a.handleAction(b)})},a.handleAction=function(a){var c=this.getContentBase(a.$trigger);if(c.length){var d=b(c.data("content-base"));if(d){var e=new d(c);if(a.handler&&e[a.handler])return e[a.handler](a),!0}else console.error("No ContentType found for "+c.data("content-base"))}return!1},a.getContentBase=function(a){return a.closest("[data-content-base]")};var g=function(a){a&&("string"==typeof a?(this.id=a,this.$=c("#"+a)):a.jquery&&(this.$=a,this.id=this.$.attr("id")))};g.prototype.getKey=function(){return this.$.data("content-key")},g.prototype.getEditUrl=function(){var a=this.$.data("content-edit-url");if(!a){var b=this.getParentContentBase("[data-content-base]");if(b)return b.getEditUrl()}return a},g.prototype.getParentContentBase=function(){var a=this.$.parent().closest("[data-content-base]");if(a.length)try{var c=b(a.data("content-base"));return new c(a)}catch(d){console.error("Could not instantiate parent content base: "+a.data("content-base"))}},g.prototype.create=function(a){this.edit(a)},g.prototype.edit=function(a){var c=this.getEditUrl(),g=this.getKey(),h=b("ui.modal").global;if(!c)return void console.error("No editUrl found for edit content action editUrl: "+c+" contentId "+g);var i=this;d.ajax(c,{data:{id:g},beforeSend:function(){h.loader()},success:function(b){h.content(b.getContent(),function(){f.bindAction(h.getBody(),"click","[data-content-save]",function(b){d.submit(h.getForm(),{success:function(c){e.isFunction(a)?a(c,h)&&h.close():(i.replaceContent(c.getContent()),h.close()),b.finish()},error:function(a){h.error(a),console.error("Error while submitting form :"+a),b.finish()}})})})},error:function(a){h.error(a),console.log("Error occured while editing content: "+a.getFirstError())}})},g.prototype.replaceContent=function(a){try{this.$.html(c(a).children())}catch(b){console.error("Error occured while replacing content: "+this.id,b)}},g.prototype["delete"]=function(){},a.Content=g}),humhub.initModule("stream",function(a,b,c){var d="#wallEntry_",e="wallStream",f=function(a){"string"==typeof a?(this.id=a,this.$=c("#"+a)):a.jquery&&(this.$=a,this.id=this.$.attr("id"))};f.prototype.remove=function(){this.$.remove()},f.prototype.getContentKey=function(){},f.prototype.edit=function(){},f.prototype["delete"]=function(){},f.prototype.getContent=function(){return this.$.find(".content")};var g=function(a){this.id=a,this.$=c("#"+a)};g.prototype.getEntry=function(b){return new a.Entry(this.$.find(d+b))},g.prototype.wallStick=function(a){c.ajax({dataType:"json",type:"post",url:a}).done(function(a){a.success?currentStream&&(c.each(a.wallEntryIds,function(a,b){currentStream.deleteEntry(b),currentStream.prependEntry(b)}),c("html, body").animate({scrollTop:0},"slow")):alert(a.errorMessage)})},g.prototype.wallUnstick=function(a){c.ajax({dataType:"json",type:"post",url:a}).done(function(a){a.success&¤tStream.showStream()})},g.prototype.wallArchive=function(a){url=wallArchiveLinkUrl.replace("-id-",a),c.ajax({dataType:"json",type:"post",url:url}).done(function(a){a.success&¤tStream&&c.each(a.wallEntryIds,function(a,b){function d(){c("#wallEntry_"+b).fadeOut("slow")}setInterval(d(),1e3)})})},g.prototype.wallUnarchive=function(a){url=wallUnarchiveLinkUrl.replace("-id-",a),c.ajax({dataType:"json",type:"post",url:url}).done(function(a){a.success&¤tStream&&c.each(a.wallEntryIds,function(a,b){currentStream.reloadWallEntry(b)})})};var h=function(){return a.mainStream||(a.mainStream=new a.Stream(e)),a.mainStream},i=function(b){return a.getStream().getEntry(b)};a["export"]({getStream:h,getEntry:i})}),humhub.initModule("ui.modal",function(a,b,c){var d=b("additions"),e=(b("actions"),[]),f='',g='',h='',i="Error",j="An unknown error occured!",k=function(a){this.$modal=c("#"+a),this.$modal.lengh||this.createModal(a),this.initModal(),e.push(this)};k.prototype.createModal=function(a){this.$modal=c(f).attr("id",a),c("body").append(this.$modal)},k.prototype.initModal=function(){this.reset();var a=this;this.getDialog().on("click","[data-modal-close]",function(){a.close()}).on("click","[data-modal-clear-error]",function(){a.clearErrorMessage()}).on("click",function(a){a.stopPropagation()}),this.$modal.on("click",function(){a.close()})},k.prototype.close=function(){var a=this;this.$modal.fadeOut("fast",function(){a.getContent().html(""),a.reset()})},k.prototype.loader=function(){this.reset(),this.show()},k.prototype.reset=function(){this.content(''),this.isFilled=!1},k.prototype.content=function(a,b){try{var c=this;this.clearErrorMessage(),this.getContent().html(a).promise().always(function(){d.applyTo(c.getContent()),!b||b(this.$modal)}),this.isFilled=!0}catch(e){console.error("Error while setting modal content",e),this.setErrorMessage(e.message),d.applyTo(c.$modal)}},k.prototype.error=function(a,b){1===arguments.length&&a&&(b=a.getFirstError?a.getFirstError():a,a=a.getErrorTitle?a.getErrorTitle():i),a=a||i,b=b||j,this.isFilled?this.setErrorMessage(b):(this.clear(),this.setTitle(a),this.setBody(""),this.setErrorMessage(b),this.$modal.show())},k.prototype.clearErrorMessage=function(){var a=this.getErrorMessage();a.length&&a.fadeOut("fast",function(){a.remove()})},k.prototype.setErrorMessage=function(a){var b=this.getErrorMessage();b.length?(b.css("opacity",0),b.text(a),b.animate({opacity:1},"fast")):this.getBody().prepend('")},k.prototype.getErrorMessage=function(){return this.getContent().find(".modal-error")},k.prototype.show=function(){this.$modal.show()},k.prototype.clear=function(){this.getContent().empty()},k.prototype.getContent=function(){return this.$modal.find(".modal-content:first")},k.prototype.getDialog=function(){return this.$modal.find(".modal-dialog")},k.prototype.getForm=function(){return this.$modal.find("form")},k.prototype.setTitle=function(a){var b=this.getHeader();b.length||(this.getContent().prepend(c(g)),b=this.getHeader()),b.find(".modal-title").html(a)},k.prototype.setBody=function(a){var b=this.getBody();b.length||(this.getContent().append(c(h)),b=this.getBody()),b.html(a)},k.prototype.getHeader=function(){return this.$modal.find(".modal-header")},k.prototype.getBody=function(){return this.$modal.find(".modal-body")},a["export"]({init:function(){a.global=new k("global-modal")},Modal:k})}); \ No newline at end of file +var humhub=humhub||function(a){var b={},c=[],d=!1,e=function(b,e){var h=g(b,!0);h.id="humhub.modules."+i(b),h.require=f,h["export"]=function(b){a.extend(h,b)},e(h,f,a),d?h.init():c.push(h)},f=function(a){var b=g(a);return b||console.warn("No module found for id: "+a),b},g=function(c,d){try{var e=i(c),f=b;return a.each(e.split("."),function(a,b){if(b in f)f=f[b];else{if(!d)return f=void 0,!1;f=f[b]={}}}),f}catch(g){return}},h={get:function(a,b,c){if(l(b)){var d=this.getModuleConfig(a)[b];return l(d)?d:c}},getModuleConfig:function(a){return this.module||(this.module={}),this.module},is:function(a,b,c){return this.get(a,b,c)===!0},set:function(b,c,d){2===arguments.length?a.extend(this.getModuleConfig(b),c):3===arguments.length&&(this.getModuleConfig(b)[c]=d)}},i=function(a){return j(j(a,"humhub."),"modules.")},j=function(a,b){return k(a,b)?a.substring(b.length,a.length):a},k=function(a,b){return a&&b?0===a.indexOf(b):!1},l=function(a){return"undefined"!=typeof a};return a(document).ready(function(){a.each(c,function(a,b){b.init&&b.init(),d=!0,console.log("Module initialized: "+b.id)})}),{initModule:e,modules:b,config:h}}($);humhub.initModule("util",function(a,b,c){var d={isFunction:function(a){return c.isFunction(a)},isObject:function(a){return c.isPlainObject(a)},isJQuery:function(a){return this.isDefined(a)&&a.jquery},isArray:function(a){return c.isArray(a)},isEmpty:function(a){return c.isEmptyObject(a)},isString:function(a){return"string"==typeof a},isNumber:function(a){return this.isDefined(a)&&!isNaN(parseFloat(a))&&isFinite(a)},isBoolean:function(a){return"boolean"==typeof a},isDefined:function(a){if(arguments.length>1){var b=!0,c=this;return this.each(arguments,function(a,b){return c.isDefined(b)?void 0:!1}),b}return"undefined"!=typeof a},inherits:function(a,b){a.prototype=Object.create(b.prototype),a._super=b.prototype}},e={cutprefix:function(a,b){return this.startsWith(a,b)?a.substring(b.length,a.length):a},cutsuffix:function(a,b){return a.slice(0,-1*b.length)},startsWith:function(a,b){return d.isDefined(a)&&d.isDefined(b)?0===a.indexOf(b):!1},endsWith:function(a,b){return d.isDefined(a)&&d.isDefined(b)?-1!==a.indexOf(b,a.length-b.length):!1}};a["export"]({object:d,string:e})}),humhub.initModule("additions",function(a,b,c){var d={};a.registerAddition=function(a,b){d[a]||(d[a]=[]),d[a].push(b)},a.applyTo=function(a){var b=c(a);c.each(d,function(a,d){c.each(d,function(d,e){c.each(b.find(a).addBack(a),function(){try{var d=c(this);e.apply(d,[d,b])}catch(f){console.error("Error while applying addition "+e+" on selector "+a)}})})})},a.init=function(){}}),humhub.initModule("client",function(a,b,c){var d=b("util").object,e=(b("scripts"),function(){}),f=function(a){this.data=a,c.extend(this,a)};f.prototype.isConfirmation=function(){return 0===this.getStatus()},f.prototype.isError=function(){return this.getStatus()>0||this.getErrors().length},f.prototype.getStatus=function(){return this.status?this.status:-1},f.prototype.getFirstError=function(){var a=this.getErrors();return a.length?a[0]:void 0},f.prototype.setAjaxError=function(a,b,c,d,e){this.xhr=a,this.textStatus=c,this.status=e||a.status,this.errors=[b]},f.prototype.getErrors=function(){var a=this.errors||[];return d.isString(a)?[a]:a},f.prototype.toString=function(){return"{ status: "+this.getStatus()+" error: "+this.getErrors()+" data: "+this.getContent()+" }"};var g=function(a,b){var b=b||{};a=d.isString(a)?c(a):a,b.type=a.attr("method")||"post",b.data=a.serialize(),i(a.attr("action"),b)},h=function(a,b){var b=b||{};return b.type="POST",i(a,b)},i=function(a,b){return new Promise(function(b,e){var g=g||{},h=g.async||!0,i=g.dataType||"json",j=function(a,b,c,h,i){if(g.error&&d.isFunction(g.error)){var j=new f;j.setAjaxError(a,c,b,h,i),g.error(j)}e(a,b,c,h,i)},k=function(a,c,d){var e=new f(a);return e.isError()?j(d,"application",e.getErrors(),a,e.getStatus()):(g.success&&(e.textStatus=c,e.xhr=d,g.success(e)),void b(e))};c.ajax({url:a,data:g.data,type:g.type,beforeSend:g.beforeSend,processData:g.processData,contentType:g.contentType,async:h,dataType:i,success:k,error:j})})};a["export"]({ajax:i,post:h,submit:g,init:e})}),humhub.initModule("ui",function(a,b,c){var d=b("additions");a.init=function(){d.registerAddition(".autosize",function(a){a.autosize()})}}),humhub.initModule("ui.modal",function(a,b,c){var d=b("util").object,e=b("additions"),f=[],g='',h='',i='',j="Error",k="An unknown error occured!",l=function(a){this.$modal=c("#"+a),this.$modal.lengh||this.createModal(a),this.initModal(),f.push(this)};l.prototype.createModal=function(a){this.$modal=c(g).attr("id",a),c("body").append(this.$modal)},l.prototype.initModal=function(){this.reset();var a=this;this.getDialog().on("click","[data-modal-close]",function(){a.close()}).on("click","[data-modal-clear-error]",function(){a.clearErrorMessage()}).on("click",function(a){a.stopPropagation()}),this.$modal.on("click",function(){a.close()})},l.prototype.close=function(){var a=this;this.$modal.fadeOut("fast",function(){a.getContent().html(""),a.reset()})},l.prototype.loader=function(){this.reset(),this.show()},l.prototype.reset=function(){this.content(''),this.isFilled=!1},l.prototype.content=function(a,b){try{var c=this;this.clearErrorMessage(),this.getContent().html(a).promise().always(function(){e.applyTo(c.getContent()),!b||b(this.$modal)}),this.isFilled=!0}catch(d){console.error("Error while setting modal content",d),this.setErrorMessage(d.message),e.applyTo(c.$modal)}},l.prototype.error=function(a,b){1===arguments.length&&a&&(b=a.getFirstError?a.getFirstError():a,a=a.getErrorTitle?a.getErrorTitle():j),a=a||j,b=b||k,this.isFilled?this.setErrorMessage(b):(this.clear(),this.setTitle(a),this.setBody(""),this.setErrorMessage(b),this.$modal.show())},l.prototype.clearErrorMessage=function(){var a=this.getErrorMessage();a.length&&a.fadeOut("fast",function(){a.remove()})},l.prototype.setErrorMessage=function(a){var b=this.getErrorMessage();b.length?(b.css("opacity",0),b.text(a),b.animate({opacity:1},"fast")):this.getBody().prepend('")},l.prototype.getErrorMessage=function(){return this.getContent().find(".modal-error")},l.prototype.show=function(){this.$modal.show()},l.prototype.clear=function(){this.getContent().empty()},l.prototype.getContent=function(){return this.$modal.find(".modal-content:first")},l.prototype.getDialog=function(){return this.$modal.find(".modal-dialog")},l.prototype.getForm=function(){return this.$modal.find("form")},l.prototype.setTitle=function(a){var b=this.getHeader();b.length||(this.getContent().prepend(c(h)),b=this.getHeader()),b.find(".modal-title").html(a)},l.prototype.setBody=function(a){var b=this.getBody();b.length||(this.getContent().append(c(i)),b=this.getBody()),b.html(a)},l.prototype.getHeader=function(){return this.$modal.find(".modal-header")},l.prototype.getBody=function(){return this.$modal.find(".modal-body")};var m=function(a,b){l.call(this,a),this.initButtons()};m.prototype.initButtons=function(a){this.$confirm=this.$modal.find("[data-modal-submit]"),this.$confirm.on("click",a)},d.inherits(m,l),a["export"]({init:function(){a.global=new l("global-modal")},Modal:l})}),humhub.initModule("actions",function(a,b,c){var d={},e=b("util").object,f=b("util").string,g=b("client");a.init=function(){this.bindAction(document,"click","[data-action-click]"),this.bindAction(document,"dblclick","[data-action-dblclick]"),this.bindAction(document,"change","[data-action-mouseout]"),b("additions").registerAddition("[data-action-load-button]",function(){var a=this;this.on("click.humhub-action-load-button",function(b){a.find(".action-loader").length||a.append('')})})},a.registerHandler=function(a,b){a&&b&&(d[a]=b)},a.registerAjaxHandler=function(a,b,f,h){h=h||{},a&&(e.isFunction(b)?(h.success=b,h.error=f):h=b,b&&(d[a]=function(a){var b=c(this).data("url-"+a.type)||c(this).data("url");g.ajax(b,h,a)}))},a.bindAction=function(a,g,i,j){a=a||document;var k=a.jquery?a:c(a);k.on(g+".humhub-action",i,function(a){a.preventDefault(),$trigger=c(this);var i=$trigger.data("action-"+g),k={type:g,$trigger:$trigger,handler:i};k.finish=function(){h(a)};try{if(e.isFunction(j))j.apply($trigger,[k]);else if(d[i]){var l=d[i];l.apply($trigger,[k])}else if(!d["humhub.modules.content.actiontHandler"](k)){var m=i.split("."),l=m[m.length-1],n=b(f.cutsuffix(i,"."+l));n[l]({type:g,$trigger:$trigger})}}catch(o){console.error('Error while handling action event for handler "'+i+'"',o),h(a)}})};var h=function(a){a.target&&($target=c(a.target),$loader=$target.find(".action-loader"),$loader.length&&$loader.remove())}}),humhub.initModule("content",function(a,b,c){var d=b("client"),e=b("util").object,f=b("actions"),g=function(a){a&&(this.$=e.isString(a)?c("#"+a):a,this.contentBase=this.$.data("content-base"))};g.prototype.getContentActions=function(){return["create","edit","delete"]},g.prototype.getKey=function(){return this.$.data("content-pk")},g.prototype.data=function(a){var b=this.$.data(a);if(!b){var c=this.getParentContentBase();if(c)return c.data(a)}return b},g.prototype.getParentContentBase=function(){var a=this.$.parent().closest("[data-content-base]");if(a.length)try{var c=b(a.data("content-base"));return new c(a)}catch(d){console.error("Could not instantiate parent content base: "+a.data("content-base"))}},g.prototype.create=function(a){indexOf(this.getContentActions(),"create")<0||this.edit(a)},g.prototype.edit=function(a){if(!(indexOf(this.getContentActions(),"edit")<0)){var c=this.data("content-edit-url"),g=this.getKey(),h=b("ui.modal").global;if(!c)return void console.error("No editUrl found for edit content action editUrl: "+c+" contentId "+g);var i=this;d.ajax(c,{data:{id:g},beforeSend:function(){h.loader()},success:function(b){h.content(b.getContent(),function(){f.bindAction(h.getBody(),"click","[data-content-save]",function(b){d.submit(h.getForm(),{success:function(c){e.isFunction(a)?a(c,h)&&h.close():(i.replaceContent(c.getContent()),h.close()),b.finish()},error:function(a){h.error(a),console.error("Error while submitting form :"+a),b.finish()}})})})},error:function(a){h.error(a),console.log("Error occured while editing content: "+a.getFirstError())}})}},g.prototype["delete"]=function(){if(!(this.getContentActions().indexOf("delete")<0)){var a=this,b=this.data("content-delete-url");b?d.post(b,{data:{id:a.getKey()},success:function(b){b.success,a.remove()},error:function(a){console.error(a)}}):console.error("Content delete was called, but no url could be determined for "+this.contentBase)}},g.prototype.replaceContent=function(a){try{var b=this;this.$.animate({opacity:0},"fast",function(){b.$.html(c(a).children()),b.$.stop().animate({opacity:1},"fast"),b.highlight&&b.highlight()})}catch(d){console.error("Error occured while replacing content: "+this.$.attr("id"),d)}},g.prototype.remove=function(){var a=this;this.$.animate({height:"toggle",opacity:"toggle"},"fast",function(){a.$.remove()})},g.getContentBase=function(a){return a.closest("[data-content-base]")},g.getInstance=function(a){a=e.isString(a)?c("#"+a):a;var d=b(a.data("content-base"));return d?new d(a):void 0};var h=function(){f.registerHandler("humhub.modules.content.actiontHandler",function(b){return a.handleAction(b)})};handleAction=function(a){var b=g.getContentBase(a.$trigger);if(b.length){var c=g.getInstance(b);if(c){if(a.handler&&c[a.handler])return c[a.handler](a),!0}else console.error("No ContentType found for "+b.data("content-base"))}return!1},a["export"]({Content:g,init:h,handleAction:handleAction})}),humhub.initModule("stream",function(a,b,c){var d=b("util"),e=d.object,f=d.string,g=b("client"),h=(b("modal"),b("content").Content),i=8,j=4,k=function(a){this.$=e.isString(a)?this.$=c("#"+a):a,h.call(this)};e.inherits(k,h),k.prototype.getContentActions=function(){return["delete","edit"]},k.prototype["delete"]=function(){var a=this.getContentInstance();a&&a["delete"]?a["delete"]():k._super["delete"].call(this)},k.prototype.reload=function(){m().reload(this)},k.prototype.edit=function(){},k.prototype.getContentInstance=function(){return h.getInstance(this.$.find("[data-content-base]"))};var l=function(a){return this.$=e.isString(a)?c("#"+a):a,this.$.length?(this.$.data("stream-contentid")&&(this.contentId=parseInt(this.$.data("stream-contentid"))),this.$stream=this.$.find(".s2_stream"),this.url=this.$.data("stream"),this.$loader=this.$stream.find(".streamLoader"),this.$content=this.$stream.find(".s2_streamContent"),this.$filter=c(".wallFilterPanel"),this.filters=[],this.sort="c",void h.call(this)):void console.error("Could not initialize stream, invalid container given"+a)};e.inherits(l,h),l.prototype.getContentActions=function(){return[]},l.prototype.init=function(){return this.clear(),this.$stream.show(),this.isShowSingleEntry()?this.loadSingleEntry(this.contentId):this.loadEntries(i),this},l.prototype.clear=function(){this.lastEntryLoaded=!1,this.readOnly=!1,this.loading=!1,this.$.find(".s2_streamContent").empty(),this.$.find(".s2_stream").hide(),this.$.find(".s2_single").hide(),this.$.find(".streamLoader").hide(),this.$.find(".emptyStreamMessage").hide(),this.$.find(".emptyFilterStreamMessage").hide(),this.$.find(".back_button_holder").hide(),this.$filter.hide()},l.prototype.loadSingleEntry=function(a){this.$.find(".back_button_holder").show(),this.loadEntries(1,a+1,"")},l.prototype.reloadEntry=function(a){var b=this;return new Promise(function(c,d){if(a=a instanceof k?a:b.getEntry(a),!a)return console.warn("Attempt to reload of non existent entry: "+a),void d();var e=a.getKey();return b._load(1,e+1,"").then(function(b){b.content[e]?(a.replaceContent(b.content[e].output),c(a)):(console.warn("Reload failed: ContentId not found in response: "+e),d())},d)})},l.prototype.loadEntries=function(a,b,d,f){if(!this.loading&&!this.lastEntryLoaded){this.$loader.show(),this.loading=!0,a=a||j,b=b||this.getLastContentId(),d=d||this.getFilterString(),f=f||this.sort;var g=this;return new Promise(function(h,i){g._load(a,b,d,f).then(function(a){g.$loader.hide(),e.isEmpty(a.content)?(g.lastEntryLoaded=!0,c("#btn-load-more").hide()):(g.lastEntryLoaded=a.is_last,g.appendEntries(a)),g.loading=!1,g.onChange(),h()})["catch"](function(a){g.loading=!1,g.$loader.hide(),i()})})}},l.prototype._load=function(a,b,c,d){return g.ajax(this.url,{data:{filters:c,sort:d,from:b,limit:a}})},l.prototype.getLastContentId=function(){var a=this.$stream.find("[data-content-pk]").last();return a.length?a.data("stream-contentid"):void 0},l.prototype.appendEntries=function(a){var b=this,d="";return c.each(a.contentIds,function(c,e){var f=b.$.find('[data-content-pk="'+e+'"]');f.length&&f.remove(),d+=a.content[e].output}),this.$content.append(d)},l.prototype.onChange=function(){this.readOnly?(c(".wallReadOnlyHide").hide(),c(".wallReadOnlyShow").show()):c(".wallReadOnlyShow").hide();var a=this.hasEntries();a||this.hasFilter()?a?this.isShowSingleEntry()||(this.$filter.show(),this.$.find(".emptyStreamMessage").hide(),this.$.find(".emptyFilterStreamMessage").hide()):this.$.find(".emptyFilterStreamMessage").hide():(this.$.find(".emptyStreamMessage").show(),this.$filter.hide())},l.prototype.isShowSingleEntry=function(){return e.isDefined(this.contentId)},l.prototype.hasEntries=function(){return this.getEntryCount()>0},l.prototype.getEntryCount=function(){return this.$.find("[data-content-pk]").length},l.prototype.hasFilter=function(){return this.filters.length>0},l.prototype.getFilterString=function(){var a="";return c.each(this.filters,function(b,c){a+=c+","}),f.cutsuffix(a,",")},l.prototype.setFilter=function(a){this.filters.indexOf(a)<0&&this.filters.push(a)},l.prototype.unsetFilter=function(a){var b=this.filters.indexOf(a);b>-1&&this.filters.splice(b,1)},l.prototype.getEntry=function(a){return new k(this.$.find('[data-content-pk="'+a+'"]'))},l.prototype.getEntryByNode=function(a){return new k(a.closest("[data-content-pk]"))};var m=function(){return a.instance||(a.instance=new l(c("[data-stream]"))),a.instance},n=function(b){return a.getStream().getEntry(b)},o=function(){var a=m().init();c(window).scroll(function(){c(window).scrollTop()==c(document).height()-c(window).height()&&(!a||a.loading||a.isShowSingleEntry()||a.lastEntryLoaded||a.loadEntries())}),a.$.on("click",".singleBackLink",function(){a.contentId=void 0,a.init(),c(this).hide()}),p()},p=function(){c(".wallFilter").click(function(){var a=c(this),b=a.children("i");b.toggleClass("fa-square-o").toggleClass("fa-check-square-o"),b.hasClass("fa-check-square-o")?m().setFilter(a.attr("id").replace("filter_","")):m().unsetFilter(a.attr("id").replace("filter_","")),m().init()}),c(".wallSorting").click(function(){var a=c(this).attr("id");c(".wallSorting").find("i").removeClass("fa-check-square-o").addClass("fa-square-o"),c("#"+a).children("i").removeClass("fa-square-o").addClass("fa-check-square-o"),a=a.replace("sorting_",""),m().sort=a,m().init()})};a["export"]({StreamEntry:k,Stream:l,getStream:m,getEntry:n,init:o})}); \ No newline at end of file diff --git a/js/humhub.client.js b/js/humhub.client.js index 3c2e56d208..3d355a686b 100644 --- a/js/humhub.client.js +++ b/js/humhub.client.js @@ -33,6 +33,7 @@ humhub.initModule('client', function (module, require, $) { */ var Response = function (data) { this.data = data; + $.extend(this, data); }; /** @@ -52,14 +53,10 @@ humhub.initModule('client', function (module, require, $) { Response.prototype.isError = function () { return this.getStatus() > 0 || this.getErrors().length; }; - - Response.prototype.getStatus = function () { - return (this.data && object.isDefined(this.data.status)) ? this.data.status : -1; - }; - Response.prototype.getErrorTitle = function() { - return (this.data) ? this.data.errorTitle : undefined; - }; + Response.prototype.getStatus = function () { + return (this.status) ? this.status : -1; + }; Response.prototype.getFirstError = function() { var errors = this.getErrors(); @@ -71,9 +68,8 @@ humhub.initModule('client', function (module, require, $) { Response.prototype.setAjaxError = function(xhr, errorThrown, textStatus,data , status) { this.xhr = xhr; this.textStatus = textStatus; - this.data = data || {}; - this.data.status = status || xhr.status; - this.data.errors = [errorThrown]; + this.status = status || xhr.status; + this.errors = [errorThrown]; }; /** @@ -82,40 +78,8 @@ humhub.initModule('client', function (module, require, $) { * @returns {array} error array or empty array */ Response.prototype.getErrors = function () { - if (this.data) { - var errors = this.data.errors || []; - return (object.isString(errors)) ? [errors] : errors; - } - return []; - }; - - /** - * Returns the raw content object. The content object can either be an - * object with multiple partials {partialId: content string} or a single content string. - * @param {type} id - * @returns {undefined|humhub.client_L5.Response.data.content}1 - */ - Response.prototype.getContent = function () { - return this.data.content; - }; - - /** - * Returns the response partial. If no id is given we return the first partial - * we find. - * @returns {humhub.client_L5.Response.data.content} - */ - Response.prototype.getPartial = function (id) { - if (!this.data) { - return; - } - //TODO: handleResponse filter... - if (object.isObject(this.data.content)) { - return (id) ? this.data.content[id] : this.data.content; - } else if (!id) { - return this.data.content; - } - - return; + var errors = this.errors || []; + return (object.isString(errors)) ? [errors] : errors; }; Response.prototype.toString = function () { @@ -126,53 +90,62 @@ humhub.initModule('client', function (module, require, $) { var cfg = cfg || {}; $form = object.isString($form) ? $($form) : $form; cfg.type = $form.attr('method') || 'post'; - cfg.data = $form.serialize() + cfg.data = $form.serialize(); ajax($form.attr('action'), cfg); }; - var ajax = function (path, cfg) { + var post = function(path, cfg) { var cfg = cfg || {}; - var async = cfg.async || true; - var dataType = cfg.dataType || "json"; + cfg.type = 'POST'; + return ajax(path, cfg); + }; - var error = function (xhr, textStatus, errorThrown, data, status) { - //Textstatus = "timeout", "error", "abort", "parsererror", "application" - if (cfg.error && object.isFunction(cfg.error)) { - var response = new Response(); - response.setAjaxError(xhr, errorThrown, textStatus, data, status); - cfg.error(response); - } else { - console.warn('Unhandled ajax error: ' + path + " type" + type + " error: " + errorThrown); - } - }; + var ajax = function (path, cfg) { + return new Promise(function(resolve, reject) { + var cfg = cfg || {}; + var async = cfg.async || true; + var dataType = cfg.dataType || "json"; - var success = function (json, textStatus, xhr) { - var response = new Response(json); - if (response.isError()) { //Application errors - return error(xhr, "application", response.getErrors(), json, response.getStatus() ); - } else if (cfg.success) { - response.textStatus = textStatus; - response.xhr = xhr; - cfg.success(response); - } - }; + var error = function (xhr, textStatus, errorThrown, data, status) { + //Textstatus = "timeout", "error", "abort", "parsererror", "application" + if (cfg.error && object.isFunction(cfg.error)) { + var response = new Response(); + response.setAjaxError(xhr, errorThrown, textStatus, data, status); + cfg.error(response); + } + reject(xhr, textStatus, errorThrown, data, status); + }; - $.ajax({ - url: path, - data: cfg.data, - type: cfg.type, - beforeSend: cfg.beforeSend, - processData: cfg.processData, - contentType: cfg.contentType, - async: async, - dataType: dataType, - success: success, - error: error + var success = function (json, textStatus, xhr) { + var response = new Response(json); + if (response.isError()) { //Application errors + return error(xhr, "application", response.getErrors(), json, response.getStatus() ); + } else if (cfg.success) { + response.textStatus = textStatus; + response.xhr = xhr; + cfg.success(response); + } + resolve(response); + }; + + $.ajax({ + url: path, + data: cfg.data, + type: cfg.type, + beforeSend: cfg.beforeSend, + processData: cfg.processData, + contentType: cfg.contentType, + async: async, + dataType: dataType, + success: success, + error: error + }); }); }; module.export({ ajax: ajax, + post: post, submit: submit, init: init }); diff --git a/js/humhub.content.js b/js/humhub.content.js index d88b6bd1f1..330f71f3e5 100644 --- a/js/humhub.content.js +++ b/js/humhub.content.js @@ -9,67 +9,28 @@ humhub.initModule('content', function(module, require, $) { var object = require('util').object; var actions = require('actions'); - module.init = function() { - actions.registerHandler('humhub.modules.content.actiontHandler', function(event) { - return module.handleAction(event); - }); - }; - - /** - * Handles the given contentAction event. The event should provide the following properties: - * - * $trigger (required) : the trigger node of the event - * handler (required) : the handler functionn name to be executed on the content - * type (optoinal) : the event type 'click', 'change',... - * - * @param {object} event - event object - * @returns {Boolean} true if the contentAction could be executed else false - */ - module.handleAction = function(event) { - var $contentBase = this.getContentBase(event.$trigger); - if($contentBase.length) { - //Initialize a content instance by means of the content-base type and execute the handler - var ContentType = require($contentBase.data('content-base')); - if(ContentType) { - var content = new ContentType($contentBase); - if(event.handler && content[event.handler]) { - content[event.handler](event); - return true; - } - } else { - console.error('No ContentType found for '+$contentBase.data('content-base')); - } - } - return false; - }; - - module.getContentBase = function($element) { - return $element.closest('[data-content-base]'); - }; - - var Content = function(id) { - if(!id) { //Create content + var Content = function(container) { + if(!container) { //Create content return; } - if (typeof id === 'string') { - this.id = id; - this.$ = $('#' + id); - } else if (id.jquery) { - this.$ = id; - this.id = this.$.attr('id'); - } + this.$ = (object.isString(container)) ? $('#' + container) : container; + this.contentBase = this.$.data('content-base'); + }; + + Content.prototype.getContentActions = function() { + return ['create','edit','delete']; }; Content.prototype.getKey = function () { - return this.$.data('content-key'); + return this.$.data('content-pk'); }; - Content.prototype.getEditUrl = function () { - var result = this.$.data('content-edit-url'); + Content.prototype.data = function(dataSuffix) { + var result = this.$.data(dataSuffix); if(!result) { - var parentContent = this.getParentContentBase('[data-content-base]'); + var parentContent = this.getParentContentBase(); if(parentContent) { - return parentContent.getEditUrl(); + return parentContent.data(dataSuffix); } } return result; @@ -89,11 +50,19 @@ humhub.initModule('content', function(module, require, $) { Content.prototype.create = function (addContentHandler) { //Note that this Content won't have an id, so the backend will create an instance + if(indexOf(this.getContentActions(), 'create') < 0) { + return; + } + this.edit(addContentHandler); }; Content.prototype.edit = function (successHandler) { - var editUrl = this.getEditUrl(); + if(indexOf(this.getContentActions(), 'edit') < 0) { + return; + } + + var editUrl = this.data('content-edit-url'); var contentId = this.getKey(); var modal = require('ui.modal').global; @@ -146,20 +115,103 @@ humhub.initModule('content', function(module, require, $) { }); }; - Content.prototype.replaceContent = function(content) { - try { - this.$.html($(content).children()); - } catch(e) { - console.error('Error occured while replacing content: '+this.id , e); + Content.prototype.delete = function () { + if(this.getContentActions().indexOf('delete') < 0) { + return; + } + + var that = this; + var url = this.data('content-delete-url'); + if(url) { + client.post(url, { + data: { + id: that.getKey() + }, + success: function(json) { + json.success; + that.remove(); + }, + error: function(json) { + console.error(json); + } + }) + } else { + console.error('Content delete was called, but no url could be determined for '+this.contentBase); } }; - Content.prototype.delete = function () { - //Search for data-content-delte-url on root. - //if(this.deleteModal) {open modal bla} - //Call this url with data-content-pk - //Trigger delete event + Content.prototype.replaceContent = function(content) { + try { + var that = this; + this.$.animate({ opacity: 0 }, 'fast', function() { + that.$.html($(content).children()); + that.$.stop().animate({ opacity: 1 }, 'fast'); + if(that.highlight) { + that.highlight(); + } + }); + } catch(e) { + console.error('Error occured while replacing content: '+this.$.attr('id') , e); + } }; - module.Content = Content; + Content.prototype.remove = function() { + var that = this; + this.$.animate({ height: 'toggle', opacity: 'toggle' }, 'fast', function() { + that.$.remove(); + //TODO: fire global event + }); + }; + + Content.getContentBase = function($element) { + return $element.closest('[data-content-base]'); + }; + + Content.getInstance = function($contentBase) { + $contentBase = (object.isString($contentBase)) ? $('#'+$contentBase) : $contentBase; + var ContentType = require($contentBase.data('content-base')); + if(ContentType) { + return new ContentType($contentBase); + } + }; + + var init = function() { + actions.registerHandler('humhub.modules.content.actiontHandler', function(event) { + return module.handleAction(event); + }); + }; + + /** + * Handles the given contentAction event. The event should provide the following properties: + * + * $trigger (required) : the trigger node of the event + * handler (required) : the handler functionn name to be executed on the content + * type (optoinal) : the event type 'click', 'change',... + * + * @param {object} event - event object + * @returns {Boolean} true if the contentAction could be executed else false + */ + handleAction = function(event) { + var $contentBase = Content.getContentBase(event.$trigger); + if($contentBase.length) { + //Initialize a content instance by means of the content-base type and execute the handler + var content = Content.getInstance($contentBase); + if(content) { + //Check if the content instance provides this actionhandler + if(event.handler && content[event.handler]) { + content[event.handler](event); + return true; + } + } else { + console.error('No ContentType found for '+$contentBase.data('content-base')); + } + } + return false; + }; + + module.export({ + Content : Content, + init : init, + handleAction: handleAction + }); }); \ No newline at end of file diff --git a/js/humhub.core.js b/js/humhub.core.js index 0400b2ea54..972aa965e2 100644 --- a/js/humhub.core.js +++ b/js/humhub.core.js @@ -149,6 +149,38 @@ var humhub = humhub || (function($) { } }; + /** + * Config implementation + */ + + var config = { + get : function(module, key, defaultVal) { + if(_isDefined(key)) { + var result = this.getModuleConfig(module)[key]; + return (_isDefined(result)) ? result : defaultVal; + } + }, + getModuleConfig: function(module) { + if(!this.module) { + this.module = {}; + } + return this.module; + }, + + is : function(module, key, defaultVal) { + return this.get(module, key,defaultVal) === true; + }, + + set : function(module, key, value) { + //Moduleid with multiple values + if(arguments.length === 2) { + $.extend(this.getModuleConfig(module), key); + } else if(arguments.length === 3) { + this.getModuleConfig(module)[key] = value; + } + } + }; + /** * Cuts the prefix humub.modules or modules. from the given value. * @param {type} value @@ -186,6 +218,10 @@ var humhub = humhub || (function($) { return val.indexOf(prefix) === 0; }; + var _isDefined = function(obj) { + return typeof obj !== 'undefined'; + }; + //Initialize all initial modules $(document).ready(function() { $.each(initialModules, function(i, module) { @@ -197,7 +233,11 @@ var humhub = humhub || (function($) { }); }); + + return { - initModule: initModule + initModule: initModule, + modules: modules, + config: config }; })($); \ No newline at end of file diff --git a/js/humhub.stream.js b/js/humhub.stream.js index 19b2a930cf..1ee4d70dff 100644 --- a/js/humhub.stream.js +++ b/js/humhub.stream.js @@ -2,183 +2,480 @@ * Core module for managing Streams and StreamItems * @type Function */ -humhub.initModule('stream', function(module, require, $) { +humhub.initModule('stream', function (module, require, $) { - var ENTRY_ID_SELECTOR_PREFIX = '#wallEntry_'; - var WALLSTREAM_ID = 'wallStream'; + var util = require('util'); + var object = util.object; + var string = util.string; + var client = require('client'); + var modal = require('modal'); + var Content = require('content').Content; + + var STREAM_INIT_COUNT = 8; + var STREAM_LOAD_COUNT = 4; + + //TODO: load streamUrl from config + //TODO: readonly /** - * Base class for all StreamItems + * Base class for all StreamContent * @param {type} id * @returns {undefined} */ - var StreamItem = function (id) { - if (typeof id === 'string') { - this.id = id; - this.$ = $('#' + id); - } else if (id.jquery) { - this.$ = id; - this.id = this.$.attr('id'); + var StreamEntry = function (id) { + this.$ = object.isString(id) ? this.$ = $('#' + id) : id; + Content.call(this); + }; + + object.inherits(StreamEntry, Content); + + StreamEntry.prototype.getContentActions = function() { + return ['delete', 'edit']; + }; + + StreamEntry.prototype.delete = function () { + var content = this.getContentInstance(); + if(content && content.delete) { + //TODO: modalconfirm + content.delete(); + } else { + StreamEntry._super.delete.call(this); } }; - - /** - * Removes the stream item from stream - */ - StreamItem.prototype.remove = function () { - this.$.remove(); + + StreamEntry.prototype.reload = function () { + getStream().reload(this); }; - - StreamItem.prototype.getContentKey = function () {} - - StreamItem.prototype.edit = function () { + + StreamEntry.prototype.edit = function () { //Search for data-content-edit-url on root. //Call this url with data-content-pk //Trigger delete event }; + + StreamEntry.prototype.getContentInstance = function () { + return Content.getInstance(this.$.find('[data-content-base]')); + }; - StreamItem.prototype.delete = function () { - //Search for data-content-delte-url on root. - //Call this url with data-content-pk - //Trigger delete event - }; - - StreamItem.prototype.getContent = function () { - return this.$.find('.content'); - }; - -/* - module.StreamItem.prototype.highlightContent = function () { - var $content = this.getContent(); - $content.addClass('highlight'); - $content.delay(200).animate({backgroundColor: 'transparent'}, 1000, function () { - $content.removeClass('highlight'); - $content.css('backgroundColor', ''); - }); - }; -*/ /** - * Stream implementation - * @param {type} id + * Stream implementation. + * + * @param {type} container id or jQuery object of the stream container * @returns {undefined} */ - var Stream = function (id) { - this.id = id; - this.$ = $('#' + id); - }; - - Stream.prototype.getEntry = function (id) { - //Search for data-content-base and try to initiate the Item class + var Stream = function (container) { + this.$ = (object.isString(container)) ? $('#' + container) : container; - return new module.Entry(this.$.find(ENTRY_ID_SELECTOR_PREFIX + id)); - }; + if (!this.$.length) { + console.error('Could not initialize stream, invalid container given'+ container); + return; + } + + //If a contentId is set on the stream root we will only show the single content + if(this.$.data('stream-contentid')) { + this.contentId = parseInt(this.$.data('stream-contentid')); + } + + this.$stream = this.$.find(".s2_stream"); + + //Cache some stream relevant data/nodes + this.url = this.$.data('stream'); //TODO: set this in config instead of data field + this.$loader = this.$stream.find(".streamLoader"); + this.$content = this.$stream.find('.s2_streamContent'); + this.$filter = $('.wallFilterPanel'); - Stream.prototype.wallStick = function (url) { - $.ajax({ - dataType: "json", - type: 'post', - url: url - }).done(function (data) { - if (data.success) { - if (currentStream) { - $.each(data.wallEntryIds, function (k, wallEntryId) { - currentStream.deleteEntry(wallEntryId); - currentStream.prependEntry(wallEntryId); - }); - $('html, body').animate({scrollTop: 0}, 'slow'); - } - } else { - alert(data.errorMessage); - } - }); + //TODO: make this configurable + this.filters = []; + this.sort = "c"; + + Content.call(this); }; - - Stream.prototype.wallUnstick = function (url) { - $.ajax({ - dataType: "json", - type: 'post', - url: url - }).done(function (data) { - if (data.success) { - //Reload the whole stream, since we have to reorder the entries - currentStream.showStream(); - } - }); + + object.inherits(Stream, Content); + + Stream.prototype.getContentActions = function() { + return []; }; - + /** - * Click Handler for Archive Link of Wall Posts - * (archiveLink.php) + * Initializes the stream, by clearing the stream and reloading initial stream entries, + * this should be called if any filter/sort settings are changed or the stream + * needs an reload. * - * @param {type} className - * @param {type} id + * @returns {humhub.stream_L5.Stream.prototype} */ - Stream.prototype.wallArchive = function (id) { - - url = wallArchiveLinkUrl.replace('-id-', id); - - $.ajax({ - dataType: "json", - type: 'post', - url: url - }).done(function (data) { - if (data.success) { - if (currentStream) { - $.each(data.wallEntryIds, function (k, wallEntryId) { - //currentStream.reloadWallEntry(wallEntryId); - // fade out post - setInterval(fadeOut(), 1000); - - function fadeOut() { - // fade out current archived post - $('#wallEntry_' + wallEntryId).fadeOut('slow'); - } - }); - } - } - }); + Stream.prototype.init = function () { + this.clear(); + this.$stream.show(); + if (this.isShowSingleEntry()) { + this.loadSingleEntry(this.contentId); + } else { + this.loadEntries(STREAM_INIT_COUNT); + } + return this; + }; + + Stream.prototype.clear = function() { + this.lastEntryLoaded = false; + this.readOnly = false; + this.loading = false; + this.$.find(".s2_streamContent").empty(); + this.$.find(".s2_stream").hide(); + this.$.find(".s2_single").hide(); + this.$.find(".streamLoader").hide(); + this.$.find(".emptyStreamMessage").hide(); + this.$.find(".emptyFilterStreamMessage").hide(); + this.$.find('.back_button_holder').hide(); + this.$filter.hide(); }; - - /** - * Click Handler for Un Archive Link of Wall Posts - * (archiveLink.php) - * - * @param {type} className - * @param {type} id - */ - Stream.prototype.wallUnarchive = function (id) { - url = wallUnarchiveLinkUrl.replace('-id-', id); - - $.ajax({ - dataType: "json", - type: 'post', - url: url - }).done(function (data) { - if (data.success) { - if (currentStream) { - $.each(data.wallEntryIds, function (k, wallEntryId) { - currentStream.reloadWallEntry(wallEntryId); - }); - - } + Stream.prototype.loadSingleEntry = function(contentId) { + this.$.find('.back_button_holder').show(); + this.loadEntries(1, (contentId + 1), ''); + }; + + Stream.prototype.reloadEntry = function(entry) { + var that = this; + return new Promise(function(resolve, reject) { + entry = (entry instanceof StreamEntry) ? entry : that.getEntry(entry); + + if(!entry) { + console.warn('Attempt to reload of non existent entry: '+entry); + reject(); + return; } + + var contentId = entry.getKey(); + return that._load(1, (contentId + 1), '').then(function(response) { + if(response.content[contentId]) { + entry.replaceContent(response.content[contentId].output); + resolve(entry); + } else { + console.warn('Reload failed: ContentId not found in response: '+contentId); + reject(); + } + }, reject); + }); + }; + + Stream.prototype.loadEntries = function (limit, from, filter, sort) { + if (this.loading || this.lastEntryLoaded) { + return; + } + + //Initialize loading process + this.$loader.show(); + this.loading = true; + + //Overwrite the stream settings if provided + limit = limit || STREAM_LOAD_COUNT; + from = from || this.getLastContentId(); + filter = filter || this.getFilterString(); + sort = sort || this.sort; + + var that = this; + return new Promise(function(resolve, reject) { + that._load(limit, from, filter,sort).then(function(response) { + that.$loader.hide(); + if (object.isEmpty(response.content)) { + that.lastEntryLoaded = true; + $('#btn-load-more').hide(); + } else { + that.lastEntryLoaded = response.is_last; + that.appendEntries(response); + } + + that.loading = false; + that.onChange(); + resolve(); + }).catch(function(err) { + //TODO: handle error + that.loading = false; + that.$loader.hide(); + reject(); + }); }); }; - var getStream = function () { - if (!module.mainStream) { - module.mainStream = new module.Stream(WALLSTREAM_ID); + Stream.prototype._load = function (limit, from, filter, sort) { + return client.ajax(this.url, { + data: { + filters: filter, + sort: sort, + from: from, + limit: limit + } + }); + }; + + Stream.prototype.getLastContentId = function () { + var $lastEntry = this.$stream.find('[data-content-pk]').last(); + if ($lastEntry.length) { + return $lastEntry.data('stream-contentid'); } - return module.mainStream; + }; + + Stream.prototype.appendEntries = function (response) { + var that = this; + var result = ''; + $.each(response.contentIds, function (i, key) { + var $entry = that.$.find('[data-content-pk="' + key + '"]'); + if ($entry.length) { + $entry.remove(); + } + result += response.content[key].output; + }); + return this.$content.append(result); + }; + + /** + * Fired when new entries are shown + */ + Stream.prototype.onChange = function () { + if (this.readOnly) { + $('.wallReadOnlyHide').hide(); + $('.wallReadOnlyShow').show(); + } else { + $('.wallReadOnlyShow').hide(); + } + + var hasEntries = this.hasEntries(); + if (!hasEntries && !this.hasFilter()) { + this.$.find('.emptyStreamMessage').show(); + this.$filter.hide(); + } else if (!hasEntries) { + this.$.find('.emptyFilterStreamMessage').hide(); + } else if(!this.isShowSingleEntry()) { + this.$filter.show(); + this.$.find('.emptyStreamMessage').hide(); + this.$.find('.emptyFilterStreamMessage').hide(); + } + + //TODO: fire global event + }; + + Stream.prototype.isShowSingleEntry = function () { + return object.isDefined(this.contentId); + }; + + Stream.prototype.hasEntries = function () { + return this.getEntryCount() > 0; + }; + + Stream.prototype.getEntryCount = function () { + return this.$.find('[data-content-pk]').length; + }; + + Stream.prototype.hasFilter = function () { + return this.filters.length > 0; + }; + + Stream.prototype.getFilterString = function () { + var result = ''; + $.each(this.filters, function(i, filter) { + result += filter+','; + }); + + return string.cutsuffix(result, ','); + }; + + Stream.prototype.setFilter = function (filterId) { + if(this.filters.indexOf(filterId) < 0) { + this.filters.push(filterId); + } + }; + + Stream.prototype.unsetFilter = function (filterId) { + var index = this.filters.indexOf(filterId); + if(index > -1) { + this.filters.splice(index, 1); + } + }; + + Stream.prototype.getEntry = function(key) { + return new StreamEntry(this.$.find('[data-content-pk="' + key + '"]')); + }; + + Stream.prototype.getEntryByNode = function($childNode) { + return new StreamEntry($childNode.closest('[data-content-pk]')); + }; + + var getStream = function () { + if (!module.instance) { + module.instance = new Stream($('[data-stream]')); + } + return module.instance; }; var getEntry = function (id) { return module.getStream().getEntry(id); }; - + + var init = function () { + var stream = getStream().init(); + $(window).scroll(function () { + if ($(window).scrollTop() == $(document).height() - $(window).height()) { + if (stream && !stream.loading && !stream.isShowSingleEntry() && !stream.lastEntryLoaded) { + stream.loadEntries(); + } + } + }); + + stream.$.on('click', '.singleBackLink', function() { + stream.contentId = undefined; + stream.init(); + $(this).hide(); + }); + + initFilterNav(); + }; + + var initFilterNav = function() { + $(".wallFilter").click(function () { + var $filter = $(this); + var checkboxi = $filter.children("i"); + checkboxi.toggleClass('fa-square-o').toggleClass('fa-check-square-o'); + if(checkboxi.hasClass('fa-check-square-o')) { + getStream().setFilter($filter.attr('id').replace('filter_', '')); + } else { + getStream().unsetFilter($filter.attr('id').replace('filter_', '')); + } + getStream().init(); + }); + + $(".wallSorting").click(function () { + var newSortingMode = $(this).attr('id'); + + // uncheck all sorting + $(".wallSorting").find('i') + .removeClass('fa-check-square-o') + .addClass('fa-square-o'); + + // check current sorting mode + $("#" + newSortingMode).children("i") + .removeClass('fa-square-o') + .addClass('fa-check-square-o'); + + // remove sorting id append + newSortingMode = newSortingMode.replace('sorting_', ''); + + // Switch sorting mode and reload stream + getStream().sort = newSortingMode; + getStream().init(); + }); + }; + module.export({ - getStream : getStream, - getEntry : getEntry + StreamEntry: StreamEntry, + Stream: Stream, + getStream: getStream, + getEntry: getEntry, + init: init }); -}); \ No newline at end of file +}); + +/* TODO: + Stream.prototype.wallStick = function (url) { + $.ajax({ + dataType: "json", + type: 'post', + url: url + }).done(function (data) { + if (data.success) { + if (currentStream) { + $.each(data.wallEntryIds, function (k, wallEntryId) { + currentStream.deleteEntry(wallEntryId); + currentStream.prependEntry(wallEntryId); + }); + $('html, body').animate({scrollTop: 0}, 'slow'); + } + } else { + alert(data.errorMessage); + } + }); + }; + + Stream.prototype.wallUnstick = function (url) { + $.ajax({ + dataType: "json", + type: 'post', + url: url + }).done(function (data) { + if (data.success) { + //Reload the whole stream, since we have to reorder the entries + currentStream.showStream(); + } + }); + }; + + /** + * Click Handler for Archive Link of Wall Posts + * (archiveLink.php) + * + * @param {type} className + * @param {type} id + + Stream.prototype.wallArchive = function (id) { + + url = wallArchiveLinkUrl.replace('-id-', id); + + $.ajax({ + dataType: "json", + type: 'post', + url: url + }).done(function (data) { + if (data.success) { + if (currentStream) { + $.each(data.wallEntryIds, function (k, wallEntryId) { + //currentStream.reloadWallEntry(wallEntryId); + // fade out post + setInterval(fadeOut(), 1000); + + function fadeOut() { + // fade out current archived post + $('#wallEntry_' + wallEntryId).fadeOut('slow'); + } + }); + } + } + }); + }; + + + /** + * Click Handler for Un Archive Link of Wall Posts + * (archiveLink.php) + * + * @param {type} className + * @param {type} id + + Stream.prototype.wallUnarchive = function (id) { + url = wallUnarchiveLinkUrl.replace('-id-', id); + + $.ajax({ + dataType: "json", + type: 'post', + url: url + }).done(function (data) { + if (data.success) { + if (currentStream) { + $.each(data.wallEntryIds, function (k, wallEntryId) { + currentStream.reloadWallEntry(wallEntryId); + }); + + } + } + }); + }; + + + /* + module.StreamItem.prototype.highlightContent = function () { + var $content = this.getContent(); + $content.addClass('highlight'); + $content.delay(200).animate({backgroundColor: 'transparent'}, 1000, function () { + $content.removeClass('highlight'); + $content.css('backgroundColor', ''); + }); + }; + */ \ No newline at end of file diff --git a/js/humhub.ui.js b/js/humhub.ui.js index 44fe7dce5a..db51a9bc59 100644 --- a/js/humhub.ui.js +++ b/js/humhub.ui.js @@ -4,5 +4,5 @@ humhub.initModule('ui', function(module, require, $) { additions.registerAddition('.autosize', function($match) { $match.autosize(); }); - } + }; }); \ No newline at end of file diff --git a/js/humhub.ui.modal.js b/js/humhub.ui.modal.js index cea4c278e1..0c8ef06afa 100644 --- a/js/humhub.ui.modal.js +++ b/js/humhub.ui.modal.js @@ -16,8 +16,8 @@ * @param {type} param2 */ humhub.initModule('ui.modal', function (module, require, $) { + var object = require('util').object; var additions = require('additions'); - var actions = require('actions'); //Keeps track of all initialized modals var modals = []; @@ -285,6 +285,18 @@ humhub.initModule('ui.modal', function (module, require, $) { return this.$modal.find('.modal-body'); }; + var ConfirmModal = function(id, confirmHandler) { + Modal.call(this, id); + this.initButtons(); + }; + + ConfirmModal.prototype.initButtons = function(confirmHandler) { + this.$confirm = this.$modal.find('[data-modal-submit]'); + this.$confirm.on('click', confirmHandler); + }; + + object.inherits(ConfirmModal, Modal); + module.export({ init: function () { module.global = new Modal('global-modal'); diff --git a/js/humhub.util.js b/js/humhub.util.js index 01dd615b68..7fc5f337c3 100644 --- a/js/humhub.util.js +++ b/js/humhub.util.js @@ -15,6 +15,9 @@ humhub.initModule('util', function(module, require, $) { isArray: function(obj) { return $.isArray(obj); }, + isEmpty: function(obj) { + return $.isEmptyObject(obj); + }, isString: function (obj) { return typeof obj === 'string'; }, @@ -46,7 +49,7 @@ humhub.initModule('util', function(module, require, $) { var string = { cutprefix : function(val, prefix) { - if(!this.startsWith(prefix)) { + if(!this.startsWith(val, prefix)) { return val; } return val.substring(prefix.length, val.length); diff --git a/node_modules/.gitignore b/node_modules/.gitignore deleted file mode 100644 index 86d0cb2726..0000000000 --- a/node_modules/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/protected/humhub/assets/AppAsset.php b/protected/humhub/assets/AppAsset.php index fa0acd900d..2c5918fe69 100755 --- a/protected/humhub/assets/AppAsset.php +++ b/protected/humhub/assets/AppAsset.php @@ -58,6 +58,7 @@ class AppAsset extends AssetBundle * https://github.com/inuyaksa/jquery.nicescroll/issues/574 */ //'humhub\assets\JqueryNiceScrollAsset', + 'humhub\assets\BluebirdAsset', 'humhub\assets\JqueryTimeAgoAsset', 'humhub\assets\JqueryKnobAsset', 'humhub\assets\JqueryWidgetAsset', diff --git a/protected/humhub/assets/BluebirdAsset.php b/protected/humhub/assets/BluebirdAsset.php new file mode 100644 index 0000000000..ebbb44e8a8 --- /dev/null +++ b/protected/humhub/assets/BluebirdAsset.php @@ -0,0 +1,38 @@ + \yii\web\View::POS_BEGIN]; + + /** + * @inheritdoc + */ + public $sourcePath = '@bower/bluebird'; + + /** + * @inheritdoc + */ + public $js = ['js/browser/bluebird.min.js']; + + /** + * @inheritdoc + */ + public $css = []; + +} diff --git a/protected/humhub/modules/content/controllers/ContentController.php b/protected/humhub/modules/content/controllers/ContentController.php index 15ae3e90f2..06fcf336e3 100644 --- a/protected/humhub/modules/content/controllers/ContentController.php +++ b/protected/humhub/modules/content/controllers/ContentController.php @@ -34,6 +34,13 @@ class ContentController extends Controller ]; } + public function actionDeleteById() + { + Yii::$app->response->format = 'json'; + $id = (int) Yii::$app->request->get('id'); + Content::findOne($id); + } + /** * Deletes a content object * @@ -44,22 +51,27 @@ class ContentController extends Controller Yii::$app->response->format = 'json'; $this->forcePostRequest(); - $json = [ - 'success' => 'false' - ]; $model = Yii::$app->request->get('model'); - $id = (int) Yii::$app->request->get('id'); + + //Due to backward compatibility we use the old delte mechanism in case a model parameter is provided + $id = (int) ($model != null) ? Yii::$app->request->get('id') : Yii::$app->request->post('id'); - $contentObj = Content::get($model, $id); + $contentObj = ($model != null) ? Content::get($model, $id) : Content::findOne($id); - if ($contentObj !== null && $contentObj->content->canDelete() && $contentObj->delete()) { + if(!$contentObj->canDelete()) { + throw new HttpException(400, Yii::t('ContentModule.controllers_ContentController', 'Could not delete content: Access denied!')); + } + + if ($contentObj !== null && $contentObj->delete()) { $json = [ 'success' => true, 'uniqueId' => $contentObj->getUniqueId(), 'model' => $model, 'pk' => $id ]; + } else { + throw new HttpException(500, Yii::t('ContentModule.controllers_ContentController', 'Could not delete content!')); } return $json; diff --git a/protected/humhub/modules/content/views/layouts/wallEntry.php b/protected/humhub/modules/content/views/layouts/wallEntry.php index 9399dc6406..465c86b36e 100644 --- a/protected/humhub/modules/content/views/layouts/wallEntry.php +++ b/protected/humhub/modules/content/views/layouts/wallEntry.php @@ -14,7 +14,8 @@ $cssClass = ($entry->sticked) ? 'wall-entry sticked-entry' : 'wall-entry'; if ($mode != "activity") : ?> -
+
diff --git a/protected/humhub/modules/content/widgets/views/deleteLink.php b/protected/humhub/modules/content/widgets/views/deleteLink.php index b0a9febaa1..721d90140e 100644 --- a/protected/humhub/modules/content/widgets/views/deleteLink.php +++ b/protected/humhub/modules/content/widgets/views/deleteLink.php @@ -6,8 +6,11 @@ use yii\helpers\Url; ?>
  • + + + 'modal_postdelete_' . $id, 'linkOutput' => 'a', 'title' => Yii::t('ContentModule.widgets_views_deleteLink', 'Confirm post deleting'), @@ -17,6 +20,6 @@ use yii\helpers\Url; 'linkContent' => ' ' . Yii::t('ContentModule.widgets_views_deleteLink', 'Delete'), 'linkHref' => Url::to(['/content/content/delete', 'model' => $model, 'id' => $id]), 'confirmJS' => 'function(json) { $(".wall_"+json.uniqueId).remove(); }' - )); + ));*/ ?>
  • \ No newline at end of file diff --git a/protected/humhub/modules/stream/actions/Stream.php b/protected/humhub/modules/stream/actions/Stream.php index 9c29977192..69eb934097 100644 --- a/protected/humhub/modules/stream/actions/Stream.php +++ b/protected/humhub/modules/stream/actions/Stream.php @@ -20,7 +20,7 @@ use humhub\modules\user\models\User; * @author luke * @since 0.11 */ -abstract class Stream extends \yii\base\Action +abstract class Stream extends Action { /** @@ -97,7 +97,6 @@ abstract class Stream extends \yii\base\Action $this->user = Yii::$app->user->getIdentity(); } - // Read parameters if (!Yii::$app->request->isConsoleRequest) { $from = Yii::$app->getRequest()->get('from', 0); @@ -191,16 +190,19 @@ abstract class Stream extends \yii\base\Action $this->activeQuery->andWhere("(content.archived != 1 OR content.archived IS NULL)"); } } + // Show only mine items if (in_array('entry_mine', $this->filters) && $this->user !== null) { $this->activeQuery->andWhere(['content.created_by' => $this->user->id]); } + // Show only items where the current user is involed if (in_array('entry_userinvoled', $this->filters) && $this->user !== null) { $this->activeQuery->leftJoin('user_follow', 'content.object_model=user_follow.object_model AND content.object_id=user_follow.object_id AND user_follow.user_id = :userId', ['userId' => $this->user->id]); $this->activeQuery->andWhere("user_follow.id IS NOT NULL"); } + if (in_array('model_posts', $this->filters)) { $this->activeQuery->andWhere(["content.object_model" => \humhub\modules\post\models\Post::className()]); } @@ -223,22 +225,21 @@ abstract class Stream extends \yii\base\Action $this->init(); - $output['contents'] = []; + $output['content'] = []; foreach ($this->activeQuery->all() as $content) { - $output['contents'][$content->id] = $this->getContentResultEntry($content); + $output['content'][$content->id] = $this->getContentResultEntry($content); } - $output['total'] = count($output['contents']); + $output['total'] = count($output['content']); $output['is_last'] = ($output['total'] < $this->activeQuery->limit); // BEGIN: TEMPORARY until JS Rewrite $output['output'] = ''; - foreach ($output['contents'] as $i => $c) { + foreach ($output['content'] as $i => $c) { $output['output'] .= $c['output']; - $output['lastEntryId'] = $i; - $output['entryIds'][] = $i; + $output['lastContentId'] = $i; + $output['contentIds'][] = $i; } $output['counter'] = $output['total']; - $output['counter'] = $output['total']; // END: Temporary return $output; diff --git a/protected/humhub/modules/stream/widgets/StreamViewer.php b/protected/humhub/modules/stream/widgets/StreamViewer.php index 9a9c318fb6..b0307739b9 100644 --- a/protected/humhub/modules/stream/widgets/StreamViewer.php +++ b/protected/humhub/modules/stream/widgets/StreamViewer.php @@ -112,10 +112,6 @@ class StreamViewer extends \yii\base\Widget { $params = [ $this->streamAction, - 'limit' => '-limit-', - 'filters' => '-filter-', - 'sort' => '-sort-', - 'from' => '-from-', 'mode' => \humhub\modules\stream\actions\Stream::MODE_NORMAL ]; diff --git a/protected/humhub/modules/stream/widgets/views/stream.php b/protected/humhub/modules/stream/widgets/views/stream.php index d5bae10949..d81a3b8cb1 100644 --- a/protected/humhub/modules/stream/widgets/views/stream.php +++ b/protected/humhub/modules/stream/widgets/views/stream.php @@ -1,24 +1,11 @@ registerJs('var streamUrl="' . $streamUrl . '"', View::POS_BEGIN); - -$jsLoadWall = "s = new Stream('#wallStream');\n"; -$wallEntryId = (int) Yii::$app->request->getQueryParam('wallEntryId'); -if ($wallEntryId != "") { - $jsLoadWall .= "s.showItem(" . $wallEntryId . ");\n"; -} else { - $jsLoadWall .= "s.showStream();\n"; -} -$jsLoadWall .= "currentStream = s;\n"; -$jsLoadWall .= "mainStream = s;\n"; -$jsLoadWall .= "$('#btn-load-more').click(function() { currentStream.loadMore(); })\n"; -$this->registerJs($jsLoadWall, View::POS_READY); +$contentId = (int) Yii::$app->request->getQueryParam('wallEntryId'); +$contentIdData = ($contentId != "") ? 'data-stream-contentid="'.$contentId.'"' : '' ; ?> + context->showFilters) { ?> -
    - + +
    + data-content-base="humhub.modules.stream.Stream" + data-content-delete-url=""> +