Implement user mentioning in frontend
Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
77
css/autocomplete.scss
Normal file
77
css/autocomplete.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ $compact-board-last-item-margin: 5px 10px 10px;
|
|||||||
@import 'icons';
|
@import 'icons';
|
||||||
@import 'animations';
|
@import 'animations';
|
||||||
@import 'compact-mode';
|
@import 'compact-mode';
|
||||||
|
@import 'autocomplete';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General styles
|
* General styles
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ import md from 'angular-markdown-it';
|
|||||||
import nganimate from 'angular-animate';
|
import nganimate from 'angular-animate';
|
||||||
import 'angular-file-upload';
|
import 'angular-file-upload';
|
||||||
import ngInfiniteScroll from 'ng-infinite-scroll';
|
import ngInfiniteScroll from 'ng-infinite-scroll';
|
||||||
|
import '../legacy/jquery.atwho.min';
|
||||||
|
import '../legacy/jquery.caret.min';
|
||||||
|
|
||||||
var app = angular.module('Deck', [
|
var app = angular.module('Deck', [
|
||||||
ngsanitize,
|
ngsanitize,
|
||||||
|
|||||||
@@ -49,31 +49,146 @@ class ActivityController {
|
|||||||
}
|
}
|
||||||
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
|
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
|
||||||
}, true);
|
}, 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 '<li>' +
|
||||||
|
'<span class="avatar-name-wrapper">' +
|
||||||
|
'<span class="avatar" ' +
|
||||||
|
'data-username="' + escapeHTML(item.id) + '" ' + // for avatars
|
||||||
|
'data-user="' + escapeHTML(item.id) + '" ' + // for contactsmenu
|
||||||
|
'data-user-display-name="' + escapeHTML(item.label) + '">' +
|
||||||
|
'</span>' +
|
||||||
|
'<strong>' + escapeHTML(item.label) + '</strong>' +
|
||||||
|
'</span></li>';
|
||||||
|
},
|
||||||
|
insertTpl: function (item) {
|
||||||
|
return '' +
|
||||||
|
'<span class="avatar-name-wrapper">' +
|
||||||
|
'<span class="avatar" ' +
|
||||||
|
'data-username="' + escapeHTML(item.id) + '" ' + // for avatars
|
||||||
|
'data-user="' + escapeHTML(item.id) + '" ' + // for contactsmenu
|
||||||
|
'data-user-display-name="' + escapeHTML(item.label) + '">' +
|
||||||
|
'</span>' +
|
||||||
|
'<strong>' + escapeHTML(item.label) + '</strong>' +
|
||||||
|
'</span>';
|
||||||
|
},
|
||||||
|
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 = $('<div/>').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(/<br\s*[\/]?>/gi, "\n"));
|
||||||
|
return $comment.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _composeHTMLMention(uid, displayName) {
|
||||||
|
var avatar = '' +
|
||||||
|
'<span class="avatar" ng-attr-size="16" ' +
|
||||||
|
'ng-attr-user="' + _.escape(uid) + '" ' +
|
||||||
|
'ng-attr-displayname="' + _.escape(displayName) + '">' +
|
||||||
|
'</span>';
|
||||||
|
|
||||||
|
var isCurrentUser = (uid === OC.getCurrentUser().uid);
|
||||||
|
|
||||||
|
return '' +
|
||||||
|
'<span class="atwho-inserted">' +
|
||||||
|
'<span class="avatar-name-wrapper' + (isCurrentUser ? ' currentUser' : '') + '">' +
|
||||||
|
avatar +
|
||||||
|
'<strong>' + _.escape(displayName) + '</strong>' +
|
||||||
|
'</span>' +
|
||||||
|
'</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
formatMessage(activity) {
|
||||||
|
let message = activity.message;
|
||||||
|
let mentions = activity.commentModel.get('mentions');
|
||||||
|
const editMode = false;
|
||||||
|
message = escapeHTML(message).replace(/\n/g, '<br/>');
|
||||||
|
|
||||||
|
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() {
|
postComment() {
|
||||||
const self = this;
|
const self = this;
|
||||||
this.status.commentCreateLoading = true;
|
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({
|
var model = this.activityservice.commentCollection.create({
|
||||||
actorId: OC.getCurrentUser().uid,
|
actorId: OC.getCurrentUser().uid,
|
||||||
actorDisplayName: OC.getCurrentUser().displayName,
|
actorDisplayName: OC.getCurrentUser().displayName,
|
||||||
actorType: 'users',
|
actorType: 'users',
|
||||||
verb: 'comment',
|
verb: 'comment',
|
||||||
message: self.$scope.newComment,
|
message: content,
|
||||||
creationDateTime: (new Date()).toUTCString()
|
creationDateTime: (new Date()).toUTCString()
|
||||||
}, {
|
}, {
|
||||||
at: 0,
|
at: 0,
|
||||||
// wait for real creation before adding
|
// wait for real creation before adding
|
||||||
wait: true,
|
wait: true,
|
||||||
success: function() {
|
success: function() {
|
||||||
console.log("SUCCESS");
|
|
||||||
self.$scope.newComment = '';
|
self.$scope.newComment = '';
|
||||||
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
|
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
|
||||||
self.status.commentCreateLoading = false;
|
self.status.commentCreateLoading = false;
|
||||||
},
|
},
|
||||||
error: function() {
|
error: function() {
|
||||||
|
self.status.commentCreateLoading = false;
|
||||||
|
OC.Notification.showTemporary(t('deck', 'Posting the comment failed.'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ var CommentCollection = OC.Backbone.Collection.extend(
|
|||||||
*
|
*
|
||||||
* @type int
|
* @type int
|
||||||
*/
|
*/
|
||||||
_limit : 2,
|
_limit : 5,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the collection
|
* Initializes the collection
|
||||||
|
|||||||
@@ -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';
|
var NS_OWNCLOUD = 'http://owncloud.org/ns';
|
||||||
/**
|
/**
|
||||||
* @class OCA.AnnouncementCenter.Comments.CommentModel
|
* @class CommentModel
|
||||||
* @classdesc
|
* @classdesc
|
||||||
*
|
*
|
||||||
* Comment
|
* Comment
|
||||||
@@ -49,7 +58,8 @@ var CommentModel = OC.Backbone.Model.extend(
|
|||||||
'creationDateTime': '{' + NS_OWNCLOUD + '}creationDateTime',
|
'creationDateTime': '{' + NS_OWNCLOUD + '}creationDateTime',
|
||||||
'objectType': '{' + NS_OWNCLOUD + '}objectType',
|
'objectType': '{' + NS_OWNCLOUD + '}objectType',
|
||||||
'objectId': '{' + NS_OWNCLOUD + '}objectId',
|
'objectId': '{' + NS_OWNCLOUD + '}objectId',
|
||||||
'isUnread': '{' + NS_OWNCLOUD + '}isUnread'
|
'isUnread': '{' + NS_OWNCLOUD + '}isUnread',
|
||||||
|
'mentions': '{' + NS_OWNCLOUD + '}mentions'
|
||||||
},
|
},
|
||||||
|
|
||||||
parse: function(data) {
|
parse: function(data) {
|
||||||
@@ -62,10 +72,32 @@ var CommentModel = OC.Backbone.Model.extend(
|
|||||||
creationDateTime: data.creationDateTime,
|
creationDateTime: data.creationDateTime,
|
||||||
objectType: data.objectType,
|
objectType: data.objectType,
|
||||||
objectId: data.objectId,
|
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() {
|
url: function() {
|
||||||
if (typeof this.get('id') !== 'undefined') {
|
if (typeof this.get('id') !== 'undefined') {
|
||||||
return this.collection.url() + this.get('id');
|
return this.collection.url() + this.get('id');
|
||||||
@@ -73,7 +105,6 @@ var CommentModel = OC.Backbone.Model.extend(
|
|||||||
return this.collection.url();
|
return this.collection.url();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default CommentModel;
|
export default CommentModel;
|
||||||
|
|||||||
1
js/legacy/jquery.atwho.min.js
vendored
Normal file
1
js/legacy/jquery.atwho.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
561
js/legacy/jquery.caret.min.js
vendored
Normal file
561
js/legacy/jquery.caret.min.js
vendored
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @author Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
(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
|
||||||
|
* <pre>
|
||||||
|
* // Get position
|
||||||
|
* var pos = $('input:first').caret();
|
||||||
|
* </pre>
|
||||||
|
* @example
|
||||||
|
* <pre>
|
||||||
|
* // Set position
|
||||||
|
* $('input:first').caret(15);
|
||||||
|
* $('input:first').caret(-3);
|
||||||
|
* </pre>
|
||||||
|
* @example
|
||||||
|
* <pre>
|
||||||
|
* // Insert text at current position
|
||||||
|
* $('input:first').caret('Some text');
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
* <pre>
|
||||||
|
* // Get selection range
|
||||||
|
* var range = $('input:first').range();
|
||||||
|
* </pre>
|
||||||
|
* @example
|
||||||
|
* <pre>
|
||||||
|
* // Set selection range
|
||||||
|
* $('input:first').range(15);
|
||||||
|
* $('input:first').range(15, 20);
|
||||||
|
* $('input:first').range(-3);
|
||||||
|
* $('input:first').range(-8, -3);
|
||||||
|
* </pre>
|
||||||
|
* @example
|
||||||
|
* <pre>
|
||||||
|
* // Replace the currently selected text
|
||||||
|
* $('input:first').range('Replacement text');
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
* <pre>
|
||||||
|
* // Select the contents of span elements when clicked
|
||||||
|
* $('span').on('click', function() { $(this).highlight(); });
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
* <pre>
|
||||||
|
* // Select some text
|
||||||
|
* $('span').selectAll();
|
||||||
|
*
|
||||||
|
* // Deselect the text
|
||||||
|
* $.deselectAll();
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
deselectAll: function() {
|
||||||
|
_deselectAll();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}(window.jQuery || window.Zepto || window.$));
|
||||||
@@ -43,12 +43,10 @@ class ActivityService {
|
|||||||
this.data[DECK_ACTIVITY_TYPE_CARD] = {};
|
this.data[DECK_ACTIVITY_TYPE_CARD] = {};
|
||||||
this.toEnhanceWithComments = [];
|
this.toEnhanceWithComments = [];
|
||||||
this.commentCollection = new CommentCollection();
|
this.commentCollection = new CommentCollection();
|
||||||
this.commentCollection._limit = this.RESULT_PER_PAGE;
|
this.commentCollection._limit = ActivityService.RESULT_PER_PAGE;
|
||||||
this.commentCollection.on('request', function() {
|
this.commentCollection.on('request', function() {
|
||||||
console.log("REQUEST");
|
|
||||||
}, this);
|
}, this);
|
||||||
this.commentCollection.on('sync', function() {
|
this.commentCollection.on('sync', function(a) {
|
||||||
console.log("SYNC");
|
|
||||||
for (let index in this.toEnhanceWithComments) {
|
for (let index in this.toEnhanceWithComments) {
|
||||||
let item = this.toEnhanceWithComments[index];
|
let item = this.toEnhanceWithComments[index];
|
||||||
item.commentModel = this.commentCollection.get(item.subject_rich[1].comment);
|
item.commentModel = this.commentCollection.get(item.subject_rich[1].comment);
|
||||||
@@ -61,8 +59,10 @@ class ActivityService {
|
|||||||
this.commentCollection.updateReadMarker();
|
this.commentCollection.updateReadMarker();
|
||||||
}
|
}
|
||||||
}, this);
|
}, this);
|
||||||
this.commentCollection.on('add', function(data) {
|
this.commentCollection.on('add', function(model, collection, options) {
|
||||||
console.log("ADD");
|
// 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);
|
||||||
this.since = {
|
this.since = {
|
||||||
deck_card: {
|
deck_card: {
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="activitytime has-tooltip live-relative-timestamp"
|
<span class="activitytime has-tooltip live-relative-timestamp"
|
||||||
data-timestamp="{{ activity.timelineTimestamp }}">{{ activity.timestamp/1000 | relativeDateFilter }}</span>
|
data-timestamp="{{ activity.timelineTimestamp }}">{{ activity.timestamp/1000 | relativeDateFilter }}</span>
|
||||||
<div class="activitymessage" ng-bind-html="activity.message"></div>
|
<div class="activitymessage" ng-if="!activity.commentModel" ng-bind-html="activity.message"></div>
|
||||||
|
<div class="activitymessage" ng-if="activity.commentModel" bind-html-compile="$ctrl.formatMessage(activity)"></div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li ng-if="$ctrl.loading"><div class="icon-loading-small"></div></li>
|
<li ng-if="$ctrl.loading"><div class="icon-loading-small"></div></li>
|
||||||
|
|||||||
Reference in New Issue
Block a user