// ==UserScript==
// @name FR Posting Form Enhancer
// @namespace http://cynwoody.googlepages.com/fr_posting_form_enhancer.html
// @description Makes the posting page easier to use.
// @date 2015-11-04
// @include http://*.freerepublic.com/perl/post*
// @include http://freerepublic.com/perl/post*
// @version 0.0.1.20151117015424
// ==/UserScript==
const INPUT_FRACTION = 0.5; // of window height to give to the main input box
function $(id) {return document.getElementById(id);}
function $x(xpath, contextNode, resultType)
{
contextNode = contextNode || document.body;
resultType = resultType || XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE;
return document.evaluate(xpath, contextNode, null, resultType, null);
}
function $xFirst(xpath, contextNode)
{
var xpr = $x(xpath, contextNode, XPathResult.FIRST_ORDERED_NODE_TYPE);
return xpr.singleNodeValue;
}
// Removes those extra blank lines that seem to crop up at the end
// of certain posts.
function removeBlankLines(doc)
{
var list = $x('.//br[@clear="all"]', doc);
for (var x=0, limit=list.snapshotLength; x<limit; ++x) {
var br = list.snapshotItem(x);
br.parentNode.removeChild(br);
}
}
// Adds a button to the HTML toolbar. Parameters are the name on the
// button, the button's handler function, the title text to display
// when the mouse hovers over the button, and the control key to
// trigger the button's handler via the keyboard.
function addButton(name, handler, title, key)
{
var b = document.createElement('input');
b.type = 'button';
b.value = name;
if (title.slice(-1) != '.')
title += ' the selection.';
var keyCode;
if (key) {
keyCode = key.charCodeAt(0);
if (key.match(/^enter$/i))
keyCode = 13;
else if (key == '<')
keyCode = ','.charCodeAt(0);
var keyName = 'Ctrl-';
if (key.match(/^[A-Z<]/)) {
keyName += 'Shift-';
keyCode = -keyCode;
}
if (key.match(/^.\+/)) {
key = key[0];
keyName += 'Alt-';
keyCode |= 1024;
}
keyName += key;
title += '[' + keyName + ']';
}
b.title = title;
b.style.cssText = 'padding-left:0px;padding-right:0px';
if (typeof handler == 'string') {
if (handler.charAt(0) == '<')
handler = wrap(addTag, handler);
else
handler = wrap(enTag, handler);
}
addKeyHandler(key, keyCode, handler);
b.addEventListener('click', handler, false);
buttons.appendChild(b);
return b;
}
// Adds a keyboard shortcut to the key handler table.
function addKeyHandler(key, keyCode, handler)
{
if (keyCode) {
if (keyHandlers[keyCode])
alert('Duplicate shortcut key: ' + key);
keyHandlers[keyCode] = handler;
}
}
// Returns a closure that invokes the handler with the supplied arg.
function wrap(handler, arg)
{
return function() {handler(arg)};
}
// Adds an extra space between buttons in the HTML toolbar.
function addSpacer()
{
var spacer = document.createElement('span');
spacer.innerHTML = ' ';
buttons.appendChild(spacer);
}
// Generates HTML to quote from a post. Italicizes the quote and adds
// a <p> tag to leave a line before the reply begins.
function quote()
{
var end = enTag('i');
var ss = replyBox.selectionStart;
var se = replyBox.selectionEnd;
setSelection(end);
addOnOwnLine('<p>');
if (ss == se)
setSelection(ss);
}
// Surrounds the selection with a pair of <blockquote> tags.
function blockQuote()
{
enclose('<blockquote>\n', '\n</blockquote>');
}
// Link-enables the selection, using a URL supplied via a prompt.
function link()
{
var url = window.prompt('Enter link URL:', 'http://');
if (!url)
return;
enclose('<a href="' + url + '">', '</a>');
}
// Adds an image, using a URL supplied via a prompt.
function image()
{
var url = window.prompt('Enter image URL:', 'http://');
if (!url)
return;
addTag('<img border=0 src="' + url + '">');
}
// Adds a table with a single cell (which contains the selection, if any).
function makeTable()
{
enclose('<table><tbody align=center>\n<tr>\n<td>',
'</td>\n</tr>\n</tbody></table>');
}
// Adds a table row.
function makeRow()
{
enclose('<tr>\n<td>', '</td>\n</tr>');
}
// Turns the selection into an ordered or unordered list, depending
// on the tagName. Changes any leading asterisks in the selection into
// <li> tags.
function makeList(tagName)
{
startTag = '<' + tagName + '>\n';
endTag = '\n</' + tagName + '>';
var s = replyBox.selectionStart;
var e = replyBox.selectionEnd;
if (e <= s) {
enclose(startTag, endTag);
return;
}
var t = replyBox.value;
var selection = t.slice(s, e);
selection = selection.replace(/^\s*\*/gm, '<li>');
setText(t.slice(0, s) + selection + t.slice(e));
setSelection(s, s + selection.length);
enclose(startTag, endTag);
}
// Makes opening and closing tags for the supplied tag name and encloses
// the selection between them.
function enTag(tagName)
{
var startTag = '<' + tagName + '>';
var endTag = '</' + tagName + '>';
return enclose(startTag, endTag);
}
// Encloses the selection between the two supplied tags. If there is
// no selection, the cursor is left positioned to add content.
function enclose(startTag, endTag)
{
var s = replyBox.selectionStart;
var e = replyBox.selectionEnd;
var t = replyBox.value;
var selection = t.slice(s, e);
var replacement;
replacement = startTag + selection + endTag;
setText(t.slice(0, s) + replacement + t.slice(e));
replyBox.focus();
if (selection.length > 0)
setSelection(s, s + replacement.length);
else
setSelection(s + startTag.length);
return s + replacement.length;
}
// Adds a tag at the cursor and leaves the cursor after it.
function addTag(tag)
{
var s = replyBox.selectionStart;
var p = replyBox.selectionEnd;
var t = replyBox.value;
replyBox.focus();
setText(t.slice(0, s) + tag + t.slice(p));
setSelection(p + tag.length);
}
// Adds the supplied tag on a line by itself in the reply box.
function addOnOwnLine(tag)
{
var p = replyBox.selectionEnd;
var t = replyBox.value;
if (t.length > 0 && p > 0 && t.charAt(p-1) != "\n")
tag = "\n" + tag;
if (p >= t.length || t.charAt(p) != '\n')
tag += "\n";
addTag(tag);
if (t.charAt(p) == "\n") {
++replyBox.selectionStart;
}
}
// Sets which text in the reply box is selected. Begins with the character
// indexed by start and ends with the character at end-1. If start == end,
// there is no selection.
function setSelection(start, end)
{
end = end || start;
replyBox.selectionStart = start;
replyBox.selectionEnd = end;
}
// Replaces the current contents of the reply box with the supplied
// new content, restores the reply box's scroll position, and
// unchecks the "I've previewed" checkbox.
function setText(newText)
{
var scrollTop = replyBox.scrollTop;
replyBox.value = newText;
replyBox.scrollTop = scrollTop;
elements.namedItem('safety').checked = false;
}
// Monitors keystrokes in the reply box and dispatches the proper
// handler if one has been defined.
function onKeyPress(event)
{
if (event.ctrlKey) {
var key = event.charCode || event.keyCode;
if (event.shiftKey)
key = -key;
if (event.altKey)
key += 1024;
var handler = keyHandlers[key];
if (handler) {
handler(event);
event.preventDefault();
}
}
}
// Adds a Ctrl-Alt keyboard shortcut to go to the Spell, Preview, or
// Post button.
function addShortcut(name, key)
{
var button = document.forms[0].elements.namedItem(name);
if (!button)
return;
button.title = '[Ctrl-Alt-' + key + ']';
var keyCode = key.charCodeAt(0);
if (key == 'enter')
keyCode = 13;
keyCode += 1024;
addKeyHandler(key, keyCode, wrap(press, button));
}
// Handles a keyboard shortcut to the Spell, Preview, or Post buttons.
function press(button)
{
button.click();
}
// Returns the character represented by an entity of the form &#n+;.
function entityToChar(entity, number) {
return String.fromCharCode(number);
}
// Scans a string and substitutes character values for all HTML entities
// of the form &#n+;
function deEntitize(s) {
return s.replace(/&#(\d+);/g, entityToChar);
}
// Substitutes an HTML entity for the passed character.
function entitize(char) {
return "&#" + char.charCodeAt() + ';';
}
// On form submission, replaces replaces any non-7-bit ASCII characters with
// the corresponding HTML entity.
function onSubmit() {
var reply = document.forms['replyForm'].reply;
reply.value = reply.value.replace(/[\u0080-\uffff]/g, entitize);
}
// *****************************************************************************
// Use the full width of the browser window.
var keyHandlers = {};
$xFirst('//table[@border=0]').width = '100%';
removeBlankLines(document.body);
// Lighten up the borders on the tables having them.
var tables = $x('//table[@border=1]');
for (var x=0; x<tables.snapshotLength; ++x) {
var table = tables.snapshotItem(x);
table.cellPadding = '3';
table.cellSpacing = '0';
table.style.borderCollapse = 'collapse';
}
// Replace the To: input TEXTAREA with an INPUT field. Wastes less vertical
// room. Make it extend clear across the window.
var elements = document.forms[0].elements;
var nameField = elements.namedItem('name');
var newNameField = document.createElement('input');
newNameField.name = nameField.name;
newNameField.value = nameField.value;
newNameField.style.width = '100%';
newNameField.style.cssText = 'font-size:inherit;' +
'font-family:DejaVu Sans Mono, monospaced;';
nameField.parentNode.replaceChild(newNameField, nameField);
// Give the reply box more vertical space and make it use the full width.
var replyBox = elements.namedItem('reply');
replyBox.style.cssText = 'width:100%;height:' +
(window.innerHeight*INPUT_FRACTION) + 'px;' +
'font-size:inherit;' +
'font-family:DejaVu Sans Mono, monospaced;';
// Let the Tagline field extend clear across.
elements.namedItem('sig').style.width = '100%';
// Add an HTML toolbar
var buttons = document.createElement('div');
buttons.style.cssFloat = 'left';
replyBox.parentNode.insertBefore(buttons, replyBox);
addButton('Quote', quote, 'Quotes', 'q');
addButton('BlockQuote', blockQuote, 'Block-quotes', 'Q');
addSpacer();
addButton('B', 'b', 'Bolds', 'b');
addButton('I', 'i', 'Italicizes', 'i').style.paddingLeft = '1px';
addButton('U', 'u', 'Underlines', 'u');
addButton('S', 's', 'Strikes out', 's');
addButton('Q', 'q', 'Curly-quotes', 'q+');
addButton('Small', 'small', 'Sets the selection in smaller font.', 'S');
addButton('Big', 'big', 'Sets the selection in larger font.', 'B');
addButton('<', wrap(addTag, '<'), 'Adds a < as text.', '<');
addSpacer();
addButton('Font', 'font', 'Encloses the selection in <font> tags', 'F');
addButton('Pre', 'pre', 'Encloses the selection in preformat tags.', 'P');
addSpacer();
addButton('UL', wrap(makeList, 'ul'),
'Makes the selection a bulleted list.', 'U');
addButton('OL', wrap(makeList, 'ol'),
'Makes the selection a numbered list.', 'O');
addButton('HR', wrap(addOnOwnLine, '<hr width=97%>'),
'Adds a horizontal rule at the cursor.', 'h');
addSpacer();
addButton('Table', makeTable, 'Adds a single-cell table at the cursor.', 'T');
addButton('TR', makeRow, 'Adds a table row at the cursor.', 'R');
addButton('TH', 'th', 'Adds a table header cell at the cursor.', 'H');
addButton('TD', 'td', 'Adds a table cell at the cursor.', 'C');
addSpacer();
addButton('Image', image, 'Adds an image tag at the cursor.', 'I');
addButton('Link', link, 'Link-enables', 'L');
addSpacer();
addButton('BR', wrap(addOnOwnLine, '<br>'),
'Adds a <br> tag at the cursor.', 'Enter');
addButton('P', wrap(addOnOwnLine, '<p>'),
'Adds a <p> tag at the cursor.', 'enter');
// Finish adding keyboard shortcuts and install the reply box's
// keyboard listener.
addShortcut('spell', 's');
addShortcut('preview', 'p');
addShortcut('post', 'enter');
replyBox.addEventListener('keypress', onKeyPress, true);
// Add a documentation link over to the right of the HTML toolbar
var help = document.createElement('div');
help.style.cssFloat = 'right';
help.innerHTML = '<a ' +
'href="http://cynwoody.googlepages.com/fr_posting_form_enhancer.html" ' +
'target=help>Help</a>';
replyBox.parentNode.insertBefore(help, replyBox);
// Compensate for strange effects seen when using the buttons to insert
// HTML before the reply box has first received the focus.
if (replyBox.value == '') {
replyBox.value = '?';
replyBox.value = '';
}
// Decode any HTML entities in the reply box, so that the user sees normal
// graphics, but schedule the reply box to be re-encoded into entities upon
// resubmission (see the onSubmit function).
replyBox.value = deEntitize(replyBox.value);
document.forms['replyForm'].onsubmit = onSubmit;
replyBox.focus();