Richtext JS Api, Disabled Archived Stream, Stream Console Fix

This commit is contained in:
buddha87 2016-12-18 23:05:34 +01:00
parent db66b3582c
commit 0cef6f0330
96 changed files with 7628 additions and 457 deletions

View File

@ -42,7 +42,7 @@
"bower-asset/jquery-color": "^2.1.2", "bower-asset/jquery-color": "^2.1.2",
"bower-asset/autosize": "1.*", "bower-asset/autosize": "1.*",
"bower-asset/nprogress": "*", "bower-asset/nprogress": "*",
"bower-asset/At.js": "0.5.0", "bower-asset/At.js": "^1.5.1",
"bower-asset/animate.css": "*", "bower-asset/animate.css": "*",
"bower-asset/html5shiv": "^3.7" "bower-asset/html5shiv": "^3.7"
}, },
@ -60,12 +60,6 @@
"scripts": { "scripts": {
"post-create-project-cmd": [ "post-create-project-cmd": [
"yii\\composer\\Installer::postCreateProject" "yii\\composer\\Installer::postCreateProject"
],
"post-install-cmd": [
"PhantomInstaller\\Installer::installPhantomJS"
],
"post-update-cmd": [
"PhantomInstaller\\Installer::installPhantomJS"
] ]
}, },
"extra": { "extra": {

View File

@ -350,7 +350,7 @@ humhub.module('action', function(module, require, $) {
// As last resort we try to call the action by namespace for handlers like humhub.modules.myModule.myAction // As last resort we try to call the action by namespace for handlers like humhub.modules.myModule.myAction
var handlerAction = event.handler.split('.').pop(); var handlerAction = event.handler.split('.').pop();
var target = require(string.cutsuffix(event.handler, '.' + handlerAction)); var target = require(string.cutSuffix(event.handler, '.' + handlerAction));
if(!_executeAction(target, handlerAction, event)) { if(!_executeAction(target, handlerAction, event)) {
module.log.error('actionHandlerNotFound', event.handler, true); module.log.error('actionHandlerNotFound', event.handler, true);
@ -576,6 +576,7 @@ humhub.module('action', function(module, require, $) {
var trigger = function($trigger, type, options) { var trigger = function($trigger, type, options) {
options.$trigger = $trigger; options.$trigger = $trigger;
// For manually triggered action events we do not need a block in most cases.
if(!options.block) { if(!options.block) {
options.block = BLOCK_NONE; options.block = BLOCK_NONE;
} }

View File

@ -3,7 +3,7 @@ humhub.module('client.pjax', function(module, require, $) {
var init = function() { var init = function() {
if (module.config.active) { if (module.config.active) {
$(document).pjax('a:not([data-pjax-prevent],[target="_blank"],[data-target])', "#layout-content", module.config.options); $(document).pjax('a:not([data-pjax-prevent],[target],[data-target],[data-toggle])', "#layout-content", module.config.options);
pjaxRedirectFix(); pjaxRedirectFix();
module.installLoader(); module.installLoader();
} }

158
js/humhub/humhub.live.js Normal file
View File

@ -0,0 +1,158 @@
humhub.module('live', function(module, require, $) {
var object = require('util').object;
var space = require('space', true);
var user = require('user', true);
var instances = [];
var _delay = 11;
var _idleFactor = 0;
var _defaultOptions = {
id: 'unknown',
min: 20,
max: 45,
idle: true,
idleFactor: 1,
active: true
};
var LiveUpdate = function(topic, options) {
this.topic = (arguments.length === 1) ? topic.topic : topic;
this.options = (arguments.length === 1) ? topic.options : options;
this.options = $.extend(_defaultOptions, this.options);
this.id = this.options.id;
};
LiveUpdate.prototype.on = function() {
this.options.active = true;
};
LiveUpdate.prototype.off = function() {
this.options.active = false;
};
LiveUpdate.prototype.isExpired = function() {
if(!this.options.active) {
return false;
} else if(!this.lastUpdate) {
return true;
}
var minDelay = this.options.min + (this.options.min * (_idleFactor * this.options.idleFactor));
if(minDelay > this.options.max) {
minDelay = this.options.max;
}
return (this.lastUpdate + minDelay) >= Date.now();
};
LiveUpdate.prototype.handleResult = function(result) {
}
LiveUpdate.prototype.validate = function() {
return !((this.topic.user && user.isGuest()) || (this.topic.space && !space.guid()));
};
var register = function(topic, options) {
var instance = new LiveUpdate(topic, options);
instances.push(instance);
return instance;
};
var init = function() {
setTimeout(_run, _delay);
};
var _run = function() {
var topics = [];
var lastUpdate = Date.now();
instances.forEach(function(update) {
if(update.isExpired() && update.validate()) {
var topic = _getServerTopic(update);
module.log.debug('Topic update:' + update.id, topic);
topics.push(topic);
update.lastUpdate = lastUpdate;
}
});
_send().then(function(result) {
instances.forEach(function(liveUpdate) {
liveUpdate.handleResult(result);
});
}).catch(function(err) {
// Silent error log
module.log.error(err);
});
}
var _getServerTopic = function(update) {
var result = {};
if(update.topic.user === true) {
result.uguid = user.guid();
} else if(update.topic.uguid) {
result.uguid = update.topic.uguid;
}
if(update.topic.space) {
result.sguid = space.guid();
} else if(update.topic.sguid) {
result.sguid = update.topic.sguid;
}
if(update.topic.suffix) {
result.suffix = update.topic.suffix;
}
if(update.topic.module) {
result.module = update.topic.module;
}
if(update.lastResult) {
result.last = update.lastResult;
}
return result;
};
var update = new LiveUpdate({
topic: {
'user': true,
'space': true,
'module': 'mail',
'suffix': 'whatever'
},
options: {
min: 10,
max: 30,
idle: true,
idleFactor: 1
}
});
/**
LiveUpdate
Settings
LastUpdate -> Date this specific update was requested the last time.
(Register To) --> function or static object
- User
- Space
- Module
(Live Settings)
- Min Duration
- Idle Factor
(Callback)
- Callback
-On/Off
*/
module.export({
init: init,
register: register
});
});

View File

@ -18,10 +18,10 @@
humhub.module('ui.modal', function(module, require, $) { humhub.module('ui.modal', function(module, require, $) {
var util = require('util'); var util = require('util');
var object = util.object; var object = util.object;
var string = util.string;
var additions = require('ui.additions'); var additions = require('ui.additions');
var loader = require('ui.loader'); var loader = require('ui.loader');
var client = require('client', true); var client = require('client', true);
var Widget = require('ui.widget').Widget;
//Keeps track of all initialized modals //Keeps track of all initialized modals
var modals = []; var modals = [];
@ -36,20 +36,23 @@ humhub.module('ui.modal', function (module, require, $) {
* *
* @param {string} id - id of the modal * @param {string} id - id of the modal
*/ */
var Modal = function (id, options) { var Modal = function(node, options) {
options = options || {}; Widget.call(this, node, options);
var selector = string.startsWith(id, '#') ? id : '#' + id; };
this.$ = $(selector);
object.inherits(Modal, Widget)
Modal.component = 'humhub-ui-modal';
;
Modal.prototype.init = function() {
if(!this.$.length) { if(!this.$.length) {
this.createModal(id); this.createModal(id);
this.reset(); this.reset();
} }
this.initModal(options); this.initModal(this.options);
modals.push(this); modals.push(this);
return this;
}; };
/** /**
@ -168,7 +171,9 @@ humhub.module('ui.modal', function (module, require, $) {
Modal.prototype.load = function(url, cfg, originalEvent) { Modal.prototype.load = function(url, cfg, originalEvent) {
var that = this; var that = this;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
if(!that.isVisible()) {
that.loader(); that.loader();
}
client.html(url, cfg, originalEvent).then(function(response) { client.html(url, cfg, originalEvent).then(function(response) {
that.setDialog(response.html); that.setDialog(response.html);
resolve(response); resolve(response);
@ -176,6 +181,21 @@ humhub.module('ui.modal', function (module, require, $) {
}); });
}; };
Modal.prototype.post = function(url, cfg, originalEvent) {
var that = this;
var cfg = cfg || {};
cfg.dataType = cfg.dataType || 'html';
return new Promise(function(resolve, reject) {
if(!that.isVisible()) {
that.loader();
}
client.post(url, cfg, originalEvent).then(function(response) {
that.setDialog(response.html);
resolve(response);
}).catch(reject);
});
};
/** /**
* Sets an errormessage and title. This function either creates an standalone * Sets an errormessage and title. This function either creates an standalone
* error modal with title and message, or adds/replaces a errorboxmessage to * error modal with title and message, or adds/replaces a errorboxmessage to
@ -391,8 +411,8 @@ humhub.module('ui.modal', function (module, require, $) {
return this.$.find('.modal-body'); return this.$.find('.modal-body');
}; };
var ConfirmModal = function (id) { var ConfirmModal = function(node, options) {
Modal.call(this, id); Modal.call(this, node, options);
}; };
object.inherits(ConfirmModal, Modal); object.inherits(ConfirmModal, Modal);
@ -448,12 +468,12 @@ humhub.module('ui.modal', function (module, require, $) {
}; };
var init = function() { var init = function() {
module.global = new Modal('#globalModal'); module.global = Modal.instance('#globalModal');
module.global.$.on('hidden.bs.modal', function(e) { module.global.$.on('hidden.bs.modal', function(e) {
module.global.reset(); module.global.reset();
}); });
module.globalConfirm = new ConfirmModal('#globalModalConfirm'); module.globalConfirm = ConfirmModal.instance('#globalModalConfirm');
module.confirm = function(cfg) { module.confirm = function(cfg) {
return module.globalConfirm.open(cfg); return module.globalConfirm.open(cfg);
}; };
@ -483,7 +503,7 @@ humhub.module('ui.modal', function (module, require, $) {
$modal.next('.modal-backdrop.in').addClass('hidden').css('zIndex', modalZIndex - 1); $modal.next('.modal-backdrop.in').addClass('hidden').css('zIndex', modalZIndex - 1);
}); });
$('.modal.in:visible:last').focus().next('.modal-backdrop.in').removeClass('hidden'); $('.modal.in:visible:last').focus().next('.modal-backdrop.in').removeClass('hidden');
} };
/** /**
* To allow other frameworks to overlay focusable nodes over an active modal we have * To allow other frameworks to overlay focusable nodes over an active modal we have
@ -577,10 +597,18 @@ humhub.module('ui.modal', function (module, require, $) {
return modal; return modal;
}; };
var post = function(evt) {
module.global.post(evt).catch(function(err) {
module.log.error(err, true);
});
};
module.export({ module.export({
init: init, init: init,
Modal: Modal, Modal: Modal,
ConfirmModal: ConfirmModal,
get: get, get: get,
post: post,
load: load, load: load,
submit: submit submit: submit
}); });

View File

@ -218,7 +218,7 @@ humhub.module('ui.picker', function(module, require, $) {
selectionWithImage: '{imageNode}<span class="with-image">{text}</span> <i class="fa fa-times-circle picker-close"></i>', selectionWithImage: '{imageNode}<span class="with-image">{text}</span> <i class="fa fa-times-circle picker-close"></i>',
selectionNoImage: '<span class="no-image">{text}</span> <i class="fa fa-times-circle picker-close"></i>', selectionNoImage: '<span class="no-image">{text}</span> <i class="fa fa-times-circle picker-close"></i>',
result: '<a href="#" tabindex="-1" style="margin-right:5px;">{imageNode} {text}</a>', result: '<a href="#" tabindex="-1" style="margin-right:5px;">{imageNode} {text}</a>',
resultDisabled: '<a href="#" title="{title}" data-placement="right" tabindex="-1" style="margin-right:5px;opacity: 0.4;cursor:not-allowed">{imageNode} {text}</a>', resultDisabled: '<a href="#" title="{disabledText}" data-placement="right" tabindex="-1" style="margin-right:5px;opacity: 0.4;cursor:not-allowed">{imageNode} {text}</a>',
imageNode: '<img class="img-rounded" src="{image}" alt="24x24" style="width:24px;height:24px;" height="24" width="24">', imageNode: '<img class="img-rounded" src="{image}" alt="24x24" style="width:24px;height:24px;" height="24" width="24">',
option: '<option value="{id}" data-image="{image}" selected>{text}</option>', option: '<option value="{id}" data-image="{image}" selected>{text}</option>',
}; };
@ -236,7 +236,7 @@ humhub.module('ui.picker', function(module, require, $) {
} }
item.imageNode = this.getImageNode(item); item.imageNode = this.getImageNode(item);
item.disabledText = item.disabledText || '';
var template = (item.disabled) ? Picker.template.resultDisabled : Picker.template.result; var template = (item.disabled) ? Picker.template.resultDisabled : Picker.template.result;
var $result = $(string.template(template, item)) var $result = $(string.template(template, item))

View File

@ -45,7 +45,7 @@ humhub.module('ui.progress', function(module, require, $) {
Progress.prototype.value = function() { Progress.prototype.value = function() {
var width = this.$progressBar[0].style.width; var width = this.$progressBar[0].style.width;
return (width) ? parseInt(string.cutsuffix(width, '%')) : 0; return (width) ? parseInt(string.cutSuffix(width, '%')) : 0;
}; };
Progress.prototype.reset = function() { Progress.prototype.reset = function() {

View File

@ -0,0 +1,445 @@
/**
* Manages the client/server communication. Handles humhub json api responses and
* pjax requests.
*/
humhub.module('ui.richtext', function(module, require, $) {
var Widget = require('ui.widget').Widget;
var util = require('util');
var object = util.object;
var string = util.string;
var Richtext = function(node, options) {
Widget.call(this, node, options);
};
object.inherits(Richtext, Widget);
Richtext.component = 'humhub-ui-richtext';
Richtext.prototype.init = function() {
this.$input = $('#' + this.$.attr('id') + '_input').hide();
this.features = [];
this.checkPlaceholder();
this.initEvents();
if(this.options.disabled) {
this.disable();
}
};
Richtext.prototype.initEvents = function() {
var that = this;
this.$.on('focus', function() {
that.checkPlaceholder(true);
// Initialize features on first focus.
if(!that.featuresInitialized) {
that.initFeatures();
}
}).on('focusout', function() {
that.update();
that.checkPlaceholder();
}).on('paste', function(event) {
event.preventDefault();
event.stopImmediatePropagation();
var text = "";
if(event.originalEvent.clipboardData) { //Forefox, Webkit
text = event.originalEvent.clipboardData.getData('text/plain');
} else if(window.clipboardData) { // IE
text = window.clipboardData.getData("Text");
}
that.insertTextAtCursor(text);
}).on('keydown', function(e) {
that.checkForEmptySpans();
}).on('keypress', function(e) {
switch(e.which) {
case 8: // Backspace
// Note chrome won't fire the backspace keypress event, but we don't need the workaround for chrome so its ok..
that.checkLinkRemoval();
break;
}
}).on('clear', function(e) {
that.clear();
}).on('disable', function(e) {
that.disable();
});
};
/**
* This is a workaround for deleting links as a whole in firefox https://bugzilla.mozilla.org/show_bug.cgi?id=685445
*/
Richtext.prototype.checkLinkRemoval = function() {
var position = this.$.caret('offset');
if(!position) {
return;
}
var that = this;
// Check if the caret position is right before a link, if yes remove link and perhaps also the parent if empty.
this.$.find('.richtext-link, .atwho-emoji').each(function() {
var $this = $(this);
var offset = $this.offset();
var right = offset.left + $this.outerWidth(true);
// The caret top position seems a bit out in some cases...
if(Math.abs(position.left - right) < 1 && Math.abs(position.top - offset.top) < 18) {
$this.remove();
// This is a workaround for a caret position issue in firefox
_checkCaretPositionAfterRemove(that.$);
return false; // leave loop
}
});
};
/**
* This will prevent an caret glitch in firefox https://bugzilla.mozilla.org/show_bug.cgi?id=904846
* @param {type} $node
* @returns {undefined}
*/
var _checkCaretPositionAfterRemove = function($node) {
if(!$node.text().length) {
var spaceText = document.createTextNode("\u00a0");
$node.prepend(spaceText);
var sel = window.getSelection();
var range = document.createRange();
range.setStart(spaceText, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
};
/**
* Empty spans prevent text deletions in some browsers, so we have to get sure there are no empty spans present.
* @param {type} $node
* @returns {undefined}
*/
Richtext.prototype.checkForEmptySpans = function($node) {
$node = $node || this.$;
$node.find('span').each(function() {
_checkEmptySpan($(this));
});
};
var _checkEmptySpan = function($node) {
if($node.is('span') && !$node.contents().length) {
var $parent = $node.parent();
$node.remove();
_checkEmptySpan($parent);
}
};
/**
* Inserts the given text at the current cursor position.
* This function gets sure not to insert unwanted html by escaping html chars.
*
* @param {type} text
* @returns {undefined}
*/
Richtext.prototype.insertTextAtCursor = function(text) {
// Get rid of unwanted html
var text = $('<div></div>').append(string.escapeHtml(text)).text();
var lastNode;
var sel = window.getSelection();
// Clear current selection, we'll overwrite selected text
var range = sel.getRangeAt(0);
range.deleteContents();
// Remove leading line-breaks and spaces
text = text.replace(/^(?:\r\n|\r|\n)/g, '').trim();
// We insert the lines reversed since we don't have to align the range
var lines = text.split(/(?:\r\n|\r|\n)/g).reverse();
$.each(lines, function(i, line) {
// Prevent break after last line
if(i !== 0) {
var br = document.createElement("br");
range.insertNode(br);
}
// Insert actual text node
var newNode = document.createTextNode(line.trim());
range.insertNode(newNode);
//Insert leading spaces as textnodes
var leadingSpaces = line.match(/^\s+/);
if(leadingSpaces) {
var spaceCount = leadingSpaces[0].length;
while(spaceCount > 0) {
var spaceNode = document.createTextNode("\u00a0");
range.insertNode(spaceNode);
spaceCount--;
}
}
// The last node of the loop is the first node in dom since we insert reversed
if(i === 0) {
lastNode = newNode;
}
});
// Align range after insertion
range.setStartAfter(lastNode);
range.setEndAfter(lastNode);
sel.removeAllRanges();
sel.addRange(range);
};
Richtext.prototype.update = function() {
this.$input.val(this.getPlainText());
};
Richtext.prototype.checkPlaceholder = function(focus) {
if(!focus && !this.$.text().trim().length) {
this.$.addClass('atwho-placeholder');
this.$.html(this.options.placeholder);
this.$.attr('spellcheck', 'false');
} else if(this.$.hasClass('atwho-placeholder')) {
this.$.removeClass('atwho-placeholder');
this.$.attr('spellcheck', 'true');
this.$.html('');
}
};
Richtext.prototype.initFeatures = function() {
var that = this;
$.each(Richtext.features, function(id, feature) {
if(!that.isFeatureEnabled(id)) {
return;
}
if(feature.atwho) {
that.initAtwhoFeature(id, feature);
}
});
// It seems atwho detatches the original element so we have to do a requery
this.$ = $('#' + this.$.attr('id'));
this.featuresInitialized = true;
};
Richtext.prototype.isFeatureEnabled = function(id) {
if(this.options.excludes && this.options.excludes.indexOf(id) >= 0) {
return false;
}
if(this.options.includes && this.options.includes.length && this.options.includes.indexOf(id) < 0) {
return false;
}
return true;
};
Richtext.prototype.initAtwhoFeature = function(id, feature) {
var options = (object.isFunction(feature.atwho))
? feature.atwho.call(this, feature)
: $.extend({}, feature.atwho);
if(object.isFunction(feature.init)) {
feature.init.call(this, feature, options);
}
if(feature.atwho) {
this.$.atwho(options);
}
this.features.push(id);
};
Richtext.prototype.disable = function(tooltip) {
tooltip = tooltip || this.options.disabledText;
this.$.removeAttr('contenteditable').attr({
disabled: 'disabled',
title : tooltip,
}).tooltip({
placement : 'bottom'
});
};
Richtext.prototype.clear = function() {
this.$.html('');
this.checkPlaceholder();
};
Richtext.prototype.focus = function() {
this.$.trigger('focus');
};
Richtext.prototype.getFeatures = function() {
var result = [];
$.each(this.features, function(i, id) {
result.push(Richtext.features[id]);
});
return result;
};
Richtext.prototype.getPlainText = function() {
// GENERATE USER GUIDS
var that = this;
var $clone = this.$.clone();
$.each(this.getFeatures(), function(id, feature) {
if(object.isFunction(feature.parse)) {
feature.parse($clone, that, feature);
}
});
var html = $clone.html();
// replace html space
html = html.replace(/\&nbsp;/g, ' ');
// rebuild tag structure for webkit browsers
html = html.replace(/\<div>\s*<br\s*\\*>\<\/div>/g, '<div></div>');
// replace all div tags with br tags (webkit)
html = html.replace(/\<div>/g, '<br>');
// replace all p tags with br tags (IE)
html = html.replace(/\<p>\<br\s*\\*>\<\/p>/g, '<br>');
html = html.replace(/\<\/p>/g, '<br>');
// remove all line breaks
html = html.replace(/(?:\r\n|\r|\n)/g, "");
// At.js adds a zwj at the end of each mentioning
html = html.replace(/\u200d/g,'');
// replace all <br> with new line break
$clone.html(html.replace(/\<br\s*\>/g, '\n'));
// return plain text without html tags
return $clone.text().trim();
};
Richtext.features = {};
Richtext.features.emoji = {
'emojis': [
"Relaxed", "Yum", "Relieved", "Hearteyes", "Cool", "Smirk",
"KissingClosedEyes", "StuckOutTongue", "StuckOutTongueWinkingEye", "StuckOutTongueClosedEyes", "Disappointed", "Frown",
"ColdSweat", "TiredFace", "Grin", "Sob", "Gasp", "Gasp2",
"Laughing", "Joy", "Sweet", "Satisfied", "Innocent", "Wink",
"Ambivalent", "Expressionless", "Sad", "Slant", "Worried", "Kissing",
"KissingHeart", "Angry", "Naughty", "Furious", "Cry", "OpenMouth",
"Fearful", "Confused", "Weary", "Scream", "Astonished", "Flushed",
"Sleeping", "NoMouth", "Mask", "Worried", "Smile", "Muscle",
"Facepunch", "ThumbsUp", "ThumbsDown", "Beers", "Cocktail", "Burger",
"PoultryLeg", "Party", "Cake", "Sun", "Fire", "Heart"
],
'atwho': {
at: ":",
highlightFirst: true,
limit: 100
},
init: function(feature, options) {
options.data = feature.emojis;
options.insertTpl = "<img data-emoji-name=';${name};' class='atwho-emoji' with='18' height='18' src='" + module.config['emoji.url'] + "${name}.svg' />";
options.displayTpl = "<li class='atwho-emoji-entry' data-value=';${name};'><img with='18' height='18' src='" + module.config['emoji.url'] + "${name}.svg' /></li>";
},
parse: function($clone) {
$clone.find('.atwho-emoji').each(function() {
$(this).replaceWith($(this).data('emoji-name'));
});
}
};
/**
* Mentioning feature supports mentionings by typing @ the default mentioning calls an url after typing three digits.
* Other mentionings can be registered by adding Richtext.features with the at option @:<prefix>
*/
Richtext.features.mentioning = {};
Richtext.features.mentioning.atwho = function() {
// this is the widget instance.
var that = this;
return {
at: "@",
data: [{image: '', 'cssClass': 'hint', name: module.text('info.minInput')}],
insertTpl: "<a href='${link}' class='atwho-user richtext-mention richtext-link' contenteditable='false' target='_blank' data-guid='${atwho-at}-${type}${guid}'>${name}</a>",
displayTpl: "<li class='hint' data-value=''>${name}</li>",
limit: 10,
highlightFirst: false,
callbacks: {
matcher: function(flag, subtext, should_start_with_space) {
var match, regexp;
regexp = new RegExp(/(\s+|^)@([\u00C0-\u1FFF\u2C00-\uD7FF\w\s\-\']*$)/);
match = regexp.exec(subtext);
this.setting.tpl = "<li class='hint' data-value=''>${name}</li>";
if(match && typeof match[2] !== 'undefined') {
return match[2];
}
return null;
},
remoteFilter: function(query, callback) {
this.setting.highlight_first = false;
// check the char length and data-query attribute for changing plugin settings for showing results
if(query.length >= 3) {
// Render loading user feedback.
this.setting.displayTpl = "<li class='hint' data-value=''>${name}</li>";
this.view.render([{"type": "test", "cssClass": "hint", "name": module.text('info.loading'), "image": "", "link": ""}]);
// set plugin settings for showing results
this.setting.highlightFirst = true;
this.setting.displayTpl = '<li class="${cssClass}" data-value="@${name}">${image} ${name}</li>';
$.getJSON(that.options.mentioningUrl, {keyword: query}, function(data) {
callback(data);
});
// reset query count
query.length = 0;
}
}
}
};
};
Richtext.features.mentioning.init = function(feature, options) {
var widget = this;
// This is a workaround for mobile browsers especially for Android Chrome which is not able to remove contenteditable="false" nodes.
// This will enable mobile browsers to delete images and links by taphold/contextmenu event.
if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|Opera Mini/i.test(navigator.userAgent)) {
this.$.on('contextmenu', 'a, img', function() {
var $this = $(this);
if($this.parent().is('span')) {
$this.parent().remove();
} else {
$this.remove();
}
widget.checkForEmptySpans();
return false;
});
}
};
/**
* Used to parse the feature elements.
*
* @param {type} $clone
* @param {type} widget
* @returns {undefined}
*/
Richtext.features.mentioning.parse = function($clone, widget) {
$clone.find('.atwho-user, .atwho-space').each(function() {
$(this).text($(this).data('guid'));
});
};
module.export({
Richtext: Richtext
});
});

View File

@ -10,6 +10,7 @@ humhub.module('ui.widget', function(module, require, $) {
var additions = require('ui.additions'); var additions = require('ui.additions');
var Component = require('action').Component; var Component = require('action').Component;
var object = require('util').object; var object = require('util').object;
var string = require('util').string;
var action = require('action'); var action = require('action');
var event = require('event'); var event = require('event');
@ -21,26 +22,61 @@ humhub.module('ui.widget', function(module, require, $) {
this.errors = []; this.errors = [];
this.options = this.initOptions(options || {}); this.options = this.initOptions(options || {});
// Use internal events this.initWidgetEvents();
event.sub(this);
if(!this.validate()) { if(!this.validate()) {
module.log.warn('Could not initialize widget.', this.errors); module.log.warn('Could not initialize widget.', this.errors);
} else { } else {
var initData = this.$.data('ui-init'); var initData = this.$.data('ui-init');
this.fire('before-init', [this, initData]); this.fire('beforeInit', [this, initData]);
this.init(initData); this.init(initData);
this.fire('after-init', [this]); this.fire('afterInit', [this]);
} }
}; };
object.inherits(Widget, Component); object.inherits(Widget, Component);
Widget.prototype.fire = function(event, args) {
if(this.$.data('action-' + event)) { Widget.prototype.initWidgetEvents = function() {
action.trigger(this.$, event, {params: args}); // Use internal event object for handling widget events.
event.sub(this);
var that = this;
// Bind dom events to widget events actions.
$.each(this.options, function(key, value) {
if(string.startsWith(key, 'widgetAction')) {
var eventType = string.cutPrefix(key, 'widgetAction').toLowerCase();
that.$.on(eventType+'.humhub:widget:events', function() {
that.fire(eventType);
});
} }
});
};
Widget.prototype.fire = function(event, args, triggerDom) {
// Trigger action if there is an action handler set
var widgetAction = 'widgetAction' + string.capitalize(event);
if(this.options[widgetAction]){
var handler = this.options[widgetAction];
if(string.startsWith(handler, 'this.')) {
handler = string.cutPrefix(handler, 'this.');
var handlerFunc = object.resolve(this, handler);
if(object.isFunction(handlerFunc)) {
handlerFunc.apply(this);
}
} else {
action.trigger(this.$, event, {handler: handler, params: args});
}
}
// Trigger internal widget event
this.trigger(event, args); this.trigger(event, args);
// If required, trigger dom event
if(triggerDom) {
this.$.trigger(event, args);
}
}; };
/** /**
@ -71,6 +107,10 @@ humhub.module('ui.widget', function(module, require, $) {
return true; return true;
}; };
Widget.prototype.isVisible = function() {
return this.$.is(':visible');
};
/** /**
* Can be overwritten to initialize the widget. * Can be overwritten to initialize the widget.
* @returns {undefined} * @returns {undefined}

View File

@ -107,17 +107,33 @@ humhub.module('util', function(module, require, $) {
} }
}; };
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
var string = { var string = {
escapeHtml: function(string) {
return String(string).replace(/[&<>"'`=\/]/g, function(s) {
return entityMap[s];
});
},
capitalize: function(string) { capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
}, },
cutprefix: function(val, prefix) { cutPrefix: function(val, prefix) {
if(!this.startsWith(val, prefix)) { if(!this.startsWith(val, prefix)) {
return val; return val;
} }
return val.substring(prefix.length, val.length); return val.substring(prefix.length, val.length);
}, },
cutsuffix: function(val, suffix) { cutSuffix: function(val, suffix) {
return val.slice(0, suffix.length * -1); return val.slice(0, suffix.length * -1);
}, },
startsWith: function(val, prefix) { startsWith: function(val, prefix) {

View File

@ -1,5 +1,6 @@
/** /**
* Handles conflicting jquery ui and bootstrap namespaces. * Handles conflicting jquery ui and bootstrap namespaces.
*/ */
$.widget.bridge('uibutton', $.ui.button); $.widget.bridge('uibutton', $.ui.button);
$.widget.bridge('uitooltip', $.ui.tooltip); $.widget.bridge('uitooltip', $.ui.tooltip);

View File

@ -1,5 +1,12 @@
<?php <?php
return [ return [
'bootstrap' => ['debug'],
'modules' => [
'debug' => [
'class' => 'yii\debug\Module',
//'allowedIPs' => ['*'],
],
],
]; ];

View File

@ -87,6 +87,7 @@ class AppAsset extends AssetBundle
'humhub\modules\content\assets\ContentAsset', 'humhub\modules\content\assets\ContentAsset',
'humhub\modules\user\assets\UserAsset', 'humhub\modules\user\assets\UserAsset',
'humhub\modules\user\assets\UserPickerAsset', 'humhub\modules\user\assets\UserPickerAsset',
'humhub\modules\post\assets\PostAsset',
'humhub\modules\comment\assets\CommentAsset', 'humhub\modules\comment\assets\CommentAsset',
'humhub\assets\NProgressAsset', 'humhub\assets\NProgressAsset',
'humhub\assets\IE9FixesAsset', 'humhub\assets\IE9FixesAsset',

View File

@ -59,6 +59,7 @@ class CoreApiAsset extends AssetBundle
'js/humhub/humhub.ui.navigation.js', 'js/humhub/humhub.ui.navigation.js',
'js/humhub/humhub.ui.gallery.js', 'js/humhub/humhub.ui.gallery.js',
'js/humhub/humhub.ui.picker.js', 'js/humhub/humhub.ui.picker.js',
'js/humhub/humhub.ui.richtext.js',
// Note this should stay at last for other click event listeners beeing able to prevent pjax handling (e.g gallery) // Note this should stay at last for other click event listeners beeing able to prevent pjax handling (e.g gallery)
'js/humhub/humhub.client.pjax.js', 'js/humhub/humhub.client.pjax.js',
]; ];

View File

@ -9,6 +9,7 @@
namespace humhub\assets; namespace humhub\assets;
use yii\web\AssetBundle; use yii\web\AssetBundle;
use yii\web\View;
/** /**
* select2 * select2
@ -17,6 +18,8 @@ use yii\web\AssetBundle;
*/ */
class PjaxAsset extends AssetBundle class PjaxAsset extends AssetBundle
{ {
public $jsOptions = ['position' => View::POS_HEAD];
/** /**
* @inheritdoc * @inheritdoc
*/ */
@ -33,7 +36,7 @@ class PjaxAsset extends AssetBundle
public $js = ['js/jquery.pjax.modified.js']; public $js = ['js/jquery.pjax.modified.js'];
public $depends = [ public $depends = [
'yii\web\YiiAsset', //'yii\web\YiiAsset',
]; ];
} }

View File

@ -11,13 +11,21 @@ HumHub Change Log
- Enh: Refactored File module (luke-) - Enh: Refactored File module (luke-)
- Enh: Added yii2-imagine Extension (luke-) - Enh: Added yii2-imagine Extension (luke-)
- Enh: Use of blueimp image gallery (buddha) - Enh: Use of blueimp image gallery (buddha)
- Enh: JS module system (buddha) - Enh: JS module system with build in logging/text/config features (buddha)
- Enh: JS core api (buddha) - Enh: JS core api under humhub namespace (buddha)
- Enh: Use of compressed assets (js,css) in production mode (buddha) - Enh: Use of compressed assets (js,css) in production mode (buddha)
- Enh: Enhanced testability (buddha) - Enh: Enhanced testability (buddha)
- Enh: Added backend group permissions (buddha) - Enh: Added administrative backend group permissions (buddha)
- Enh: Enhanced AccessControll filter with permission rules. (buddha) - Enh: Enhanced AccessControl filter with permission rules. (buddha)
- Enh: Splitted less files to facilitate theming. (buddha) - Enh: Splitted less files to facilitate theming. (buddha)
- Enh: Added user status bar for user feedback (buddha) - Enh: Added user status bar for user feedback (buddha)
- Enh: Better UserFeedack (buttons/messages) / Replacement of old DataSaved widget (buddha)
- Enh: Overwrite default permission settings (buddha) - Enh: Overwrite default permission settings (buddha)
- Enh: SpaceChooser rewrite with following spaces and remote search (buddha) - Enh: SpaceChooser rewrite with following spaces and remote search (buddha)
- Enh: Modal widget rewrite.
- Enh: Enhanced Archived Space handling (buddha)
- Enh: Upload widget rewrite. (buddha)
- Enh: Picker widgets rewrite (UserPicker/SpacePicker/MultiselectDropdown). (buddha)
- Enh: Richtext widget rewrite. (buddha)
- Enh: Removed almost all inline JS blocks. (buddha)
- Enh: StreamAction now uses flexible StreamQuery Model.

View File

@ -47,6 +47,12 @@ Please see the following file for 1.2 changes (at the buttom):
- modules/space/views/membership/invite.php - modules/space/views/membership/invite.php
- modules/comment/widget/views/showComment.php - modules/comment/widget/views/showComment.php
- modules/files/widget/views/showFiles.php - modules/files/widget/views/showFiles.php
- modules/post/widget/views/post.php
- Richtext rewrite:
- modules/comment/widget/views/form.php
- modules/comment/views/comment/edit.php
- modules/post/views/post/edit.php
- modules/post/widget/views/form.php
- UserPicker rewrite: - UserPicker rewrite:
- modules/admin/views/group/edit.php - modules/admin/views/group/edit.php
- modules/admin/views/group/members.php - modules/admin/views/group/members.php

View File

@ -17,13 +17,17 @@ humhub.module('comment', function(module, require, $) {
client.submit(evt, {dataType: 'html'}).then(function(response) { client.submit(evt, {dataType: 'html'}).then(function(response) {
that.addComment(response.html); that.addComment(response.html);
that.getInput().val('').trigger('autosize.resize'); that.getInput().val('').trigger('autosize.resize');
that.getRichtext().addClass('atwho-placeholder').focus(); that.getRichtext().$.addClass('atwho-placeholder').focus();
that.getUpload().reset(); that.getUpload().reset();
}).catch(function(err) { }).catch(function(err) {
module.log.error(err, true); module.log.error(err, true);
}); });
}; };
Form.prototype.getRichtext = function(html) {
return Widget.instance(this.$.find('div.humhub-ui-richtext'));
};
Form.prototype.addComment = function(html) { Form.prototype.addComment = function(html) {
var $html = $(html).hide(); var $html = $(html).hide();
this.getCommentsContainer().append($html); this.getCommentsContainer().append($html);
@ -42,10 +46,6 @@ humhub.module('comment', function(module, require, $) {
return this.$.find('textarea'); return this.$.find('textarea');
}; };
Form.prototype.getRichtext = function() {
return this.$.find('[contenteditable]');
};
var Comment = function(node) { var Comment = function(node) {
Content.call(this, node); Content.call(this, node);
additions.observe(this.$); additions.observe(this.$);
@ -58,7 +58,7 @@ humhub.module('comment', function(module, require, $) {
var that = this; var that = this;
client.post(evt, {dataType: 'html'}).then(function(response) { client.post(evt, {dataType: 'html'}).then(function(response) {
that.$.find('.comment_edit_content').replaceWith(response.html); that.$.find('.comment_edit_content').replaceWith(response.html);
that.$.find('[contenteditable]').focus(); that.getRichtext().focus();
that.$.find('.comment-cancel-edit-link').show(); that.$.find('.comment-cancel-edit-link').show();
that.$.find('.comment-edit-link').hide(); that.$.find('.comment-edit-link').hide();
}).finally(function() { }).finally(function() {
@ -66,6 +66,10 @@ humhub.module('comment', function(module, require, $) {
}); });
}; };
Comment.prototype.getRichtext = function() {
return Widget.instance(this.$.find('div.humhub-ui-richtext'));
};
Comment.prototype.delete = function() { Comment.prototype.delete = function() {
this.super('delete', {modal: module.config.modal.delteConfirm}).then(function($confirm) { this.super('delete', {modal: module.config.modal.delteConfirm}).then(function($confirm) {
if($confirm) { if($confirm) {

View File

@ -10,17 +10,13 @@ use yii\helpers\Url;
<?php $form = CActiveForm::begin(); ?> <?php $form = CActiveForm::begin(); ?>
<?= Html::hiddenInput('contentModel', $contentModel); ?> <?= Html::hiddenInput('contentModel', $contentModel); ?>
<?= Html::hiddenInput('contentId', $contentId); ?> <?= Html::hiddenInput('contentId', $contentId); ?>
<?= $form->textArea($comment, 'message', array('class' => 'form-control', 'id' => 'comment_input_' . $comment->id, 'placeholder' => Yii::t('CommentModule.views_edit', 'Edit your comment...'))); ?>
<!-- create contenteditable div for HEditorWidget to place the data --> <?= humhub\widgets\RichtextField::widget([
<div id="comment_input_<?= $comment->id; ?>_contenteditable" class="form-control atwho-input"
contenteditable="true"><?= \humhub\widgets\RichText::widget(['text' => $comment->message, 'edit' => true]); ?></div>
<?= \humhub\widgets\RichTextEditor::widget(array(
'id' => 'comment_input_'.$comment->id, 'id' => 'comment_input_'.$comment->id,
'inputContent' => $comment->message, 'placeholder' => Yii::t('CommentModule.views_edit', 'Edit your comment...'),
'record' => $comment, 'model' => $comment,
));?> 'attribute' => 'message'
]); ?>
<div class="comment-buttons"> <div class="comment-buttons">

View File

@ -11,9 +11,11 @@ use yii\helpers\Url;
<?php echo Html::hiddenInput('contentModel', $modelName); ?> <?php echo Html::hiddenInput('contentModel', $modelName); ?>
<?php echo Html::hiddenInput('contentId', $modelId); ?> <?php echo Html::hiddenInput('contentId', $modelId); ?>
<?php echo Html::textarea("message", "", array('id' => 'newCommentForm_' . $id, 'rows' => '1', 'class' => 'form-control autosize commentForm', 'placeholder' => Yii::t('CommentModule.widgets_views_form', 'Write a new comment...'))); ?> <?= humhub\widgets\RichtextField::widget([
'id' => 'newCommentForm_' . $id,
<?php echo humhub\widgets\RichTextEditor::widget(['id' => 'newCommentForm_' . $id]); ?> 'placeholder' => Yii::t('CommentModule.widgets_views_form', 'Write a new comment...'),
'name' => 'message'
]); ?>
<div class="comment-buttons"> <div class="comment-buttons">
<?= humhub\modules\file\widgets\UploadButton::widget([ <?= humhub\modules\file\widgets\UploadButton::widget([

View File

@ -8,10 +8,7 @@ humhub.module('content.form', function(module, require, $) {
var object = require('util').object; var object = require('util').object;
var client = require('client'); var client = require('client');
var config = require('config').module(module);
var event = require('event'); var event = require('event');
var Widget = require('ui.widget').Widget; var Widget = require('ui.widget').Widget;
var instance; var instance;
@ -28,15 +25,20 @@ humhub.module('content.form', function(module, require, $) {
// Hide options by default // Hide options by default
$('.contentForm_options').hide(); $('.contentForm_options').hide();
$('#contentFormError').hide(); $('#contentFormError').hide();
// Remove info text from the textinput
$('#contentFormBody').click(function() {
// Hide options by default
$('.contentForm_options').fadeIn();
});
this.setDefaultVisibility(); this.setDefaultVisibility();
this.$.fadeIn('fast'); this.$.fadeIn('fast');
if(!module.config['disabled']) {
// Remove info text from the textinput
$('#contentFormBody').on('click.humhub:content:form', function() {
// Hide options by default
$('.contentForm_options').fadeIn();
});
} else {
$('#contentFormBody').find('.humhub-ui-richtext').trigger('disable');
}
}; };
CreateForm.prototype.actions = function() { CreateForm.prototype.actions = function() {
@ -80,8 +82,7 @@ humhub.module('content.form', function(module, require, $) {
this.resetFilePreview(); this.resetFilePreview();
$('#public').attr('checked', false); $('#public').attr('checked', false);
$('#contentForm_message_contenteditable').addClass('atwho-placeholder').attr('spellcheck', 'false'); $('#contentFormBody').find('.humhub-ui-richtext').trigger('clear');
$('#contentFormBody').find('.atwho-input').trigger('clear');
}; };
CreateForm.prototype.resetNotifyUser = function() { CreateForm.prototype.resetNotifyUser = function() {
@ -118,22 +119,22 @@ humhub.module('content.form', function(module, require, $) {
}; };
CreateForm.prototype.setDefaultVisibility = function() { CreateForm.prototype.setDefaultVisibility = function() {
if(config['defaultVisibility']) { if(module.config['defaultVisibility']) {
this.setPublicVisibility(); this.setPublicVisibility();
} else { } else {
this.setPrivateVisibility(); this.setPrivateVisibility();
} }
} };
CreateForm.prototype.setPublicVisibility = function() { CreateForm.prototype.setPublicVisibility = function() {
$('#contentForm_visibility').prop("checked", true); $('#contentForm_visibility').prop("checked", true);
$('#contentForm_visibility_entry').html('<i class="fa fa-lock"></i>' + config['text']['makePrivate']); $('#contentForm_visibility_entry').html('<i class="fa fa-lock"></i>' + module.text(['makePrivate']));
$('.label-public').removeClass('hidden'); $('.label-public').removeClass('hidden');
}; };
CreateForm.prototype.setPrivateVisibility = function() { CreateForm.prototype.setPrivateVisibility = function() {
$('#contentForm_visibility').prop("checked", false); $('#contentForm_visibility').prop("checked", false);
$('#contentForm_visibility_entry').html('<i class="fa fa-unlock"></i>' + config['text']['makePublic']); $('#contentForm_visibility_entry').html('<i class="fa fa-unlock"></i>' + module.text(['makePublic']));
$('.label-public').addClass('hidden'); $('.label-public').addClass('hidden');
}; };
@ -143,12 +144,21 @@ humhub.module('content.form', function(module, require, $) {
}; };
var init = function() { var init = function() {
instance = Widget.instance($(CREATE_FORM_ROOT_SELECTOR)); var $root = $(CREATE_FORM_ROOT_SELECTOR);
if($root.length) {
instance = Widget.instance($root);
}
}; };
var unload = function() {
instance = undefined;
}
module.export({ module.export({
CreateForm: CreateForm, CreateForm: CreateForm,
instance: instance, instance: instance,
init: init init: init,
initOnPjaxLoad: true,
unload: unload
}); });
}); });

View File

@ -214,4 +214,14 @@ abstract class ContentContainerActiveRecord extends ActiveRecord
return []; return [];
} }
/**
* Returns weather or not the contentcontainer is archived. (Default false).
* @return boolean
* @since 1.2
*/
public function isArchived()
{
return false;
}
} }

View File

@ -336,7 +336,7 @@ class Content extends \humhub\components\ActiveRecord
*/ */
public function isArchived() public function isArchived()
{ {
return ($this->archived); return $this->archived || $this->getContainer()->isArchived();
} }
/** /**

View File

@ -19,7 +19,8 @@ class ContentFixture extends ActiveFixture
public $depends = [ public $depends = [
'humhub\modules\content\tests\codeception\fixtures\ContentContainerFixture', 'humhub\modules\content\tests\codeception\fixtures\ContentContainerFixture',
'humhub\modules\post\tests\codeception\fixtures\PostFixture', 'humhub\modules\post\tests\codeception\fixtures\PostFixture',
'humhub\modules\comment\tests\codeception\fixtures\CommentFixture' 'humhub\modules\comment\tests\codeception\fixtures\CommentFixture',
'humhub\modules\like\tests\codeception\fixtures\LikeFixture'
]; ];
} }

View File

@ -32,7 +32,7 @@ class ArchiveLink extends \yii\base\Widget
*/ */
public function run() public function run()
{ {
if (!Yii::$app->controller instanceof ContentContainerController || !$this->content->content->canArchive()) { if (!Yii::$app->controller instanceof ContentContainerController || !$this->content->content->canArchive() || $this->content->content->getContainer()->isArchived()) {
return; return;
} }

View File

@ -30,6 +30,10 @@ class DeleteLink extends \yii\base\Widget
*/ */
public function run() public function run()
{ {
if($this->content->content->isArchived()) {
return;
}
if ($this->content->content->canEdit()) { if ($this->content->content->canEdit()) {
return $this->render('deleteLink', array( return $this->render('deleteLink', array(
'model' => $this->content->content->object_model, 'model' => $this->content->content->object_model,

View File

@ -18,14 +18,13 @@ $this->registerJsConfig('content.form', [
<div class="panel panel-default clearfix"> <div class="panel panel-default clearfix">
<div class="panel-body" id="contentFormBody" style="display:none;" data-action-component="content.form.CreateForm" > <div class="panel-body" id="contentFormBody" style="display:none;" data-action-component="content.form.CreateForm" >
<?= Html::beginForm($submitUrl, 'POST'); ?> <?= Html::beginForm($submitUrl, 'POST'); ?>
<?= $form; ?> <?= $form; ?>
<div id="notifyUserContainer" class="form-group" style="margin-top: 15px;display:none;"> <div id="notifyUserContainer" class="form-group" style="margin-top: 15px;display:none;">
<?php $memberPickerUrl = ($contentContainer instanceof Space) ? $contentContainer->createUrl('/space/membership/search') : null ?>
<?= humhub\modules\user\widgets\UserPickerField::widget([ <?= humhub\modules\user\widgets\UserPickerField::widget([
'id' => 'notifyUserInput', 'id' => 'notifyUserInput',
'url' => $memberPickerUrl, 'url' => ($contentContainer instanceof Space) ? $contentContainer->createUrl('/space/membership/search') : null,
'formName' => 'notifyUserInput', 'formName' => 'notifyUserInput',
'maxSelection' => 10, 'maxSelection' => 10,
'disabledItems' => [Yii::$app->user->guid], 'disabledItems' => [Yii::$app->user->guid],
@ -36,7 +35,6 @@ $this->registerJsConfig('content.form', [
<?= Html::hiddenInput("containerGuid", $contentContainer->guid); ?> <?= Html::hiddenInput("containerGuid", $contentContainer->guid); ?>
<?= Html::hiddenInput("containerClass", get_class($contentContainer)); ?> <?= Html::hiddenInput("containerClass", get_class($contentContainer)); ?>
<ul id="contentFormError"></ul> <ul id="contentFormError"></ul>
<div class="contentForm_options"> <div class="contentForm_options">

View File

@ -102,7 +102,7 @@ class DashboardStreamTest extends HumHubDbTestCase
$post2->save(); $post2->save();
$w2 = $post2->content->id; $w2 = $post2->content->id;
$this->assertEquals($this->getStreamActionIds(2), array($w2, $w1)); $this->assertEquals($this->getStreamActionIds(2), [$w2, $w1]);
$this->becomeUser('User3'); $this->becomeUser('User3');
$ids = $this->getStreamActionIds(2); $ids = $this->getStreamActionIds(2);

View File

@ -0,0 +1,24 @@
actor: Tester
namespace: like
settings:
bootstrap: _bootstrap.php
suite_class: \PHPUnit_Framework_TestSuite
colors: true
shuffle: false
memory_limit: 1024M
log: true
# This value controls whether PHPUnit attempts to backup global variables
# See https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.backupGlobals
backup_globals: true
paths:
tests: codeception
log: codeception/_output
data: codeception/_data
helpers: codeception/_support
envs: config/env
config:
# the entry script URL (with host info) for functional and acceptance tests
# PLEASE ADJUST IT TO THE ACTUAL ENTRY SCRIPT URL
test_entry_url: http://localhost:8080/index-test.php

View File

@ -0,0 +1,57 @@
<?php
/**
* This is the initial test bootstrap, which will load the default test bootstrap from the humhub core
*/
// Parse the environment arguments (Note: only simple --env ENV is supported no comma sepration merge...)
$argv = $_SERVER['argv'];
$env = [];
for ($i = 0; $i < count($argv); $i++) {
if ($argv[$i] === '--env') {
$env[] = explode(',', $argv[++$i]);
}
}
// If environment was set try loading special environment config else load default
if (count($env) > 0) {
\Codeception\Configuration::append(['environment' => $env]);
echo 'Run execution environment: ' . $env[0][0] . PHP_EOL;
$envCfgFile = dirname(__DIR__) . '/config/env/test.' . $env[0][0] . '.php';
if (file_exists($envCfgFile)) {
$cfg = array_merge(require_once(__DIR__ . '/../config/test.php'), require_once($envCfgFile));
}
}
// If no environment is set we have to load the default config
if (!isset($cfg)) {
$cfg = require_once(__DIR__ . '/../config/test.php');
}
// If no humhub_root is given we assume our module is in the a root to be in /protected/humhub/modules/<module>/tests/codeception directory
$cfg['humhub_root'] = isset($cfg['humhub_root']) ? $cfg['humhub_root'] : dirname(__DIR__) . '/../../../../..';
echo 'Using HumHub Root: ' . $cfg['humhub_root'] . PHP_EOL;
// Load default test bootstrap
require_once($cfg['humhub_root'] . '/protected/humhub/tests/codeception/_bootstrap.php');
// Overwrite the default test alias
Yii::setAlias('@tests', dirname(__DIR__));
Yii::setAlias('@env', '@tests/config/env');
Yii::setAlias('@root', $cfg['humhub_root']);
Yii::setAlias('@humhubTests', $cfg['humhub_root'] . '/protected/humhub/tests');
// Load all supporting test classes needed for test execution
\Codeception\Util\Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/_support'));
\Codeception\Util\Autoload::addNamespace('tests\codeception\fixtures', Yii::getAlias('@humhubTests/codeception/fixtures'));
\Codeception\Util\Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/_pages'));
if(isset($cfg['modules'])) {
\Codeception\Configuration::append(['humhub_modules' => $cfg['modules']]);
}
if(isset($cfg['fixtures'])) {
\Codeception\Configuration::append(['fixtures' => $cfg['fixtures']]);
}
?>

View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -0,0 +1,26 @@
<?php
namespace like;
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = null)
*
* @SuppressWarnings(PHPMD)
*/
class AcceptanceTester extends \AcceptanceTester
{
use _generated\AcceptanceTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,26 @@
<?php
namespace like;
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = null)
*
* @SuppressWarnings(PHPMD)
*/
class FunctionalTester extends \FunctionalTester
{
use _generated\FunctionalTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,26 @@
<?php
namespace like;
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = null)
*
* @SuppressWarnings(PHPMD)
*/
class UnitTester extends \UnitTester
{
use _generated\UnitTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,38 @@
<?php //[STAMP] 0468b7e2480518455b63a22d3aa6f7c2
namespace like\_generated;
// This class was automatically generated by build task
// You should not change it manually as it will be overwritten on next build
// @codingStandardsIgnoreFile
use tests\codeception\_support\CodeHelper;
trait UnitTesterActions
{
/**
* @return \Codeception\Scenario
*/
abstract protected function getScenario();
/**
* [!] Method is generated. Documentation taken from corresponding module.
*
*
* @see \tests\codeception\_support\CodeHelper::assertContainsError()
*/
public function assertContainsError($model, $message) {
return $this->getScenario()->runStep(new \Codeception\Step\Action('assertContainsError', func_get_args()));
}
/**
* [!] Method is generated. Documentation taken from corresponding module.
*
*
* @see \tests\codeception\_support\CodeHelper::assertNotContainsError()
*/
public function assertNotContainsError($model, $message) {
return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotContainsError', func_get_args()));
}
}

View File

@ -0,0 +1,23 @@
# Codeception Test Suite Configuration
# suite for acceptance tests.
# perform tests in browser using the Selenium-like tools.
# powered by Mink (http://mink.behat.org).
# (tip: that's what your customer will see).
# (tip: test your ajax and javascript by one of Mink drivers).
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
class_name: AcceptanceTester
modules:
enabled:
- WebDriver
- tests\codeception\_support\WebHelper
- tests\codeception\_support\DynamicFixtureHelper
config:
WebDriver:
url: http://localhost:8080/
browser: phantomjs
window_size: 1024x768
restart: true
lang: en

View File

@ -0,0 +1,6 @@
<?php
/**
* Initialize the HumHub Application for functional testing. The default application configuration for this suite can be overwritten
* in @tests/config/functional.php
*/
require(Yii::getAlias('@humhubTests/codeception/acceptance/_bootstrap.php'));

View File

@ -0,0 +1,3 @@
<?php
return \tests\codeception\_support\HumHubTestConfiguration::getSuiteConfig('functional');

View File

@ -0,0 +1,3 @@
<?php
return \tests\codeception\_support\HumHubTestConfiguration::getSuiteConfig('unit');

View File

@ -0,0 +1,19 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2015 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\like\tests\codeception\fixtures;
use yii\test\ActiveFixture;
class LikeFixture extends ActiveFixture
{
public $modelClass = 'humhub\modules\like\models\Like';
public $dataFile = '@modules/like/tests/codeception/fixtures/data/like.php';
}

View File

@ -0,0 +1,9 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
return [];

View File

@ -0,0 +1,18 @@
# Codeception Test Suite Configuration
# suite for functional (integration) tests.
# emulate web requests and make application process them.
# (tip: better to use with frameworks).
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
class_name: FunctionalTester
modules:
enabled:
- Filesystem
- Yii2
- tests\codeception\_support\TestHelper
- tests\codeception\_support\DynamicFixtureHelper
- tests\codeception\_support\HumHubHelper
config:
Yii2:
configFile: 'codeception/config/functional.php'

View File

@ -0,0 +1,6 @@
<?php
/**
* Initialize the HumHub Application for functional testing. The default application configuration for this suite can be overwritten
* in @tests/config/functional.php
*/
require(Yii::getAlias('@humhubTests/codeception/functional/_bootstrap.php'));

View File

@ -0,0 +1,9 @@
# Codeception Test Suite Configuration
# suite for unit (internal) tests.
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
class_name: UnitTester
modules:
enabled:
- tests\codeception\_support\CodeHelper

View File

@ -0,0 +1,6 @@
<?php
/**
* Initialize the HumHub Application for functional testing. The default application configuration for this suite can be overwritten
* in @tests/config/functional.php
*/
require(Yii::getAlias('@humhubTests/codeception/unit/_bootstrap.php'));

View File

@ -0,0 +1,5 @@
<?php
/**
* This config is shared by all suites (unit/functional/acceptance) and can be overwritten by a suite config (e.g. functional.php)
*/
return [];

View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -0,0 +1,5 @@
<?php
/**
* Here you can overwrite the default config for the functional suite. The default config resides in @humhubTests/codeception/config/config.php
*/
return [];

View File

@ -0,0 +1,11 @@
<?php
return [
'fixtures' => [
'default',
'content' => 'humhub\modules\content\tests\codeception\fixtures\ContentFixture',
]
];

View File

@ -0,0 +1,5 @@
<?php
/**
* Here you can overwrite your functional humhub config. The default config resiedes in @humhubTests/codeception/config/config.php
*/
return [];

View File

@ -0,0 +1,26 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2015 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\post\assets;
use yii\web\AssetBundle;
class PostAsset extends AssetBundle
{
public $sourcePath = '@post/resources';
public $css = [];
public $js = [
'js/humhub.post.js'
];
public $depends = [
'humhub\assets\CoreApiAsset'
];
}

View File

@ -0,0 +1,83 @@
humhub.module('post', function(module, require, $) {
var Widget = require('ui.widget').Widget;
var object = require('util').object;
var Post = function(node, options) {
Widget.call(this, node, options);
};
object.inherits(Post, Widget);
Post.prototype.getDefaultOptions = function() {
return {
collapse: 310,
readMore: module.text('default.readMore'),
readLess: module.text('default.readLess')
};
};
Post.prototype.init = function() {
this.initCollapse();
};
Post.prototype.initCollapse = function() {
var that = this;
var height = this.$.outerHeight();
this.$collapseButton = this.$.siblings('.showMore');
if(this.options.prevCollapse) {
return;
}
// If height expands the max height we init the collapse post logic
if(height > this.options.collapse) {
if(!this.$collapseButton.length) {
this.$collapseButton = $(Post.templates.showMore);
this.$.after(this.$collapseButton);
}
// Init collapse button
this.$collapseButton.on('click', function(evt) {
evt.preventDefault();
that.toggleCollapse();
}).show();
// Set init state
if(this.data('state') !== 'expanded') {
this.collapse();
} else {
this.expand();
}
}
};
Post.prototype.toggleCollapse = function() {
if(this.$.data('state') === 'collapsed') {
this.expand();
} else {
this.collapse();
}
};
Post.prototype.collapse = function() {
this.$.css({'display': 'block', 'max-height': this.options.collapse+'px'});
this.$collapseButton.html('<i class="fa fa-arrow-down"></i> ' + this.options.readMore);
this.$.data('state', 'collapsed');
};
Post.prototype.expand = function() {
this.$.css('max-height', '');
this.$collapseButton.html('<i class="fa fa-arrow-up"></i> ' + this.options.readLess);
this.$.data('state', 'expanded');
};
Post.templates = {
showMore : '<a href="#" style="display:block;margin: 5px 0;"></a>'
};
module.export({
Post: Post
});
});

View File

@ -1,30 +1,29 @@
<?php <?php
use humhub\compat\CActiveForm; use humhub\compat\CActiveForm;
use yii\helpers\Html;
?> ?>
<div class="content content_edit" id="post_edit_<?php echo $post->id; ?>"> <div class="content content_edit" id="post_edit_<?php echo $post->id; ?>">
<?php $form = CActiveForm::begin(['id' => 'post-edit-form_' . $post->id]); ?> <?php $form = CActiveForm::begin(['id' => 'post-edit-form_' . $post->id]); ?>
<!-- create contenteditable div for HEditorWidget to place the data --> <!-- create contenteditable div for HEditorWidget to place the data -->
<div id="post_input_<?php echo $post->id; ?>_contenteditable" class="form-control atwho-input" contenteditable="true"> <?= humhub\widgets\RichtextField::widget([
<?php echo \humhub\widgets\RichText::widget(['text' => $post->message, 'edit' => true]); ?> 'id' => 'post_input_'. $post->id,
</div> 'placeholder' => Yii::t('PostModule.views_edit', 'Edit your post...'),
'model' => $post,
<?= Html::activeTextarea($post, 'message', ['class' => 'form-control', 'id' => 'post_input_' . $post->id, 'placeholder' => Yii::t('PostModule.views_edit', 'Edit your post...')]) ?> 'attribute' => 'message'
]); ?>
<?= \humhub\widgets\RichTextEditor::widget(['id' => 'post_input_' . $post->id, 'inputContent' => $post->message, 'record' => $post]); ?>
<div class="comment-buttons"> <div class="comment-buttons">
<?= \humhub\modules\file\widgets\UploadButton::widget([ <?=
\humhub\modules\file\widgets\UploadButton::widget([
'id' => 'post_upload_' . $post->id, 'id' => 'post_upload_' . $post->id,
'model' => $post, 'model' => $post,
'dropZone' => '#post_edit_' . $post->id . ':parent', 'dropZone' => '#post_edit_' . $post->id . ':parent',
'preview' => '#post_upload_preview_' . $post->id, 'preview' => '#post_upload_preview_' . $post->id,
'progress' => '#post_upload_progress_' . $post->id 'progress' => '#post_upload_progress_' . $post->id
]) ?> ])
?>
<!-- editSubmit action of surrounding StreamEntry component --> <!-- editSubmit action of surrounding StreamEntry component -->
<button type="submit" class="btn btn-default btn-sm btn-comment-submit" data-ui-loader data-action-click="editSubmit" data-action-url="<?= $post->content->container->createUrl('/post/post/edit', ['id' => $post->id]) ?>"> <button type="submit" class="btn btn-default btn-sm btn-comment-submit" data-ui-loader data-action-click="editSubmit" data-action-url="<?= $post->content->container->createUrl('/post/post/edit', ['id' => $post->id]) ?>">
@ -35,12 +34,14 @@ use yii\helpers\Html;
<div id="post_upload_progress_<?= $post->id ?>" style="display:none;margin:10px 0px;"></div> <div id="post_upload_progress_<?= $post->id ?>" style="display:none;margin:10px 0px;"></div>
<?= \humhub\modules\file\widgets\FilePreview::widget([ <?=
\humhub\modules\file\widgets\FilePreview::widget([
'id' => 'post_upload_preview_' . $post->id, 'id' => 'post_upload_preview_' . $post->id,
'options' => ['style' => 'margin-top:10px'], 'options' => ['style' => 'margin-top:10px'],
'model' => $post, 'model' => $post,
'edit' => true 'edit' => true
]) ?> ])
?>
<?php CActiveForm::end(); ?> <?php CActiveForm::end(); ?>
</div> </div>

View File

@ -1,14 +0,0 @@
<?php
use yii\helpers\Html;
?>
<?php echo Html::textarea("message", '', array('id' => 'contentForm_message', 'class' => 'form-control autosize contentForm', 'rows' => '1', 'placeholder' => Yii::t("PostModule.widgets_views_postForm", "What's on your mind?"))); ?>
<?php
/* Modify textarea for mention input */
echo \humhub\widgets\RichTextEditor::widget(array(
'id' => 'contentForm_message',
));
?>

View File

@ -1,14 +1,7 @@
<?php <?= humhub\widgets\RichtextField::widget([
use yii\helpers\Html;
?>
<?php echo Html::textarea("message", '', array('id' => 'contentForm_message', 'class' => 'form-control autosize contentForm', 'rows' => '1', 'placeholder' => Yii::t("PostModule.widgets_views_postForm", "What's on your mind?"))); ?>
<?php
/* Modify textarea for mention input */
echo \humhub\widgets\RichTextEditor::widget(array(
'id' => 'contentForm_message', 'id' => 'contentForm_message',
)); 'placeholder' => Yii::t("PostModule.widgets_views_postForm", "What's on your mind?"),
?> 'name' => 'message',
'disabled' => (property_exists(Yii::$app->controller, 'contentContainer') && Yii::$app->controller->contentContainer->isArchived()),
'disabledText' => Yii::t("PostModule.widgets_views_postForm", "This space is archived."),
]);?>

View File

@ -1,60 +1,3 @@
<?php <span data-ui-widget="post.Post" data-state="collapsed" data-ui-init id="post-content-<?= $post->id; ?>" style="overflow: hidden; margin-bottom: 5px;">
<?= humhub\widgets\RichText::widget(['text' => $post->message, 'record' => $post]) ?>
use yii\helpers\Html;
$richOutput = humhub\widgets\RichText::widget(['text' => $post->message, 'record' => $post]);
?>
<span id="post-content-<?php echo $post->id; ?>" style="overflow: hidden; margin-bottom: 5px;">
<?php print $richOutput; ?>
</span> </span>
<a class="more-link-post hidden" id="more-link-post-<?php echo $post->id; ?>" data-state="down"
style="margin: 20px 0 20px 0;" href="javascript:showMore(<?php echo $post->id; ?>);"><i
class="fa fa-arrow-down"></i> <?php echo Yii::t('PostModule.widgets_views_post', 'Read full post...'); ?>
</a>
<script type="text/javascript">
$(document).ready(function () {
// save the count of characters
var _words = '<?php echo strlen(strip_tags($richOutput)); ?>';
var _postHeight = $('#post-content-<?php echo $post->id; ?>').outerHeight();
if (_postHeight > 310) {
// show more-button
$('#more-link-post-<?php echo $post->id; ?>').removeClass('hidden');
// set limited height
$('#post-content-<?php echo $post->id; ?>').css({'display': 'block', 'max-height': '310px'});
}
});
function showMore(post_id) {
// set current state
var _state = $('#more-link-post-' + post_id).attr('data-state');
if (_state == "down") {
$('#post-content-' + post_id).css('max-height', '2000px');
// set new link content
$('#more-link-post-' + post_id).html('<i class="fa fa-arrow-up"></i> <?php echo Html::encode(Yii::t('PostModule.widgets_views_post', 'Collapse')); ?>');
// update link state
$('#more-link-post-' + post_id).attr('data-state', 'up');
} else {
// set back to limited length
$('#post-content-' + post_id).css('max-height', '310px');
// set new link content
$('#more-link-post-' + post_id).html('<i class="fa fa-arrow-down"></i> <?php echo Html::encode(Yii::t('PostModule.widgets_views_post', 'Read full post...')); ?>');
// update link state
$('#more-link-post-' + post_id).attr('data-state', 'down');
}
}
</script>

View File

@ -13,7 +13,7 @@ use yii\web\AssetBundle;
class SpaceAsset extends AssetBundle class SpaceAsset extends AssetBundle
{ {
public $jsOptions = ['position' => \yii\web\View::POS_END]; public $jsOptions = ['position' => \yii\web\View::POS_BEGIN];
public $sourcePath = '@space/resources'; public $sourcePath = '@space/resources';
public $css = []; public $css = [];
public $js = [ public $js = [

View File

@ -362,6 +362,17 @@ class Space extends ContentContainerActiveRecord implements \humhub\modules\sear
$this->save(); $this->save();
} }
/**
* Returns wether or not a Space is archived.
*
* @return boolean
* @since 1.2
*/
public function isArchived()
{
return $this->status === self::STATUS_ARCHIVED;
}
/** /**
* Creates an url in space scope. * Creates an url in space scope.
* (Adding sguid parameter to identify current space.) * (Adding sguid parameter to identify current space.)

View File

@ -64,6 +64,15 @@ class DefaultController extends Controller
$this->ownerOnly(); $this->ownerOnly();
$space = $this->getSpace(); $space = $this->getSpace();
$space->archive(); $space->archive();
if(Yii::$app->request->isAjax) {
Yii::$app->response->format = 'json';
return [
'success' => true,
'space' => \humhub\modules\space\controllers\BrowseController::getSpaceResult($space, true, ['isMember' => true])
];
}
return $this->redirect($space->createUrl('/space/manage')); return $this->redirect($space->createUrl('/space/manage'));
} }
@ -75,6 +84,15 @@ class DefaultController extends Controller
$this->ownerOnly(); $this->ownerOnly();
$space = $this->getSpace(); $space = $this->getSpace();
$space->unarchive(); $space->unarchive();
if(Yii::$app->request->isAjax) {
Yii::$app->response->format = 'json';
return [
'success' => true,
'space' => \humhub\modules\space\controllers\BrowseController::getSpaceResult($space, true, ['isMember' => true])
];
}
return $this->redirect($space->createUrl('/space/manage')); return $this->redirect($space->createUrl('/space/manage'));
} }

View File

@ -32,11 +32,16 @@ use humhub\modules\space\modules\manage\widgets\DefaultMenu;
<?php echo \humhub\widgets\DataSaved::widget(); ?> <?php echo \humhub\widgets\DataSaved::widget(); ?>
<div class="pull-right"> <div class="pull-right">
<?php if ($model->status == Space::STATUS_ENABLED) { ?> <?php if ($model->status == Space::STATUS_ENABLED || $model->status == Space::STATUS_ARCHIVED) : ?>
<?php echo Html::a(Yii::t('SpaceModule.views_admin_edit', 'Archive'), $model->createUrl('/space/manage/default/archive'), array('class' => 'btn btn-warning', 'data-post' => 'POST')); ?> <a href="#" <?= $model->status == Space::STATUS_ARCHIVED ? 'style="display:none;"' : '' ?> class="btn btn-warning archive"
<?php } elseif ($model->status == Space::STATUS_ARCHIVED) { ?> data-action-click="space.archive" data-action-url="<?= $model->createUrl('/space/manage/default/archive') ?>" data-ui-loader>
<?php echo Html::a(Yii::t('SpaceModule.views_admin_edit', 'Unarchive'), $model->createUrl('/space/manage/default/unarchive'), array('class' => 'btn btn-warning', 'data-post' => 'POST')); ?> <?= Yii::t('SpaceModule.views_admin_edit', 'Archive') ?>
<?php } ?> </a>
<a href="#" <?= $model->status == Space::STATUS_ENABLED ? 'style="display:none;"' : '' ?> class="btn btn-warning unarchive"
data-action-click="space.unarchive" data-action-url="<?= $model->createUrl('/space/manage/default/unarchive') ?>" data-ui-loader>
<?= Yii::t('SpaceModule.views_admin_edit', 'Unarchive') ?>
</a>
<?php endif; ?>
</div> </div>
<?php ActiveForm::end(); ?> <?php ActiveForm::end(); ?>

View File

@ -1,4 +1,4 @@
<?php //[STAMP] 25c2b4e8b8fd2158213a3465a3ef3b60 <?php //[STAMP] 74c0c3a2840b2412882616390669c863
namespace space\_generated; namespace space\_generated;
// This class was automatically generated by build task // This class was automatically generated by build task

View File

@ -1,4 +1,4 @@
<?php //[STAMP] 0468b7e2480518455b63a22d3aa6f7c2 <?php //[STAMP] e93907d5924b39a3cd4134cc6a6ea3a3
namespace space\_generated; namespace space\_generated;
// This class was automatically generated by build task // This class was automatically generated by build task

View File

@ -67,7 +67,7 @@ use yii\helpers\Url;
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn btn-primary" <a href="#" class="btn btn-primary"
data-action-click="ui.modal.submit" data-action-click="ui.modal.post"
data-ui-loader data-ui-loader
data-action-url="<?= Url::to(['/space/create/invite', 'spaceId' => $space->id]) ?>"> data-action-url="<?= Url::to(['/space/create/invite', 'spaceId' => $space->id]) ?>">
<?= Yii::t('SpaceModule.views_create_create', 'Next'); ?> <?= Yii::t('SpaceModule.views_create_create', 'Next'); ?>

View File

@ -44,7 +44,11 @@ class Chooser extends Widget
} }
$query->joinWith('space'); $query->joinWith('space');
$query->where(['space_membership.user_id' => Yii::$app->user->id, 'space_membership.status' => Membership::STATUS_MEMBER]); $query->where([
'space_membership.user_id' => Yii::$app->user->id,
'space_membership.status' => Membership::STATUS_MEMBER,
'space.status' => \humhub\modules\space\models\Space::STATUS_ENABLED
]);
return $query; return $query;
} }

View File

@ -56,6 +56,9 @@ class SpaceChooserItem extends Widget
} else if($this->isFollowing) { } else if($this->isFollowing) {
$badge = '<i class="fa fa-star badge-space pull-right type tt" title="' . Yii::t('SpaceModule.widgets_spaceChooserItem', 'You are following this space') . '" aria-hidden="true"></i>'; $badge = '<i class="fa fa-star badge-space pull-right type tt" title="' . Yii::t('SpaceModule.widgets_spaceChooserItem', 'You are following this space') . '" aria-hidden="true"></i>';
$data = 'data-space-following'; $data = 'data-space-following';
} else if($this->space->isArchived()) {
$badge = '<i class="fa fa-history badge-space pull-right type tt" title="' . Yii::t('SpaceModule.widgets_spaceChooserItem', 'This space is archived') . '" aria-hidden="true"></i>';
$data = 'data-space-archived';
} else { } else {
$data = 'data-space-none'; $data = 'data-space-none';
} }

View File

@ -76,9 +76,9 @@ $this->registerJsConfig('space.chooser', [
<?php if ($canCreateSpace): ?> <?php if ($canCreateSpace): ?>
<li> <li>
<div class="dropdown-footer"> <div class="dropdown-footer">
<?= Html::a(Yii::t('SpaceModule.widgets_views_spaceChooser', 'Create new space'), <a href="#" class="btn btn-info col-md-12" data-action-click="ui.modal.load" data-action-url="<?= Url::to(['/space/create/create']) ?>">
Url::to(['/space/create/create']), <?= Yii::t('SpaceModule.widgets_views_spaceChooser', 'Create new space') ?>
['class' => 'btn btn-info col-md-12', 'data-target' => '#globalModal']);?> </a>
</div> </div>
</li> </li>
<?php endif; ?> <?php endif; ?>

View File

@ -140,8 +140,6 @@ abstract class Stream extends Action
if (!Yii::$app->request->isConsoleRequest) { if (!Yii::$app->request->isConsoleRequest) {
$this->streamQuery->load(Yii::$app->request->get()); $this->streamQuery->load(Yii::$app->request->get());
$this->setActionSettings();
if (Yii::$app->getRequest()->get('mode', $this->mode) === self::MODE_ACTIVITY) { if (Yii::$app->getRequest()->get('mode', $this->mode) === self::MODE_ACTIVITY) {
$this->streamQuery->includes(\humhub\modules\activity\models\Activity::className()); $this->streamQuery->includes(\humhub\modules\activity\models\Activity::className());
$this->streamQuery->query()->leftJoin('activity', 'content.object_id=activity.id AND content.object_model=:activityModel', ['activityModel' => \humhub\modules\activity\models\Activity::className()]); $this->streamQuery->query()->leftJoin('activity', 'content.object_id=activity.id AND content.object_model=:activityModel', ['activityModel' => \humhub\modules\activity\models\Activity::className()]);
@ -154,6 +152,8 @@ abstract class Stream extends Action
} }
} }
$this->setActionSettings();
// Build query and set activeQuery. // Build query and set activeQuery.
$this->activeQuery = $this->streamQuery->query(true); $this->activeQuery = $this->streamQuery->query(true);
$this->user = $this->streamQuery->user; $this->user = $this->streamQuery->user;

View File

@ -0,0 +1,355 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2016 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\stream\actions;
use Yii;
use yii\base\Action;
use humhub\modules\content\models\Content;
use humhub\modules\user\models\User;
use yii\base\ActionEvent;
use yii\base\Exception;
/**
* Stream is the basic action for content streams.
*
* @author luke
* @since 0.11
*/
abstract class Stream_old extends Action
{
/**
* @event ActionEvent Event triggered before this action is run.
* This can be used for example to customize [[activeQuery]] before it gets executed.
* @since 1.1.1
*/
const EVENT_BEFORE_RUN = 'beforeRun';
/**
* @event ActionEvent Event triggered after this action is run.
* @since 1.1.1
*/
const EVENT_AFTER_RUN = 'afterRun';
/**
* Constants used for sorting
*/
const SORT_CREATED_AT = 'c';
const SORT_UPDATED_AT = 'u';
/**
* Modes
*/
const MODE_NORMAL = "normal";
const MODE_ACTIVITY = "activity";
/**
* Maximum wall entries per request
*/
const MAX_LIMIT = 50;
/**
* @var string
*/
public $mode;
/**
* Used to load single content entries.
* @since 1.2
*/
public $contentId;
/**
* First wall entry id to deliver
*
* @var int
*/
public $from;
/**
* Sorting Mode
*
* @var int
*/
public $sort;
/**
* Maximum wall entries to return
* @var int
*/
public $limit = 4;
/**
* Filters
*
* @var array
*/
public $filters = [];
/**
* @var \yii\db\ActiveQuery
*/
public $activeQuery;
/**
* Optional stream user
* if no user is specified, the current logged in user will be used.
*
* @var User
*/
public $user;
/**
* @inheritdocs
*/
public function init()
{
$this->activeQuery = Content::find();
// If no user is set, take current if logged in
if ($this->user === null && !Yii::$app->user->isGuest) {
$this->user = Yii::$app->user->getIdentity();
}
// Read parameters
if (!Yii::$app->request->isConsoleRequest) {
$this->contentId = Yii::$app->getRequest()->get('id');
$from = Yii::$app->getRequest()->get('from', 0);
if ($from != 0) {
$this->from = (int) $from;
}
/**
* Sorting
*/
$sort = Yii::$app->getRequest()->get('sort', Yii::$app->getModule('content')->settings->get('stream.defaultSort'));
if ($sort === static::SORT_CREATED_AT || $sort === static::SORT_UPDATED_AT) {
$this->sort = $sort;
} else {
$this->sort = static::SORT_CREATED_AT;
}
$limit = Yii::$app->getRequest()->get('limit', '');
if ($limit != "" && $limit <= self::MAX_LIMIT) {
$this->limit = $limit;
}
$mode = Yii::$app->getRequest()->get('mode', '');
if ($mode != "" && ($mode == self::MODE_ACTIVITY || $mode == self::MODE_NORMAL)) {
$this->mode = $mode;
}
foreach (explode(',', Yii::$app->getRequest()->get('filters', "")) as $filter) {
$this->filters[] = trim($filter);
}
}
$this->setupCriteria();
$this->setupFilters();
}
public function setupCriteria()
{
$this->activeQuery->joinWith('createdBy');
$this->activeQuery->joinWith('contentContainer');
$this->activeQuery->limit($this->limit);
$this->activeQuery->andWhere(['user.status' => User::STATUS_ENABLED]);
/**
* Handle Stream Mode (Normal Stream or Activity Stream)
*/
if ($this->mode == self::MODE_ACTIVITY) {
$this->activeQuery->andWhere(['content.object_model' => \humhub\modules\activity\models\Activity::className()]);
// Dont show own activities
if ($this->user !== null) {
$this->activeQuery->leftJoin('activity', 'content.object_id=activity.id AND content.object_model=:activityModel', ['activityModel' => \humhub\modules\activity\models\Activity::className()]);
$this->activeQuery->andWhere('content.created_by != :userId', array(':userId' => $this->user->id));
}
} else {
$this->activeQuery->andWhere(['!=', 'content.object_model', \humhub\modules\activity\models\Activity::className()]);
}
if($this->isSingleContentQuery()) {
$this->activeQuery->andWhere(['content.id' => $this->contentId]);
return;
}
/**
* Setup Sorting
*/
if ($this->sort == self::SORT_UPDATED_AT) {
$this->activeQuery->orderBy('content.stream_sort_date DESC');
if ($this->from != "") {
$this->activeQuery->andWhere("content.stream_sort_date < (SELECT updated_at FROM content wd WHERE wd.id=" . $this->from . ")");
}
} else {
$this->activeQuery->orderBy('content.id DESC');
if ($this->from != "")
$this->activeQuery->andWhere("content.id < " . $this->from);
}
}
/**
* Setup additional filters
*/
public function setupFilters()
{
if (in_array('entry_files', $this->filters)) {
$fileSelector = (new \yii\db\Query())
->select(["id"])
->from('file')
->where('file.object_model=content.object_model AND file.object_id=content.object_id')
->limit(1);
$fileSelectorSql = Yii::$app->db->getQueryBuilder()->build($fileSelector)[0];
$this->activeQuery->andWhere('(' . $fileSelectorSql . ') IS NOT NULL');
}
// Setup Post specific filters
if (in_array('posts_links', $this->filters)) {
$this->activeQuery->leftJoin('post', 'content.object_id=post.id AND content.object_model=:postModel', ['postModel' => \humhub\modules\post\models\Post::className()]);
$this->activeQuery->andWhere("post.url is not null");
}
// Only apply archived filter when we should load more than one entry
if ($this->limit != 1) {
if (!in_array('entry_archived', $this->filters)) {
$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 userinvolvedz
if (in_array('entry_userinvolved', $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()]);
}
// Visibility filters
if (in_array('visibility_private', $this->filters)) {
$this->activeQuery->andWhere(['content.visibility' => Content::VISIBILITY_PRIVATE]);
}
if (in_array('visibility_public', $this->filters)) {
$this->activeQuery->andWhere(['content.visibility' => Content::VISIBILITY_PUBLIC]);
}
}
public function isSingleContentQuery() {
return $this->contentId != null;
}
/**
* @inheritdoc
*/
public function run()
{
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
$output = [];
$this->init();
$output['content'] = [];
foreach ($this->activeQuery->all() as $content) {
try {
$output['content'][$content->id] = static::getContentResultEntry($content);
} catch(Exception $e) {
// We do not want to kill the stream action in prod environments in case the rendering of an entry fails.
if(YII_ENV_PROD) {
Yii::error($e);
} else {
throw $e;
}
}
}
$output['total'] = count($output['content']);
$output['isLast'] = ($output['total'] < $this->activeQuery->limit);
$output['contentOrder'] = array_keys($output['content']);
return $output;
}
/**
* Returns an array contains all informations required to display a content
* in stream.
*
* @param Content $content the content
* @return array
*/
public static function getContentResultEntry(Content $content)
{
$result = [];
// Get Underlying Object (e.g. Post, Poll, ...)
$underlyingObject = $content->getPolymorphicRelation();
if ($underlyingObject === null) {
throw new Exception('Could not get contents underlying object!');
}
$underlyingObject->populateRelation('content', $content);
$result['output'] = Yii::$app->controller->renderAjax('@humhub/modules/content/views/layouts/wallEntry', [
'entry' => $content,
'user' => $underlyingObject->content->createdBy,
'object' => $underlyingObject,
'content' => $underlyingObject->getWallOut()
], true);
$result['sticked'] = (boolean) $content->sticked;
$result['archived'] = (boolean) $content->archived;
$result['guid'] = $content->guid;
$result['id'] = $content->id;
return $result;
}
/**
* Is inital stream requests (show first stream content)
*
* @return boolean Is initial request
*/
protected function isInitialRequest()
{
return ($this->from == '' && $this->limit != 1);
}
/**
* This method is called right before `run()` is executed.
* You may override this method to do preparation work for the action run.
* If the method returns false, it will cancel the action.
*
* @return boolean whether to run the action.
*/
protected function beforeRun()
{
$event = new ActionEvent($this);
$this->trigger(self::EVENT_BEFORE_RUN, $event);
return $event->isValid;
}
/**
* This method is called right after `run()` is executed.
* You may override this method to do post-processing work for the action run.
*/
protected function afterRun()
{
$event = new ActionEvent($this);
$this->trigger(self::EVENT_AFTER_RUN, $event);
}
}

View File

@ -238,8 +238,10 @@ humhub.module('stream', function(module, require, $) {
// Sinc the response does not only include the node itself we have to search it. // Sinc the response does not only include the node itself we have to search it.
that.$ = $newEntry.find(DATA_STREAM_ENTRY_SELECTOR) that.$ = $newEntry.find(DATA_STREAM_ENTRY_SELECTOR)
.addBack(DATA_STREAM_ENTRY_SELECTOR); .addBack(DATA_STREAM_ENTRY_SELECTOR);
$newEntry.fadeIn(resolve); $newEntry.fadeIn('fast', function() {
that.apply(); that.apply();
resolve();
});
}); });
}); });
@ -294,6 +296,8 @@ humhub.module('stream', function(module, require, $) {
if(response.success) { if(response.success) {
that.reload().then(function() { that.reload().then(function() {
module.log.success('success.unarchive', true); module.log.success('success.unarchive', true);
}).catch(function(err) {
module.log.error('error.default', true);
}); });
} }
}).catch(function(e) { }).catch(function(e) {
@ -417,31 +421,11 @@ humhub.module('stream', function(module, require, $) {
this.loading = false; this.loading = false;
this.$content.empty(); this.$content.empty();
this.$.hide(); this.$.hide();
//this.$.find(".s2_single").hide();
this.hideLoader(); this.hideLoader();
this.$filter.hide(); //this.$filter.hide();
this.$.trigger('humhub:modules:stream:clear', this); this.$.trigger('humhub:modules:stream:clear', this);
}; };
/**
* Loads a single stream entry by a given content id.
*
* @param {type} contentId
* @returns {undefined}
*/
Stream.prototype.loadEntry = function(contentId, cfg) {
cfg = cfg || {};
cfg['contentId'] = contentId;
var that = this;
return new Promise(function(resolve, reject) {
that.loadEntries(cfg).then(function($entryNode) {
resolve($entryNode);
}).catch(reject);
});
};
/** /**
* Reloads a given entry either by providing the contentId or a StreamEntry instance. * Reloads a given entry either by providing the contentId or a StreamEntry instance.
* This function returns a Promise instance. * This function returns a Promise instance.
@ -456,7 +440,8 @@ humhub.module('stream', function(module, require, $) {
if(!entry) { if(!entry) {
module.log.warn('Attempt to reload non existing entry'); module.log.warn('Attempt to reload non existing entry');
return reject(); reject();
return;
} }
var contentId = entry.getKey(); var contentId = entry.getKey();
@ -472,6 +457,18 @@ humhub.module('stream', function(module, require, $) {
}); });
}; };
/**
* Loads a single stream entry by a given content id.
*
* @param {type} contentId
* @returns {undefined}
*/
Stream.prototype.loadEntry = function(contentId, cfg) {
cfg = cfg || {};
cfg['contentId'] = contentId;
return this.loadEntries(cfg);
};
/** /**
* Loads new entries to a stream by the given stream settings. * Loads new entries to a stream by the given stream settings.
* *
@ -491,7 +488,7 @@ humhub.module('stream', function(module, require, $) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var $result; var $result;
// Don't proceed if stream is already loading // Don't proceed if stream is already loading
if(that.loading || that.lastEntryLoaded) { if(!cfg.contentId && (that.loading || that.lastEntryLoaded)) {
resolve(); resolve();
return; return;
} }
@ -583,17 +580,22 @@ humhub.module('stream', function(module, require, $) {
Stream.prototype.prependEntry = function(html) { Stream.prototype.prependEntry = function(html) {
var $html = $(html).hide(); var $html = $(html).hide();
this.$content.prepend($html); this.$content.prepend($html);
var that = this;
$html.fadeIn('fast', function() {
additions.applyTo($html); additions.applyTo($html);
$html.fadeIn(); that.onChange();
this.onChange(); });
}; };
Stream.prototype.appendEntry = function(html) { Stream.prototype.appendEntry = function(html) {
var $html = $(html).hide(); var $html = $(html).hide();
this.$content.append($html); this.$content.append($html);
var that = this;
$html.fadeIn('fast', function() {
additions.applyTo($html); additions.applyTo($html);
$html.fadeIn(); that.onChange();
this.onChange(); });
}; };
@ -614,7 +616,6 @@ humhub.module('stream', function(module, require, $) {
result += response.content[key].output; result += response.content[key].output;
}); });
var $result = $(result).hide(); var $result = $(result).hide();
if(cfg['preventInsert']) { if(cfg['preventInsert']) {
@ -778,7 +779,8 @@ humhub.module('stream', function(module, require, $) {
this.$.find('.emptyStreamMessage').show(); this.$.find('.emptyStreamMessage').show();
this.$filter.hide(); this.$filter.hide();
} else if(!hasEntries) { } else if(!hasEntries) {
this.$.find('.emptyFilterStreamMessage').hide(); this.$.find('.emptyFilterStreamMessage').show();
this.$filter.show();
} else if(!this.isShowSingleEntry()) { } else if(!this.isShowSingleEntry()) {
this.$filter.show(); this.$filter.show();
this.$.find('.emptyStreamMessage').hide(); this.$.find('.emptyStreamMessage').hide();

View File

@ -28,7 +28,7 @@ class StreamQuery extends \yii\base\Model
const FILTER_FILES = "entry_files"; const FILTER_FILES = "entry_files";
const FILTER_ARCHIVED = "entry_archived"; const FILTER_ARCHIVED = "entry_archived";
const FILTER_MINE = "entry_mine"; const FILTER_MINE = "entry_mine";
const FILTER_INVOLVED = "entry_userinvoled"; const FILTER_INVOLVED = "entry_userinvolved";
const FILTER_PRIVATE = "visibility_private"; const FILTER_PRIVATE = "visibility_private";
const FILTER_PUBLIC = "visibility_public"; const FILTER_PUBLIC = "visibility_public";
@ -113,7 +113,6 @@ class StreamQuery extends \yii\base\Model
* @var \yii\db\ActiveQuery * @var \yii\db\ActiveQuery
*/ */
protected $_query; protected $_query;
private $_built = false; private $_built = false;
public function rules() public function rules()
@ -238,6 +237,7 @@ class StreamQuery extends \yii\base\Model
if ($build) { if ($build) {
$this->setupQuery(); $this->setupQuery();
} }
return $this->_query; return $this->_query;
} }
@ -246,6 +246,7 @@ class StreamQuery extends \yii\base\Model
if (!$this->_built) { if (!$this->_built) {
$this->setupQuery(); $this->setupQuery();
} }
return $this->_query->all(); return $this->_query->all();
} }
@ -336,7 +337,8 @@ class StreamQuery extends \yii\base\Model
} }
} }
public function isFIlter($filter) { public function isFIlter($filter)
{
return in_array($filter, $this->filters); return in_array($filter, $this->filters);
} }
@ -356,7 +358,7 @@ class StreamQuery extends \yii\base\Model
$this->filterMine(); $this->filterMine();
} }
// Show only items where the current user is involed // Show only items where the current user is invovled
if ($this->isFilter(self::FILTER_INVOLVED)) { if ($this->isFilter(self::FILTER_INVOLVED)) {
$this->filterInvolved(); $this->filterInvolved();
} }

View File

@ -6,7 +6,6 @@ use stream\AcceptanceTester;
class StreamCest class StreamCest
{ {
public function testDeletePost(AcceptanceTester $I) public function testDeletePost(AcceptanceTester $I)
{ {
$I->amUser(); $I->amUser();
@ -14,8 +13,8 @@ class StreamCest
$I->wantToTest('the deletion of a stream entry'); $I->wantToTest('the deletion of a stream entry');
$I->amGoingTo('create a new post and delete it afterwards'); $I->amGoingTo('create a new post and delete it afterwards');
$I->click('#contentForm_message_contenteditable'); $I->click('#contentForm_message');
$I->fillField('#contentForm_message_contenteditable', 'This is my stream test post!'); $I->fillField('#contentForm_message', 'This is my stream test post!');
$I->click('#post_submit_button'); $I->click('#post_submit_button');
$newEntrySelector = '[data-content-key="12"]'; $newEntrySelector = '[data-content-key="12"]';
@ -29,8 +28,8 @@ class StreamCest
$I->click('Delete'); $I->click('Delete');
$I->waitForElementVisible('#globalModalConfirm', 5); $I->waitForElementVisible('#globalModalConfirm', 5);
$I->see('Do you really want to perform this action?'); $I->see('Confirm post deletion');
$I->click('Confirm'); $I->click('Delete', '#globalModalConfirm');
$I->seeSuccess('The content has been deleted'); $I->seeSuccess('The content has been deleted');
} }
@ -144,6 +143,20 @@ class StreamCest
$I->waitForElementVisible($newEntrySelector); $I->waitForElementVisible($newEntrySelector);
$I->see('This is my first stream test post', '.wall-entry'); $I->see('This is my first stream test post', '.wall-entry');
$I->amGoingTo('edit load the edit form');
$I->click('.preferences', $newEntrySelector);
$I->waitForText('Edit', 10);
$I->click('Edit', $newEntrySelector);
$I->waitForElementVisible($newEntrySelector . ' .content_edit', 20);
$I->amGoingTo('cancel my edit');
$I->click('.preferences', $newEntrySelector);
$I->waitForText('Cancel Edit', 10);
$I->click('Cancel Edit', $newEntrySelector);
$I->waitForElementNotVisible($newEntrySelector . ' .content_edit', 20);
$I->waitForElementVisible($newEntrySelector . ' .content', 20);
$I->see('This is my first stream test post!', $newEntrySelector);
$I->amGoingTo('edit my new post'); $I->amGoingTo('edit my new post');
$I->click('.preferences', $newEntrySelector); $I->click('.preferences', $newEntrySelector);
$I->waitForText('Edit', 10); $I->waitForText('Edit', 10);
@ -152,7 +165,6 @@ class StreamCest
$I->waitForElementVisible($newEntrySelector . ' .content_edit', 20); $I->waitForElementVisible($newEntrySelector . ' .content_edit', 20);
$I->fillField($newEntrySelector . ' [contenteditable]', 'This is my edited post!'); $I->fillField($newEntrySelector . ' [contenteditable]', 'This is my edited post!');
$I->click('Save', $newEntrySelector); $I->click('Save', $newEntrySelector);
;
$I->seeSuccess('Saved'); $I->seeSuccess('Saved');
$I->seeElement($newEntrySelector); $I->seeElement($newEntrySelector);
@ -182,14 +194,53 @@ class StreamCest
$I->click('Delete'); $I->click('Delete');
$I->waitForElementVisible('#globalModalConfirm', 5); $I->waitForElementVisible('#globalModalConfirm', 5);
$I->see('Do you really want to perform this action?'); $I->see('Confirm post deletion');
$I->click('Confirm'); $I->click('Delete', '#globalModalConfirm');
$I->seeSuccess('The content has been deleted'); $I->seeSuccess('The content has been deleted');
$I->see('This space is still empty!'); $I->see('This space is still empty!');
$I->dontSeeElement('#filter'); $I->dontSeeElement('#filter');
} }
public function testFilterInvolved(AcceptanceTester $I)
{
$I->amUser();
$I->amOnSpace2();
$I->waitForElementVisible('#filter');
$I->click('.stream-filter', '#filter');
$I->waitForElementVisible('#filter_entry_userinvolved');
$I->click('#filter_entry_userinvolved');
$I->waitForText('No matches with your selected filters!');
$I->createPost('Involved Post.');
$I->dontSee('No matches with your selected filters!');
$I->amGoingTo('Reset filter');
$I->click('.stream-filter', '#filter');
$I->waitForElementVisible('#filter_entry_userinvolved');
$I->click('#filter_entry_userinvolved');
$I->waitForElementVisible('[data-content-key="10"]');
$I->click('Comment', '[data-content-key="10"]');
$I->waitForElementVisible('#newCommentForm_humhubmodulespostmodelsPost_10');
$I->fillField('#newCommentForm_humhubmodulespostmodelsPost_10', 'My Comment');
$I->click('Send', '#comment_create_form_humhubmodulespostmodelsPost_10');
$I->waitForText('My Comment', 30, '#comment_humhubmodulespostmodelsPost_10 .comment');
$I->click('Like', '[data-content-key="11"]');
$I->click('.stream-filter', '#filter');
$I->waitForElementVisible('#filter_entry_userinvolved');
$I->click('#filter_entry_userinvolved');
$I->wait(1);
$I->waitForText('Involved Post.');
$I->seeElement('[data-content-key="10"]');
$I->seeElement('[data-content-key="11"]');
$I->seeElement('[data-content-key="12"]');
}
public function testSortStream(AcceptanceTester $I) public function testSortStream(AcceptanceTester $I)
{ {
$I->amUser(); $I->amUser();

View File

@ -89,7 +89,7 @@ class StreamViewer extends \yii\base\Widget
// Add default Filters // Add default Filters
if (count($this->filters) === 0) { if (count($this->filters) === 0) {
$this->filters['filter_entry_userinvoled'] = Yii::t('ContentModule.widgets_views_stream', 'Where I´m involved'); $this->filters['filter_entry_userinvolved'] = Yii::t('ContentModule.widgets_views_stream', 'Where I´m involved');
$this->filters['filter_entry_mine'] = Yii::t('ContentModule.widgets_views_stream', 'Created by me'); $this->filters['filter_entry_mine'] = Yii::t('ContentModule.widgets_views_stream', 'Created by me');
$this->filters['filter_entry_files'] = Yii::t('ContentModule.widgets_views_stream', 'Content with attached files'); $this->filters['filter_entry_files'] = Yii::t('ContentModule.widgets_views_stream', 'Content with attached files');
$this->filters['filter_posts_links'] = Yii::t('ContentModule.widgets_views_stream', 'Posts with links'); $this->filters['filter_posts_links'] = Yii::t('ContentModule.widgets_views_stream', 'Posts with links');
@ -135,7 +135,7 @@ class StreamViewer extends \yii\base\Widget
*/ */
public function run() public function run()
{ {
return $this->render('stream', ['streamUrl' => $this->getStreamUrl(), 'showFilters' => $this->showFilters, 'filters' => $this->filters]); return $this->render('stream', ['streamUrl' => $this->getStreamUrl(), 'showFilters' => $this->showFilters, 'filters' => $this->filters, '', 'contentContainer' => $this->contentContainer]);
} }
} }

View File

@ -1,7 +1,6 @@
<?php <?php
use yii\helpers\Url; use yii\helpers\Url;
use yii\web\View;
\humhub\modules\stream\assets\StreamAsset::register($this); \humhub\modules\stream\assets\StreamAsset::register($this);
@ -27,6 +26,9 @@ $this->registerJsVar('defaultStreamSort', ($defaultStreamSort != '') ? $defaultS
?> ?>
<!-- Stream filter section --> <!-- Stream filter section -->
<?php if($contentContainer && $contentContainer->isArchived()) : ?>
<span class="label label-warning pull-right" style="margin-top:10px;"><?php echo Yii::t('ContentModule.widgets_views_label', 'Archived'); ?></span>
<?php endif; ?>
<?php if ($this->context->showFilters) { ?> <?php if ($this->context->showFilters) { ?>
<ul class="nav nav-tabs wallFilterPanel" id="filter" style="display: none;"> <ul class="nav nav-tabs wallFilterPanel" id="filter" style="display: none;">
<li class=" dropdown"> <li class=" dropdown">

View File

@ -1,4 +1,4 @@
<?php //[STAMP] 890a71cd7f21af7a261c0d3eceef09fb <?php //[STAMP] 11d1dbef7e50b480be5fa82a7c393e3e
namespace user\_generated; namespace user\_generated;
// This class was automatically generated by build task // This class was automatically generated by build task

View File

@ -19,4 +19,5 @@ modules:
url: http://localhost:8080/ url: http://localhost:8080/
browser: phantomjs browser: phantomjs
window_size: 1024x768 window_size: 1024x768
restart: true
lang: en lang: en

View File

@ -2,7 +2,6 @@
namespace user\acceptance; namespace user\acceptance;
use user\AcceptanceTester; use user\AcceptanceTester;
use tests\codeception\_pages\DirectoryPage;
use tests\codeception\_pages\AccountSettingsPage; use tests\codeception\_pages\AccountSettingsPage;
class ProfileCest class ProfileCest
@ -16,7 +15,9 @@ class ProfileCest
$I->clickAccountDropDown(); $I->clickAccountDropDown();
$I->click('Account settings'); $I->click('Account settings');
$I->expectTo('see the profile edit form'); $I->expectTo('see the profile edit form');
$I->seeElement('#profile-tabs');
$I->waitForElementVisible('#profile-tabs', 20);
//$I->seeElement();
$I->amGoingTo('fill only my firstname'); $I->amGoingTo('fill only my firstname');
$I->fillField('#profile-firstname', 'MyFirstName'); $I->fillField('#profile-firstname', 'MyFirstName');
@ -29,7 +30,7 @@ class ProfileCest
$I->amGoingTo('fill all required fields plus birthday and hide year field'); $I->amGoingTo('fill all required fields plus birthday and hide year field');
$I->fillField('#profile-lastname', 'MyLastName'); $I->fillField('#profile-lastname', 'MyLastName');
$I->fillField('#profile-birthday', '4/16/87'); $I->fillField('#profile-birthday', '4/16/87');
$I->click('/html/body/div[3]/div/div[2]/div/div[3]/form/div/div[1]/div[11]/label/div'); // Hide year in profile $I->click('.field-profile-birthday_hide_year div'); // Hide year in profile
$I->scrollToTop(); $I->scrollToTop();
@ -61,10 +62,11 @@ class ProfileCest
$I->wantTo('ensure that my profile works as expected.'); $I->wantTo('ensure that my profile works as expected.');
$I->amUser2(); $I->amUser2();
DirectoryPage::openBy($I); $directory = $I->amOnDirectory();
$I->click('Members'); $directory->clickMembers();
$I->click('User1'); $I->click('User1');
$I->waitForText('Profile menu');
$I->expectTo('see the profile of User2'); $I->expectTo('see the profile of User2');
$I->see('Follow'); $I->see('Follow');
$I->see('Stream'); $I->see('Stream');
@ -75,15 +77,13 @@ class ProfileCest
$I->see('Peter'); $I->see('Peter');
$I->see('Tester'); $I->see('Tester');
AccountSettingsPage::openBy($I); $accountSettings = AccountSettingsPage::openBy($I);
$I->click('Security'); $accountSettings->clickSecurity();
#$I->wait(2);
$I->selectOption('select[data-attribute0*=ViewAboutPage]', 'Deny'); $I->selectOption('select[data-attribute0*=ViewAboutPage]', 'Deny');
$I->amUser1(true); $I->amUser1(true);
DirectoryPage::openBy($I); $directory = $I->amOnDirectory();
$I->click('Members'); $directory->clickMembers();
$I->click('User2'); $I->click('User2');
} }
} }

View File

@ -9,7 +9,9 @@ class UserFollowCest
{ {
$I->wantTo('test the user follow by directory link'); $I->wantTo('test the user follow by directory link');
$I->amUser1(); $I->amUser1();
//$I->wait(300);
$I->amOnProfile(); $I->amOnProfile();
$I->createPost('New User1 profile post'); $I->createPost('New User1 profile post');

View File

@ -1,19 +0,0 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2015 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\tests\codeception\fixtures;
use yii\test\ActiveFixture;
class ProfileFixture extends ActiveFixture
{
public $modelClass = 'humhub\modules\user\models\Profile';
public $dataFile = '@modules/user/tests/codeception/fixtures/data/profile.php';
}

View File

@ -16,8 +16,10 @@ class UserFixture extends ActiveFixture
public $modelClass = 'humhub\modules\user\models\User'; public $modelClass = 'humhub\modules\user\models\User';
public $dataFile = '@modules/user/tests/codeception/fixtures/data/user.php'; public $dataFile = '@modules/user/tests/codeception/fixtures/data/user.php';
public $depends = [ public $depends = [
'humhub\modules\user\tests\codeception\fixtures\UserProfileFixture',
'humhub\modules\content\tests\codeception\fixtures\ContentContainerFixture', 'humhub\modules\content\tests\codeception\fixtures\ContentContainerFixture',
'humhub\modules\user\tests\codeception\fixtures\UserPasswordFixture', 'humhub\modules\user\tests\codeception\fixtures\UserPasswordFixture',
'humhub\modules\user\tests\codeception\fixtures\UserFollowFixture',
'humhub\modules\user\tests\codeception\fixtures\GroupFixture' 'humhub\modules\user\tests\codeception\fixtures\GroupFixture'
]; ];

View File

@ -10,13 +10,10 @@ namespace humhub\modules\user\tests\codeception\fixtures;
use yii\test\ActiveFixture; use yii\test\ActiveFixture;
class UserFixture extends ActiveFixture class UserProfileFixture extends ActiveFixture
{ {
public $modelClass = 'humhub\modules\user\models\Profile'; public $modelClass = 'humhub\modules\user\models\Profile';
public $dataFile = '@modules/user/tests/codeception/fixtures/data/profile.php'; public $dataFile = '@modules/user/tests/codeception/fixtures/data/profile.php';
public $depends = [
'humhub\modules\user\tests\codeception\fixtures\UserProfileFixture'
];
} }

View File

@ -13,4 +13,10 @@ class AccountSettingsPage extends BasePage
public $route = 'user/account/edit'; public $route = 'user/account/edit';
public function clickSecurity()
{
$this->actor->click('Security');
$this->actor->waitForText('Security settings', 30);
}
} }

View File

@ -13,4 +13,10 @@ class DirectoryPage extends BasePage
public $route = 'directory/directory'; public $route = 'directory/directory';
public function clickMembers()
{
$this->actor->click('Member');
$this->actor->waitForText('Member directory', 30);
}
} }

View File

@ -16,6 +16,7 @@ class ProfilePage extends BasePage
public function clickAbout() public function clickAbout()
{ {
$this->actor->click('About'); $this->actor->click('About');
$this->actor->waitForText('About this user', 30);
} }
} }

View File

@ -64,8 +64,8 @@ class AcceptanceTester extends \Codeception\Actor
public function createPost($text) public function createPost($text)
{ {
$this->click('#contentForm_message_contenteditable'); $this->click('#contentForm_message');
$this->fillField('#contentForm_message_contenteditable', $text); $this->fillField('#contentForm_message', $text);
$this->click('#post_submit_button'); $this->click('#post_submit_button');
$this->waitForText($text, 30, '.wall-entry'); $this->waitForText($text, 30, '.wall-entry');
} }
@ -159,6 +159,11 @@ class AcceptanceTester extends \Codeception\Actor
$this->wait(2); $this->wait(2);
} }
public function amOnDirectory()
{
return tests\codeception\_pages\DirectoryPage::openBy($this);
}
public function amOnProfile() public function amOnProfile()
{ {
return tests\codeception\_pages\ProfilePage::openBy($this); return tests\codeception\_pages\ProfilePage::openBy($this);

View File

@ -106,7 +106,6 @@ class DynamicFixtureHelper extends Module
{ {
return [ return [
'user' => ['class' => \humhub\modules\user\tests\codeception\fixtures\UserFixture::className()], 'user' => ['class' => \humhub\modules\user\tests\codeception\fixtures\UserFixture::className()],
'profile' => ['class' => \humhub\modules\user\tests\codeception\fixtures\ProfileFixture::className()],
'group' => ['class' => \humhub\modules\user\tests\codeception\fixtures\GroupFixture::className()], 'group' => ['class' => \humhub\modules\user\tests\codeception\fixtures\GroupFixture::className()],
'group_permission' => ['class' => \humhub\modules\user\tests\codeception\fixtures\GroupPermissionFixture::className()], 'group_permission' => ['class' => \humhub\modules\user\tests\codeception\fixtures\GroupPermissionFixture::className()],
'settings' => ['class' => \humhub\tests\codeception\fixtures\SettingFixture::className()], 'settings' => ['class' => \humhub\tests\codeception\fixtures\SettingFixture::className()],

View File

@ -61,6 +61,13 @@ class CoreJsConfig extends Widget
'error.unknown' => Yii::t('base', 'No error information given.'), 'error.unknown' => Yii::t('base', 'No error information given.'),
] ]
], ],
'ui.richtext' => [
'emoji.url' => Yii::getAlias('@web/img/emoji/'),
'text' => [
'info.minInput' => Yii::t('base', 'Please type at least 3 characters'),
'info.loading' => Yii::t('base', 'Loading...'),
]
],
'log' => [ 'log' => [
'traceLevel' => (YII_DEBUG) ? 'DEBUG' : 'INFO', 'traceLevel' => (YII_DEBUG) ? 'DEBUG' : 'INFO',
'text' => [ 'text' => [
@ -86,6 +93,12 @@ class CoreJsConfig extends Widget
'showMore' => Yii::t('base', 'Show more'), 'showMore' => Yii::t('base', 'Show more'),
] ]
], ],
'post' => [
'text' => [
'default.readMore' => Yii::t('PostModule.widgets_views_post', 'Read full post...'),
'default.readLess' => Yii::t('PostModule.widgets_views_post', 'Collapse'),
]
],
'content' => [ 'content' => [
'modal' => [ 'modal' => [
'permalink' => [ 'permalink' => [

View File

@ -25,6 +25,7 @@ class GlobalConfirmModal extends \yii\base\Widget
{ {
return \humhub\widgets\Modal::widget([ return \humhub\widgets\Modal::widget([
'id' => 'globalModalConfirm', 'id' => 'globalModalConfirm',
'jsWidget' => 'ui.modal.ConfirmModal',
'size' => 'extra-small', 'size' => 'extra-small',
'centerText' => true, 'centerText' => true,
'backdrop' => false, 'backdrop' => false,

View File

@ -61,16 +61,12 @@ class JsWidget extends Widget
{ {
$attributes = $this->getAttributes(); $attributes = $this->getAttributes();
$attributes['data'] = $this->getData(); $attributes['data'] = $this->getData();
$attributes['id'] = $this->id; $attributes['id'] = $this->getId();
$this->setDefaultOptions(); $this->setDefaultOptions();
$result = \yii\helpers\ArrayHelper::merge($attributes, $this->options); $result = \yii\helpers\ArrayHelper::merge($attributes, $this->options);
if(empty($result['id'])) {
$result['id'] = $this->getId(true);
}
if(!$this->visible) { if(!$this->visible) {
if(isset($result['style'])) { if(isset($result['style'])) {
$result['style'] .= ';display:none;'; $result['style'] .= ';display:none;';
@ -86,7 +82,7 @@ class JsWidget extends Widget
{ {
// Set event data // Set event data
foreach($this->events as $event => $handler) { foreach($this->events as $event => $handler) {
$this->options['data']['action-'.$event] = $handler; $this->options['data']['widget-action-'.$event] = $handler;
} }
$this->options['data']['ui-widget'] = $this->jsWidget; $this->options['data']['ui-widget'] = $this->jsWidget;
@ -96,6 +92,14 @@ class JsWidget extends Widget
} }
} }
public function getId($autoGenerate = true)
{
if($this->id) {
return $this->id;
}
return parent::getId($autoGenerate);
}
protected function getData() protected function getData()
{ {
return []; return [];

View File

@ -15,13 +15,12 @@ use yii\base\Widget;
* *
* @author luke * @author luke
*/ */
class Modal extends Widget class Modal extends JsWidget
{ {
/** /*
* Unique modal container id * @inheritdoc
* @var type
*/ */
public $id; public $jsWidget = 'ui.modal.Modal';
/** /**
* Header text * Header text
@ -104,22 +103,45 @@ class Modal extends Widget
public function run() public function run()
{ {
$modalData = '';
$modalData .= !$this->backdrop ? 'data-backdrop="static"' : '';
$modalData .= !$this->keyboard ? 'data-keyboard="false"' : '';
$modalData .= $this->show ? 'data-show="true"' : '';
return $this->render('modal', [ return $this->render('modal', [
'id' => $this->id, 'id' => $this->id,
'options' => $this->getOptions(),
'header' => $this->header, 'header' => $this->header,
'body' => $this->body, 'body' => $this->body,
'footer' => $this->footer, 'footer' => $this->footer,
'animation' => $this->animation, 'animation' => $this->animation,
'size' => $this->size, 'size' => $this->size,
'modalData' => $modalData,
'centerText' => $this->centerText, 'centerText' => $this->centerText,
'initialLoader' => $this->initialLoader 'initialLoader' => $this->initialLoader
]); ]);
} }
public function getAttributes()
{
return [
'class' => "modal",
'tabindex' => "-1",
'role' => "dialog",
'aria-hidden' => "true"
];
}
public function getData()
{
$result = [];
if(!$this->backdrop) {
$result['backdrop'] = 'static';
}
if(!$this->keyboard) {
$result['keyboard'] = 'false';
}
if($this->show) {
$result['show'] = 'true';
}
return $result;
}
} }

View File

@ -18,7 +18,7 @@ use humhub\libs\ParameterEvent;
* *
* @author luke * @author luke
*/ */
class RichText extends \humhub\components\Widget class RichText extends JsWidget
{ {
/** /**
@ -166,7 +166,7 @@ REGEXP;
$user = \humhub\modules\user\models\User::findOne(['guid' => $hit[2]]); $user = \humhub\modules\user\models\User::findOne(['guid' => $hit[2]]);
if ($user !== null) { if ($user !== null) {
if ($buildAnchors) { if ($buildAnchors) {
return ' <span contenteditable="false"><a href="' . $user->getUrl() . '" target="_self" class="atwho-user" data-user-guid="@-u' . $user->guid . '">@' . Html::encode($user->getDisplayName()) . '&#x200b;</a></span>' . $hit[3]; return ' <span contenteditable="false"><a href="' . $user->getUrl() . '" target="_self" class="atwho-user" data-guid="@-u' . $user->guid . '">@' . Html::encode($user->getDisplayName()) . '&#x200b;</a></span>' . $hit[3];
} }
return " @" . Html::encode($user->getDisplayName()) . $hit[3]; return " @" . Html::encode($user->getDisplayName()) . $hit[3];
} }
@ -175,7 +175,7 @@ REGEXP;
if ($space !== null) { if ($space !== null) {
if ($buildAnchors) { if ($buildAnchors) {
return ' <span contenteditable="false"><a href="' . $space->getUrl() . '" target="_self" class="atwho-user" data-user-guid="@-s' . $space->guid . '">@' . Html::encode($space->name) . '&#x200b;</a></span>' . $hit[3]; return ' <span contenteditable="false"><a href="' . $space->getUrl() . '" target="_self" class="atwho-user" data-guid="@-s' . $space->guid . '">@' . Html::encode($space->name) . '&#x200b;</a></span>' . $hit[3];
} }
return " @" . Html::encode($space->name) . $hit[3]; return " @" . Html::encode($space->name) . $hit[3];
} }

View File

@ -0,0 +1,191 @@
<?php
namespace humhub\widgets;
use Yii;
use yii\helpers\Html;
use \yii\helpers\Url;
use humhub\widgets\RichText;
/**
* @package humhub.modules_core.user.widgets
* @since 1.2
* @author buddha
*/
class RichtextField extends JsWidget
{
/**
* Defines the javascript picker implementation.
*
* @var string
*/
public $jsWidget = 'ui.richtext.Richtext';
/**
* Minimum character input before triggering search query.
*
* @var integer
*/
public $minInput = 3;
/**
* If the ActiveForm is set, it will be used to create the picker field,
* otherwise it's created by Html::activeDropDownList
*
* @var \yii\widgets\ActiveForm
*/
public $form;
/**
* Model instance. Requires the setting of an model $attribute.
*
* @var \yii\db\ActiveRecord
*/
public $model;
/**
* Model attribute which holds the picker value. The referenced model attribute has to be an
* array.
*
* @var string
*/
public $attribute;
/**
* Input form name.
* This can be provided if no form and model is provided for custom input field setting.
*
* @var type
*/
public $name;
/**
* Can be used to overwrite the default placeholder.
*
* @var string
*/
public $placeholder;
/**
* The url used for the default @ metioning.
* If there is no $searchUrl is given, the $searchRoute will be used instead.
*
* @var string
*/
public $mentioningUrl;
/**
* Route used for the default @ mentioning. This will only be used if
* not $searchUrl is given.
*
* @var string
*/
protected $mentioningRoute = "/search/search/mentioning";
/**
* Richtext features supported for within this feature.
* By default all features will be included.
*
* @var array
*/
public $includes = [];
/**
* Richtext features not supported in this richtext feature.
*
* @var array
*/
public $excludes = [];
/**
* Can be used to set the value in case no $model and $attribute is provided.
* $model and $attribute is provided.
*
* @var string
*/
public $value;
/**
* If set to true the picker will be focused automatically.
*
* @var boolean
*/
public $focus = false;
/**
* Disables the input field.
* @var boolean
*/
public $disabled = false;
/**
* Will be used as userfeedback, why this richtext is disabled.
*
* @var string
*/
public $disabledText = false;
/**
* @inheritdoc
* @var boolean
*/
public $init = true;
public $visible = true;
/**
* @inhertidoc
*/
public function run()
{
$inputOptions = $this->getAttributes();
$inputOptions['id'] = $this->getId(true).'_input';
$inputOptions['style'] = 'display:none;color';
unset($inputOptions['contenteditable']);
$modelAttribute = $this->attribute;
if ($this->form != null) {
$input = $this->form->field($this->model, $this->attribute)->textarea($inputOptions);
$richText = Html::tag('div', RichText::widget(['text' => $this->model->$modelAttribute, 'edit' => true]), $this->getOptions());
} else if ($this->model != null) {
$input = Html::activeTextarea($this->model, $this->attribute, $inputOptions);
$richText = Html::tag('div', RichText::widget(['text' => $this->model->$modelAttribute, 'edit' => true]), $this->getOptions());
} else {
$input = Html::textarea(((!$this->name) ? 'richtext' : $this->name), $this->value, $inputOptions);
$richText = Html::tag('div', RichText::widget(['text' => $this->value, 'edit' => true]),$this->getOptions());
}
return $input.$richText;
}
public function getData()
{
$result = [
'excludes' => $this->excludes,
'includes' => $this->includes,
'mentioning-url' => $this->getMentioningUrl(),
'placeholder' => $this->placeholder,
];
if($this->disabled) {
$result['disabled'] = true;
$result['disabled-text'] = $this->disabledText;
}
return $result;
}
public function getAttributes()
{
return [
'class' => "atwho-input form-control humhub-ui-richtext",
'contenteditable' => "true",
];
}
public function getMentioningUrl()
{
return ($this->mentioningUrl) ? $this->mentioningUrl : Url::to([$this->mentioningRoute]);
}
}

View File

@ -1,4 +1,5 @@
<div id="<?= $id ?>" <?= $modalData ?> class="modal" tabindex="-1" role="dialog" aria-hidden="true"> <?= yii\helpers\Html::beginTag('div', $options) ?>
<?= \humhub\widgets\ModalDialog::widget([ <?= \humhub\widgets\ModalDialog::widget([
'header' => $header, 'header' => $header,
'animation' => $animation, 'animation' => $animation,
@ -8,4 +9,5 @@
'footer' => $footer, 'footer' => $footer,
'initialLoader' => $initialLoader 'initialLoader' => $initialLoader
]); ?> ]); ?>
</div>
<?= yii\helpers\Html::endTag('div') ?>

View File

@ -39,11 +39,11 @@ use yii\helpers\Url;
$editableContent.atwho({ $editableContent.atwho({
at: "@", at: "@",
data: [{image: '', 'cssClass': 'hint', name: "<?= Yii::t('base', 'Please type at least 3 characters') ?>"}], data: [{image: '', 'cssClass': 'hint', name: "<?= Yii::t('base', 'Please type at least 3 characters') ?>"}],
insert_tpl: "<a href='${link}' target='_blank' class='atwho-user' data-user-guid='${atwho-at}-${type}${guid}'>${atwho-data-value}&#x200b;</a>", insertTpl: "<a href='${link}' target='_blank' class='atwho-user' data-user-guid='${atwho-at}-${type}${guid}'>${name}&#x200b;</a>",
tpl: "<li class='hint' data-value=''>${name}</li>", displayTpl: "<li class='hint' data-value=''>${name}</li>",
search_key: "name", search_key: "name",
limit: 10, limit: 10,
highlight_first: false, highlightFirst: false,
callbacks: { callbacks: {
matcher: function (flag, subtext, should_start_with_space) { matcher: function (flag, subtext, should_start_with_space) {
var match, regexp; var match, regexp;
@ -58,18 +58,18 @@ use yii\helpers\Url;
return null; return null;
}, },
remote_filter: function (query, callback) { remoteFilter: function (query, callback) {
this.setting.highlight_first = false; this.setting.highlight_first = false;
// Render loading user feedback. // Render loading user feedback.
this.setting.tpl = "<li class='hint' data-value=''>${name}</li>"; this.setting.displayTpl = "<li class='hint' data-value=''>${name}</li>";
this.view.render([{"type":"test","cssClass": "hint", "name":"<?= Yii::t('base', 'Loading...') ?>","image":"","link":""}]); this.view.render([{"type":"test","cssClass": "hint", "name":"<?= Yii::t('base', 'Loading...') ?>","image":"","link":""}]);
// check the char length and data-query attribute for changing plugin settings for showing results // check the char length and data-query attribute for changing plugin settings for showing results
if (query.length >= 3 && $('#<?= $id; ?>_contenteditable').attr('data-query') == '1') { if (query.length >= 3 && $('#<?= $id; ?>_contenteditable').attr('data-query') == '1') {
// set plugin settings for showing results // set plugin settings for showing results
this.setting.highlight_first = true; this.setting.highlightFirst = true;
this.setting.tpl = '<li class="${cssClass}" data-value="@${name}">${image} ${name}</li>'; this.setting.displayTpl = '<li class="${cssClass}" data-value="@${name}">${image} ${name}</li>';
$.getJSON("<?php echo Url::to([$userSearchUrl]); ?>", {keyword: query}, function (data) { $.getJSON("<?php echo Url::to([$userSearchUrl]); ?>", {keyword: query}, function (data) {
callback(data); callback(data);
}); });
@ -81,10 +81,10 @@ use yii\helpers\Url;
} }
}).atwho({ }).atwho({
at: ":", at: ":",
insert_tpl: "<img data-emoji-name=';${name};' class='atwho-emoji' with='18' height='18' src='<?php echo Yii::getAlias('@web/img/emoji/${name}.svg'); ?>' />", insertTpl: "<img data-emoji-name=';${name};' class='atwho-emoji' with='18' height='18' src='<?php echo Yii::getAlias('@web/img/emoji/${name}.svg'); ?>' />",
tpl: "<li class='atwho-emoji-entry' data-value=';${name};'><img with='18' height='18' src='<?php echo Yii::getAlias('@web/img/emoji/${name}.svg'); ?>'/></li>", displayTpl: "<li class='atwho-emoji-entry' data-value=';${name};'><img with='18' height='18' src='<?php echo Yii::getAlias('@web/img/emoji/${name}.svg'); ?>'/></li>",
data: emojis_list, data: emojis_list,
highlight_first: true, highlightFirst: true,
limit: 100 limit: 100
}); });
@ -218,12 +218,14 @@ use yii\helpers\Url;
// remove all line breaks // remove all line breaks
html = html.replace(/(?:\r\n|\r|\n)/g, ""); html = html.replace(/(?:\r\n|\r|\n)/g, "");
// At.js adds a zwj at the end of each mentioning
html = html.replace(/\u200d/g,'');
// replace all <br> with new line break // replace all <br> with new line break
element.html(html.replace(/\<br\s*\>/g, '\n')); element.html(html.replace(/\<br\s*\>/g, '\n'));
// return plain text without html tags // return plain text without html tags
return element.text().trim(); return element.text().trim();
} }
/** /**

File diff suppressed because one or more lines are too long