- // ==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;
- }
- })();