diff --git a/css/autocomplete.scss b/css/autocomplete.scss
new file mode 100644
index 000000000..0837b3878
--- /dev/null
+++ b/css/autocomplete.scss
@@ -0,0 +1,77 @@
+/**
+ * based upon apps/comments/js/vendor/At.js/dist/css/jquery.atwho.css,
+ * only changed colors and font-weight
+ */
+
+.atwho-view {
+ position:absolute;
+ top: 0;
+ left: 0;
+ display: none;
+ margin-top: 18px;
+ background: var(--color-main-background);
+ color: var(--color-main-text);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ box-shadow: 0 0 5px var(--color-box-shadow);
+ min-width: 120px;
+ z-index: 11110 !important;
+}
+
+.atwho-view .atwho-header {
+ padding: 5px;
+ margin: 5px;
+ cursor: pointer;
+ border-bottom: solid 1px var(--color-border);
+ color: var(--color-main-text);
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.atwho-view .atwho-header .small {
+ color: var(--color-main-text);
+ float: right;
+ padding-top: 2px;
+ margin-right: -5px;
+ font-size: 12px;
+ font-weight: normal;
+}
+
+.atwho-view .atwho-header:hover {
+ cursor: default;
+}
+
+.atwho-view .cur {
+ background: var(--color-primary);
+ color: var(--color-primary-text);
+}
+.atwho-view .cur small {
+ color: var(--color-primary-text);
+}
+.atwho-view strong {
+ color: var(--color-main-text);
+ font-weight: normal;
+}
+.atwho-view .cur strong {
+ color: var(--color-primary-text);
+ font-weight: normal;
+}
+.atwho-view ul {
+ /* width: 100px; */
+ list-style:none;
+ padding:0;
+ margin:auto;
+ max-height: 200px;
+ overflow-y: auto;
+}
+.atwho-view ul li {
+ display: block;
+ padding: 5px 10px;
+ border-bottom: 1px solid var(--color-border);
+ cursor: pointer;
+}
+.atwho-view small {
+ font-size: smaller;
+ color: var(--color-main-text);
+ font-weight: normal;
+}
diff --git a/css/style.scss b/css/style.scss
index 104375287..57887e77f 100644
--- a/css/style.scss
+++ b/css/style.scss
@@ -41,6 +41,7 @@ $compact-board-last-item-margin: 5px 10px 10px;
@import 'icons';
@import 'animations';
@import 'compact-mode';
+@import 'autocomplete';
/**
* General styles
diff --git a/js/app/App.js b/js/app/App.js
index f47a34fc3..332a318ec 100644
--- a/js/app/App.js
+++ b/js/app/App.js
@@ -49,6 +49,8 @@ import md from 'angular-markdown-it';
import nganimate from 'angular-animate';
import 'angular-file-upload';
import ngInfiniteScroll from 'ng-infinite-scroll';
+import '../legacy/jquery.atwho.min';
+import '../legacy/jquery.caret.min';
var app = angular.module('Deck', [
ngsanitize,
diff --git a/js/controller/ActivityController.js b/js/controller/ActivityController.js
index 43c2a4855..f964b5300 100644
--- a/js/controller/ActivityController.js
+++ b/js/controller/ActivityController.js
@@ -49,31 +49,146 @@ class ActivityController {
}
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
}, true);
+
+
+ let $target = $('.newCommentForm .message');
+ if (!$target) {
+ return;
+ }
+ $target.atwho({
+ at: "@",
+ data:[{id: 'johndoe', label: 'John Doe'}],
+ callbacks: {
+ highlighter: function (li) {
+ // misuse the highlighter callback to instead of
+ // highlighting loads the avatars.
+ var $li = $(li);
+ $li.find('.avatar').avatar(undefined, 32);
+ return $li;
+ },
+ sorter: function (q, items) { return items; }
+ },
+ displayTpl: function (item) {
+ return '
' +
+ '' +
+ '' +
+ '' +
+ '' + escapeHTML(item.label) + '' +
+ '';
+ },
+ insertTpl: function (item) {
+ return '' +
+ '' +
+ '' +
+ '' +
+ '' + escapeHTML(item.label) + '' +
+ '';
+ },
+ searchKey: "label"
+ });
+ $target.on('inserted.atwho', function (je, $el) {
+ $(je.target).find(
+ 'span[data-username="' + $el.find('[data-username]').data('username') + '"]'
+ ).avatar();
+ });
}
+ commentBodyToPlain(content) {
+ let $comment = $('').html(content);
+ $comment.find('.avatar-name-wrapper').each(function () {
+ var $this = $(this);
+ var $inserted = $this.parent();
+ $inserted.html('@' + $this.find('.avatar').data('username'));
+ });
+ $comment.html(OCP.Comments.richToPlain($comment.html()));
+ $comment.html($comment.html().replace(/
/gi, "\n"));
+ return $comment.text();
+ }
+
+ static _composeHTMLMention(uid, displayName) {
+ var avatar = '' +
+ '' +
+ '';
+
+ var isCurrentUser = (uid === OC.getCurrentUser().uid);
+
+ return '' +
+ '' +
+ '' +
+ avatar +
+ '' + _.escape(displayName) + '' +
+ '' +
+ '';
+ }
+
+ formatMessage(activity) {
+ let message = activity.message;
+ let mentions = activity.commentModel.get('mentions');
+ const editMode = false;
+ message = escapeHTML(message).replace(/\n/g, '
');
+
+ for(var i in mentions) {
+ if(!mentions.hasOwnProperty(i)) {
+ return;
+ }
+ var mention = '@' + mentions[i].mentionId;
+ // escape possible regex characters in the name
+ mention = mention.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+ const displayName = ActivityController._composeHTMLMention(mentions[i].mentionId, mentions[i].mentionDisplayName);
+ // replace every mention either at the start of the input or after a whitespace
+ // followed by a non-word character.
+ message = message.replace(new RegExp("(^|\\s)(" + mention + ")\\b", 'g'),
+ function(match, p1) {
+ // to get number of whitespaces (0 vs 1) right
+ return p1+displayName;
+ }
+ );
+
+ }
+ if(editMode !== true) {
+ message = OCP.Comments.plainToRich(message);
+ }
+ return message;
+ }
postComment() {
const self = this;
this.status.commentCreateLoading = true;
+
+ let content = this.commentBodyToPlain(self.$scope.newComment);
+ if (content.length < 1) {
+ self.status.commentCreateLoading = false;
+ OC.Notification.showTemporary(t('deck', 'Please provide a content for your comment.'));
+ return;
+ }
var model = this.activityservice.commentCollection.create({
actorId: OC.getCurrentUser().uid,
actorDisplayName: OC.getCurrentUser().displayName,
actorType: 'users',
verb: 'comment',
- message: self.$scope.newComment,
+ message: content,
creationDateTime: (new Date()).toUTCString()
}, {
at: 0,
// wait for real creation before adding
wait: true,
success: function() {
- console.log("SUCCESS");
self.$scope.newComment = '';
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
self.status.commentCreateLoading = false;
},
error: function() {
-
+ self.status.commentCreateLoading = false;
+ OC.Notification.showTemporary(t('deck', 'Posting the comment failed.'));
}
});
}
diff --git a/js/legacy/commentcollection.js b/js/legacy/commentcollection.js
index e4faf4e5b..6c7427abf 100644
--- a/js/legacy/commentcollection.js
+++ b/js/legacy/commentcollection.js
@@ -45,7 +45,7 @@ var CommentCollection = OC.Backbone.Collection.extend(
*
* @type int
*/
- _limit : 2,
+ _limit : 5,
/**
* Initializes the collection
diff --git a/js/legacy/commentmodel.js b/js/legacy/commentmodel.js
index 3ecc86249..bf3245ab7 100644
--- a/js/legacy/commentmodel.js
+++ b/js/legacy/commentmodel.js
@@ -1,7 +1,16 @@
+/*
+ * Copyright (c) 2016
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
var NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * @class OCA.AnnouncementCenter.Comments.CommentModel
+ * @class CommentModel
* @classdesc
*
* Comment
@@ -49,7 +58,8 @@ var CommentModel = OC.Backbone.Model.extend(
'creationDateTime': '{' + NS_OWNCLOUD + '}creationDateTime',
'objectType': '{' + NS_OWNCLOUD + '}objectType',
'objectId': '{' + NS_OWNCLOUD + '}objectId',
- 'isUnread': '{' + NS_OWNCLOUD + '}isUnread'
+ 'isUnread': '{' + NS_OWNCLOUD + '}isUnread',
+ 'mentions': '{' + NS_OWNCLOUD + '}mentions'
},
parse: function(data) {
@@ -62,10 +72,32 @@ var CommentModel = OC.Backbone.Model.extend(
creationDateTime: data.creationDateTime,
objectType: data.objectType,
objectId: data.objectId,
- isUnread: (data.isUnread === 'true')
+ isUnread: (data.isUnread === 'true'),
+ mentions: this._parseMentions(data.mentions)
};
},
+ _parseMentions: function(mentions) {
+ if(_.isUndefined(mentions)) {
+ return {};
+ }
+ var result = {};
+ for(var i in mentions) {
+ var mention = mentions[i];
+ if(_.isUndefined(mention.localName) || mention.localName !== 'mention') {
+ continue;
+ }
+ result[i] = {};
+ for (var child = mention.firstChild; child; child = child.nextSibling) {
+ if(_.isUndefined(child.localName) || !child.localName.startsWith('mention')) {
+ continue;
+ }
+ result[i][child.localName] = child.textContent;
+ }
+ }
+ return result;
+ },
+
url: function() {
if (typeof this.get('id') !== 'undefined') {
return this.collection.url() + this.get('id');
@@ -73,7 +105,6 @@ var CommentModel = OC.Backbone.Model.extend(
return this.collection.url();
}
}
-
});
export default CommentModel;
diff --git a/js/legacy/jquery.atwho.min.js b/js/legacy/jquery.atwho.min.js
new file mode 100644
index 000000000..d1e60152b
--- /dev/null
+++ b/js/legacy/jquery.atwho.min.js
@@ -0,0 +1 @@
+!function(t,e){"function"==typeof define&&define.amd?define(["jquery"],function(t){return e(t)}):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(this,function(t){var e,i;i={ESC:27,TAB:9,ENTER:13,CTRL:17,A:65,P:80,N:78,LEFT:37,UP:38,RIGHT:39,DOWN:40,BACKSPACE:8,SPACE:32},e={beforeSave:function(t){return r.arrayToDefaultHash(t)},matcher:function(t,e,i,n){var r,o,s,a,h;return t=t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),i&&(t="(?:^|\\s)"+t),r=decodeURI("%C3%80"),o=decodeURI("%C3%BF"),h=n?" ":"",a=new RegExp(t+"([A-Za-z"+r+"-"+o+"0-9_"+h+"'.+-]*)$|"+t+"([^\\x00-\\xff]*)$","gi"),s=a.exec(e),s?s[2]||s[1]:null},filter:function(t,e,i){var n,r,o,s;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],~new String(o[i]).toLowerCase().indexOf(t.toLowerCase())&&n.push(o);return n},remoteFilter:null,sorter:function(t,e,i){var n,r,o,s;if(!t)return e;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],o.atwho_order=new String(o[i]).toLowerCase().indexOf(t.toLowerCase()),o.atwho_order>-1&&n.push(o);return n.sort(function(t,e){return t.atwho_order-e.atwho_order})},tplEval:function(t,e){var i,n,r;r=t;try{return"string"!=typeof t&&(r=t(e)),r.replace(/\$\{([^\}]*)\}/g,function(t,i,n){return e[i]})}catch(n){return i=n,""}},highlighter:function(t,e){var i;return e?(i=new RegExp(">\\s*([^<]*?)("+e.replace("+","\\+")+")([^<]*)\\s*<","ig"),t.replace(i,function(t,e,i,n){return"> "+e+""+i+""+n+" <"})):t},beforeInsert:function(t,e,i){return t},beforeReposition:function(t){return t},afterMatchFailed:function(t,e){}};var n;n=function(){function e(e){this.currentFlag=null,this.controllers={},this.aliasMaps={},this.$inputor=t(e),this.setupRootElement(),this.listen()}return e.prototype.createContainer=function(e){var i;return null!=(i=this.$el)&&i.remove(),t(e.body).append(this.$el=t(""))},e.prototype.setupRootElement=function(e,i){var n,r;if(null==i&&(i=!1),e)this.window=e.contentWindow,this.document=e.contentDocument||this.window.document,this.iframe=e;else{this.document=this.$inputor[0].ownerDocument,this.window=this.document.defaultView||this.document.parentWindow;try{this.iframe=this.window.frameElement}catch(r){if(n=r,this.iframe=null,t.fn.atwho.debug)throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n"+n)}}return this.createContainer((this.iframeAsRoot=i)?this.document:document)},e.prototype.controller=function(t){var e,i,n,r;if(this.aliasMaps[t])i=this.controllers[this.aliasMaps[t]];else{r=this.controllers;for(n in r)if(e=r[n],n===t){i=e;break}}return i?i:this.controllers[this.currentFlag]},e.prototype.setContextFor=function(t){return this.currentFlag=t,this},e.prototype.reg=function(t,e){var i,n;return n=(i=this.controllers)[t]||(i[t]=this.$inputor.is("[contentEditable]")?new l(this,t):new s(this,t)),e.alias&&(this.aliasMaps[e.alias]=t),n.init(e),this},e.prototype.listen=function(){return this.$inputor.on("compositionstart",function(t){return function(e){var i;return null!=(i=t.controller())&&i.view.hide(),t.isComposing=!0,null}}(this)).on("compositionend",function(t){return function(e){return t.isComposing=!1,setTimeout(function(e){return t.dispatch(e)}),null}}(this)).on("keyup.atwhoInner",function(t){return function(e){return t.onKeyup(e)}}(this)).on("keydown.atwhoInner",function(t){return function(e){return t.onKeydown(e)}}(this)).on("blur.atwhoInner",function(t){return function(e){var i;return(i=t.controller())?(i.expectedQueryCBId=null,i.view.hide(e,i.getOpt("displayTimeout"))):void 0}}(this)).on("click.atwhoInner",function(t){return function(e){return t.dispatch(e)}}(this)).on("scroll.atwhoInner",function(t){return function(){var e;return e=t.$inputor.scrollTop(),function(i){var n,r;return n=i.target.scrollTop,e!==n&&null!=(r=t.controller())&&r.view.hide(i),e=n,!0}}}(this)())},e.prototype.shutdown=function(){var t,e,i;i=this.controllers;for(t in i)e=i[t],e.destroy(),delete this.controllers[t];return this.$inputor.off(".atwhoInner"),this.$el.remove()},e.prototype.dispatch=function(t){var e,i,n,r;n=this.controllers,r=[];for(e in n)i=n[e],r.push(i.lookUp(t));return r},e.prototype.onKeyup=function(e){var n;switch(e.keyCode){case i.ESC:e.preventDefault(),null!=(n=this.controller())&&n.view.hide();break;case i.DOWN:case i.UP:case i.CTRL:case i.ENTER:t.noop();break;case i.P:case i.N:e.ctrlKey||this.dispatch(e);break;default:this.dispatch(e)}},e.prototype.onKeydown=function(e){var n,r;if(r=null!=(n=this.controller())?n.view:void 0,r&&r.visible())switch(e.keyCode){case i.ESC:e.preventDefault(),r.hide(e);break;case i.UP:e.preventDefault(),r.prev();break;case i.DOWN:e.preventDefault(),r.next();break;case i.P:if(!e.ctrlKey)return;e.preventDefault(),r.prev();break;case i.N:if(!e.ctrlKey)return;e.preventDefault(),r.next();break;case i.TAB:case i.ENTER:case i.SPACE:if(!r.visible())return;if(!this.controller().getOpt("spaceSelectsMatch")&&e.keyCode===i.SPACE)return;if(!this.controller().getOpt("tabSelectsMatch")&&e.keyCode===i.TAB)return;r.highlighted()?(e.preventDefault(),r.choose(e)):r.hide(e);break;default:t.noop()}},e}();var r,o=[].slice;r=function(){function i(e,i){this.app=e,this.at=i,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.expectedQueryCBId=null,this.setting=null,this.query=null,this.pos=0,this.range=null,0===(this.$el=t("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=t("")),this.model=new u(this),this.view=new c(this)}return i.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},i.prototype.init=function(e){return this.setting=t.extend({},this.setting||t.fn.atwho["default"],e),this.view.init(),this.model.reload(this.setting.data)},i.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},i.prototype.callDefault=function(){var i,n,r,s;s=arguments[0],i=2<=arguments.length?o.call(arguments,1):[];try{return e[s].apply(this,i)}catch(r){return n=r,t.error(n+" Or maybe At.js doesn't have function "+s)}},i.prototype.trigger=function(t,e){var i,n;return null==e&&(e=[]),e.push(this),i=this.getOpt("alias"),n=i?t+"-"+i+".atwho":t+".atwho",this.$inputor.trigger(n,e)},i.prototype.callbacks=function(t){return this.getOpt("callbacks")[t]||e[t]},i.prototype.getOpt=function(t,e){var i,n;try{return this.setting[t]}catch(n){return i=n,null}},i.prototype.insertContentFor=function(e){var i,n;return n=this.getOpt("insertTpl"),i=t.extend({},e.data("item-data"),{"atwho-at":this.at}),this.callbacks("tplEval").call(this,n,i,"onInsert")},i.prototype.renderView=function(t){var e;return e=this.getOpt("searchKey"),t=this.callbacks("sorter").call(this,this.query.text,t.slice(0,1001),e),this.view.render(t.slice(0,this.getOpt("limit")))},i.arrayToDefaultHash=function(e){var i,n,r,o;if(!t.isArray(e))return e;for(o=[],i=0,r=e.length;r>i;i++)n=e[i],t.isPlainObject(n)?o.push(n):o.push({name:n});return o},i.prototype.lookUp=function(t){var e,i;if((!t||"click"!==t.type||this.getOpt("lookUpOnClick"))&&(!this.getOpt("suspendOnComposing")||!this.app.isComposing))return(e=this.catchQuery(t))?(this.app.setContextFor(this.at),(i=this.getOpt("delay"))?this._delayLookUp(e,i):this._lookUp(e),e):(this.expectedQueryCBId=null,e)},i.prototype._delayLookUp=function(t,e){var i,n;return i=Date.now?Date.now():(new Date).getTime(),this.previousCallTime||(this.previousCallTime=i),n=e-(i-this.previousCallTime),n>0&&e>n?(this.previousCallTime=i,this._stopDelayedCall(),this.delayedCallTimeout=setTimeout(function(e){return function(){return e.previousCallTime=0,e.delayedCallTimeout=null,e._lookUp(t)}}(this),e)):(this._stopDelayedCall(),this.previousCallTime!==i&&(this.previousCallTime=0),this._lookUp(t))},i.prototype._stopDelayedCall=function(){return this.delayedCallTimeout?(clearTimeout(this.delayedCallTimeout),this.delayedCallTimeout=null):void 0},i.prototype._generateQueryCBId=function(){return{}},i.prototype._lookUp=function(e){var i;return i=function(t,e){return t===this.expectedQueryCBId?e&&e.length>0?this.renderView(this.constructor.arrayToDefaultHash(e)):this.view.hide():void 0},this.expectedQueryCBId=this._generateQueryCBId(),this.model.query(e.text,t.proxy(i,this,this.expectedQueryCBId))},i}();var s,a=function(t,e){function i(){this.constructor=t}for(var n in e)h.call(e,n)&&(t[n]=e[n]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},h={}.hasOwnProperty;s=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return a(i,e),i.prototype.catchQuery=function(){var t,e,i,n,r,o,s;return e=this.$inputor.val(),t=this.$inputor.caret("pos",{iframe:this.app.iframe}),s=e.slice(0,t),r=this.callbacks("matcher").call(this,this.at,s,this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),n="string"==typeof r,n&&r.length0?t.getRangeAt(0):void 0},n.prototype._setRange=function(e,i,n){return null==n&&(n=this._getRange()),n&&i?(i=t(i)[0],"after"===e?(n.setEndAfter(i),n.setStartAfter(i)):(n.setEndBefore(i),n.setStartBefore(i)),n.collapse(!1),this._clearRange(n)):void 0},n.prototype._clearRange=function(t){var e;return null==t&&(t=this._getRange()),e=this.app.window.getSelection(),null==this.ctrl_a_pressed?(e.removeAllRanges(),e.addRange(t)):void 0},n.prototype._movingEvent=function(t){var e;return"click"===t.type||(e=t.which)===i.RIGHT||e===i.LEFT||e===i.UP||e===i.DOWN},n.prototype._unwrap=function(e){var i;return e=t(e).unwrap().get(0),(i=e.nextSibling)&&i.nodeValue&&(e.nodeValue+=i.nodeValue,t(i).remove()),e},n.prototype.catchQuery=function(e){var n,r,o,s,a,h,l,u,c,p,f,d;if((d=this._getRange())&&d.collapsed){if(e.which===i.ENTER)return(r=t(d.startContainer).closest(".atwho-query")).contents().unwrap(),r.is(":empty")&&r.remove(),(r=t(".atwho-query",this.app.document)).text(r.text()).contents().last().unwrap(),void this._clearRange();if(/firefox/i.test(navigator.userAgent)){if(t(d.startContainer).is(this.$inputor))return void this._clearRange();e.which===i.BACKSPACE&&d.startContainer.nodeType===document.ELEMENT_NODE&&(c=d.startOffset-1)>=0?(o=d.cloneRange(),o.setStart(d.startContainer,c),t(o.cloneContents()).contents().last().is(".atwho-inserted")&&(a=t(d.startContainer).contents().get(c),this._setRange("after",t(a).contents().last()))):e.which===i.LEFT&&d.startContainer.nodeType===document.TEXT_NODE&&(n=t(d.startContainer.previousSibling),n.is(".atwho-inserted")&&0===d.startOffset&&this._setRange("after",n.contents().last()))}if(t(d.startContainer).closest(".atwho-inserted").addClass("atwho-query").siblings().removeClass("atwho-query"),(r=t(".atwho-query",this.app.document)).length>0&&r.is(":empty")&&0===r.text().length&&r.remove(),this._movingEvent(e)||r.removeClass("atwho-inserted"),r.length>0)switch(e.which){case i.LEFT:return this._setRange("before",r.get(0),d),void r.removeClass("atwho-query");case i.RIGHT:return this._setRange("after",r.get(0).nextSibling,d),void r.removeClass("atwho-query")}if(r.length>0&&(f=r.attr("data-atwho-at-query"))&&(r.empty().html(f).attr("data-atwho-at-query",null),this._setRange("after",r.get(0),d)),o=d.cloneRange(),o.setStart(d.startContainer,0),u=this.callbacks("matcher").call(this,this.at,o.toString(),this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),h="string"==typeof u,0===r.length&&h&&(s=d.startOffset-this.at.length-u.length)>=0&&(d.setStart(d.startContainer,s),r=t("",this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass("atwho-query"),d.surroundContents(r.get(0)),l=r.contents().last().get(0),l&&(/firefox/i.test(navigator.userAgent)?(d.setStart(l,l.length),d.setEnd(l,l.length),this._clearRange(d)):this._setRange("after",l,d))),!(h&&u.length=0&&(this._movingEvent(e)&&r.hasClass("atwho-inserted")?r.removeClass("atwho-query"):!1!==this.callbacks("afterMatchFailed").call(this,this.at,r)&&this._setRange("after",this._unwrap(r.text(r.text()).contents().first()))),null)}},n.prototype.rect=function(){var e,i,n;return n=this.query.el.offset(),n&&this.query.el[0].getClientRects().length?(this.app.iframe&&!this.app.iframeAsRoot&&(i=(e=t(this.app.iframe)).offset(),n.left+=i.left-this.$inputor.scrollLeft(),n.top+=i.top-this.$inputor.scrollTop()),n.bottom=n.top+this.query.el.height(),n):void 0},n.prototype.insert=function(t,e){var i,n,r,o,s;return this.$inputor.is(":focus")||this.$inputor.focus(),n=this.getOpt("functionOverrides"),n.insert?n.insert.call(this,t,e):(o=""===(o=this.getOpt("suffix"))?o:o||" ",i=e.data("item-data"),this.query.el.removeClass("atwho-query").addClass("atwho-inserted").html(t).attr("data-atwho-at-query",""+i["atwho-at"]+this.query.text).attr("contenteditable","false"),(r=this._getRange())&&(this.query.el.length&&r.setEndAfter(this.query.el[0]),r.collapse(!1),r.insertNode(s=this.app.document.createTextNode(""+o)),this._setRange("after",s,r)),this.$inputor.is(":focus")||this.$inputor.focus(),this.$inputor.change())},n}(r);var u;u=function(){function e(t){this.context=t,this.at=this.context.at,this.storage=this.context.$inputor}return e.prototype.destroy=function(){return this.storage.data(this.at,null)},e.prototype.saved=function(){return this.fetch()>0},e.prototype.query=function(t,e){var i,n,r;return n=this.fetch(),r=this.context.getOpt("searchKey"),n=this.context.callbacks("filter").call(this.context,t,n,r)||[],i=this.context.callbacks("remoteFilter"),n.length>0||!i&&0===n.length?e(n):i.call(this.context,t,e)},e.prototype.fetch=function(){return this.storage.data(this.at)||[]},e.prototype.save=function(t){return this.storage.data(this.at,this.context.callbacks("beforeSave").call(this.context,t||[]))},e.prototype.load=function(t){return!this.saved()&&t?this._load(t):void 0},e.prototype.reload=function(t){return this._load(t)},e.prototype._load=function(e){return"string"==typeof e?t.ajax(e,{dataType:"json"}).done(function(t){return function(e){return t.save(e)}}(this)):this.save(e)},e}();var c;c=function(){function e(e){this.context=e,this.$el=t(""),this.$elUl=this.$el.children(),this.timeoutID=null,this.context.$el.append(this.$el),this.bindEvent()}return e.prototype.init=function(){var t,e;return e=this.context.getOpt("alias")||this.context.at.charCodeAt(0),t=this.context.getOpt("headerTpl"),t&&1===this.$el.children().length&&this.$el.prepend(t),this.$el.attr({id:"at-view-"+e})},e.prototype.destroy=function(){return this.$el.remove()},e.prototype.bindEvent=function(){var e,i,n;return e=this.$el.find("ul"),i=0,n=0,e.on("mousemove.atwho-view","li",function(r){return function(r){var o;if((i!==r.clientX||n!==r.clientY)&&(i=r.clientX,n=r.clientY,o=t(r.currentTarget),!o.hasClass("cur")))return e.find(".cur").removeClass("cur"),o.addClass("cur")}}(this)).on("click.atwho-view","li",function(i){return function(n){return e.find(".cur").removeClass("cur"),t(n.currentTarget).addClass("cur"),i.choose(n),n.preventDefault()}}(this))},e.prototype.visible=function(){return t.expr.filters.visible(this.$el[0])},e.prototype.highlighted=function(){return this.$el.find(".cur").length>0},e.prototype.choose=function(t){var e,i;return(e=this.$el.find(".cur")).length&&(i=this.context.insertContentFor(e),this.context._stopDelayedCall(),this.context.insert(this.context.callbacks("beforeInsert").call(this.context,i,e,t),e),this.context.trigger("inserted",[e,t]),this.hide(t)),this.context.getOpt("hideWithoutSuffix")?this.stopShowing=!0:void 0},e.prototype.reposition=function(e){var i,n,r,o;return i=this.context.app.iframeAsRoot?this.context.app.window:window,e.bottom+this.$el.height()-t(i).scrollTop()>t(i).height()&&(e.bottom=e.top-this.$el.height()),e.left>(r=t(i).width()-this.$el.width()-5)&&(e.left=r),n={left:e.left,top:e.bottom},null!=(o=this.context.callbacks("beforeReposition"))&&o.call(this.context,n),this.$el.offset(n),this.context.trigger("reposition",[n])},e.prototype.next=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),e=t.next(),e.length||(e=this.$el.find("li:first")),e.addClass("cur"),i=e[0],n=i.offsetTop+i.offsetHeight+(i.nextSibling?i.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,n-this.$el.height()))},e.prototype.prev=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),i=t.prev(),i.length||(i=this.$el.find("li:last")),i.addClass("cur"),n=i[0],e=n.offsetTop+n.offsetHeight+(n.nextSibling?n.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,e-this.$el.height()))},e.prototype.scrollTop=function(t){var e;return e=this.context.getOpt("scrollDuration"),e?this.$elUl.animate({scrollTop:t},e):this.$elUl.scrollTop(t)},e.prototype.show=function(){var t;return this.stopShowing?void(this.stopShowing=!1):(this.visible()||(this.$el.show(),this.$el.scrollTop(0),this.context.trigger("shown")),(t=this.context.rect())?this.reposition(t):void 0)},e.prototype.hide=function(t,e){var i;if(this.visible())return isNaN(e)?(this.$el.hide(),this.context.trigger("hidden",[t])):(i=function(t){return function(){return t.hide()}}(this),clearTimeout(this.timeoutID),this.timeoutID=setTimeout(i,e))},e.prototype.render=function(e){var i,n,r,o,s,a,h;if(!(t.isArray(e)&&e.length>0))return void this.hide();for(this.$el.find("ul").empty(),n=this.$el.find("ul"),h=this.context.getOpt("displayTpl"),r=0,s=e.length;s>r;r++)o=e[r],o=t.extend({},o,{"atwho-at":this.context.at}),a=this.context.callbacks("tplEval").call(this.context,h,o,"onDisplay"),i=t(this.context.callbacks("highlighter").call(this.context,a,this.context.query.text)),i.data("item-data",o),n.append(i);return this.show(),this.context.getOpt("highlightFirst")?n.find("li:first").addClass("cur"):void 0},e}();var p;p={load:function(t,e){var i;return(i=this.controller(t))?i.model.load(e):void 0},isSelecting:function(){var t;return!!(null!=(t=this.controller())?t.view.visible():void 0)},hide:function(){var t;return null!=(t=this.controller())?t.view.hide():void 0},reposition:function(){var t;return(t=this.controller())?t.view.reposition(t.rect()):void 0},setIframe:function(t,e){return this.setupRootElement(t,e),null},run:function(){return this.dispatch()},destroy:function(){return this.shutdown(),this.$inputor.data("atwho",null)}},t.fn.atwho=function(e){var i,r;return i=arguments,r=null,this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function(){var o,s;return(s=(o=t(this)).data("atwho"))||o.data("atwho",s=new n(this)),"object"!=typeof e&&e?p[e]&&s?r=p[e].apply(s,Array.prototype.slice.call(i,1)):t.error("Method "+e+" does not exist on jQuery.atwho"):s.reg(e.at,e)}),null!=r?r:this},t.fn.atwho["default"]={at:void 0,alias:void 0,data:null,displayTpl:"${name}",insertTpl:"${atwho-at}${name}",headerTpl:null,callbacks:e,functionOverrides:{},searchKey:"name",suffix:void 0,hideWithoutSuffix:!1,startWithSpace:!0,acceptSpaceBar:!1,highlightFirst:!0,limit:5,maxLen:20,minLen:0,displayTimeout:300,delay:null,spaceSelectsMatch:!1,tabSelectsMatch:!0,editableAtwhoQueryAttrs:{},scrollDuration:150,suspendOnComposing:!0,lookUpOnClick:!0},t.fn.atwho.debug=!1});
\ No newline at end of file
diff --git a/js/legacy/jquery.caret.min.js b/js/legacy/jquery.caret.min.js
new file mode 100644
index 000000000..183c47b8e
--- /dev/null
+++ b/js/legacy/jquery.caret.min.js
@@ -0,0 +1,561 @@
+/*
+ * @copyright Copyright (c) 2018 Julius Härtl
+ *
+ * @author Julius Härtl
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+(function($, undefined) {
+
+ var _input = document.createElement('input');
+
+ var _support = {
+ setSelectionRange: ('setSelectionRange' in _input) || ('selectionStart' in _input),
+ createTextRange: ('createTextRange' in _input) || ('selection' in document)
+ };
+
+ var _rNewlineIE = /\r\n/g,
+ _rCarriageReturn = /\r/g;
+
+ var _getValue = function(input) {
+ if (typeof(input.value) !== 'undefined') {
+ return input.value;
+ }
+ return $(input).text();
+ };
+
+ var _setValue = function(input, value) {
+ if (typeof(input.value) !== 'undefined') {
+ input.value = value;
+ } else {
+ $(input).text(value);
+ }
+ };
+
+ var _getIndex = function(input, pos) {
+ var norm = _getValue(input).replace(_rCarriageReturn, '');
+ var len = norm.length;
+
+ if (typeof(pos) === 'undefined') {
+ pos = len;
+ }
+
+ pos = Math.floor(pos);
+
+ // Negative index counts backward from the end of the input/textarea's value
+ if (pos < 0) {
+ pos = len + pos;
+ }
+
+ // Enforce boundaries
+ if (pos < 0) { pos = 0; }
+ if (pos > len) { pos = len; }
+
+ return pos;
+ };
+
+ var _hasAttr = function(input, attrName) {
+ return input.hasAttribute ? input.hasAttribute(attrName) : (typeof(input[attrName]) !== 'undefined');
+ };
+
+ /**
+ * @class
+ * @constructor
+ */
+ var Range = function(start, end, length, text) {
+ this.start = start || 0;
+ this.end = end || 0;
+ this.length = length || 0;
+ this.text = text || '';
+ };
+
+ Range.prototype.toString = function() {
+ return JSON.stringify(this, null, ' ');
+ };
+
+ var _getCaretW3 = function(input) {
+ return input.selectionStart;
+ };
+
+ /**
+ * @see http://stackoverflow.com/q/6943000/467582
+ */
+ var _getCaretIE = function(input) {
+ var caret, range, textInputRange, rawValue, len, endRange;
+
+ // Yeah, you have to focus twice for IE 7 and 8. *cries*
+ input.focus();
+ input.focus();
+
+ range = document.selection.createRange();
+
+ if (range && range.parentElement() === input) {
+ rawValue = _getValue(input);
+
+ len = rawValue.length;
+
+ // Create a working TextRange that lives only in the input
+ textInputRange = input.createTextRange();
+ textInputRange.moveToBookmark(range.getBookmark());
+
+ // Check if the start and end of the selection are at the very end
+ // of the input, since moveStart/moveEnd doesn't return what we want
+ // in those cases
+ endRange = input.createTextRange();
+ endRange.collapse(false);
+
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+ caret = rawValue.replace(_rNewlineIE, '\n').length;
+ } else {
+ caret = -textInputRange.moveStart("character", -len);
+ }
+
+ return caret;
+ }
+
+ // NOTE: This occurs when you highlight part of a textarea and then click in the middle of the highlighted portion in IE 6-10.
+ // There doesn't appear to be anything we can do about it.
+// alert("Your browser is incredibly stupid. I don't know what else to say.");
+// alert(range + '\n\n' + range.parentElement().tagName + '#' + range.parentElement().id);
+
+ return 0;
+ };
+
+ /**
+ * Gets the position of the caret in the given input.
+ * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
+ * @returns {Number}
+ * @see http://stackoverflow.com/questions/263743/how-to-get-cursor-position-in-textarea/263796#263796
+ */
+ var _getCaret = function(input) {
+ if (!input) {
+ return undefined;
+ }
+
+ // Mozilla, et al.
+ if (_support.setSelectionRange) {
+ return _getCaretW3(input);
+ }
+ // IE
+ else if (_support.createTextRange) {
+ return _getCaretIE(input);
+ }
+
+ return undefined;
+ };
+
+ var _setCaretW3 = function(input, pos) {
+ input.setSelectionRange(pos, pos);
+ };
+
+ var _setCaretIE = function(input, pos) {
+ var range = input.createTextRange();
+ range.move('character', pos);
+ range.select();
+ };
+
+ /**
+ * Sets the position of the caret in the given input.
+ * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
+ * @param {Number} pos
+ * @see http://parentnode.org/javascript/working-with-the-cursor-position/
+ */
+ var _setCaret = function(input, pos) {
+ input.focus();
+
+ pos = _getIndex(input, pos);
+
+ // Mozilla, et al.
+ if (_support.setSelectionRange) {
+ _setCaretW3(input, pos);
+ }
+ // IE
+ else if (_support.createTextRange) {
+ _setCaretIE(input, pos);
+ }
+ };
+
+ /**
+ * Inserts the specified text at the current caret position in the given input.
+ * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
+ * @param {String} text
+ * @see http://parentnode.org/javascript/working-with-the-cursor-position/
+ */
+ var _insertAtCaret = function(input, text) {
+ var curPos = _getCaret(input);
+
+ var oldValueNorm = _getValue(input).replace(_rCarriageReturn, '');
+
+ var newLength = +(curPos + text.length + (oldValueNorm.length - curPos));
+ var maxLength = +input.getAttribute('maxlength');
+
+ if(_hasAttr(input, 'maxlength') && newLength > maxLength) {
+ var delta = text.length - (newLength - maxLength);
+ text = text.substr(0, delta);
+ }
+
+ _setValue(input, oldValueNorm.substr(0, curPos) + text + oldValueNorm.substr(curPos));
+
+ _setCaret(input, curPos + text.length);
+ };
+
+ var _getInputRangeW3 = function(input) {
+ var range = new Range();
+
+ range.start = input.selectionStart;
+ range.end = input.selectionEnd;
+
+ var min = Math.min(range.start, range.end);
+ var max = Math.max(range.start, range.end);
+
+ range.length = max - min;
+ range.text = _getValue(input).substring(min, max);
+
+ return range;
+ };
+
+ /** @see http://stackoverflow.com/a/3648244/467582 */
+ var _getInputRangeIE = function(input) {
+ var range = new Range();
+
+ input.focus();
+
+ var selection = document.selection.createRange();
+
+ if (selection && selection.parentElement() === input) {
+ var len, normalizedValue, textInputRange, endRange, start = 0, end = 0;
+ var rawValue = _getValue(input);
+
+ len = rawValue.length;
+ normalizedValue = rawValue.replace(/\r\n/g, "\n");
+
+ // Create a working TextRange that lives only in the input
+ textInputRange = input.createTextRange();
+ textInputRange.moveToBookmark(selection.getBookmark());
+
+ // Check if the start and end of the selection are at the very end
+ // of the input, since moveStart/moveEnd doesn't return what we want
+ // in those cases
+ endRange = input.createTextRange();
+ endRange.collapse(false);
+
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+ start = end = len;
+ } else {
+ start = -textInputRange.moveStart("character", -len);
+ start += normalizedValue.slice(0, start).split("\n").length - 1;
+
+ if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
+ end = len;
+ } else {
+ end = -textInputRange.moveEnd("character", -len);
+ end += normalizedValue.slice(0, end).split("\n").length - 1;
+ }
+ }
+
+ /// normalize newlines
+ start -= (rawValue.substring(0, start).split('\r\n').length - 1);
+ end -= (rawValue.substring(0, end).split('\r\n').length - 1);
+ /// normalize newlines
+
+ range.start = start;
+ range.end = end;
+ range.length = range.end - range.start;
+ range.text = normalizedValue.substr(range.start, range.length);
+ }
+
+ return range;
+ };
+
+ /**
+ * Gets the selected text range of the given input.
+ * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
+ * @returns {Range}
+ * @see http://stackoverflow.com/a/263796/467582
+ * @see http://stackoverflow.com/a/2966703/467582
+ */
+ var _getInputRange = function(input) {
+ if (!input) {
+ return undefined;
+ }
+
+ // Mozilla, et al.
+ if (_support.setSelectionRange) {
+ return _getInputRangeW3(input);
+ }
+ // IE
+ else if (_support.createTextRange) {
+ return _getInputRangeIE(input);
+ }
+
+ return undefined;
+ };
+
+ var _setInputRangeW3 = function(input, startPos, endPos) {
+ input.setSelectionRange(startPos, endPos);
+ };
+
+ var _setInputRangeIE = function(input, startPos, endPos) {
+ var tr = input.createTextRange();
+ tr.moveEnd('textedit', -1);
+ tr.moveStart('character', startPos);
+ tr.moveEnd('character', endPos - startPos);
+ tr.select();
+ };
+
+ /**
+ * Sets the selected text range of (i.e., highlights text in) the given input.
+ * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
+ * @param {Number} startPos Zero-based index
+ * @param {Number} endPos Zero-based index
+ * @see http://parentnode.org/javascript/working-with-the-cursor-position/
+ * @see http://stackoverflow.com/a/2966703/467582
+ */
+ var _setInputRange = function(input, startPos, endPos) {
+ startPos = _getIndex(input, startPos);
+ endPos = _getIndex(input, endPos);
+
+ // Mozilla, et al.
+ if (_support.setSelectionRange) {
+ _setInputRangeW3(input, startPos, endPos);
+ }
+ // IE
+ else if (_support.createTextRange) {
+ _setInputRangeIE(input, startPos, endPos);
+ }
+ };
+
+ /**
+ * Replaces the currently selected text with the given string.
+ * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
+ * @param {String} text New text that will replace the currently selected text.
+ * @see http://parentnode.org/javascript/working-with-the-cursor-position/
+ */
+ var _replaceInputRange = function(input, text) {
+ var $input = $(input);
+
+ var oldValue = $input.val();
+ var selection = _getInputRange(input);
+
+ var newLength = +(selection.start + text.length + (oldValue.length - selection.end));
+ var maxLength = +$input.attr('maxlength');
+
+ if($input.is('[maxlength]') && newLength > maxLength) {
+ var delta = text.length - (newLength - maxLength);
+ text = text.substr(0, delta);
+ }
+
+ // Now that we know what the user selected, we can replace it
+ var startText = oldValue.substr(0, selection.start);
+ var endText = oldValue.substr(selection.end);
+
+ $input.val(startText + text + endText);
+
+ // Reset the selection
+ var startPos = selection.start;
+ var endPos = startPos + text.length;
+
+ _setInputRange(input, selection.length ? startPos : endPos, endPos);
+ };
+
+ var _selectAllW3 = function(elem) {
+ var selection = window.getSelection();
+ var range = document.createRange();
+ range.selectNodeContents(elem);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ };
+
+ var _selectAllIE = function(elem) {
+ var range = document.body.createTextRange();
+ range.moveToElementText(elem);
+ range.select();
+ };
+
+ /**
+ * Select all text in the given element.
+ * @param {HTMLElement} elem Any block or inline element other than a form element.
+ */
+ var _selectAll = function(elem) {
+ var $elem = $(elem);
+ if ($elem.is('input, textarea') || elem.select) {
+ $elem.select();
+ return;
+ }
+
+ // Mozilla, et al.
+ if (_support.setSelectionRange) {
+ _selectAllW3(elem);
+ }
+ // IE
+ else if (_support.createTextRange) {
+ _selectAllIE(elem);
+ }
+ };
+
+ var _deselectAll = function() {
+ if (document.selection) {
+ document.selection.empty();
+ }
+ else if (window.getSelection) {
+ window.getSelection().removeAllRanges();
+ }
+ };
+
+ $.extend($.fn, {
+
+ /**
+ * Gets or sets the position of the caret or inserts text at the current caret position in an input or textarea element.
+ * @returns {Number|jQuery} The current caret position if invoked as a getter (with no arguments)
+ * or this jQuery object if invoked as a setter or inserter.
+ * @see http://web.archive.org/web/20080704185920/http://parentnode.org/javascript/working-with-the-cursor-position/
+ * @since 1.0.0
+ * @example
+ *
+ * // Get position
+ * var pos = $('input:first').caret();
+ *
+ * @example
+ *
+ * // Set position
+ * $('input:first').caret(15);
+ * $('input:first').caret(-3);
+ *
+ * @example
+ *
+ * // Insert text at current position
+ * $('input:first').caret('Some text');
+ *
+ */
+ caret: function() {
+ var $inputs = this.filter('input, textarea');
+
+ // getCaret()
+ if (arguments.length === 0) {
+ var input = $inputs.get(0);
+ return _getCaret(input);
+ }
+ // setCaret(position)
+ else if (typeof arguments[0] === 'number') {
+ var pos = arguments[0];
+ $inputs.each(function(_i, input) {
+ _setCaret(input, pos);
+ });
+ }
+ // insertAtCaret(text)
+ else {
+ var text = arguments[0];
+ $inputs.each(function(_i, input) {
+ _insertAtCaret(input, text);
+ });
+ }
+
+ return this;
+ },
+
+ /**
+ * Gets or sets the selection range or replaces the currently selected text in an input or textarea element.
+ * @returns {Range|jQuery} The current selection range if invoked as a getter (with no arguments)
+ * or this jQuery object if invoked as a setter or replacer.
+ * @see http://stackoverflow.com/a/2966703/467582
+ * @since 1.0.0
+ * @example
+ *
+ * // Get selection range
+ * var range = $('input:first').range();
+ *
+ * @example
+ *
+ * // Set selection range
+ * $('input:first').range(15);
+ * $('input:first').range(15, 20);
+ * $('input:first').range(-3);
+ * $('input:first').range(-8, -3);
+ *
+ * @example
+ *
+ * // Replace the currently selected text
+ * $('input:first').range('Replacement text');
+ *
+ */
+ range: function() {
+ var $inputs = this.filter('input, textarea');
+
+ // getRange() = { start: pos, end: pos }
+ if (arguments.length === 0) {
+ var input = $inputs.get(0);
+ return _getInputRange(input);
+ }
+ // setRange(startPos, endPos)
+ else if (typeof arguments[0] === 'number') {
+ var startPos = arguments[0];
+ var endPos = arguments[1];
+ $inputs.each(function(_i, input) {
+ _setInputRange(input, startPos, endPos);
+ });
+ }
+ // replaceRange(text)
+ else {
+ var text = arguments[0];
+ $inputs.each(function(_i, input) {
+ _replaceInputRange(input, text);
+ });
+ }
+
+ return this;
+ },
+
+ /**
+ * Selects all text in each element of this jQuery object.
+ * @returns {jQuery} This jQuery object
+ * @see http://stackoverflow.com/a/11128179/467582
+ * @since 1.5.0
+ * @example
+ *
+ * // Select the contents of span elements when clicked
+ * $('span').on('click', function() { $(this).highlight(); });
+ *
+ */
+ selectAll: function() {
+ return this.each(function(_i, elem) {
+ _selectAll(elem);
+ });
+ }
+
+ });
+
+ $.extend($, {
+ /**
+ * Deselects all text on the page.
+ * @returns {jQuery} The jQuery function
+ * @since 1.5.0
+ * @example
+ *
+ * // Select some text
+ * $('span').selectAll();
+ *
+ * // Deselect the text
+ * $.deselectAll();
+ *
+ */
+ deselectAll: function() {
+ _deselectAll();
+ return this;
+ }
+ });
+
+}(window.jQuery || window.Zepto || window.$));
diff --git a/js/service/ActivityService.js b/js/service/ActivityService.js
index 7e6c8db03..1f932420c 100644
--- a/js/service/ActivityService.js
+++ b/js/service/ActivityService.js
@@ -43,12 +43,10 @@ class ActivityService {
this.data[DECK_ACTIVITY_TYPE_CARD] = {};
this.toEnhanceWithComments = [];
this.commentCollection = new CommentCollection();
- this.commentCollection._limit = this.RESULT_PER_PAGE;
+ this.commentCollection._limit = ActivityService.RESULT_PER_PAGE;
this.commentCollection.on('request', function() {
- console.log("REQUEST");
}, this);
- this.commentCollection.on('sync', function() {
- console.log("SYNC");
+ this.commentCollection.on('sync', function(a) {
for (let index in this.toEnhanceWithComments) {
let item = this.toEnhanceWithComments[index];
item.commentModel = this.commentCollection.get(item.subject_rich[1].comment);
@@ -61,8 +59,10 @@ class ActivityService {
this.commentCollection.updateReadMarker();
}
}, this);
- this.commentCollection.on('add', function(data) {
- console.log("ADD");
+ this.commentCollection.on('add', function(model, collection, options) {
+ // we need to update the model, because it consists of client data
+ // only, but the server might add meta data, e.g. about mentions
+ model.fetch();
}, this);
this.since = {
deck_card: {
diff --git a/templates/part.card.activity.html b/templates/part.card.activity.html
index 99a0791e9..62a9a7a28 100644
--- a/templates/part.card.activity.html
+++ b/templates/part.card.activity.html
@@ -34,7 +34,8 @@
{{ activity.timestamp/1000 | relativeDateFilter }}
-
+
+