// ==UserScript==
// @name PimpMyNotabenoid
// @description This script adds some usefull features like hot-keys to notabenoid.org (Notabenoid)
// @namespace [email protected]
//
// @version 7.9.0
//
// @include http://notabenoid.org/*
// @include http://www.notabenoid.org/*
//
// @grant none
// ==/UserScript==
// Wrap whole code into one function. Needed to inject the code into the page
function PimpMyNotabenoid() {
/*
* jQuery Hotkeys Plugin
* Copyright 2010, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Based upon the plugin by Tzury Bar Yochay:
* http://github.com/tzuryby/hotkeys
*
* Original idea by:
* Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
*/
(function(jQuery){
jQuery.hotkeys = {
version: "0.8",
specialKeys: {
8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del",
96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
},
shiftNums: {
"`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&",
"8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<",
".": ">", "/": "?", "\\": "|"
}
};
function keyHandler( handleObj ) {
// Only care when a possible input has been specified
if ( typeof handleObj.data !== "string" ) {
return;
}
var origHandler = handleObj.handler;
var keys = handleObj.data.toLowerCase().split(" ");
handleObj.handler = function( event ) {
// Don't fire in text-accepting inputs that we didn't directly bind to
if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) ||
event.target.type === "text") ) {
return;
}
// Keypress represents characters, not special keys
var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ],
character = String.fromCharCode( event.which ).toLowerCase(),
key, modif = "", possible = {};
// check combinations (alt|ctrl|shift+anything)
if ( event.altKey && special !== "alt" ) {
modif += "alt+";
}
if ( event.ctrlKey && special !== "ctrl" ) {
modif += "ctrl+";
}
// TODO: Need to make sure this works consistently across platforms
if ( event.metaKey && !event.ctrlKey && special !== "meta" ) {
modif += "meta+";
}
if ( event.shiftKey && special !== "shift" ) {
modif += "shift+";
}
if ( special ) {
possible[ modif + special ] = true;
} else {
possible[ modif + character ] = true;
possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true;
// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
if ( modif === "shift+" ) {
possible[ jQuery.hotkeys.shiftNums[ character ] ] = true;
}
}
for ( var i = 0, l = keys.length; i < l; i++ ) {
if ( possible[ keys[i] ] ) {
return origHandler.apply( this, arguments );
}
}
};
}
jQuery.each([ "keydown", "keyup", "keypress" ], function() {
jQuery.event.special[ this ] = { add: keyHandler };
});
})( jQuery );
var U = {
username: null,
orig_hovered_id: "", // "o13857594" id of the hovered translation area (current line)
tr_hovered_id: "", // "t13857594" id of the hovered translation (current translation)
newtr_text: "", // text for the next new variant
init: function() {
U.username = $("#header-submenu strong:first").text();
// inject our CSS
$("<style type='text/css'>\n"
+ ".div_hovered { background-color: #EEE; }\n"
+ ".tdt_hovered { }\n" // вроде и без фона нормально
+ ".deleting { background-color: #f5989d; }\n"
+ ".translator td.tdu_todo { background-color: #EEEEBB }\n"
// чтобы не прыгало из-за появляющихся и исчезающих кнопок на новой строке
+ ".translator td.t a.e, .translator td.t a.x { display: inline; visibility: hidden; }"
+ ".translator tr:hover td.t a.e, .translator tr:hover td.t a.x { visibility: visible; } "
+ "</style>").appendTo("head");
// remove standard edit handlers
$("#Tr")
.off("click", "td.u a.t", T.tr)
.off("click", "td.t a.e", T.tr_edit)
.off("click", "td.t a.x", T.tr_rm);
// inject our own edit handlers
T.tr = U.tr;
T.tr_edit = U.tr_edit;
T.tr_rm = U.tr_rm;
// activate our edit handlers
$("#Tr")
.on("click", "td.u a.t", U.tr)
.on("click", "td.t a.e", U.tr_edit)
.on("click", "td.t a.x", U.tr_rm);
// setup our character counter
// (the native one was started every time the edit box was open, so it overloaded the processor after some work on the page. So we start it only once.)
T.tr_ccnt.update = U.tr_ccnt_update;
setInterval(U.tr_ccnt_update, 1000);
// hovering handlers
$("#Tr").on("mouseenter", "td.t > div", U.tr_mouseenter);
$("#Tr").on("mouseleave", "td.t > div", U.tr_mouseleave);
$("#Tr").on("mouseenter", "td.t", U.orig_mouseenter);
$("#Tr").on("mouseleave", "td.t", U.orig_mouseleave);
// 'e' for edit of the hightlighted variant
$(document).bind("keydown", "e", U.tr_hovered_edit);
// 'v' for new variant of the hightlighted row
$(document).bind("keydown", "v", U.tr_hovered_newtr);
// 'c' for new variant of the hightlighted row as a copy of the highlighted variant
$(document).bind("keydown", "c", U.tr_hovered_newtr_withcopy);
// 'q' for new variant of the hightlighted row as a copy of the original text (creates and save as right away)
$(document).bind("keydown", "q", U.tr_hovered_newtr_asorig);
// 'o' take the translation variant over
$(document).bind("keydown", "o", U.tr_hovered_take_over);
// 'd' for selecting of the hightlighted variant for deletion
$(document).bind("keydown", "d", U.tr_hovered_mark_delete);
// 'y' for confirming a deletion
$(document).bind("keydown", "y", U.tr_marked_delete);
// 'm' for hightlighting rows with multiple variants or with comments
$(document).bind("keydown", "m", U.highlight_todo_rows);
// 'g' to goto line
//$(document).bind("keydown", "g", U.goto_line);
},
log: function (msg) {
if (console) console.log("PMN3: " + msg);
},
getCurrentTextarea: function() {
var $ta = $("#form-tr [name=Translation\\[body\\]]");
if ($ta.size() != 1) return null;
return $ta;
},
getCurrentTextInEditor: function() {
var $ta = U.getCurrentTextarea();
if ($ta == null) return "";
return $ta.val();
},
highlight_todo_rows: function() {
$("#Tr tbody tr").each(function() {
var $tr = $(this);
var hit = ($tr.find("td.u a.c").text().length > 0)
|| ($tr.find("td.t > div").size() != 1);
$tr.find("td.u").toggleClass("tdu_todo", hit);
});
},
goto_line: function() {
var to = prompt("Введите номер абзаца для быстрого перехода", "");
if (to && parseInt(to) != 0) {
var orig_id = "?"; // как-то не найти айдишник строки
location.href = "/book/" + Book.id + "/" + Chap.id + "/" + orig_id;
}
},
tr_mouseenter: function(evt) {
U.tr_hovered_id = $(this).attr("id");
$(this).addClass("div_hovered");
},
tr_mouseleave: function(evt) {
U.tr_hovered_id = "";
$(this).removeClass("div_hovered deleting");
},
orig_mouseenter: function(evt) {
U.orig_hovered_id = $(this).parent().attr("id");
$(this).addClass("tdt_hovered");
},
orig_mouseleave: function(evt) {
U.orig_hovered_id = "";
$(this).removeClass("tdt_hovered");
},
tr_getText: function($tr) {
if (!$tr || !$tr.length) return "";
return $tr.find("span.b").text();
},
tr_hovered_getText: function() {
if (U.tr_hovered_id == "") return "";
return $("#" + U.tr_hovered_id + " span.b").text();
},
tr_hovered_edit: function(evt) {
if (evt) evt.preventDefault();
if (U.tr_hovered_id) {
$("#" + U.tr_hovered_id + " a.e").click();
return false;
}
// if nothing under the mouse than try to add the new translation
return U.tr_hovered_newtr(evt);
},
tr_hovered_newtr: function(evt) {
if (evt) evt.preventDefault();
if (U.orig_hovered_id == "") return false;
$("#" + U.orig_hovered_id + " td.u a.t").click();
return false;
},
tr_hovered_newtr_withcopy: function(evt) {
if (evt) evt.preventDefault();
if (U.orig_hovered_id == "") return false;
U.newtr_text = U.tr_hovered_getText();
return U.tr_hovered_newtr(evt);
},
tr_hovered_newtr_asorig: function(evt) {
if (evt) evt.preventDefault();
if (U.orig_hovered_id == "") return false;
var $tr = $("#" + U.orig_hovered_id);
var orig_text = $tr.find("td.o span.b").text();
U.tr_add_now($tr, orig_text);
return false;
},
tr_hovered_take_over: function(evt) {
if (evt) evt.preventDefault();
if (U.tr_hovered_id == "") return false;
var divId = "#" + U.tr_hovered_id;
var $div = $(divId);
var tr_text = $div.find("span.b").text();
var $tr = $div.closest("tr");
// check user have add new variant button
if ($tr.find("td.u a.t").size() == 0) return;
// check user can delete the variant under mouse
if ($div.find("a.x").size() == 0) return;
// check the variant under mouse is not of user
if ($div.find("a.user").text() == U.username) return;
if (U.tr_add_now($tr, tr_text)) {
$div = $(divId); // the DOM was refreshed by tr_add_now, so get the div again
U.tr_remove($div);
}
},
// adds new variant to row $tr with text new_text
tr_add_now: function($tr, new_text) {
if (!$tr || $tr.size() != 1) return false;
var orig_id = $tr.attr("id").substr(1);
var saved = false;
$.ajax({
url: "/book/" + Book.id + "/" + Chap.id + "/" + orig_id + "/translate",
type: 'POST',
data: {'Translation[body]': new_text, ajax: 1},
async: false,
dataType: "json",
success: function(data) {
if (data.status == "error") return !!alert(data.error);
$tr.children("td.t").html(data.text);
T.setStats(data.n_vars, data.d_vars, data.n_verses);
saved = true;
}
});
return saved;
},
tr_hovered_mark_delete: function(evt) {
if (evt) evt.preventDefault();
if (U.tr_hovered_id == "") return false;
var $div = $("#" + U.tr_hovered_id);
$div.addClass("deleting");
},
tr_marked_delete: function(evt) {
if (evt) evt.preventDefault();
$("td.t > div.deleting").each(function () {
U.tr_remove($(this));
});
},
tr: function(evt) {
if (evt) evt.preventDefault();
T.tr_next = null;
var $tr = $(this).closest("tr");
var orig_id = $tr.attr("id").substr(1);
if (T.editing_start("tradd", orig_id)) return;
var html =
"<div class='tr-editor'><form id='form-tr' method='post' action='/book/" + Book.id + "/" + Chap.id + "/" + orig_id + "/translate'>" +
"<textarea name='Translation[body]'></textarea>" +
"<button type='submit' class='btn btn-mini btn-primary' title='Ctrl+Enter – сохранить и перейти к следующему\nCtrl+Shift+Enter – сохранить.'>Добавить</button> " +
"<button type='button' class='btn btn-mini cancel' onclick='T.editing_stop()'>Отмена</button> " +
"<span id='tr-ccnt' title='Длина в символах'>Оригинал: <b class='o'>?</b> / Перевод: <b class='t'>?</b></span>" +
"</form></div>";
$tr.children("td.t").append(html);
$ta = $("#form-tr textarea");
$ta.val(U.newtr_text);
U.newtr_text = "";
U.tr_setupEditor(orig_id, $ta);
$("#form-tr").ajaxForm({
dataType: "json",
data: {ajax: 1},
beforeSubmit: function() {
$("#form-tr :submit").attr("disabled", true);
},
success: function(data) {
if (data.error) {
$("#form-tr :submit").attr("disabled", false);
alert(data.error);
return false;
}
U.tr_editorFinished("tradd", orig_id, $tr, data);
}
});
},
tr_edit: function(evt) {
if (evt) evt.preventDefault();
T.tr_next = null;
var $tr = $(this).closest("tr");
var orig_id = $tr.attr("id").substr(1);
var $div = $(this).closest("div");
var tr_id = $div.attr("id").substr(1);
var tr_text = $div.find("span.b").text();
T.editing_start("tredit", tr_id);
T.editing_html = $div.html();
var html =
"<div class='tr-editor'><form id='form-tr' method='post' action='/book/" + Book.id + "/" + Chap.id + "/" + orig_id + "/translate?tr_id=" + tr_id + "'>" +
"<textarea name='Translation[body]'></textarea>" +
"<button type='submit' class='btn btn-mini btn-primary'>Сохранить</button> " +
"<button type='button' class='btn btn-mini cancel' onclick='T.editing_stop()'>Отмена</button> " +
(Book.membership.status != 2 ? "<small class='help-inline'>рейтинг будет обнулён</small>": "") +
"<span id='tr-ccnt' title='Длина в символах'>Оригинал: <b class='o'>?</b> / Перевод: <b class='t'>?</b></span>" +
"</form></div>";
$div.html(html);
$ta = $("#form-tr textarea");
$ta.val(tr_text);
U.newtr_text = ""; // clear the var because it may be set previously by Ctlr-Shift-Down but there are already variants
U.tr_setupEditor(orig_id, $ta);
$("#form-tr").ajaxForm({
dataType: "json",
data: {ajax: 1},
beforeSubmit: function() {
if (tr_text == U.getCurrentTextInEditor()) {
U.tr_editorFinished("tredit", tr_id, null, null);
return false;
}
$("#form-tr :submit").attr("disabled", true);
},
success: function(data) {
if (data.error) {
$("#form-tr :submit").attr("disabled", false);
alert(data.error);
return false;
}
U.tr_editorFinished("tredit", tr_id, $tr, data);
}
})
},
tr_editorFinished: function(editing_mode, editing_id, $tr, data) {
// если еще не открыли другой редактор, то закрываем себя
if (T.editing_mode == editing_mode && T.editing_id == editing_id) {
T.editing_stop();
}
if (data && $tr) {
$tr.children("td.t").html(data.text);
T.setStats(data.n_vars, data.d_vars, data.n_verses);
}
// После сохранения нового варианта и он единственный, а след. поле пустое, то прыгать на след. поле
if (!T.tr_next && editing_mode == "tradd" && $tr) {
var $tr_next = $tr.next();
if ($tr_next.size() != 0 && $tr.find("td.t > div").size() == 1 && $tr_next.find("td.t > div").size() == 0) {
T.tr_next = $tr_next.find("td.u a.t");
}
}
// jump to the next edit
setTimeout(function() {
if (T.tr_next && T.tr_next.length) {
T.tr_next.click();
}
}, 0);
},
tr_setupEditor: function(orig_id, $ta) {
T.tr_ccnt.init(orig_id);
U.tr_ccnt_update();
$ta.elastic();
$ta.focus();
U.scrollIntoView($ta);
$ta.bind("keydown", "ctrl+return", function(e) {
$("#form-tr :submit").click();
});
$ta.bind("keydown", "esc", function(e) {
$("#form-tr .cancel").click();
});
$ta.bind("keydown", "ctrl+down", U.tr_save_and_goto_next);
$ta.bind("keydown", "ctrl+shift+down", U.tr_save_and_goto_next_with_orig);
$ta.bind("keydown", "ctrl+up", U.tr_save_and_goto_prev);
$ta.bind("keydown", "ctrl+k", U.ta_korrekt);
},
ta_korrekt: function(evt) {
if (evt) evt.preventDefault();
var lines = U.getCurrentTextInEditor().split(/\n/);
var text = "";
/*
Это тестовый текст.
- Тире в начале строки.
Тире в конце строки -
Тире - бывает и в середине.
"Слово" в "кавычках"
Закрывающая строка
*/
for (var i in lines) {
var line = lines[i];
line = line.replace(/^\-\s/g, "— ");
line = line.replace(/\s\-/g, " —");
line = line.replace(/\s\-\s/g, " — ");
line = line.replace(/\"([\u0400-\u04FF\w])/g, "«$1");
line = line.replace(/([\u0400-\u04FF\w\.\?\!])\"/g, "$1»");
if (i > 0) text += "\n";
text += line;
}
//console.log(text);
U.getCurrentTextarea().val(text);
},
tr_rm: function(evt) {
if (evt) evt.preventDefault();
var $div = $(this).closest("div");
$div.addClass("deleting");
if (!confirm("Вы уверены, что хотите удалить этот вариант перевода? Отменить эту процедуру нельзя.")) {
$div.removeClass("deleting");
return;
}
U.tr_remove($div);
},
tr_remove: function($div) {
var tr_id = $div.attr("id").substr(1);
var $tr = $div.closest("tr");
var orig_id = $tr.attr("id").substr(1);
$div.addClass("deleting");
$.ajax({
url: "/book/" + Book.id + "/" + Chap.id + "/" + orig_id + "/tr_rm",
dataType: "json",
type: "POST",
data: {tr_id: tr_id},
async: false,
success: function(data) {
$div.removeClass("deleting");
if(data.error) {
alert(data.error);
return false;
} else if(data.status == "ok") {
$div.remove();
T.setStats(data.n_vars, data.d_vars, data.n_verses);
} else {
alert("Произошла какая-то ошибка, удалить перевод не удалось. Попробуйте обновить страницу.");
return false;
}
},
error: function(xhr) {
$div.removeClass("deleting");
}
});
},
tr_save_and_goto_next: function(evt, with_orig) {
if (evt) evt.preventDefault();
var $tr = $("#form-tr").closest("tr");
if ($tr.size() == 0) return;
var $tr_next = $tr.next("tr");
U.tr_save_and_goto($tr_next, with_orig);
},
tr_save_and_goto_next_with_orig: function(evt) {
U.tr_save_and_goto_next(evt, true);
},
tr_save_and_goto_prev: function(evt) {
if (evt) evt.preventDefault();
var $tr = $("#form-tr").closest("tr");
if ($tr.size() == 0) return;
var $tr_next = $tr.prev("tr");
U.tr_save_and_goto($tr_next);
},
tr_save_and_goto: function($tr_next, with_orig) {
if ($tr_next == null || $tr_next.size() != 1) return;
// TODO: escape U.username - http://forum.jquery.com/topic/add-a-jquery-selector-escape-function
// try to find user's own best variant
var $but_next = $tr_next.find("td.t div.best:has(a.user:contains('" + U.username + "')) a.e:first");
if ($but_next.size() == 0) {
// try to find user's own variant
$but_next = $tr_next.find("td.t div:has(a.user:contains('" + U.username + "')) a.e:first");
}
if ($but_next.size() == 0) {
// try to find bold variant
$but_next = $tr_next.find("td.t div.best a.e");
}
if ($but_next.size() == 0) {
// try to find any editable variant
$but_next = $tr_next.find("td.t a.e:first");
}
if ($but_next.size() == 0) {
// try to find add new variant button
$but_next = $tr_next.find("td.u a.t");
}
if (with_orig && $but_next.length) {
U.newtr_text = U.tr_getText($tr_next);
}
if ($.trim(U.getCurrentTextInEditor()) == "") {
if (T.editing_mode == "tradd") {
T.editing_stop();
if ($but_next.size() == 1) $but_next.click();
} else if (T.editing_mode == "tredit") {
var $tr = $("#t" + T.editing_id);
T.editing_stop();
U.tr_remove($tr);
if ($but_next.size() == 1) $but_next.click();
}
} else {
T.tr_next = $but_next;
$("#form-tr :submit").click();
}
},
tr_ccnt_update: function() {
var t = U.getCurrentTextInEditor().replace(/\n/g, "");
$("#tr-ccnt b.t").text(t.length);
},
scrollIntoView: function($element) {
if (!$element) return;
var margin = 100;
var containerTop = $(document).scrollTop();
var containerBottom = containerTop + $(window).height();
var elemTop = $element.offset().top;
var elemBottom = elemTop + $element.height();
if (elemTop - margin < containerTop) {
$(document).scrollTop(elemTop - margin);
} else if (elemBottom + margin > containerBottom) {
$(document).scrollTop(elemBottom - $(window).height() + margin);
}
}
};
$(document).ready(function() {
// Добавляем записку о себе в подвал
var $fd = $("footer div.container div.row");
if ($fd.size() != 0) {
$fd.after(
"<div>"
+ "<br>На сайте работает плагин PimpMyNotabenoid"
+ "<br>Разработчики рекомедуют <a href=\"http://www.youtube.com/watch?v=V3uunhQIMIY\">Проект Венера</a>"
+ "<br>Об ошибках в плагине сообщайте <a href=\"http://vk.com/igormukhin\">разработчику</a>"
+ "</div>"
);
}
// Проверяем в переводчике ли мы
if ($("table.translator").size() == 0) {
U.log("Not in translator... Exiting...");
return;
}
// checking notabenoid javascript version
//var supportedTranslateJsVersion = 18;
//if ($("head script[src$='translate.js?" + supportedTranslateJsVersion + "']").size() == 0) {
// U.log("This version of notabenoid's translate.js is not supported.");
// return;
//}
// Начинаем
U.log("Initializing...");
U.init();
U.log("Finished initialization...");
});
} // end of PimpMyNotabenoid()
/*
* Inject the whole script into the page. We will need to manipulate internal objects of Notabenoid engine.
*/
(function page_scope_runner() {
var script = document.createElement('script');
script.setAttribute("type", "text/javascript");
script.textContent = "(" + PimpMyNotabenoid.toString() + ")();";
document.body.appendChild(script);
})();