To allow scrolling in Safari mobile, we set the content of text_view_comments frame in a jQuery UI layout.
So the automated scrolling operations in c_sync.js must be adjustable to the right part to scroll.
Also, if a comment have to be shown outside of the current viewport, we scroll the correct part to that viewport
and then set the comment top Y offset to juste what it needs to avoid the "Add comment" button after scrolling
operation.
If not in Safari mobile, we add an offset here to avoid comment to display under the "Add comment" button.
Db = function() {
//initial comment db as objs (TREE LIKE MODEL : replies are included in comment.replies)
this.comments = null; // current set of (filtered) comments
this.allComments = null; // all server database comments
// obj
// keys : commentDbId as string
// values : comment db as obj
this.commentsByDbId = {};
this.allCommentsByDbId = {};
// dictionary (always contains all comments (no reply) whatever the filter
// order key --> ordered array of comment ids (no reply)
this.ordered_comment_ids = {}; // all server database comments
}
Db.prototype = {
//////////////////////////////
// CORE FUNCTIONS
//////////////////////////////
init : function() {
// at first server side ordered comment by asc ids, replies by creation date :
this.allComments = CY.JSON.parse(sv_comments) ;
if (sv_read_only) {
this.initToReadOnly() ;
}
this._computeAllCommentsByDbId() ;
this._reorder() ;
},
_del : function (arr, dic, id) {
// first recursively remove all replies
var comment = dic[id] ;
for (var i = 0 ; i < comment.replies.length ; i++) {
var rid = comment.replies[i].id ;
this._del(comment.replies, dic, rid) ;
i--;
}
for (var i = 0, ilen = arr.length ; i < ilen ; i++) {
if (arr[i].id == id) {
arr.splice(i, 1) ;
delete dic[id] ;
break ;
}
}
},
del : function(comment) {
var arr = (comment.reply_to_id == null) ? this.comments : this.commentsByDbId[comment.reply_to_id].replies ;
this._del(arr, this.commentsByDbId, comment.id) ;
arr = (comment.reply_to_id == null) ? this.allComments : this.allCommentsByDbId[comment.reply_to_id].replies ;
this._del(arr, this.allCommentsByDbId, comment.id) ;
this._reorder() ;
},
// maintains the ordered lists
_reorder : function() {
// scope (order by start_wrapper, start_offset, end_wrapper, end_offset
var a = [] ;
for (var i = 0, ilen = this.allComments.length ; i < ilen ; i++) {
var comment = this.allComments[i] ;
var found = false ;
for (var j = 0, jlen = a.length ; j < jlen ; j++) {
var id = a[j] ;
var comment2 = this.allCommentsByDbId[id] ;
if ((comment.start_wrapper < comment2.start_wrapper)
||
((comment.start_wrapper == comment2.start_wrapper) && (comment.start_offset < comment2.start_offset) )
||
((comment.start_wrapper == comment2.start_wrapper) && (comment.start_offset == comment2.start_offset) && (comment.end_wrapper < comment2.end_wrapper) )
||
((comment.start_wrapper == comment2.start_wrapper) && (comment.start_offset == comment2.start_offset) && (comment.end_wrapper == comment2.end_wrapper) && (comment.end_offset < comment2.end_offset) ) ) {
a.splice(j, 0, comment.id) ;
found = true ;
break ;
}
}
if (!found) {
a.push(comment.id) ;
}
}
this.ordered_comment_ids['scope'] = a ;
// modified thread
a = [] ;
var mod = {} ; // we'll aggregate modification dates in this assoc array id --> latest modification
for (var i = 0, ilen = this.allComments.length ; i < ilen ; i++) {
var comment = this.allComments[i] ;
var commentModif = comment.modified ;
mod[comment.id] = this._latest_mod(comment);
}
for (var id in mod) {
var numberId = this.allCommentsByDbId[id].id
var found = false ;
for (var i = 0, ilen = a.length ; i < ilen ; i++) {
var id2 = a[i] ;
if (mod[id] < mod[id2]) {
a.splice(i, 0, numberId) ;
found = true ;
break ;
}
}
if (!found) {
a.push(numberId) ;
}
}
this.ordered_comment_ids['modif_thread'] = a ;
},
// Finds recursively the last modification date of a thread.
_latest_mod : function(comment) {
var latest_mod = comment.modified;
for (var i=0; i<comment.replies.length; i++) {
var reply = comment.replies[i] ;
var reply_mod = this._latest_mod(reply);
if (reply_mod > latest_mod)
latest_mod = reply_mod;
}
return latest_mod;
},
// EDIT OR ADD CASE : when just added id is max and so both (comments and replies) initial id asc order remains
_upd : function(arr, dic, c) {
var found = false ;
for (var i = 0, ilen = arr.length ; i < ilen ; i++) {
if (arr[i].id == c.id) { // edit
arr.splice(i, 1, c) ;
found = true ;
break ;
}
}
if (!found) { // add
arr.push(c) ;
}
dic[c.id] = c ;
},
// EDIT OR ADD CASE : when just added id is max and so both (comments and replies) initial id asc order respected
upd : function(comment) {
var arr = (comment.reply_to_id == null) ? this.allComments : this.allCommentsByDbId[comment.reply_to_id].replies ;
this._upd(arr, this.allCommentsByDbId, comment) ;
var cloneComment = CY.clone(comment) ;
arr = (comment.reply_to_id == null) ? this.comments : this.commentsByDbId[comment.reply_to_id].replies ;
this._upd(arr, this.commentsByDbId, cloneComment) ;
this._reorder() ;
},
// initializes this.comments
// commentId is the result of a computeFilterResults call : no assumption can be made on the order of ids (!)
// so we'll loop through allComments to carry order from allComments to comments
initComments : function(commentIds) {
this.comments = [] ;
for (var i = 0, ilen = this.allComments.length ; i < ilen ; i++) {
var index = CY.Array.indexOf(commentIds, this.allComments[i].id) ;
if (index != -1) {
var cloneComment = CY.clone(this.allComments[i]) ;
this.comments.push(cloneComment) ;
}
}
this._computeCommentsByDbId() ;
},
_computeCommentsByDbId : function() {
this.commentsByDbId = {} ;
var flatComments = this.getThreads(this.comments) ;
for ( var i = 0; i < flatComments.length; i++)
this.commentsByDbId[flatComments[i].id] = flatComments[i];
},
_computeAllCommentsByDbId : function() {
this.allCommentsByDbId = {} ;
var flatComments = this.getThreads(this.allComments) ;
for (var i = 0; i < flatComments.length; i++)
this.allCommentsByDbId[flatComments[i].id] = flatComments[i];
},
// returns threads :
// given an array [comment1, comment2, comment3], this function will return [comment1, comment1reply1, comment1reply1reply1, comment1reply1reply2, comment2, comment3, comment3reply1]
//note : will return top parents ordered the way comments are
getThreads : function(comments) {
var ret = [] ;
for (var i = 0 ; i < comments.length ; i++) {
ret.push(comments[i]) ;
if (comments[i].replies.length > 0)
ret = ret.concat(this.getThreads(comments[i].replies)) ;
}
return ret ;
},
_getPath : function(dic, comment) {
var ret = [comment] ;
var c = comment ;
while (c.reply_to_id != null) {
c = dic[c.reply_to_id] ;
ret.push(c) ;
}
return ret ;
},
// returns comments as array : [comment, ..., comment's top parent]
getPath : function(comment) {
return this._getPath(this.commentsByDbId, comment) ;
},
// getCommentFromIComment ...
getComment : function(dbId) {
return this.commentsByDbId[dbId] ;
},
getCommentByIdKey : function(id_key) {
for (var id in this.commentsByDbId) {
var comment = this.commentsByDbId[id] ;
if (comment.id_key == id_key) {
return comment ;
}
}
return null ;
},
isChild : function(commentDbId, parentDbId) {
var comment = this.commentsByDbId[commentDbId] ;
var isChild = (commentDbId == parentDbId) ;
while ((!isChild) && (comment.reply_to_id != null)) {
comment = this.commentsByDbId[comment.reply_to_id] ;
isChild = (comment.id == parentDbId) ; ;
}
return isChild ;
},
initToReadOnly : function(commentDbId, parentDbId) {
for (var i = 0, ilen = this.allComments.length ; i < ilen ; i++) {
var comment = this.allComments[i] ;
for (var prop in comment) {
if (0 == prop.indexOf("can_") && typeof comment[prop] === 'boolean')
comment[prop] = false ;
}
}
},
//////////////////////////////
// BROWSING FUNCTIONS
//////////////////////////////
browsingIndex : function(dbId) {
var indx = {} ;
for (var order in this.ordered_comment_ids) {
var inFilter = CY.Array.filter(this.ordered_comment_ids[order], function(id) {return (id in this.commentsByDbId);}, this) ;
indx[order] = CY.Array.indexOf(inFilter, dbId ) ;
}
//indx['total'] = this.ordered_comment_ids['scope'].length
return indx ;
},
browse : function(order, whereto, dbId) {
//var arr = this.ordered_comment_ids[gConf['defaultBrowsingOrder']] ;
// CY.log(order) ;
var arr = this.ordered_comment_ids[order] ;
if (arr.length > 0) {
var starti = -1 ;
if ((whereto == 'prev') ||Â (whereto == 'next')) {
for (var i = 0 ; i < arr.length ; i++) {
var id = arr[i] ;
if (id == dbId) {
starti = (whereto == 'prev') ? i - 1 : i + 1 ;
starti = (arr.length + starti) % arr.length ; // to guaranty a positive value
break ;
}
}
if (starti == -1) {
CY.error("internal error in db browse (was called with a dbId that isn't among the filtered ones)") ;
return null;
}
}
if (whereto == 'last') {
starti = arr.length - 1 ;
}
if (whereto == 'first') {
starti = 0 ;
}
for (var i = starti, j = 0 ; (i >= 0) && (i < arr.length) ; j++ ) {
var id = arr[i] ;
if (id in this.commentsByDbId) // checking id is among the filtered ones
return this.commentsByDbId[id] ;
if ((whereto == 'prev') ||Â (whereto == 'last'))
i = i - 1 ;
else
i = i + 1 ;
i = (arr.length + i) % arr.length ; // to guaranty a positive value
if (j > arr.length)// to prevent an infinite loop
break ;
}
CY.error("internal error in db browse (could not find any filtered comment)") ;
}
return null;
},
//////////////////////////////
// FILTER FUNCTIONS
//////////////////////////////
//returns the list of commentIds satisfying the filter
computeFilterResults : function(filterGETValues) {
var filterData = {} ;
if (filterGETValues) {
for (key in filterGETValues) {
if (key.indexOf('filter_') == 0)
filterData[key.substr('filter_'.length)] = filterGETValues[key];
}
}
else {
if (gLayout.isInFrame())
filterData = parent.f_getFrameFilterData() ;
}
var cWithNameIds = [] ;
var rWithNameIds = [] ;
var filterName = "" ;
if ('name' in filterData)
filterName = filterData['name'] ;
this.filterByName(filterName, cWithNameIds, rWithNameIds) ;
var cAfterDateIds = [] ;
var rAfterDateIds = [] ;
var filterDate = "" ;
if ('date' in filterData)
filterDate = filterData['date'] ;
this.filterByDate(filterDate, cAfterDateIds, rAfterDateIds) ;
var cWithTextIds = [] ;
var rWithTextIds = [] ;
var filterText = "" ;
if ('text' in filterData)
filterText = filterData['text'] ;
this.filterByText(filterText, cWithTextIds, rWithTextIds) ;
var cWithTagIds = [] ;
var rWithTagIds = [] ;
var filterTag = "" ;
if ('tag' in filterData)
filterTag = filterData['tag'] ;
this.filterByTag(filterTag, cWithTagIds, rWithTagIds) ;
var cWithCatIds = [] ;
var rWithCatIds = [] ;
var filterCat = "" ;
if ('cat' in filterData)
filterCat = filterData['cat'] ;
this.filterByCat(filterCat, cWithCatIds, rWithCatIds) ;
var cWithStateIds = [] ;
var rWithStateIds = [] ;
var filterState = "" ;
if ('state' in filterData)
filterState = filterData['state'] ;
this.filterByState(filterState, cWithStateIds, rWithStateIds) ;
var commentIds = [] ;
var replyIds = [] ;
// find intersections
for (var i = 0, ilen = cWithNameIds.length ; i < ilen ; i++) {
var id = cWithNameIds[i] ;
if ((CY.Array.indexOf(cAfterDateIds, id) != -1) && (CY.Array.indexOf(cWithTextIds,id) != -1) && (CY.Array.indexOf(cWithTagIds,id) != -1) && (CY.Array.indexOf(cWithCatIds,id) != -1) && (CY.Array.indexOf(cWithStateIds,id) != -1)) {
commentIds.push(id) ;
}
}
for (var i = 0, ilen = rWithNameIds.length ; i < ilen ; i++) {
var id = rWithNameIds[i] ;
if ((CY.Array.indexOf(rAfterDateIds,id) != -1) && (CY.Array.indexOf(rWithTextIds,id) != -1) && (CY.Array.indexOf(rWithTagIds,id) != -1) && (CY.Array.indexOf(rWithCatIds,id) != -1) && (CY.Array.indexOf(rWithStateIds,id) != -1)) {
replyIds.push(id) ;
}
}
var nbReplies = replyIds.length, nbComments = commentIds.length ;
var nbDiscussions = nbComments ;
// look for comments to add because a reply satisfies the filter
// CY.log('replyIds:') ;
// CY.log(replyIds) ;
// CY.log('this.allCommentsByDbId :');CY.A
// CY.log(this.allCommentsByDbId);
for (var i = 0, ilen = replyIds.length ; i < ilen ; i++) {
var id = replyIds[i] ;
var reply = this.allCommentsByDbId[id] ;
var parents = this._getPath(this.allCommentsByDbId, reply) ;
var topComment = parents[parents.length - 1] ;
var id = topComment.id ;
if (CY.Array.indexOf(commentIds,id) == -1) {
commentIds.push(id) ;
nbDiscussions++ ;
}
}
return {'commentIds': commentIds,'nbDiscussions':nbDiscussions, 'nbComments':nbComments, 'nbReplies':nbReplies} ;
},
filterByText : function(text, cWithTextIds, rWithTextIds) {
var re = new RegExp(text, "gi");
for (var id in this.allCommentsByDbId) {
var comment = this.allCommentsByDbId[id] ;
if (text == "" || re.exec(comment.title) != null || re.exec(comment.content) != null) { // search only in the comment (not the comment scope) for now
if (comment.reply_to_id == null)
cWithTextIds.push(comment.id);
else
rWithTextIds.push(comment.id) ;
}
}
},
filterByName : function(name, cWithNameIds, rWithNameIds) {
for (var id in this.allCommentsByDbId) {
var comment = this.allCommentsByDbId[id] ;
if (name == "" || comment.name == name) { // sensitive exact match for now
if (comment.reply_to_id == null)
cWithNameIds.push(comment.id);
else
rWithNameIds.push(comment.id) ;
}
}
},
// warning : tags are case sensitive
filterByTag : function(tag, cWithTagIds, rWithTagIds) {
// cf ", ".join... in client.py
var re0 = new RegExp("^" + tag + "$", '');
var re1 = new RegExp("^" + tag + ", ", '');
var re2 = new RegExp(", " + tag + ", ", '');
var re3 = new RegExp(", " + tag + "$", '');
for (var id in this.allCommentsByDbId) {
var comment = this.allCommentsByDbId[id] ;
if ((tag == "") || re0.exec(comment.tags) != null || re1.exec(comment.tags) != null || re2.exec(comment.tags) != null || re3.exec(comment.tags) != null) { // search only in the comment (not the comment scope) for now
if (comment.reply_to_id == null)
cWithTagIds.push(comment.id);
else
rWithTagIds.push(comment.id) ;
}
}
},
filterByCat : function(cat, cWithCatIds, rWithCatIds) {
for (var id in this.allCommentsByDbId) {
var comment = this.allCommentsByDbId[id] ;
if (cat == undefined || cat == "" || comment.category == cat) {
if (comment.reply_to_id == null) {
cWithCatIds.push(comment.id);
if (comment.replies.length) {
for (var reply in comment.replies) {
rWithCatIds.push(comment.replies[reply].id) ;
}
}
}
}
}
},
filterByState : function(state, cWithStateIds, rWithStateIds) {
for (var id in this.allCommentsByDbId) {
var comment = this.allCommentsByDbId[id] ;
if (state == "" || comment.state == state) {
if (comment.reply_to_id == null)
cWithStateIds.push(comment.id);
else
rWithStateIds.push(comment.id) ;
}
}
},
filterByDate : function(date_str, cAfterDateIds, rAfterDateIds) {
var date = (date_str == "") ? 0 : parseInt(date_str) ;
for (var id in this.allCommentsByDbId) {
var comment = this.allCommentsByDbId[id] ;
if (comment.modified > date) {
if (comment.reply_to_id == null)
cAfterDateIds.push(comment.id);
else
rAfterDateIds.push(comment.id) ;
}
}
},
// filterByDate : function(date_str, cAfterDateIds, rAfterDateIds) {
// var date = (date_str == "") ? "" : Date.parseDate(date_str, sv_client_date_fmt).getTime() ;
// for (var id in this.allCommentsByDbId) {
// var comment = this.allCommentsByDbId[id] ;
// // TODO : created should be the date not a string !!
// var create_date = (date_str == "") ? "" : Date.parseDate(comment.created_str, sv_client_date_fmt).getTime() ;
// if (date_str == "" || create_date > date) {
// if (comment.reply_to_id == null)
// cAfterDateIds.push(comment.id);
// else
// rAfterDateIds.push(comment.id) ;
// }
// }
// },
//////////////////////////////
// COUNT FUNCTIONS
//////////////////////////////
getCommentsAndRepliesCounts : function(all) {
var cCount = 0 ;
var rCount = 0 ;
var arr = (all) ? this.allComments:this.comments;
var flatComments = this.getThreads(arr) ;
for ( var i = 0; i < flatComments.length; i++) {
if (flatComments[i].reply_to_id == null)
cCount++ ;
else
rCount++ ;
}
return [cCount, rCount] ;
},
// counts both comments and comments
getCommentsNb : function(all) {
var arr = (all) ? this.allComments:this.comments;
return this.getThreads(arr).length ;
},
getFilteredCommentIdsAsString : function() {
var ret = "" ;
for (var id in this.commentsByDbId)
ret = ret + id + "," ;
return ret ;
}
}