escgo! colors in webchat - modern

Adds an option to make text bold/italic?/underlined/colorful in the escgo! chat

// ==UserScript==
// @name         escgo! colors in webchat - modern
// @version      0.7.6
// @description  Adds an option to make text bold/italic?/underlined/colorful in the escgo! chat
// @author       Andrei Felix
// @match        http://www.escgo.com/wp-content/uploads/euwebirc-master/static/qui.html
// @match        http://www.escgo.com/wp-content/uploads/euwebirc-master2/static/qui.html
// @match        http://webchat.euirc.net/
// @icon         http://www.escgo.com/wp-content/uploads/2017/04/cropped-escgologolarge-32x32.png
// @grant        none
// @run-at       document-end
// @namespace https://greasyfork.org/users/868721
// ==/UserScript==

(function() {
	// function that waits; by default, it waits 1 second
	function sleep(ms = 1000) { return new Promise(resolve => { window.setTimeout(resolve, ms) }) }
	
	// this function checks for the existence of the chat box every second
	async function getTextBox() {
		let firstLoop = true, result;
		while (true) {
			if (firstLoop) firstLoop = false; else await sleep();
			result = document.querySelector(".bottomboundpanel .input input.keyboard-input");
			if (result) return result;
		}
	}
	
	// this adds a style element containing everything needed for this script
	function addCustomCss() {
		let ircStyle = getComputedStyle(document.querySelector(".lines"));
		let bgColor = ircStyle.backgroundColor;
		let fgColor = ircStyle.color;
		let bdColor = getComputedStyle(document.querySelector(".bottomboundpanel")).borderTopColor;
		let btnColor = getComputedStyle(document.querySelector(".tab-selected")).backgroundColor;
		let customCss = `
.qwebirc-qui.bottomboundpanel form {
	padding-left: 1.3em;
	padding-right: 2px;
}
.qwebirc-qui .input input.keyboard-input {
	padding-left: 2px;
}
#formatArea {
	position: absolute;
	width: 1.2em;
	height: 1.2em;
	left: 0;
	top: 0;
	bottom: 0;
	margin: 0;
	padding: 0;
}
#formatBtn {
	position: relative;
	width: 100%;
	height: 100%;
	left: 0;
	top: 0;
	text-align: center;
	background: ${btnColor};
	font-weight: bold;
	font-style: italic;
	text-decoration: underline;
	color: cyan;
	cursor: default;
}
#formatMenu {
	display: none;
	position: absolute;
	top: auto;
	left: 0;
	bottom: 100%;
	padding: 0.1em 0 0.1em 0.3em;
	border: 1px ${bdColor} solid;
	background: ${bgColor};
	white-space: nowrap;
	font-size: 85%;
	text-align: left;
}
#formatMenu .colourline {
	display: inline-block;
	white-space: nowrap;
}
#formatArea:focus #formatMenu,
#formatArea:focus-within #formatMenu,
#formatArea:hover #formatMenu,
#formatMenu.forceOpen {
	display: block;
}
.formatLabel {
	color: ${fgColor};
	margin-right: 0.1em;
	margin-bottom: 0.1em;
	line-height: 1;
}
.formatStyleBtn {
	display: inline-block;
	opacity: 0.9;
	background: #cccccc;
	color: black;
	font-size: 95%;
	text-align: center;
	margin-right: 0.3em;
	margin-bottom: 0.1em;
	padding: 0.2em;
	width: 1.2em;
	height: 1.2em;
	border: 1px #666666 solid;
	user-select: none;
	vertical-align: middle;
	-webkit-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	cursor: default;
}
.formatStyleBtn:hover {
	opacity: 1;
	outline: 2px ${fgColor} solid;
	outline-offset: -1px;
}
.formatStyleBtn:active {
	opacity: 0.65;
}
.colourline .formatStyleBtn {
	opacity: 1;
}
#formatBoldBtn {
	font-weight: bold;
}
#formatItalicBtn {
	font-style: italic;
}
#formatUnderlineBtn {
	text-decoration: underline;
}
#formatMenu #formatColorAdvanced.colourline {
	display: none;
	padding-left: 1.95em;
}
#formatColorPreview {
	padding: 2px;
	width: 8.87em;
	height: 1.2em;
	border: 1px #666666 solid;
	margin-right: 0.3em;
	margin-bottom: 0.1em;
	text-align: center;
}
.formatColorPicked {
	background: transparent;
	outline: 2px ${fgColor} none;
	outline-offset: -1px;
}
#formatMenu .Xc99,
#formatMenu .XcDef.XbcDef.invertDef {
	color: ${fgColor};
}
#formatMenu .Xbc99 {
	background: transparent;
}
#formatMenu .XcDef {
	color: ${bgColor};
}
#formatMenu .XbcDef,
#formatMenu #formatColorFg.Xbc99 {
	background: ${fgColor};
}
#formatMenu .XcDef.XbcDef.invertDef{
	background: ${bgColor};
}
`
		let cuCss = document.createElement("style");
		cuCss.innerHTML = customCss;
		document.head.appendChild(cuCss);
	}
	
	// this creates labels in my little formatting menu
	function createFormatMenuLabel(text, inline) {
		let label = document.createElement("div");
		label.className = "formatLabel";
		if (inline) label.style.display = "inline-block";
		label.textContent = text;
		return label;
	}
	
	// this is a template used for buttons that affect formatting in some way
	function createFormatStyleButton(textBox, text, tooltip, id, delim, param, classes = [], picker, pickerCompat) {
		let button = document.createElement("div");
		button.classList.add("formatStyleBtn");
		classes.forEach(cl => { button.classList.add(cl) });
		if (id) button.id = id;
		if (tooltip) button.title = tooltip;
		button.addEventListener("click", (e) => {
			e.preventDefault();
			e.stopPropagation();
			if (!pickerCompat || !e.shiftKey) { // buttons that are incompatible with the "picker" will cancel it
				picker.disable();
				let ss = textBox.selectionStart;
				let se = textBox.selectionEnd;
				let endTag = delim;
				if (ss == se) endTag = "";
				let currValue = textBox.value;
				textBox.value = currValue.slice(0, ss) + delim + param + currValue.slice(ss, se) + endTag + currValue.slice(se);
				textBox.selectionStart = textBox.selectionEnd = se + delim.length + param.length + endTag.length;
				textBox.focus();
			}
			else if (e.shiftKey) picker.setColor(parseInt(param));
		});
		button.textContent = text;
		return button;
	}
	
	// advanced color selection UI ("picker") activated by holding Shift
	// this allows simultaneous entry of a foreground and a background color
	function advancedPicker(textBox, formatMenu) {
		let preview, okBtn, cancelBtn, fgShow, bgShow, that = this;
		let colorBoxClasses = "formatStyleBtn formatColorPicked";
		
		// these properties describe the state of the "picker"
		this.selection = null;
		this.fg = null;
		this.bg = null;
		this.btn99 = null;
		
		// the following lines describe the UI for the "picker"
		this.DOM = document.createElement("div");
		this.DOM.id = "formatColorAdvanced";
		this.DOM.className = "colourline";
		this.DOM.appendChild(createFormatMenuLabel("Advanced:", false));
		
		okBtn = document.createElement("div");
		okBtn.className = "formatStyleBtn";
		okBtn.textContent = "OK";
		okBtn.style.width = "3.39em";
		this.DOM.appendChild(okBtn);
		
		cancelBtn = document.createElement("div");
		cancelBtn.className = "formatStyleBtn";
		cancelBtn.textContent = "Cancel";
		cancelBtn.style.width = "5em";
		this.DOM.appendChild(cancelBtn);
		
		preview = document.createElement("div");
		preview.id = "formatColorPreview";
		preview.textContent = "Preview";
		this.DOM.appendChild(preview);
		
		this.DOM.appendChild(createFormatMenuLabel("Fore:", true));
		
		fgShow = document.createElement("div");
		fgShow.className = colorBoxClasses;
		fgShow.id = "formatColorFg";
		this.DOM.appendChild(fgShow);
		
		this.DOM.appendChild(createFormatMenuLabel("Back:", true));
		
		bgShow = document.createElement("div");
		bgShow.className = colorBoxClasses;
		this.DOM.appendChild(bgShow);
		
		// the "picker" submits when releasing Shift
		// this is equivalent to clicking OK (while holding said Shift)
		// the "picker" can be canceled by clicking Cancel (as seen further down below)
		let shiftUpFn = function (e) {
			if (!e || e.key === "Shift" || e.which === 16) {
				let delim = "\x03";
				let param = String(that.fg).padStart(2, "0");
				if (that.bg !== null) param += "," + String(that.bg).padStart(2, "0");
				let ss = textBox.selectionStart;
				let se = textBox.selectionEnd;
				let endTag = delim;
				if (ss == se) endTag = "";
				let currValue = textBox.value;
				textBox.value = currValue.slice(0, ss) + delim + param + currValue.slice(ss, se) + endTag + currValue.slice(se);
				textBox.selectionStart = textBox.selectionEnd = se + delim.length + param.length + endTag.length;
				that.disable();
				textBox.focus();
			}
		}
		
		// the "picker" is canceled if the window loses focus
		let blurFn = function (e) { that.disable(); textBox.focus(); }
		
		this.set99 = function(button) { this.btn99 = button; }
		
		// this initializes the "picker" if it's not initialized
		this.enable = function() {
			if (this.selection === null) {
				this.DOM.style.display = "inline-block";
				this.selection = 1;
				formatMenu.classList.add("forceOpen");
				fgShow.style.outlineStyle = "solid";
				this.btn99.classList.remove("invertDef");
				window.addEventListener("keyup", shiftUpFn);
				window.addEventListener("blur", blurFn);
			}
		}
		
		// this restores the "picker" to its original state
		this.disable = function() {
			this.DOM.style.display = "";
			this.selection = this.fg = this.bg = null;
			formatMenu.classList.remove("forceOpen");
			fgShow.style.outlineStyle = "";
			fgShow.className = colorBoxClasses;
			bgShow.style.outlineStyle = "";
			bgShow.className = colorBoxClasses;
			preview.className = "";
			this.btn99.classList.remove("invertDef");
			window.removeEventListener("keyup", shiftUpFn);
			window.removeEventListener("blur", blurFn);
		}
		
		// normally, after choosing a foreground color,
		// the "picker" expects a background color,
		// but this allows users to set the focus back to the
		// foreground color if needed
		this.switch = function (boxIndex) {
			if (boxIndex == 1) {
				this.selection = 1;
				fgShow.style.outlineStyle = "solid";
				bgShow.style.outlineStyle = "";
				this.btn99.classList.remove("invertDef");
			}
			else {
				this.selection = 2;
				fgShow.style.outlineStyle = "";
				bgShow.style.outlineStyle = "solid";
				this.btn99.classList.add("invertDef");
			}
			textBox.focus();
		}
		
		// this effectively registers a color selection
		this.setColor = function (color) {
			this.enable();
			if (this.selection == 1) {
				this.fg = color;
				fgShow.className = colorBoxClasses + " Xbc" + color;
				this.switch(2);
			}
			else {
				this.bg = color;
				bgShow.className = colorBoxClasses + " Xbc" + color;
				this.switch(1);
			}
			preview.className = "Xc" + this.fg + " Xbc" + this.bg;
		}
		
		// this makes the "picker" buttons work
		okBtn.addEventListener("click", function() { shiftUpFn(); });
		cancelBtn.addEventListener("click", function() { blurFn(); });
		fgShow.addEventListener("click", function() { that.switch(1); });
		bgShow.addEventListener("click", function() { that.switch(2); });
	}
	
	// this creates the formatting menu; it appears when hovering over the A
	function constructFormatMenu(textBox) {
		let formatMenu = document.createElement("div");
		formatMenu.id = "formatMenu";
		formatMenu.className = "dropdownmenu";
		let picker = new advancedPicker(textBox, formatMenu);
		
		// "B", "I", "U", "N" buttons; "N" negates every other tag
		// unlike the web chat, mIRC does recognize italic text
		// TODO: the status of "I" is subject to consideration, ask
		let fontStyleLabel = createFormatMenuLabel("Font style:", true);
		fontStyleLabel.style.width = "5.75em";
		formatMenu.appendChild(fontStyleLabel);
		formatMenu.appendChild(createFormatStyleButton(textBox, "B", "bold", "formatBoldBtn", "\x02", "", [], picker, false));
		formatMenu.appendChild(createFormatStyleButton(textBox, "I?", "italic?", "formatItalicBtn", "\x1D", "", [], picker, false));
		formatMenu.appendChild(createFormatStyleButton(textBox, "U", "underline", "formatUnderlineBtn", "\x1F", "", [], picker, false));
		formatMenu.appendChild(createFormatStyleButton(textBox, "N", "normal", undefined, "\x0F", "", [], picker, false));
		formatMenu.appendChild(document.createElement("br"));
		
		// I added tooltips because colors are confusing
		// "dark" gray is darker in mIRC, but lighter in browsers
		// the numbers help with readability
		// 0 = color code is shown in white; 1 = color code is shown in black
		let colors = [
			["white", 1], ["black", 0], ["dark blue", 0], ["green", 0], ["red", 0], ["dark red", 0],
			["purple", 0], ["orange", 1], ["yellow", 1], ["light green", 0], ["teal", 0], ["cyan", 1], ["blue", 0],
			["fuchsia", 0], ["\"dark\" gray", 1], ["gray", 0]
		];
		
		// dummy element because I don't want to redefine the colors
		let dummyColourline = document.createElement("div");
		dummyColourline.classList.add("colourline");
		dummyColourline.appendChild(createFormatMenuLabel("Font colour:", false));
		
		// this is where the color buttons are actually created
		colors.forEach((color, back) => {
			let desc = color[0], fore = color[1];
			if ((back != 0) && (back % 6 == 0)) dummyColourline.appendChild(document.createElement("br"));
			dummyColourline.appendChild(createFormatStyleButton(textBox, String(back), desc, undefined, "\x03", String(back).padStart(2, "0"), ["Xc" + fore, "Xbc" + back], picker, true));
		});
		
		// 2 extra buttons for default formatting + just the symbol
		let defaultStyleBtn = createFormatStyleButton(textBox, "99", "default", undefined, "\x03", "99", ["XcDef", "XbcDef"], picker, true);
		picker.set99(defaultStyleBtn);
		dummyColourline.appendChild(defaultStyleBtn);
		dummyColourline.appendChild(createFormatStyleButton(textBox, "\u2514", "reset/manual entry", undefined, "\x03", "", [], picker, false));
		
		formatMenu.appendChild(dummyColourline);
		formatMenu.appendChild(picker.DOM);
		return formatMenu;
	}
	
	// this adds the formatting menu to the DOM once the textbox is found
	getTextBox().then(textBox => {
		addCustomCss();
		
		let contForm = textBox.parentElement;
		let contDiv = contForm.parentElement;
		
		let formatArea = document.createElement("div");
		formatArea.id = "formatArea";
		
		let formatBtn = document.createElement("div");
		formatBtn.id = "formatBtn";
		formatBtn.textContent = "A";
		
		formatArea.appendChild(formatBtn);
		formatArea.appendChild(constructFormatMenu(textBox));
		
		contDiv.insertBefore(formatArea, contForm);
	});
})();