XenForo Post Style

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

  1. // ==UserScript==
  2. // @name XenForo Post Style
  3. // @namespace Makaze
  4. // @include *
  5. // @grant none
  6. // @version 5.0.4
  7. // @description Allows a XenForo user to add a custom BBCode template to their posts. Optional automatic formatting and keyboard shortcut included.
  8. // ==/UserScript==
  9.  
  10. /*
  11. XENFORO POST TEMPLATE SCRIPT
  12. PRESS Alt+T TO APPLY ON ANY PAGE
  13.  
  14. AUTOMATIC FORMATTING OPTIONAL
  15. */
  16.  
  17. function getPosition(element) {
  18. var xPosition = 0,
  19. yPosition = 0;
  20.  
  21. while (element) {
  22. xPosition += (element.offsetLeft
  23. + element.clientLeft);
  24. yPosition += (element.offsetTop
  25. + element.clientTop);
  26. element = element.offsetParent;
  27. }
  28. return {x: xPosition, y: yPosition};
  29. }
  30.  
  31. Math.easeInOutQuad = function (time, start, change, duration) {
  32. time /= duration / 2;
  33. if (time < 1) {
  34. return change / 2 * time * time + start;
  35. }
  36. time--;
  37. return -change / 2 * (time * (time - 2) - 1) + start;
  38. };
  39.  
  40. function scrollTo(element, to, duration) {
  41. var start = element.scrollTop,
  42. change = to - start,
  43. currentTime = 0,
  44. increment = 1;
  45.  
  46. var animateScroll = function() {
  47. var val = Math.easeInOutQuad(currentTime, start, change, duration);
  48. element.scrollTop = val;
  49. currentTime += increment;
  50. if (currentTime < duration) {
  51. setTimeout(animateScroll, increment);
  52. }
  53. };
  54.  
  55. animateScroll();
  56. }
  57.  
  58. function selectRange(elem, start, end) {
  59. var range;
  60.  
  61. if (elem.setSelectionRange) {
  62. elem.focus();
  63. elem.setSelectionRange(start, end);
  64. } else if (elem.createTextRange) {
  65. range = elem.createTextRange();
  66. range.collapse(true);
  67. range.moveEnd('character', end);
  68. range.moveStart('character', start);
  69. range.select();
  70. }
  71. }
  72.  
  73. function cursor(elem, position) {
  74. selectRange(elem, position, position);
  75. }
  76.  
  77. function applyTemplate(elem) {
  78. var formContext = elem,
  79. opts = (localStorage.getItem('MakazeScriptOptions')) ? JSON.parse(localStorage.getItem('MakazeScriptOptions')) : {},
  80. htmlPrefix = (opts.hasOwnProperty('xf_template_htmlPrefix')) ? opts.xf_template_htmlPrefix : '',
  81. htmlSuffix = (opts.hasOwnProperty('xf_template_htmlSuffix')) ? opts.xf_template_htmlSuffix : '',
  82. bbPrefix = (opts.hasOwnProperty('xf_template_bbPrefix')) ? opts.xf_template_bbPrefix : '',
  83. bbSuffix = (opts.hasOwnProperty('xf_template_bbSuffix')) ? opts.xf_template_bbSuffix : '',
  84. thisList,
  85. thisLink,
  86. instance,
  87. postForm,
  88. plainText,
  89. i = 0;
  90.  
  91. var applyTemplateEvent = function(event) {
  92. var parent = event.target;
  93. applyToChildren(parent);
  94. event.target.removeEventListener('keydown', applyTemplateEvent, false);
  95. };
  96.  
  97. var applyToChildren = function(parent) {
  98. var thisChild,
  99. applyTo,
  100. i = 0;
  101.  
  102. for (i = 0; i < parent.childNodes.length; i++) {
  103. thisChild = parent.childNodes[i];
  104. if (thisChild.innerHTML) {
  105. applyTo = thisChild.innerHTML.replace(
  106. /\[quote([^]+)\[\/quote\]/gi, htmlSuffix + '[quote$1[/quote]' + htmlPrefix
  107. );
  108.  
  109. thisChild.innerHTML = htmlPrefix + applyTo + htmlSuffix;
  110. }
  111. }
  112. };
  113.  
  114. while (formContext.getElementsByClassName('redactor_box')[0] == null && formContext.parentNode) {
  115. formContext = formContext.parentNode;
  116. }
  117.  
  118. if (opts.hasOwnProperty('xf_template_auto')) {
  119. for (i = 0; i < formContext.getElementsByClassName('redactor_MessageEditor').length; i++) {
  120. instance = formContext.getElementsByClassName('redactor_MessageEditor')[i];
  121. postForm = instance.contentWindow.document;
  122. if (!postForm.body.textContent.length) {
  123. postForm.body.addEventListener('keydown', applyTemplateEvent, false);
  124. } else {
  125. applyToChildren(postForm.body);
  126. }
  127. }
  128. for (i = 0; i < formContext.getElementsByClassName('bbCodeEditorContainer').length; i++) {
  129. plainText = formContext.getElementsByClassName('bbCodeEditorContainer')[i].getElementsByTagName('textarea')[0];
  130. plainText.value = bbPrefix + plainText.value + bbSuffix;
  131. cursor(plainText, plainText.value.length - bbSuffix.length);
  132. }
  133. } else {
  134. thisList = document.getElementById('AccountMenu').getElementsByClassName('blockLinksList')[0];
  135. for (i = 0; i < thisList.getElementsByTagName('a').length; i++) {
  136. thisLink = thisList.getElementsByTagName('a')[i];
  137. if (thisLink.href.match(/account\/personal\-details/gi) && thisLink.href.substr(window.location.href.length - 14, 14) !== '#Post_Template') {
  138. thisLink.href = thisLink.href + '#Post_Template';
  139. thisLink.click();
  140. break;
  141. }
  142. }
  143. }
  144. }
  145.  
  146. function xenForoMessage(msg, success) {
  147. if (success) {
  148. $('#templateMessage .content').html(msg);
  149. console.log(msg);
  150. } else {
  151. $('#templateMessage .content').html('<strong>Error:</strong> ' + msg);
  152. console.log('Error:', msg);
  153. }
  154. $('#templateMessage').slideDown('medium');
  155. $('#templateMessage .content').animate({
  156. 'opacity': 1
  157. }, 'fast');
  158. setTimeout(function() {
  159. $('#templateMessage').slideUp('medium');
  160. $('#templateMessage .content').animate({
  161. 'opacity': 0
  162. }, 'fast');
  163. }, 1500);
  164. }
  165.  
  166. function runInGlobal(code) {
  167. var scripts = document.createElement('script');
  168. scripts.type = 'text/javascript';
  169. scripts.id = 'runInGlobal';
  170. scripts.appendChild(document.createTextNode(
  171. code +
  172. '\n\n' +
  173. 'document.getElementById(\'runInGlobal\').remove();'
  174. ));
  175.  
  176. (document.head || document.body || document.documentElement).appendChild(scripts);
  177. }
  178.  
  179. function saveTemplateSettings() {
  180. if (!document.getElementById('htmlPrefixField').value.length) {
  181. xenForoMessage('HTML prefix required.', false);
  182. return false;
  183. }
  184.  
  185. if (!document.getElementById('htmlSuffixField').value.length) {
  186. xenForoMessage('HTML suffix required.', false);
  187. return false;
  188. }
  189.  
  190. if (!document.getElementById('bbPrefixField').value.length) {
  191. xenForoMessage('BBCode prefix required.', false);
  192. return false;
  193. }
  194.  
  195. if (!document.getElementById('bbSuffixField').value.length) {
  196. xenForoMessage('BBCode suffix required.', false);
  197. return false;
  198. }
  199. var opts = (localStorage.getItem('MakazeScriptOptions')) ? JSON.parse(localStorage.getItem('MakazeScriptOptions')) : {};
  200.  
  201. opts.xf_template_auto = (document.getElementById('autoApplyField').options[document.getElementById('autoApplyField').selectedIndex].value === 'true');
  202. opts.xf_template_htmlPrefix = document.getElementById('htmlPrefixField').value;
  203. opts.xf_template_htmlSuffix = document.getElementById('htmlSuffixField').value;
  204. opts.xf_template_bbPrefix = document.getElementById('bbPrefixField').value;
  205. opts.xf_template_bbSuffix = document.getElementById('bbSuffixField').value;
  206. localStorage.setItem('MakazeScriptOptions', JSON.stringify(opts));
  207.  
  208. xenForoMessage('Your settings have been saved.', true);
  209. }
  210.  
  211. var applyHandler = function() {
  212. applyTemplate(this);
  213. };
  214.  
  215. if (document.documentElement.id === "XenForo") {
  216. var opts = (localStorage.getItem('MakazeScriptOptions')) ? JSON.parse(localStorage.getItem('MakazeScriptOptions')) : {},
  217. autoApply = (opts.hasOwnProperty('xf_template_auto')) ? opts.xf_template_auto : false,
  218. htmlPrefix = (opts.hasOwnProperty('xf_template_htmlPrefix')) ? opts.xf_template_htmlPrefix : '',
  219. htmlSuffix = (opts.hasOwnProperty('xf_template_htmlSuffix')) ? opts.xf_template_htmlSuffix : '',
  220. bbPrefix = (opts.hasOwnProperty('xf_template_bbPrefix')) ? opts.xf_template_bbPrefix : '',
  221. bbSuffix = (opts.hasOwnProperty('xf_template_bbSuffix')) ? opts.xf_template_bbSuffix : '',
  222. instance,
  223. buttonsContext,
  224. richInstance,
  225. richDoc,
  226. field,
  227. i = 0;
  228.  
  229. // Button creation and auto-application
  230.  
  231. if (document.getElementsByClassName('MessageEditor')[0] != null) {
  232. for (i = 0; i < document.getElementsByClassName('MessageEditor').length; i++) {
  233. instance = document.getElementsByClassName('MessageEditor')[i];
  234. buttonsContext = instance;
  235.  
  236. while (buttonsContext.getElementsByClassName('submitUnit')[0] == null && buttonsContext.parentNode) {
  237. buttonsContext = buttonsContext.parentNode;
  238. }
  239.  
  240. buttonsContext = buttonsContext.getElementsByClassName('submitUnit')[0].getElementsByClassName('button primary')[0].parentNode;
  241.  
  242. var applyButton = document.createElement('input'),
  243. applyButtonSpacer = document.createTextNode(String.fromCharCode(160));
  244. applyButton.type = 'button';
  245. applyButton.value = 'Apply Style';
  246. applyButton.className = 'button JsOnly applyButton';
  247. applyButton.onclick = applyHandler;
  248.  
  249. buttonsContext.appendChild(applyButtonSpacer);
  250. buttonsContext.appendChild(applyButton);
  251.  
  252. if (autoApply.toString() === 'true') {
  253. applyTemplate(instance);
  254. }
  255. }
  256.  
  257. document.addEventListener('keydown', function(e) {
  258. var i = 0,
  259. keyInstance;
  260.  
  261. if (e.keyCode == 84 && e.altKey) {
  262. for (i = 0; i < document.getElementsByClassName('MessageEditor').length; i++) {
  263. keyInstance = document.getElementsByClassName('MessageEditor')[i];
  264. applyTemplate(keyInstance);
  265. }
  266. }
  267. }, false);
  268.  
  269. var applyToRichHandler = function(e) {
  270. if (e.keyCode == 84 && e.altKey) {
  271. applyTemplate(richInstance);
  272. }
  273. };
  274.  
  275. if (document.getElementsByClassName('redactor_MessageEditor')[0] != null) {
  276. for (i = 0; i < document.getElementsByClassName('redactor_MessageEditor').length; i++) {
  277. richInstance = document.getElementsByClassName('redactor_MessageEditor')[i];
  278. richDoc = richInstance.contentWindow.document;
  279. richDoc.addEventListener('keydown', applyToRichHandler, false);
  280. }
  281. }
  282. }
  283. if (window.location.href.match(/account\/personal\-details/gi)) {
  284. // Define xenForoMessage and saveTemplateSettings
  285.  
  286. runInGlobal(
  287. xenForoMessage.toString() +
  288. saveTemplateSettings.toString()
  289. );
  290.  
  291. // Settings creation
  292.  
  293. var optionsContainer = document.createElement('fieldset'),
  294.  
  295. header = document.createElement('dl'),
  296. headerDT = document.createElement('dt'),
  297. headerDD = document.createElement('dd'),
  298. headerDD_Text = document.createTextNode('Post Style'),
  299.  
  300. autoApplyField = document.createElement('dl'),
  301. autoApplyFieldDT = document.createElement('dt'),
  302. autoApplyFieldDT_Text = document.createTextNode('Auto-apply:'),
  303. autoApplyFieldDD = document.createElement('dd'),
  304. autoApplyFieldDD_Select = document.createElement('select'),
  305. autoApplyFieldDD_Select_true = document.createElement('option'),
  306. autoApplyFieldDD_Select_true_Text = document.createTextNode('True'),
  307. autoApplyFieldDD_Select_false = document.createElement('option'),
  308. autoApplyFieldDD_Select_false_Text = document.createTextNode('False'),
  309.  
  310. htmlPrefixField = document.createElement('dl'),
  311. htmlPrefixFieldDT = document.createElement('dt'),
  312. htmlPrefixFieldDT_Text = document.createTextNode('HTML Prefix:'),
  313. htmlPrefixFieldDD = document.createElement('dd'),
  314. htmlPrefixFieldDD_input = document.createElement('input'),
  315.  
  316. htmlSuffixField = document.createElement('dl'),
  317. htmlSuffixFieldDT = document.createElement('dt'),
  318. htmlSuffixFieldDT_Text = document.createTextNode('HTML Suffix:'),
  319. htmlSuffixFieldDD = document.createElement('dd'),
  320. htmlSuffixFieldDD_input = document.createElement('input'),
  321.  
  322. bbPrefixField = document.createElement('dl'),
  323. bbPrefixFieldDT = document.createElement('dt'),
  324. bbPrefixFieldDT_Text = document.createTextNode('BBCode Prefix:'),
  325. bbPrefixFieldDD = document.createElement('dd'),
  326. bbPrefixFieldDD_input = document.createElement('input'),
  327.  
  328. bbSuffixField = document.createElement('dl'),
  329. bbSuffixFieldDT = document.createElement('dt'),
  330. bbSuffixFieldDT_Text = document.createTextNode('BBCode Suffix:'),
  331. bbSuffixFieldDD = document.createElement('dd'),
  332. bbSuffixFieldDD_input = document.createElement('input'),
  333.  
  334. submitField = document.createElement('dl'),
  335. submitFieldDT = document.createElement('dt'),
  336. submitFieldDD = document.createElement('dd'),
  337. submitFieldDD_input = document.createElement('input'),
  338.  
  339. templateMessage = document.createElement('div'),
  340. templateMessage_content = document.createElement('div'),
  341. templateMessage_content_Text = document.createTextNode('Your settings have been saved.');
  342.  
  343. // Load input settings
  344.  
  345. htmlPrefixFieldDD_input.value = htmlPrefix;
  346. htmlSuffixFieldDD_input.value = htmlSuffix;
  347. bbPrefixFieldDD_input.value = bbPrefix;
  348. bbSuffixFieldDD_input.value = bbSuffix;
  349.  
  350. // Header
  351.  
  352. headerDD.setAttribute('style', 'font-weight: bolder; font-size: 130%; width: 40%; text-decoration: underline;');
  353. headerDD.appendChild(headerDD_Text);
  354.  
  355. header.className = 'ctrlUnit';
  356. header.appendChild(headerDT);
  357. header.appendChild(headerDD);
  358.  
  359. // Auto apply field
  360.  
  361. autoApplyFieldDT.appendChild(autoApplyFieldDT_Text);
  362.  
  363. autoApplyFieldDD_Select_true.value = true;
  364. autoApplyFieldDD_Select_true.appendChild(autoApplyFieldDD_Select_true_Text);
  365.  
  366. autoApplyFieldDD_Select_false.value = false;
  367. autoApplyFieldDD_Select_false.appendChild(autoApplyFieldDD_Select_false_Text);
  368.  
  369. autoApplyFieldDD_Select.id = 'autoApplyField';
  370. autoApplyFieldDD_Select.className = 'textCtrl';
  371. autoApplyFieldDD_Select.appendChild(autoApplyFieldDD_Select_true);
  372. autoApplyFieldDD_Select.appendChild(autoApplyFieldDD_Select_false);
  373.  
  374. autoApplyFieldDD.appendChild(autoApplyFieldDD_Select);
  375.  
  376. autoApplyField.className = 'ctrlUnit';
  377. autoApplyField.appendChild(autoApplyFieldDT);
  378. autoApplyField.appendChild(autoApplyFieldDD);
  379.  
  380. // HTML prefix field
  381.  
  382. htmlPrefixFieldDT.appendChild(htmlPrefixFieldDT_Text);
  383.  
  384. htmlPrefixFieldDD_input.type = 'text';
  385. htmlPrefixFieldDD_input.id = 'htmlPrefixField';
  386. htmlPrefixFieldDD_input.className = 'textCtrl OptOut';
  387.  
  388. htmlPrefixFieldDD.appendChild(htmlPrefixFieldDD_input);
  389.  
  390. htmlPrefixField.className = 'ctrlUnit';
  391. htmlPrefixField.appendChild(htmlPrefixFieldDT);
  392. htmlPrefixField.appendChild(htmlPrefixFieldDD);
  393.  
  394. // HTML suffix field
  395.  
  396. htmlSuffixFieldDT.appendChild(htmlSuffixFieldDT_Text);
  397.  
  398. htmlSuffixFieldDD_input.type = 'text';
  399. htmlSuffixFieldDD_input.id = 'htmlSuffixField';
  400. htmlSuffixFieldDD_input.className = 'textCtrl OptOut';
  401.  
  402. htmlSuffixFieldDD.appendChild(htmlSuffixFieldDD_input);
  403.  
  404. htmlSuffixField.className = 'ctrlUnit';
  405. htmlSuffixField.appendChild(htmlSuffixFieldDT);
  406. htmlSuffixField.appendChild(htmlSuffixFieldDD);
  407.  
  408. // BBCode prefix field
  409.  
  410. bbPrefixFieldDT.appendChild(bbPrefixFieldDT_Text);
  411.  
  412. bbPrefixFieldDD_input.type = 'text';
  413. bbPrefixFieldDD_input.id = 'bbPrefixField';
  414. bbPrefixFieldDD_input.className = 'textCtrl OptOut';
  415.  
  416. bbPrefixFieldDD.appendChild(bbPrefixFieldDD_input);
  417.  
  418. bbPrefixField.className = 'ctrlUnit';
  419. bbPrefixField.appendChild(bbPrefixFieldDT);
  420. bbPrefixField.appendChild(bbPrefixFieldDD);
  421.  
  422. // BBCode suffix field
  423.  
  424. bbSuffixFieldDT.appendChild(bbSuffixFieldDT_Text);
  425.  
  426. bbSuffixFieldDD_input.type = 'text';
  427. bbSuffixFieldDD_input.id = 'bbSuffixField';
  428. bbSuffixFieldDD_input.className = 'textCtrl OptOut';
  429.  
  430. bbSuffixFieldDD.appendChild(bbSuffixFieldDD_input);
  431.  
  432. bbSuffixField.className = 'ctrlUnit';
  433. bbSuffixField.appendChild(bbSuffixFieldDT);
  434. bbSuffixField.appendChild(bbSuffixFieldDD);
  435.  
  436. // Submit field
  437.  
  438. submitFieldDD_input.type = 'button';
  439. submitFieldDD_input.id = 'submitTemplate';
  440. submitFieldDD_input.className = 'button';
  441. submitFieldDD_input.value = 'Save';
  442. submitFieldDD_input.setAttribute('onClick', 'saveTemplateSettings();');
  443.  
  444. submitFieldDD.appendChild(submitFieldDD_input);
  445.  
  446. submitField.className = 'ctrlUnit';
  447. submitField.appendChild(submitFieldDT);
  448. submitField.appendChild(submitFieldDD);
  449.  
  450. // Template message
  451.  
  452. templateMessage_content.className = 'content baseHtml';
  453. templateMessage_content.style.opacity = 0;
  454. templateMessage_content.appendChild(templateMessage_content_Text);
  455.  
  456. templateMessage.id = 'templateMessage';
  457. templateMessage.className = 'xenOverlay timedMessage';
  458. templateMessage.setAttribute('style', 'top: 0px; left: 0px; position: fixed; display: none;');
  459. templateMessage.appendChild(templateMessage_content);
  460.  
  461. // Build it all
  462.  
  463. optionsContainer.id = 'templateOptionsContainer';
  464. optionsContainer.appendChild(header);
  465. optionsContainer.appendChild(autoApplyField);
  466. optionsContainer.appendChild(htmlPrefixField);
  467. optionsContainer.appendChild(htmlSuffixField);
  468. optionsContainer.appendChild(bbPrefixField);
  469. optionsContainer.appendChild(bbSuffixField);
  470. optionsContainer.appendChild(submitField);
  471.  
  472. document.getElementsByClassName('OptOut')[document.getElementsByClassName('OptOut').length - 1].parentNode.insertBefore(optionsContainer, document.getElementsByClassName('OptOut')[document.getElementsByClassName('OptOut').length - 1]);
  473.  
  474. (document.body || document.documentElement).appendChild(templateMessage);
  475.  
  476. // Load auto apply setting
  477.  
  478. for (i = 0, field = document.getElementById('autoApplyField'); i < field.options.length; i++) {
  479. if (field.options[i].value === autoApply.toString()) {
  480. field.selectedIndex = i;
  481. }
  482. }
  483. if (window.location.href.substr(window.location.href.length - 14, 14) === '#Post_Template') {
  484. scrollTo(document.body, getPosition(document.getElementById('templateOptionsContainer')).y, 100);
  485. runInGlobal('xenForoMessage(\'Post Template installed. Customize your settings.\', true);');
  486. }
  487. }
  488. }