XenForo Post Style

Allows a XenForo user to add a custom BBCode template to their posts. Optional automatic formatting and keyboard shortcut included.

// ==UserScript==
// @name	XenForo Post Style
// @namespace	Makaze
// @include	*
// @grant	none
// @version	5.0.4
// @description Allows a XenForo user to add a custom BBCode template to their posts. Optional automatic formatting and keyboard shortcut included.
// ==/UserScript==

/*
 XENFORO POST TEMPLATE SCRIPT
 
 PRESS Alt+T TO APPLY ON ANY PAGE

 AUTOMATIC FORMATTING OPTIONAL
*/

function getPosition(element) {
	var xPosition = 0,
	yPosition = 0;

	while (element) {
		xPosition += (element.offsetLeft
			+ element.clientLeft);
		yPosition += (element.offsetTop
			+ element.clientTop);
		element = element.offsetParent;
	}
	return {x: xPosition, y: yPosition};
}

Math.easeInOutQuad = function (time, start, change, duration) {
	time /= duration / 2;
	if (time < 1) {
		return change / 2 * time * time + start;
	}
	time--;
	return -change / 2 * (time * (time - 2) - 1) + start;
};

function scrollTo(element, to, duration) {
	var start = element.scrollTop,
	change = to - start,
	currentTime = 0,
	increment = 1;

	var animateScroll = function() {        
		var val = Math.easeInOutQuad(currentTime, start, change, duration);                        
		element.scrollTop = val; 
		currentTime += increment;
		if (currentTime < duration) {
			setTimeout(animateScroll, increment);
		}
	};

	animateScroll();
}

function selectRange(elem, start, end) {
	var range;

	if (elem.setSelectionRange) {
		elem.focus();
		elem.setSelectionRange(start, end);
	} else if (elem.createTextRange) {
		range = elem.createTextRange();
		range.collapse(true);
		range.moveEnd('character', end);
		range.moveStart('character', start);
		range.select();
	}
}

function cursor(elem, position) {
	selectRange(elem, position, position);
}

function applyTemplate(elem) {
	var formContext = elem,
	opts = (localStorage.getItem('MakazeScriptOptions')) ? JSON.parse(localStorage.getItem('MakazeScriptOptions')) : {},
	htmlPrefix = (opts.hasOwnProperty('xf_template_htmlPrefix')) ? opts.xf_template_htmlPrefix : '',
	htmlSuffix = (opts.hasOwnProperty('xf_template_htmlSuffix')) ? opts.xf_template_htmlSuffix : '',
	bbPrefix = (opts.hasOwnProperty('xf_template_bbPrefix')) ? opts.xf_template_bbPrefix : '',
	bbSuffix = (opts.hasOwnProperty('xf_template_bbSuffix')) ? opts.xf_template_bbSuffix : '',
	thisList,
	thisLink,
	instance,
	postForm,
	plainText,
	i = 0;

	var applyTemplateEvent = function(event) {
		var parent = event.target;
		applyToChildren(parent);
		event.target.removeEventListener('keydown', applyTemplateEvent, false);
	};

	var applyToChildren = function(parent) {
		var thisChild,
		applyTo,
		i = 0;

		for (i = 0; i < parent.childNodes.length; i++) {
			thisChild = parent.childNodes[i];
			if (thisChild.innerHTML) {
				applyTo = thisChild.innerHTML.replace(
					/\[quote([^]+)\[\/quote\]/gi, htmlSuffix + '[quote$1[/quote]' + htmlPrefix
				);

				thisChild.innerHTML = htmlPrefix + applyTo + htmlSuffix;
			}
		}
	};

	while (formContext.getElementsByClassName('redactor_box')[0] == null && formContext.parentNode) {
		formContext = formContext.parentNode;
	}

	if (opts.hasOwnProperty('xf_template_auto')) {
		for (i = 0; i < formContext.getElementsByClassName('redactor_MessageEditor').length; i++) {
			instance = formContext.getElementsByClassName('redactor_MessageEditor')[i];
			postForm = instance.contentWindow.document;
			if (!postForm.body.textContent.length) {
				postForm.body.addEventListener('keydown', applyTemplateEvent, false);
			} else {
				applyToChildren(postForm.body);
			}
		}
		for (i = 0; i < formContext.getElementsByClassName('bbCodeEditorContainer').length; i++) {
			plainText = formContext.getElementsByClassName('bbCodeEditorContainer')[i].getElementsByTagName('textarea')[0];
			plainText.value = bbPrefix + plainText.value + bbSuffix;
			cursor(plainText, plainText.value.length - bbSuffix.length);
		}
	} else {
		thisList = document.getElementById('AccountMenu').getElementsByClassName('blockLinksList')[0];
		for (i = 0; i < thisList.getElementsByTagName('a').length; i++) {
			thisLink = thisList.getElementsByTagName('a')[i];
			if (thisLink.href.match(/account\/personal\-details/gi) && thisLink.href.substr(window.location.href.length - 14, 14) !== '#Post_Template') {
				thisLink.href = thisLink.href + '#Post_Template';
				thisLink.click();
				break;
			}
		}
	}
}

function xenForoMessage(msg, success) {
	if (success) {
		$('#templateMessage .content').html(msg);
		console.log(msg);
	} else {
		$('#templateMessage .content').html('<strong>Error:</strong> ' + msg);
		console.log('Error:', msg);
	}
	$('#templateMessage').slideDown('medium');
	$('#templateMessage .content').animate({
		'opacity': 1
	}, 'fast');
	setTimeout(function() {
		$('#templateMessage').slideUp('medium');
		$('#templateMessage .content').animate({
			'opacity': 0
		}, 'fast');
	}, 1500);
}

function runInGlobal(code) {
	var scripts = document.createElement('script');
	scripts.type = 'text/javascript';
	scripts.id = 'runInGlobal';
	scripts.appendChild(document.createTextNode(
		code +
		'\n\n' +
		'document.getElementById(\'runInGlobal\').remove();'
	));

	(document.head || document.body || document.documentElement).appendChild(scripts);
}

function saveTemplateSettings() {
	if (!document.getElementById('htmlPrefixField').value.length) {
		xenForoMessage('HTML prefix required.', false);
		return false;
	}

	if (!document.getElementById('htmlSuffixField').value.length) {
		xenForoMessage('HTML suffix required.', false);
		return false;
	}

	if (!document.getElementById('bbPrefixField').value.length) {
		xenForoMessage('BBCode prefix required.', false);
		return false;
	}

	if (!document.getElementById('bbSuffixField').value.length) {
		xenForoMessage('BBCode suffix required.', false);
		return false;
	}
	
	var opts = (localStorage.getItem('MakazeScriptOptions')) ? JSON.parse(localStorage.getItem('MakazeScriptOptions')) : {};

	opts.xf_template_auto = (document.getElementById('autoApplyField').options[document.getElementById('autoApplyField').selectedIndex].value === 'true');
	opts.xf_template_htmlPrefix = document.getElementById('htmlPrefixField').value;
	opts.xf_template_htmlSuffix = document.getElementById('htmlSuffixField').value;
	opts.xf_template_bbPrefix = document.getElementById('bbPrefixField').value;
	opts.xf_template_bbSuffix = document.getElementById('bbSuffixField').value;
	localStorage.setItem('MakazeScriptOptions', JSON.stringify(opts));

	xenForoMessage('Your settings have been saved.', true);
}

var applyHandler = function() {
	applyTemplate(this);
};

if (document.documentElement.id === "XenForo") {
	var opts = (localStorage.getItem('MakazeScriptOptions')) ? JSON.parse(localStorage.getItem('MakazeScriptOptions')) : {},
	autoApply = (opts.hasOwnProperty('xf_template_auto')) ? opts.xf_template_auto : false,
	htmlPrefix = (opts.hasOwnProperty('xf_template_htmlPrefix')) ? opts.xf_template_htmlPrefix : '',
	htmlSuffix = (opts.hasOwnProperty('xf_template_htmlSuffix')) ? opts.xf_template_htmlSuffix : '',
	bbPrefix = (opts.hasOwnProperty('xf_template_bbPrefix')) ? opts.xf_template_bbPrefix : '',
	bbSuffix = (opts.hasOwnProperty('xf_template_bbSuffix')) ? opts.xf_template_bbSuffix : '',
	instance,
	buttonsContext,
	richInstance,
	richDoc,
	field,
	i = 0;

	// Button creation and auto-application

	if (document.getElementsByClassName('MessageEditor')[0] != null) {
		for (i = 0; i < document.getElementsByClassName('MessageEditor').length; i++) {
			instance = document.getElementsByClassName('MessageEditor')[i];
			buttonsContext = instance;

			while (buttonsContext.getElementsByClassName('submitUnit')[0] == null && buttonsContext.parentNode) {
				buttonsContext = buttonsContext.parentNode;
			}

			buttonsContext = buttonsContext.getElementsByClassName('submitUnit')[0].getElementsByClassName('button primary')[0].parentNode;

			var applyButton = document.createElement('input'),
			applyButtonSpacer = document.createTextNode(String.fromCharCode(160));
			applyButton.type = 'button';
			applyButton.value = 'Apply Style';
			applyButton.className = 'button JsOnly applyButton';
			applyButton.onclick = applyHandler;

			buttonsContext.appendChild(applyButtonSpacer);
			buttonsContext.appendChild(applyButton);

			if (autoApply.toString() === 'true') {
				applyTemplate(instance);
			}
		}

		document.addEventListener('keydown', function(e) {
			var i = 0,
			keyInstance;

			if (e.keyCode == 84 && e.altKey) {
				for (i = 0; i < document.getElementsByClassName('MessageEditor').length; i++) {
					keyInstance = document.getElementsByClassName('MessageEditor')[i];
					applyTemplate(keyInstance);
				}
			}
		}, false);

		var applyToRichHandler = function(e) {
			if (e.keyCode == 84 && e.altKey) {
				applyTemplate(richInstance);
			}
		};

		if (document.getElementsByClassName('redactor_MessageEditor')[0] != null) {
			for (i = 0; i < document.getElementsByClassName('redactor_MessageEditor').length; i++) {
				richInstance = document.getElementsByClassName('redactor_MessageEditor')[i];
				richDoc = richInstance.contentWindow.document;
				richDoc.addEventListener('keydown', applyToRichHandler, false);
			}
		}
	}
	
	if (window.location.href.match(/account\/personal\-details/gi)) {
		// Define xenForoMessage and saveTemplateSettings

		runInGlobal(
			xenForoMessage.toString() +
			saveTemplateSettings.toString()
		);

		// Settings creation

		var optionsContainer = document.createElement('fieldset'),

		header = document.createElement('dl'),
		headerDT = document.createElement('dt'),
		headerDD = document.createElement('dd'),
		headerDD_Text = document.createTextNode('Post Style'),

		autoApplyField = document.createElement('dl'),
		autoApplyFieldDT = document.createElement('dt'),
		autoApplyFieldDT_Text = document.createTextNode('Auto-apply:'),
		autoApplyFieldDD = document.createElement('dd'),
		autoApplyFieldDD_Select = document.createElement('select'),
		autoApplyFieldDD_Select_true = document.createElement('option'),
		autoApplyFieldDD_Select_true_Text = document.createTextNode('True'),
		autoApplyFieldDD_Select_false = document.createElement('option'),
		autoApplyFieldDD_Select_false_Text = document.createTextNode('False'),

		htmlPrefixField = document.createElement('dl'),
		htmlPrefixFieldDT = document.createElement('dt'),
		htmlPrefixFieldDT_Text = document.createTextNode('HTML Prefix:'),
		htmlPrefixFieldDD = document.createElement('dd'),
		htmlPrefixFieldDD_input = document.createElement('input'),

		htmlSuffixField = document.createElement('dl'),
		htmlSuffixFieldDT = document.createElement('dt'),
		htmlSuffixFieldDT_Text = document.createTextNode('HTML Suffix:'),
		htmlSuffixFieldDD = document.createElement('dd'),
		htmlSuffixFieldDD_input = document.createElement('input'),

		bbPrefixField = document.createElement('dl'),
		bbPrefixFieldDT = document.createElement('dt'),
		bbPrefixFieldDT_Text = document.createTextNode('BBCode Prefix:'),
		bbPrefixFieldDD = document.createElement('dd'),
		bbPrefixFieldDD_input = document.createElement('input'),

		bbSuffixField = document.createElement('dl'),
		bbSuffixFieldDT = document.createElement('dt'),
		bbSuffixFieldDT_Text = document.createTextNode('BBCode Suffix:'),
		bbSuffixFieldDD = document.createElement('dd'),
		bbSuffixFieldDD_input = document.createElement('input'),

		submitField = document.createElement('dl'),
		submitFieldDT = document.createElement('dt'),
		submitFieldDD = document.createElement('dd'),
		submitFieldDD_input = document.createElement('input'),

		templateMessage = document.createElement('div'),
		templateMessage_content = document.createElement('div'),
		templateMessage_content_Text = document.createTextNode('Your settings have been saved.');

		// Load input settings

		htmlPrefixFieldDD_input.value = htmlPrefix;
		htmlSuffixFieldDD_input.value = htmlSuffix;
		bbPrefixFieldDD_input.value = bbPrefix;
		bbSuffixFieldDD_input.value = bbSuffix;

		// Header

		headerDD.setAttribute('style', 'font-weight: bolder; font-size: 130%; width: 40%; text-decoration: underline;');
		headerDD.appendChild(headerDD_Text);

		header.className = 'ctrlUnit';
		header.appendChild(headerDT);
		header.appendChild(headerDD);

		// Auto apply field

		autoApplyFieldDT.appendChild(autoApplyFieldDT_Text);

		autoApplyFieldDD_Select_true.value = true;
		autoApplyFieldDD_Select_true.appendChild(autoApplyFieldDD_Select_true_Text);

		autoApplyFieldDD_Select_false.value = false;
		autoApplyFieldDD_Select_false.appendChild(autoApplyFieldDD_Select_false_Text);

		autoApplyFieldDD_Select.id = 'autoApplyField';
		autoApplyFieldDD_Select.className = 'textCtrl';
		autoApplyFieldDD_Select.appendChild(autoApplyFieldDD_Select_true);
		autoApplyFieldDD_Select.appendChild(autoApplyFieldDD_Select_false);

		autoApplyFieldDD.appendChild(autoApplyFieldDD_Select);

		autoApplyField.className = 'ctrlUnit';
		autoApplyField.appendChild(autoApplyFieldDT);
		autoApplyField.appendChild(autoApplyFieldDD);

		// HTML prefix field

		htmlPrefixFieldDT.appendChild(htmlPrefixFieldDT_Text);

		htmlPrefixFieldDD_input.type = 'text';
		htmlPrefixFieldDD_input.id = 'htmlPrefixField';
		htmlPrefixFieldDD_input.className = 'textCtrl OptOut';

		htmlPrefixFieldDD.appendChild(htmlPrefixFieldDD_input);

		htmlPrefixField.className = 'ctrlUnit';
		htmlPrefixField.appendChild(htmlPrefixFieldDT);
		htmlPrefixField.appendChild(htmlPrefixFieldDD);

		// HTML suffix field

		htmlSuffixFieldDT.appendChild(htmlSuffixFieldDT_Text);

		htmlSuffixFieldDD_input.type = 'text';
		htmlSuffixFieldDD_input.id = 'htmlSuffixField';
		htmlSuffixFieldDD_input.className = 'textCtrl OptOut';

		htmlSuffixFieldDD.appendChild(htmlSuffixFieldDD_input);

		htmlSuffixField.className = 'ctrlUnit';
		htmlSuffixField.appendChild(htmlSuffixFieldDT);
		htmlSuffixField.appendChild(htmlSuffixFieldDD);

		// BBCode prefix field

		bbPrefixFieldDT.appendChild(bbPrefixFieldDT_Text);

		bbPrefixFieldDD_input.type = 'text';
		bbPrefixFieldDD_input.id = 'bbPrefixField';
		bbPrefixFieldDD_input.className = 'textCtrl OptOut';

		bbPrefixFieldDD.appendChild(bbPrefixFieldDD_input);

		bbPrefixField.className = 'ctrlUnit';
		bbPrefixField.appendChild(bbPrefixFieldDT);
		bbPrefixField.appendChild(bbPrefixFieldDD);

		// BBCode suffix field

		bbSuffixFieldDT.appendChild(bbSuffixFieldDT_Text);

		bbSuffixFieldDD_input.type = 'text';
		bbSuffixFieldDD_input.id = 'bbSuffixField';
		bbSuffixFieldDD_input.className = 'textCtrl OptOut';

		bbSuffixFieldDD.appendChild(bbSuffixFieldDD_input);

		bbSuffixField.className = 'ctrlUnit';
		bbSuffixField.appendChild(bbSuffixFieldDT);
		bbSuffixField.appendChild(bbSuffixFieldDD);

		// Submit field

		submitFieldDD_input.type = 'button';
		submitFieldDD_input.id = 'submitTemplate';
		submitFieldDD_input.className = 'button';
		submitFieldDD_input.value = 'Save';
		submitFieldDD_input.setAttribute('onClick', 'saveTemplateSettings();');

		submitFieldDD.appendChild(submitFieldDD_input);

		submitField.className = 'ctrlUnit';
		submitField.appendChild(submitFieldDT);
		submitField.appendChild(submitFieldDD);

		// Template message

		templateMessage_content.className = 'content baseHtml';
		templateMessage_content.style.opacity = 0;
		templateMessage_content.appendChild(templateMessage_content_Text);

		templateMessage.id = 'templateMessage';
		templateMessage.className = 'xenOverlay timedMessage';
		templateMessage.setAttribute('style', 'top: 0px; left: 0px; position: fixed; display: none;');
		templateMessage.appendChild(templateMessage_content);

		// Build it all

		optionsContainer.id = 'templateOptionsContainer';
		optionsContainer.appendChild(header);
		optionsContainer.appendChild(autoApplyField);
		optionsContainer.appendChild(htmlPrefixField);
		optionsContainer.appendChild(htmlSuffixField);
		optionsContainer.appendChild(bbPrefixField);
		optionsContainer.appendChild(bbSuffixField);
		optionsContainer.appendChild(submitField);

		document.getElementsByClassName('OptOut')[document.getElementsByClassName('OptOut').length - 1].parentNode.insertBefore(optionsContainer, document.getElementsByClassName('OptOut')[document.getElementsByClassName('OptOut').length - 1]);

		(document.body || document.documentElement).appendChild(templateMessage);

		// Load auto apply setting

		for (i = 0, field = document.getElementById('autoApplyField'); i < field.options.length; i++) {
			if (field.options[i].value === autoApply.toString()) {
				field.selectedIndex = i;
			}
		}
		
		if (window.location.href.substr(window.location.href.length - 14, 14) === '#Post_Template') {
			scrollTo(document.body, getPosition(document.getElementById('templateOptionsContainer')).y, 100);
			runInGlobal('xenForoMessage(\'Post Template installed. Customize your settings.\', true);');
		}
	}
}