// ==UserScript==
// @name [Trello] Advanced Comments
// @name:tr [Trello] Gelişmiş Yorumlar
// @description It makes comment groups for repliying comments on Trello.
// @description:tr Trello'da birbirine cevap olarak yazılmış yorumları gruplar.
// @author nht.ctn
// @namespace https://github.com/nhtctn
// @license MIT
// @version 1.0
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEa8AABGvAff9S4QAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuOWwzfk4AAAXvSURBVHhe7Zt7bBRFHMePRCL4iI9EiVG6t3d716MUSimCAbQ+QiUqRg0xEBMT/8FHYkx8RAKJ/QPambnrgxIk1ESNJmICCYTER0h8EBVEgloDWC3t3e3sXssrAhVp76C3/rYdIge/a3ePa+/G3Df5pE1mfzO/329nZmd29jwllVRSSeOt5c37ps6JdFXMDEfrfJSv0ML6ywo1X59Oe4ex/9cYf0Vj8edCjdG6+c3RqtpNh28S5nKptt66rpweXRgM66tVyrep1Oguo+ZQGU1Y7jDTXmIaYL/Lx/g7IUjefc18qmimuDQ70nFjgPGVcHe3geOn8YDywoDK+Bdeoq+aRfTbRPOFk9rI74W7/JFCjX8QZ8cVGDYpL+Pb/Sz2kMeyJgmXJkLWpACJP64y80fMsUKgUrPDR/jT456IQMRYAGNyH+ZEMeCl5sEQ47XC3fypnHXeDOO7HbpdGmu4uDDTMCw/rGqN3Srcvzb5mD7PS40o3ljxAvMSD1J9sQgjN/nDxvOQ0STWgByYKV/YeFGE406wKHnb7k54xXKhhY11Iixngmf6W1hFo/HUxyetPdFB68jx1Kh81T1gLf3gxFX2Xpaw2J6zVkcvbneJQ30pa8v+vy0t0ntVHaMBq09nSbCXqm7vfGVrn3V2cMhyqhPnhqxAU2YAa3afEaXOtBmScLm9E+zltggTl6+BV0Lwrhc1z249KdxyriXvZ/aCnUfOixJn+jWRzLB3BDFTfhpfKMLNlNbWdT085n5DDcdgxaenhFvOdeUw2PW7uwTYQ+Vye+cYsYp6ZKOlEn0NbjA2ciUgYfmo2SLCHtGCDT3TFGKewy52gmwJAJLlJOYV4duLHbMRucgxEiYAeoGxaTj4mnZrMoz9E9hFTpExAQox+pX62BRPMMyXYhe4QcYE2PiZ/iRMfrwVK3SDrAnwUf1dj8qM77FCN0jcA37ywFhIYIVukDUBMPf1eryEX8QK3SBrAmCbP+SBpS9a6IZ8JGDHYXcJ+CWXpTAC9ABjECtwQz4S8Mbnp0WJM7X+0J9hnwsKNZIelRpxrNAN+UiAAqyFHeHe+KB1wEhmZT9PWpHv+i01nOlDLngp57D9NXZjhW7IRwIKgcr41x7YI6/HCt0gawKgBzQMv+rGCt0gawLK7HcD4MskmAdi+AXOkDEBCoHxf+kgBZ4Er2EXOUXGBMDct3o4eFv2UTQ8Eo5hFzpBtgTACvBk7ZUHJyNnALjBWMiWgKxnBTAXfIkZjIVMCYC9zx77gFeEnKnKhp5pCksYmOFoyJMA89gsatwjwsWlkVh1GTH78Qpw7m8/bqVFYE50Yciy5m7sQ+saN0jifDAcXyTCHF2hJn2JAgZoRVlo29tvXXRwNpK6mLYavjmL1jFewKSX0lj8CRGeM5VHYg+C4RmswmzMaesbPvDIznFr9oaJvfP2225Y8T0qwnInLRKv8DLzKFaxHBjczxJzRTi5qYb23AJPh614A8UL3PnPtIauO0QY164AjT0DQ8LEGismYEF3ykf5C1kfddei2ZHuOyEJaMOFZvrIJ3RtFc38duFu/qXR+HKs8YJCjHMqM9vmR/5QhZvjJ5gLdqBOFACV8g4v4a8u3jxBH0xWR6IKrKQuYM5MCMRMwdje56f62sqmWEi4NXGC7WMT6phjzDQE4eDLE/szN+M0zDUHVKJ/EmD6mmDEeLimPXGDcGXiVUF7ysCxnD+HtRdUPhZ/pLb+2ynV635WZpHOmpnh7kVV4a66qpZoHfSuB6pZ57wq0umtqT9YuEBRjbwxymmXaAOPpUOhxp6gqE0++Sl/EwtsbKArM3NL0X7i7kQa0ZflNPERIx5gscdENXIq2KQvhuAH0ACzYO8gYbiss38/IKqRUzNa9Jkwcf2FBYljJn2MvxdY/+fdogp5ZS8lISCHx2XmMZgjaKglfpcwl18KM1/Cg72EOaAQvt1eFtesOjhZmP1/NHJS9N+PnBRipL3UOAJje6NGepYtkX18O9GMSHQBjOmV9l9pf75WUkkllTSmPJ5/AYI7+zVxRO70AAAAAElFTkSuQmCC
// @match https://trello.com/*
// @grant GM_addStyle
// @run-at document-end
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// ==/UserScript==
/* global $ */
/*jshint esversion: 6 */
(function() {
'use strict';
// Bu betiğe özgü fonksiyonlar
const c = (x) => console.log(x);
const link = (x) => $(x).closest('.phenom').find('.phenom-date > a.date, .phenom-meta > a.date').attr('href');
const date = (x) => new Date( $(x).closest('.phenom').find('.phenom-date > a.date, .phenom-meta > a.date').attr('dt') ).getTime();
const type = (x) => ($(x).closest('.phenom').is('.mod-comment-type')) ? "com" : "attach";
const getLang = () => $('html').attr("lang");
const getItem = (x) => {
if ($.type(x) == "string") return $('.card-detail-window a.date[href*="' + x + '"]').closest('.phenom');
else if ($.type(x) == "object") return $(x).closest('.phenom');
else return c( 'id fonksiyonunda belirsiz tip: ' + $.type(x) );
};
const id = (x) => {
if ($.type(x) == "string") return x.replace(/.+\#((comment|action)-.+)/, "$1");
else if ($.type(x) == "object") return $(x).closest('.phenom').find('a.date').attr('href').replace(/.+\#((comment|action)-.+)/, "$1");
else return c( 'id fonksiyonunda belirsiz tip: ' + $.type(x) );
};
const key = (x) => {
if ($.type(x) == "string") return x.replace(/(.+)?(comment|action|group|replies|)-(.+)/, "$3");
else if ($.type(x) == "object") return $(x).closest('.phenom').find('a.date').attr('href').replace(/.+\#(comment|action)-(.+)/, "$2");
else return c( 'key fonksiyonunda belirsiz tip: ' + $.type(x) );
};
let removeSlctr = `p > a:first-child[href*="trello.com"][href*="#comment-"], p > a:first-child[href*="trello.com"][href*="#action-"],
p > span.atMention:nth-child(2), span.atMention:nth-child(3),
p > br:nth-child(2), p > br:nth-child(3), p > br:nth-child(4),
.phenom-reactions .js-attach-link`;
// Yorum grubu oluştur.
function groupDiv(target, comfor, link) {
getItem(target).before('<div id="group-' + key(comfor) + '"><div id="replies-' + key(comfor) + '"></div></div>');
getItem(comfor).prependTo( '.card-detail-window #group-' + key(comfor) );
getItem(link).prependTo( '.card-detail-window #replies-' + key(comfor) );
let dummyCom = $('.card-detail-window .window-module > .new-comment.js-new-comment').clone().removeClass("js-new-comment is-focused is-show-controls").addClass("new-group-comment");
dummyCom.find('.js-new-comment-input').removeClass('js-new-comment-input').css("height", "20.0348px").val("");
dummyCom.appendTo('.card-detail-window #group-' + key(comfor)).wrap('<div class="group-comment-div"></div>');
$('.card-detail-window #group-' + key(comfor) + ' .new-group-comment').click(function(){ textAreaShifter(this); });
}
// Bir kart ilk açıldığındaki işlemler
let cardUrl = '';
waitForKeyElements('.card-detail-window p.u-bottom a.show-more.js-show-all-actions', cardWindow);
function cardWindow () {
things = [];
cardUrl = window.location.href.replace(/(.+trello\.com\/c\/.+\/\d+).+/, "$1");
let intCount = 0;
// Yeterince bekleyip tüm etkinlikleri göstere tıkla
let myTimeOut = setInterval(function () {
let moreButton = $('.card-detail-window p.u-bottom a.show-more.js-show-all-actions').removeAttr("href").css("cursor", "pointer");
moreButton[0].click();
if ( $('.card-detail-window .js-show-all-actions').is(":hidden")) clearInterval(myTimeOut);
else if (intCount++ > 40) clearInterval(myTimeOut);
}, 500);
GM_addStyle(`
.window-overlay > .window {width: 868px;}
.card-detail-window > .window-main-col {width: 652px;}
.card-detail-window > .window-sidebar {width: calc(100% - 700px);}
.mod-card-back > [id^="group-"] {padding: 8px 0 2px 0;}
[id^="group-"] [id^="group-"] {padding: 0 0 6px 0;}
[id^="group-"] > [id^="replies-"] {padding-left: 6%;}
[id^="group-"] .mod-comment-type:not(.mod-highlighted) {padding: 0 0 6px 0}
[id^="group-"] .mod-comment-type.mod-highlighted {padding: 0 0 6px 48px}
.group-comment-div {padding-left: 6%; padding-top: 4px}
.artificialReply {text-decoration: none;}
.artificialReply:hover {text-decoration: underline;}
`);
}
// Yeni bir etkinlik saptandığındaki işlemler
let things = [];
waitForKeyElements('.card-detail-window .mod-card-back .phenom a.date', function(newElement){ addUnseen(newElement); });
function addUnseen(el) {
// Yeniyse kaydet.
let isUnseen = true;
for (let x = 0; x < things.length; x++) {
isUnseen = (things[x].link == link(el)) ? false : true;
if(!isUnseen) break;
}
if (isUnseen) {
let newThings = [];
$('.card-detail-window .mod-card-back .phenom a.date').each(function(){
newThings.push({
link: link( $(this) ),
id: id( $(this) ),
date: date( $(this) ),
type: type( $(this) ),
comFor: findReply( $(this) ),
});
things = uniqArray(newThings);
arraySorter(things, "object", "date");
});
//console.log(things);
newThing();
}
}
function findReply(this_) { // Buraya başka kartın yorum linki olmasın diye koşul koyulacak.
if (type(this_) == "com") {
let firstNd = $(this_.closest('.mod-comment-type').find('.current-comment p')[0].firstChild);
if (firstNd.is("a") && firstNd.attr("href").search(/trello\.com\/.+\#(comment|action)-/) > 0 ) return id( firstNd.attr("href") );
else return null;
}
else return null;
}
// Bir tepkiye ilk raslandığındaki işlemler
waitForKeyElements('.card-detail-window .mod-comment-type > .phenom-reactions .reactions-add-icon', function(elem) {moveReactions(elem);});
function moveReactions(this_) {
// Tepkilerin yerini değiştir
let react = this_.closest('.phenom-reactions');
let com = this_.closest('.mod-comment-type');
com.attr("id", id(this_));
com.find('.phenom-meta').css("display", "contents");
if (com.find('.rightOfDate').length <= 0) {
com.find('.phenom-date').after('<div class="rightOfDate" style="display: contents;"></div>');
react.css("display", "inline-flex").css("float", "right").css("margin-right", "8px").appendTo('#' + id(this_) + ' .rightOfDate');
}
// Yanıtla butonuyla link ekle
let cardUrl = window.location.href.replace(/(.+trello\.com\/c\/.+\/\d+).+/, "$1");
react.find('.js-reply-to-action, .js-reply-to-all-action').click(function(){
$('.js-new-comment textarea.comment-box-input').val( cardUrl + '#' + id(this_) );
});
// Silme işleminde yeniden sırala
react.find(".js-confirm-delete-action").click(function(){
waitForKeyElements('.js-confirm.nch-button--danger', function(){
$('.js-confirm.nch-button--danger').one("click", function(){
setTimeout( function() {newThing();}, 200);
});
}, true);
});
}
// Başkası yorum silip sayfa bozulduysa diye arada kontrol et.
setInterval(function(){
if($('[id^="group-"]'). length > 0 && $('[id^="group-"] > [id^="replies-"]:first-child'). length > 0) {
newThing();
}
}, 5000);
function putReply(el) {
// Actionlara yanıtla butonu ekle.
if (el.type == "attach" && $('#' + el.id + ' .artificialReply').length <= 0) {
let reply = ($('html').attr("lang") == "tr") ? "Yanıtla" : "Reply";
$('#' + el.id + ' a.date').after(' - <a class="js-reply-to-all-action artificialReply" href="#">' + reply + '</a>');
$('#' + el.id + ' .artificialReply').click(function(){
let mentions = replyMentions(getItem(this));
let val = (cardUrl + '#' + id(this) + ' ' + mentions + ' ');
$('.js-new-comment textarea.comment-box-input').val(val);
$('.js-new-comment textarea.comment-box-input').focus();
});
}
}
// Yeni bir eylem saptandığındaki işlemler
function newThing() {
if ($('[id^="group-"]').length > 0) clearResiduals();
for (let x = 0; x < things.length; x++) {
// Her bir eyleme id ekle.
let th = things[x];
if (getItem(th.id).attr("id") == undefined) getItem(th.id).attr("id", th.id);
// Eylem yorumsa
let isComForExist = (th.comFor != null && getItem(th.comFor).length > 0);
if (isComForExist) {
let isComForNonThreaded = getItem(th.comFor).closest('[id^="group-"]').length <= 0;
let isComForRegular = getItem(th.comFor).is('[id^="group-"] .mod-comment-type:first-child, [id^="replies-"] .mod-comment-type:last-child');
if (isComForNonThreaded) {
// Yorumun atası herhangi bir grupta değil. Yeni grup aç.
groupDiv(th.id, th.comFor, th.id);
}
else {
// Yorumun atası bir grupta. Toptan grubu yorumun olduğu yere taşı.
let repliesId = getItem(th.comFor).closest('[id^="group-"]').find('[id^="replies-"]').attr("id");
if (repliesId != getItem(th.id).closest('[id^="group-"]').find('[id^="replies-"]').attr("id")) { //Birbirini hedef gösteren yorumlarda buga girmesin diye.
$('#' + repliesId).closest('.mod-card-back > [id^="group-"]').insertBefore(getItem(th.id));
if (isComForRegular) {
getItem(th.id).appendTo( '#' + repliesId ); // Ata grubun atası ya son mesajıysa yorumu grubun altına taşı.
}
else {
groupDiv(th.comFor, th.comFor, th.id); // Ata grubun ortasındaysa yeni alt grup aç.
}
}
}
// Cevap olarak yazılmış bir yorum ise linki, alıntıları, satır atlamayı, bağlantı olarak ekle seçeneğini gizle.
getItem(th.id).find(removeSlctr).hide();
// c(comItem(th.id).find(removeSlctr)[0].getAttribute("style"));
let attachLinks = getItem(th.id).find('.phenom-reactions .js-attach-link');
let afterAttach = $(attachLinks[0].nextSibling);
if (!(afterAttach.is('a, span'))) afterAttach.remove();
}
else {
putReply(th);
}
}
// Çift grup kontrol.
if ($('[id^="group-"] [id^="group-"]').length > 0) doubleGroup();
}
function clearResiduals() {
//c("residual clean");
$('.group-comment-div, #newCommentWatcher').remove();
$('[id^="replies-"]').children(':first-child').unwrap('[id^="replies-"]');
$('[id^="group-"]').children(':first-child').unwrap('[id^="group-"]');
let residual = $('[id^="group-"], [id^="replies-"], .group-comment-div');
for (let x = 0; x < residual.length; x++) {
if ( $(residual[x]).children('.mod-comment-type').length <= 0 ) {
$(residual[x]).remove();
}
}
}
// Grup içindeki grupları denetle, gerekliyse dışarı at
function doubleGroup() {
//c("doubleGroup");
$('[id^="group-"] [id^="group-"] > .mod-comment-type .current-comment p').each(function(){
let firstNd = $(this.firstChild);
let realGroup = $(this).closest('[id^="group-"] [id^="group-"]');
if (firstNd.is("a") && firstNd.attr("href").search(/trello\.com\/+c\/.+\/.+\#comment-/) > 0 ) {
// Burayı henüz test edemedim.
let href = id( $(firstNd).attr("href") );
let isRelated = realGroup.closest('.js-list-actions.mod-card-back > [id^="group-"]').find('a.date[href*="' + href + '"]').length > 0;
if (!isRelated) realGroup.insertBefore( realGroup.closest('.js-list-actions.mod-card-back > [id^="group-"]') );
}
else {
c("doubleGroup problemi çözüldü");
realGroup.insertBefore( realGroup.closest('.js-list-actions.mod-card-back > [id^="group-"]') );
}
});
}
// Grupların altındaki klon yorum alanlarına tıklandığında
function textAreaShifter(dum) {
let val = '';
if ( $(dum).closest('[id^="group-"]').length > 0 ) {
// Klon grubun altındaysa içine yazdırılacak yazıyı oluştur.
let lastGroupCom = $(dum).closest('[id^="group-"]').children('[id^="replies-"]').children('.mod-comment-type:last-of-type');
let pageUrl = window.location.href.replace(/(.+trello\.com\/c\/.+\/\d+).+/, "$1");
let mentions = replyMentions(lastGroupCom);
val = (pageUrl + '#' + id( link(lastGroupCom) ) + ' ' + mentions + ' ');
// Grubun altında işi kalmayıp aktifliğini kaybettiyse geri yolla ki yanıtla tuşu kullanıldığında yerinde olsun.
let groupComTimeOut = setInterval(function () {
let pasiveGroupCom = $('.card-detail-window .js-new-comment:not(.is-focused, .card-detail-window .is-show-controls)');
if (pasiveGroupCom.length > 0) {
swaper($('.card-detail-window .js-new-comment'), $('.card-detail-window .window-module > .new-group-comment'));
$('.card-detail-window .js-new-comment .js-new-comment-input').val("");
clearInterval(groupComTimeOut);
}
}, 100);
}
// Klonla orijinal text editor'ü yer değiştir. Klon grup altına da gidebilir, orijinal yorum alanına da.
swaper($('.card-detail-window .js-new-comment'), $(dum).closest('.new-group-comment'));
$('.card-detail-window .js-new-comment .js-new-comment-input').val(val).click().focus();
}
// Yanıtlanacak cevap için atıfları ayarla
function replyMentions(el) {
if (id(el).search(/action-/) >= 0) return $(el).find('.phenom-creator .member-avatar').attr("title").replace(/(.+) \((.+)\)/, "\@$2");
let myName = $('.card-detail-window .new-comment .member-avatar').attr("title").replace(/(.+) \((.+)\)/, "\@$2");
let ment = [];
el.find('.atMention').each(function(){ment.push($(this).text());});
ment.push( el.find('.phenom-creator .member-avatar').attr("title").replace(/(.+) \((.+)\)/, "\@$2") );
ment = arrayRemover(ment, myName);
return ment.toString().replace(/\,/g, " ");
}
// Hazır fonsiyonlar
function uniqArray(array) {
if ($.type(array[0]) == "object") {
let objs = [];
return array.filter(function(item) {
return JSON.stringify(objs).search(JSON.stringify(item)) >= 0 ? false : objs.push(item);
});
}
else {
var seen = {};
return array.filter(function(item) {
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
});
}
}
function arrayRemover(array, removeThis, objectType) {
let resultArray = [];
if ($.type(array[0]) == "object") {
if ($.type(removeThis) == "array") {
for(let o = 0; o < array.length; o++){
for (let i = 0; i < removeThis.length; i++) {
if ( array[o][objectType] == removeThis[i][objectType] ) break;
else if (i+1 == removeThis.length) resultArray.push(array[o]);
}
}
}
else {
resultArray = $.grep(array, function(value) {
return value[objectType] != removeThis;
});
}
}
else {
if ($.type(removeThis) == "array") {
for(let o = 0; o < array.length; o++){
let newArray = [];
for (let i = 0; i < removeThis.length; i++) {
if ( array[o] == removeThis[i] ) break;
else if (i+1 == removeThis.length) resultArray.push(array[o]);
}
}
}
else {
resultArray = $.grep(array, function(value) {
return value != removeThis;
});
}
}
return resultArray;
}
function swaper(el1, el2) {
$(el1).before('<div id="dummyDiv1"></div>');
$(el2).before('<div id="dummyDiv2"></div>');
$(el1).appendTo('#dummyDiv2').unwrap('#dummyDiv2');
$(el2).appendTo('#dummyDiv1').unwrap('#dummyDiv1');
}
function arraySorter(array, objectOrNot, objectType) {
if (objectOrNot == "object") {
array.sort(function(a, b) {
var x = ( isFinite(a[objectType]) ) ? Number(a[objectType]) : a[objectType].toString().toLowerCase();
var y = ( isFinite(b[objectType]) ) ? Number(b[objectType]) : b[objectType].toString().toLowerCase();
if (x < y) {return -1;}
if (x > y) {return 1;}
return 0;
});
}
else if (objectOrNot == "nonObject") {
array.sort();
}
}
function waitForKeyElements (
selectorTxt, /* Required: The jQuery selector string that specifies the desired element(s). */
actionFunction, /* Required: The code to run when elements are found. It is passed a jNode to the matched element. */
bWaitOnce, /* Optional: If false, will continue to scan for new elements even after the first match is found. */
iframeSelector /* Optional: If set, identifies the iframe to search. */
) {
var targetNodes, btargetsFound;
if (typeof iframeSelector == "undefined")
targetNodes = $(selectorTxt);
else
targetNodes = $(iframeSelector).contents().find(selectorTxt);
if (targetNodes && targetNodes.length > 0) {
btargetsFound = true;
/*--- Found target node(s). Go through each and act if they are new. */
targetNodes.each(function() {
var jThis = $(this);
var alreadyFound = jThis.data('alreadyFound') || false;
if (!alreadyFound) {
//--- Call the payload function.
var cancelFound = actionFunction(jThis);
if (cancelFound)
btargetsFound = false;
else
jThis.data('alreadyFound', true);
}
});
}
else {
btargetsFound = false;
}
//--- Get the timer-control variable for this selector.
var controlObj = waitForKeyElements.controlObj || {};
var controlKey = selectorTxt.replace(/[^\w]/g, "_");
var timeControl = controlObj[controlKey];
//--- Now set or clear the timer as appropriate.
if (btargetsFound && bWaitOnce && timeControl) {
//--- The only condition where we need to clear the timer.
clearInterval(timeControl);
delete controlObj[controlKey];
}
else {
//--- Set a timer, if needed.
if (!timeControl) {
timeControl = setInterval(function() {
waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector);
},
300
);
controlObj [controlKey] = timeControl;
}
}
waitForKeyElements.controlObj = controlObj;
}
})();