AuthorTodayImprover

The script allows you to select text and use the context menu when reading a book.

Instalează acest script?
Script sugerat de autor

Poate îți va plăcea șiAuthorTodayExtractor.

Instalează acest script
// ==UserScript==
// @name           AuthorTodayImprover
// @name:ru        AuthorTodayImprover
// @namespace      90h.yy.zz
// @version        0.2.0
// @author         Ox90
// @match          https://author.today/reader/*
// @description    The script allows you to select text and use the context menu when reading a book.
// @description:ru Скрипт возвращает возможность выделять текст и использовать контекстное меню при чтении на сайте.
// @run-at         document-start
// @license        MIT
// ==/UserScript==

(function start() {
	"use strict";

const reader_selectors = [ "#reader.page-content", "div.text-wrapper" ];

let active = true;

/**
 * Старт скрипта. Вызывается после загрузки DOM-дерева.
 *
 * @return void
 */
function run() {
	if (testNoselect()) {
		active = getValue("disabled", "no") !== "yes";
		if (active) processReaderElements();
		updateToolbar();
	}
}

/**
 * Обрабатывает все возможные элементы читалки по списку селекторов
 *
 * @return void
 */
function processReaderElements() {
	reader_selectors.forEach(sel => {
		const el = document.querySelector(sel);
		el && processElement(el);
	});
}

/**
 * Обрабатывает переданный DOM-элемент.
 * Если у элемента нет класса loading, то он обрабатывается немедленно,
 * в ином случае на него вешается наблюдатель, а обработка откладывается.
 *
 * @param Element el Обрабатываемый DOM-элемент
 *
 * @return void
 */
function processElement(el) {
	if (!el.classList.contains("loading")) {
		fixStyles(removeEvents(el));
		return;
	}
	(new MutationObserver(function(mutations, observer) {
		observer.disconnect();
		processElement(el);
	})).observe(el, { attributes: true, attributeFilter: [ "class" ] });
}

/**
 * Подменяет переданный элемент новым. В новый элемент переносятся дети из старого элемента
 * и производится копирование атрибутов из старого элемента в новый за исключением атрибутов,
 * которые блокируют привычную функциональность как: выделение текста, вызов контекстное меню.
 * el.cloneNode(true) здесь не используется, чтобы избежать потери обработчиков у вложенных элементов.
 *
 * @param Element el DOM-элемент для замены
 *
 * @return Element Вновь созданный элемент
 */
function removeEvents(el) {
	const evil_attributes = [
		"oncontextmenu", "onmousedown", "onselectstart", "unselectable"
	];

	const new_el = document.createElement(el.tagName);
	for (let attr of el.attributes) {
		if (!evil_attributes.includes(attr.name)) {
			new_el.setAttribute(attr.name, attr.value);
		}
	}
	while (el.childNodes[0]) {
		new_el.appendChild(el.childNodes[0]);
	}
	el.replaceWith(new_el);
	return new_el;
}

/**
 * Тестирует стили на наличие запрета выделять текст
 *
 * @return bool
 */
function testNoselect() {
	return reader_selectors.some(sel => !!document.querySelector(sel + ".noselect"));
}

/**
 * Исправляет стили элемента таким образом, чтобы они не блокировали привычную для пользователя функциональность.
 *
 * @param Element el DOM-элемент для исравления
 *
 * @return void
 */
function fixStyles(el) {
	el.classList.remove("noselect");
	el.style.userSelect = "text";
}

/**
 * Добавляет кнопку скрипта на тулбар или обновляет, если уже добавлена
 *
 * @return void
 */
function updateToolbar() {
	let mobile = false;
	let tb = document.querySelector("section>header>nav>.navbar-right");
	if (!tb) {
		mobile = true;
		tb = document.querySelector("div.main-navbar div.right");
	}
	if (!tb) return;
	const bt = (() => {
		let el = tb.querySelector(".ati-button");
		if (!el) {
			if (!mobile) {
				el = document.createElement("button");
				el.classList.add("btn", "btn-brd", "btn-only-icon", "pull-left", "mr", "hint-bottom");
				el.setAttribute("style", "padding:0 !important");
			} else {
				el = document.createElement("a");
				el.classList.add("link", "icon-only", "open-popup");
				el.href = "";
			}
			el.classList.add("ati-button");
			el.textContent = "ATI";
			el.addEventListener("click", event => {
				event.preventDefault();
				active = !active;
				setValue("disabled", active && "no" || "yes");
				updateToolbar();
				if (active) {
					processReaderElements();
				} else {
					document.location.reload();
				}
			});
		}
		return el;
	})();
	if (!mobile) {
		bt.style.color = active && "green" || "gray";
	} else {
		bt.style.color = active && "white" || "darkgray";
	}
	bt.title = "ATI: Копирование текста " + (active && "разблокировано" || "заблокировано");
	tb.prepend(bt);
}

/**
 * Извлекает и возвращает сохраненный элемент данных из локального хранилища
 *
 * @param string name   Имя
 * @param string defval Значение по умолчанию
 *
 * @return string
 */
function getValue(name, defval) {
	if (localStorage !== undefined) {
		const val = localStorage.getItem("ati-" + name);
		if (val !== null) return val;
	}
	return defval;
}

/**
 * Сохраняет переданный элемент данных в локальном хранилище
 *
 * @param string name  Имя
 * @param mixed  value Значение
 *
 * @return void
 */
function setValue(name, value) {
	if (localStorage !== undefined) {
		localStorage.setItem("ati-" + name, value);
	}
}

//----------

if (document.readyState === "loading") window.addEventListener("DOMContentLoaded", run);
	else run();

})();