%PDF- %PDF-
| Direktori : /home/vacivi36/intranet.vacivitta.com.br/assets/4d9fc017/js/ |
| Current File : /home/vacivi36/intranet.vacivitta.com.br/assets/4d9fc017/js/humhub.stream.Stream.js |
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2018 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*
*/
/**
* Core module for managing Streams and StreamItems
* @type Function
*/
humhub.module('stream.Stream', function (module, require, $) {
var util = require('util');
var object = util.object;
var string = util.string;
var Widget = require('ui.widget').Widget;
var additions = require('ui.additions');
var StreamEntry = require('stream').StreamEntry;
var filterModule = require('ui.filter');
var Filter = filterModule.Filter;
var StreamRequest = require('stream').StreamRequest;
var loader = require('ui.loader');
var event = require('event');
var EVENT_AFTER_ADD_ENTRIES = 'humhub:stream:afterAddEntries';
var EVENT_BEFORE_ADD_ENTRIES = 'humhub:stream:beforeAddEntries';
var EVENT_INITIALIZED = 'humhub:stream:initialized';
/**
* Number of initial stream entries loaded when stream is initialized.
* @type Number
*/
var STREAM_INIT_COUNT = 8;
/**
* Number of stream entries loaded with each request (except initial request)
* @type Number
*/
var STREAM_LOAD_COUNT = 4;
/**
* If a data-stream-contentid is set on the stream root only one entry will
* be loaded. e.g. for permalinks
* @type String
*/
var DATA_STREAM_CONTENTID = 'stream-contentid';
/**
* If a data-stream-commentid is set on the stream the Comment will be marked
* @type String
*/
var DATA_STREAM_COMMENTID = 'stream-commentid';
var StreamState = function (stream) {
this.stream = stream;
this.lastContentId = 0;
this.lastEntryLoaded = false;
this.loading = false;
};
var StreamLoader = function (stream) {
this.stream = stream;
};
StreamLoader.prototype.show = function (show) {
if (show !== false && !this.stream.$content.find('.loader').length) {
loader.remove(this.stream.$content);
loader.append(this.stream.$content);
} else if (!show) {
loader.remove(this.stream.$content);
}
};
/**
* Generic Stream implementation.
*
* @param {type} container id or jQuery object of the stream container
* @returns {undefined}
*/
var Stream = Widget.extend();
Stream.prototype.onClear = function () {/* abstract onClear function */};
Stream.prototype.initScroll = function () {
if (window.IntersectionObserver && this.options.scrollSupport) {
var options = { root: this.$content[0], rootMargin: "50px" };
options = this.options.scrollOptions ? $.extend(options, this.options.scrollOptions) : options;
var $streamEnd = $('<div class="stream-end"></div>');
this.$content.append($streamEnd);
var that = this;
var observer = new IntersectionObserver(function (entries) {
if (that.preventScrollLoading()) {
return;
}
if (entries.length && entries[0].isIntersecting) {
that.load().finally(function () {
that.state.scrollLock = false;
});
}
}, options);
observer.observe($streamEnd[0]);
}
};
Stream.prototype.preventScrollLoading = function () {
return this.state.scrollLock || !this.canLoadMore() || !this.state.lastRequest || this.state.firstRequest.isSingleEntryRequest()
};
Stream.prototype.initEvents = function () {/* abstract initScroll function */};
Stream.prototype.onUpdateAvailable = function (events) {
var that = this;
if (this.options.autoUpdate) {
that.loadUpdate();
}
};
Stream.prototype.initDefaultEvents = function () {
var that = this;
event.on('humhub:modules:content:live:NewContent.stream', function (evt, events) {
if (!events
|| !that.state.initialized
|| !events.length
|| that.hasActiveFilters()
|| (that.state.firstRequest && that.state.firstRequest.isSingleEntryRequest())
|| !that.isUpdateAvailable(events)) {
return;
}
that.onUpdateAvailable();
});
this.on(EVENT_INITIALIZED, function () {
that.initScroll();
});
};
Stream.prototype.isUpdateAvailable = function (events) {
return false;
};
/**
* Initializes the stream configuration with default values.
*
* @returns {object}
*/
Stream.prototype.getDefaultOptions = function () {
return {
contentSelector: "[data-stream-content]",
streamEntryClass: StreamEntry,
loadCount: STREAM_LOAD_COUNT,
initLoadCount: STREAM_INIT_COUNT
};
};
/**
* Initializes the stream by clearing the stream state and dom and reloading initial stream entries,
* this should be called if any filter/sort settings are changed or the stream needs to be reloaded.
*
* @returns {Promise}
*/
Stream.prototype.init = function () {
if (this.state) {
// When reloading the stream we ignore the content id
this.$.data(DATA_STREAM_CONTENTID, null);
this.$.data(DATA_STREAM_COMMENTID, null);
}
this.state = new StreamState(this);
if (!this.$content) {
this.initWidget();
}
return this.clear()
.show()
.loadInit()
.then($.proxy(this.handleResponse, this))
.then($.proxy(this.updateTop, this))
.then($.proxy(this.triggerInitEvent, this))
.catch($.proxy(this.handleLoadError, this));
};
/**
* Refreshes the first loaded entry. Note this entry should only be updated after initialization or when
* loading updates to the top. Do not update this value when appending new content created by the user itself, e.g post form!
*
* @param response
* @returns {*}
*/
Stream.prototype.updateTop = function (response) {
if (response) {
this.topEntry = this.firstEntry(true);
}
return response;
};
Stream.prototype.triggerInitEvent = function (response) {
this.trigger(EVENT_INITIALIZED, this);
return response;
};
Stream.prototype.initWidget = function () {
this.$content = this.$.find(this.options.contentSelector);
this.loader = this.options.loader || new StreamLoader(this);
this.initDefaultEvents();
this.initEvents();
this.initFilter();
};
Stream.prototype.initFilter = function () {
if(this.options.filter) {
this.filter = this.options.filter;
} else {
this.filter = filterModule.findFilterByComponent(this) || new Filter();
}
if(object.isString(this.filter)) {
this.filter = Widget.instance(this.filter);
}
var that = this;
this.filter.on('afterChange', function () {
that.init();
})
};
Stream.prototype.loadInit = function () {
// content Id data is only relevant for the first request
var contentId = this.$.data(DATA_STREAM_CONTENTID);
var commentId = this.$.data(DATA_STREAM_COMMENTID);
var requestData = {
contentId: contentId,
viewContext: contentId ? 'detail' : null,
limit: this.options.initLoadCount
};
if (commentId) {
requestData.commentId = commentId;
}
this.state.firstRequest = new StreamRequest(this, requestData);
return this.state.firstRequest.load();
};
Stream.prototype.handleResponse = function (request) {
// If request is undefined the request was blocked @see canLoadMore
if (!request) {
return Promise.resolve();
}
if (request.isLastEntryResponse()) {
return Promise.resolve(this.handleLastEntryLoaded());
} else if (request.options.insertAfter) {
return this.handleInsertAfterResponse(request);
} else if (request.options.prepend) {
return this.prependResponseEntries(request);
} else {
return this.handleLoadMoreResponse(request);
}
};
Stream.prototype.handleLoadError = function(err) {
if (err.errorThrown === 'abort') {
module.log.warn('Stream request aborted!');
} else {
module.log.error(err, true);
this.$content.append('Stream could not be initialized!');
}
};
/**
* Loads a single stream entry by a given content id.
*
* @param {type} contentId
* @returns {undefined}
*/
Stream.prototype.loadEntry = function (contentId) {
return new StreamRequest(this, {contentId: contentId}).load();
};
Stream.prototype.canLoadMore = function () {
return !this.isLoading() && !this.state.lastEntryLoaded;
};
Stream.prototype.isLoading = function () {
return this.state.loading === true;
};
Stream.prototype.lastEntryLoaded = function () {
return this.state.lastEntryLoaded === true;
};
Stream.prototype.loadUpdate = function () {
var topEntry = (this.topEntry) ? this.topEntry : Widget.instance(this.$.find(StreamEntry.SELECTOR+':first'));
var from = topEntry ? topEntry.getKey() : 0;
return this.load({
'to': from,
'prepend': true,
'respectPinned': true,
'loader': false,
'limit': 20
}).then($.proxy(this.updateTop, this));
};
Stream.prototype.load = function (options) {
return new StreamRequest(this, options).load()
.then($.proxy(this.handleResponse, this))
.catch($.proxy(this.handleLoadError, this));
};
/**
* @deprecated since v1.3 use load() instead
* @param options
*/
Stream.prototype.loadEntries = function (options) {
return this.load(options);
};
/**
* Clears the stream content.
*
* @returns {undefined}
*/
Stream.prototype.clear = function () {
this.hide();
this.$content.empty();
this.loader.show(false);
this.trigger('humhub:stream:clear', this);
this.onClear();
return this;
};
Stream.prototype.handleLastEntryLoaded = function() {
this.state.lastEntryLoaded = true;
this.trigger('humhub:stream:lastEntryLoaded', [this]);
this.onChange('afterLoadEntries');
};
Stream.prototype.handleLoadMoreResponse = function(request) {
this.state.lastEntryLoaded = request.response.isLast;
this.state.lastContentId = request.response.lastContentId;
return this.addResponseEntries(request, request.options);
};
Stream.prototype.handleInsertAfterResponse = function(request) {
this.addResponseEntries(request);
};
Stream.prototype.appendResponseEntries = function (request, options) {
return this.addResponseEntries(request, options);
};
Stream.prototype.prependResponseEntries = function (request) {
return this.addResponseEntries(request, {prepend : true});
};
Stream.prototype.insertResponseEntriesAfter = function (request, entryId) {
return this.addResponseEntries(request, {insertAfter: entryId});
};
/**
* Adds entries contained in a StreamRequest response.
*
* The way the result is added to the stream is defined by the following options:
*
* - prepend: Prepends the entries of the response
* - insertAfter: Inserts the result after an already existing entry with the given contentId
*
* If no specific options is set, the entries are just appended to the bottom of the stream content.
*
* @param request
* @param options
*/
Stream.prototype.addResponseEntries = function (request, options) {
options = $.extend(request.options, options || {});
var that = this;
this.removeResponseEntries(request);
var $result = $(request.getResultHtml());
if(!$result.length) {
that.onChange(request);
return Promise.resolve();
}
this.$.trigger(EVENT_BEFORE_ADD_ENTRIES, [request.response, request, $result]);
var promise;
if (options.prepend) {
promise = this.prependEntry($result, options.respectPinned);
} else if (options.insertAfter) {
promise = this.after($result, options.insertAfter);
} else {
promise = this.appendEntry($result);
}
return promise.then(function () {
that.trigger(EVENT_AFTER_ADD_ENTRIES, [request.response, request, $result]);
return request;
});
};
/**
* Removes already loaded entries from the stream which are also contained in the response.
*
* @param request
*/
Stream.prototype.removeResponseEntries = function (request) {
var that = this;
request.forEachResult(function(key) {
var $entry = that.entry(key);
if ($entry) {
$entry.remove();
}
});
};
/**
* Prepends the given entry html to the stream and respects pinned posts if the respectPinnedPosts is set to true.
*
* @param html
* @param respectPinned
*/
Stream.prototype.prependEntry = function (html, respectPinned) {
var firstEntry = this.firstEntry(respectPinned);
if(firstEntry) {
return firstEntry.isPinned()
? this.after(html, firstEntry.$)
: this.before(html, firstEntry.$);
}
return this._streamEntryAnimation(html, function ($html) {
this.$content.prepend($html);
});
};
/**
* Appends the given entry html to the stream after the given entryNode the entry node can either.
*
* @param html
* @param $entryNode
*/
Stream.prototype.after = function (html, $entryNode) {
return this._streamEntryAnimation(html, function ($html) {
$entryNode.after($html);
});
};
/**
* Appends the given entry html to the stream after the given entryNode the entry node can either.
*
* @param html
* @param $entryNode
*/
Stream.prototype.before = function (html, $entryNode) {
return this._streamEntryAnimation(html, function ($html) {
$entryNode.before($html);
});
};
/**
* Appends an entry html to the end of the stream content.
* @param html
*/
Stream.prototype.appendEntry = function (html) {
return this._streamEntryAnimation(html, function ($html) {
var $streamEnd = this.$content.find('.stream-end:first');
if ($streamEnd.length) {
$streamEnd.before($html)
} else {
this.$content.append($html);
}
});
};
/**
* Triggers an stream entry fade animation.
*
* @param html
* @param insert
* @returns {Promise}
* @private
*/
Stream.prototype._streamEntryAnimation = function (html, insert) {
var that = this;
return new Promise(function (resolve, reject) {
var $html = $(html);
// Filter out all script/links and text nodes
var $elements = $html.not('script, link').filter(function () {
return this.nodeType === 1; // filter out text nodes
});
// We use opacity because some additions require the actual size of the elements.
$elements.css('opacity', 0);
// call insert callback
insert.call(that, $html);
// apply additions to elements and fade them in.
additions.applyTo($elements);
setTimeout(function() {
$.when($elements.hide().css('opacity', 1).fadeIn('fast')).then(function () {
that.onChange();
resolve();
});
});
});
};
/**
* Reloads a given entry either by providing the contentId or a StreamEntry instance.
* This function returns a Promise instance.
*
* @param {string|StreamEntry} entry
* @returns {Promise}
*/
Stream.prototype.reloadEntry = function (entry) {
var that = this;
return new Promise(function (resolve, reject) {
entry = (object.isString(entry)) ? that.entry(entry) : entry;
if (!entry) {
reject('Attempt to reload non existing entry');
return;
}
entry.loader();
that.loadEntry(entry.getKey()).then(function (request) {
var $entryNode = $(request.getResultHtml());
// If no entry was returned it means it is not visible in the current scope
if (!$entryNode || !$entryNode.length) {
entry.remove();
resolve(entry);
} else {
entry.replace($entryNode).then(resolve);
}
}, reject).finally(function () {
entry.loader(false);
});
});
};
Stream.prototype.onChange = function (request) {
var hasEntries = this.hasEntries();
this.$.find('.streamMessage').remove();
this.clearFilterErrors();
if (this.hasFilterErrors()) {
this.displayFilterErrors();
} else if (!hasEntries && this.isShowSingleEntry()) {
// we only show an error if we load a single entry we are not allowed to view, otherwise just reload the stream
if(request && request.response && request.response.errorCode && request.response.errorCode === 403) {
this.setStreamMessage(request.response.error);
} else {
// e.g. after content deletion in single entry stream
var that = this;
setTimeout(function() {that.init()}, 50);
}
} else if (!hasEntries) {
this.onEmptyStream();
} else if (this.isShowSingleEntry()) {
this.onSingleEntryStream();
} else {
this.filter.show();
$('[data-stream-create-content="' + this.options.uiWidget + '"]').show();
}
};
Stream.prototype.hasFilter = function (filter) {
return this.filter.hasFilter(filter);
};
Stream.prototype.onEmptyStream = function () {
var hasActiveFilters = this.hasActiveFilters();
this.$.find('.streamMessage').remove();
if(!this.isShowSingleEntry()) {
var message = (hasActiveFilters) ? this.options.streamEmptyFilterMessage : this.options.streamEmptyMessage;
this.setStreamMessage(message, hasActiveFilters);
}
if(!hasActiveFilters) {
this.filter.hide();
} else {
this.filter.show();
}
};
Stream.prototype.setStreamMessage = function (message, $filter) {
this.$content.append(string.template(this.static('templates').streamMessage, {
message: message,
cssClass: ($filter) ? this.options.streamEmptyFilterClass : this.options.streamEmptyClass,
}));
};
Stream.prototype.hasFilterErrors = function () {
return this.request &&
this.request.response &&
typeof this.request.response.filterErrors === 'object';
};
Stream.prototype.displayFilterErrors = function () {
if (!this.hasFilterErrors()) {
return;
}
var errors = this.request.response.filterErrors;
for (var filter in errors) {
var filterInput = this.filter.$.find('[data-filter-category="' + filter + '"]');
if (filterInput.length) {
filterInput.parent()
.addClass('has-error')
.append('<div class="help-block help-block-error">' + errors[filter] + '</div>');
}
}
};
Stream.prototype.clearFilterErrors = function () {
this.filter.$.find('[data-filter-category]').parent()
.removeClass('has-error')
.find('div.help-block.help-block-error').remove();
};
Stream.prototype.onSingleEntryStream = function () {
this.filter.hide();
};
Stream.templates = {
streamMessage: '<div class="streamMessage {cssClass}"><div class="panel"><div class="panel-body">{message}</div></div></div>'
};
/**
* Checks if the stream is single entry mode.
* @returns {boolean}
*/
Stream.prototype.isShowSingleEntry = function () {
return this.state.lastRequest
&& this.$.data(DATA_STREAM_CONTENTID) === this.state.lastRequest.contentId
&& this.state.lastRequest.isSingleEntryRequest();
};
/**
* Checks if the stream has entries loaded.
*
* @returns {boolean}
*/
Stream.prototype.hasEntries = function () {
return this.getEntryCount() > 0;
};
Stream.prototype.hasActiveFilters = function () {
return this.filter && this.filter.getActiveFilterCount({exclude: ['sort', 'scope']}) > 0;
};
/**
* Returns the count of loaded stream entries.
*
* @returns {humhub_stream_L5.Stream.$.find.length}
*/
Stream.prototype.getEntryCount = function () {
return this.$.find(StreamEntry.SELECTOR).length;
};
/**
* Returns all stream entry nodes.
*
* @returns {unresolved}
*/
Stream.prototype.getEntryNodes = function () {
return this.$.find(StreamEntry.SELECTOR);
};
Stream.prototype.updateFilterCount = function () {
var count = this.$.data('filters') ? this.$.data('filters').length : 0;
count += $('#stream_filter_content_type').val() ? $('#stream_filter_content_type').val().length : 0;
count += $('#stream_filter_topic').val() ? $('#stream_filter_topic').val().length : 0;
var $filterCount = $('#stream-filter-toggle').find('.filterCount');
if(count) {
if(!$filterCount.length) {
$filterCount = $('<small class="filterCount"></small>').insertBefore($('#stream-filter-toggle').find('.caret'));
}
$filterCount.html(' <b>('+count+')</b> ');
} else if($filterCount.length) {
$filterCount.remove();
}
};
/**
* Adds a given filterId to the filter array.
*
* @param {type} filterId
* @returns {undefined}
*/
Stream.prototype.setFilter = function (filterId) {
var filters = this.$.data('filters') || [];
if (filters.indexOf(filterId) < 0) {
filters.push(filterId);
}
this.$.data('filters', filters);
return this;
};
/**
* Clears a given filter.
*
* @param {type} filterId
* @returns {undefined}
*/
Stream.prototype.unsetFilter = function (filterId) {
var filters = this.$.data('filters') || [];
var index = filters.indexOf(filterId);
if (index > -1) {
filters.splice(index, 1);
}
this.$.data('filters', filters);
return this;
};
/**
* Returns a StreamEntry instance for a given content id.
* @returns StreamEntry
* @param ignorePinned
* @param ignoreInjected
*/
Stream.prototype.firstEntry = function(ignorePinned, ignoreInjected) {
var selector = '[data-stream-entry]';
if(ignorePinned) {
selector += ':not([data-stream-pinned="1"])';
}
if(ignoreInjected !== false) {
selector += ':not([data-stream-injected])';
}
selector += ':first';
return this.entry(this.$.find(selector));
};
/**
* Returns a StreamEntry instance for a given content id.
* @param {type} key
* @returns StreamEntry
*/
Stream.prototype.entry = function (key) {
var $entryNode = object.isString(key) || object.isNumber(key)
? this.$.find(StreamEntry.SELECTOR + '[data-content-key="' + key + '"]')
: $(key);
if(!$entryNode.length) {
return null;
}
return new this.options.streamEntryClass($entryNode);
};
/**
* Creates a new StreamEntry out of the given childNode.
* @param {type} $childNode
* @returns StreamEntry
* @deprecated since 1.5 use entry() instead
*/
Stream.prototype.getEntryByNode = function ($childNode) {
return new this.cfg.streamEntryClass($childNode.closest(StreamEntry.SELECTOR));
};
Stream.prototype.actionLoadMore = function (evt) {
this.loadEntries().finally(function () {
evt.finish();
});
};
module.export = Stream;
});