The version of vichan running on lainchan.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

384 lines
14KB

  1. /*
  2. * comment-toolbar.js
  3. * - Adds a toolbar above the commenting area containing most of 8Chan's formatting options
  4. * - Press Esc to close quick-reply window when it's in focus
  5. *
  6. * Usage:
  7. * $config['additional_javascript'][] = 'js/jquery.min.js';
  8. * $config['additional_javascript'][] = 'js/comment-toolbar.js';
  9. */
  10. if (active_page == 'thread' || active_page == 'index') {
  11. var formatText = (function($){
  12. "use strict";
  13. var self = {};
  14. self.rules = {
  15. spoiler: {
  16. text: _('Spoiler'),
  17. key: 's',
  18. multiline: false,
  19. exclusiveline: false,
  20. prefix:'**',
  21. suffix:'**'
  22. },
  23. italics: {
  24. text: _('Italics'),
  25. key: 'i',
  26. multiline: false,
  27. exclusiveline: false,
  28. prefix: "''",
  29. suffix: "''"
  30. },
  31. bold: {
  32. text: _('Bold'),
  33. key: 'b',
  34. multiline: false,
  35. exclusiveline: false,
  36. prefix: "'''",
  37. suffix: "'''"
  38. },
  39. underline: {
  40. text: _('Underline'),
  41. key: 'u',
  42. multiline: false,
  43. exclusiveline: false,
  44. prefix:'__',
  45. suffix:'__'
  46. },
  47. code: {
  48. text: _('Code'),
  49. key: 'f',
  50. multiline: true,
  51. exclusiveline: false,
  52. prefix: '[code]',
  53. suffix: '[/code]'
  54. },
  55. strike: {
  56. text: _('Strike'),
  57. key: 'd',
  58. multiline:false,
  59. exclusiveline:false,
  60. prefix:'~~',
  61. suffix:'~~'
  62. },
  63. heading: {
  64. text: _('Heading'),
  65. key: 'r',
  66. multiline:false,
  67. exclusiveline:true,
  68. prefix:'==',
  69. suffix:'=='
  70. }
  71. };
  72. self.toolbar_wrap = function(node) {
  73. var parent = $(node).parents('form[name="post"]');
  74. self.wrap(parent.find('#body')[0],'textarea[name="body"]', parent.find('.format-text > select')[0].value, false);
  75. };
  76. self.wrap = function(ref, target, option, expandedwrap) {
  77. // clean and validate arguments
  78. if (ref == null) return;
  79. var settings = {multiline: false, exclusiveline: false, prefix:'', suffix: null};
  80. $.extend(settings,JSON.parse(localStorage.formatText_rules)[option]);
  81. // resolve targets into array of proper node elements
  82. // yea, this is overly verbose, oh well.
  83. var res = [];
  84. if (target instanceof Array) {
  85. for (var indexa in target) {
  86. if (target.hasOwnProperty(indexa)) {
  87. if (typeof target[indexa] == 'string') {
  88. var nodes = $(target[indexa]);
  89. for (var indexb in nodes) {
  90. if (indexa.hasOwnProperty(indexb)) res.push(nodes[indexb]);
  91. }
  92. } else {
  93. res.push(target[indexa]);
  94. }
  95. }
  96. }
  97. } else {
  98. if (typeof target == 'string') {
  99. var nodes = $(target);
  100. for (var index in nodes) {
  101. if (nodes.hasOwnProperty(index)) res.push(nodes[index]);
  102. }
  103. } else {
  104. res.push(target);
  105. }
  106. }
  107. target = res;
  108. //record scroll top to restore it later.
  109. var scrollTop = ref.scrollTop;
  110. //We will restore the selection later, so record the current selection
  111. var selectionStart = ref.selectionStart;
  112. var selectionEnd = ref.selectionEnd;
  113. var text = ref.value;
  114. var before = text.substring(0, selectionStart);
  115. var selected = text.substring(selectionStart, selectionEnd);
  116. var after = text.substring(selectionEnd);
  117. var whiteSpace = [" ","\t"];
  118. var breakSpace = ["\r","\n"];
  119. var cursor;
  120. // handles multiline selections on formatting that doesn't support spanning over multiple lines
  121. if (!settings.multiline) selected = selected.replace(/(\r|\n|\r\n)/g,settings.suffix +"$1"+ settings.prefix);
  122. // handles formatting that requires it to be on it's own line OR if the user wishes to expand the wrap to the nearest linebreak
  123. if (settings.exclusiveline || expandedwrap) {
  124. // buffer the begining of the selection until a linebreak
  125. cursor = before.length -1;
  126. while (cursor >= 0 && breakSpace.indexOf(before.charAt(cursor)) == -1) {
  127. cursor--;
  128. }
  129. selected = before.substring(cursor +1) + selected;
  130. before = before.substring(0, cursor +1);
  131. // buffer the end of the selection until a linebreak
  132. cursor = 0;
  133. while (cursor < after.length && breakSpace.indexOf(after.charAt(cursor)) == -1) {
  134. cursor++;
  135. }
  136. selected += after.substring(0, cursor);
  137. after = after.substring(cursor);
  138. }
  139. // set values
  140. var res = before + settings.prefix + selected + settings.suffix + after;
  141. $(target).val(res);
  142. // restore the selection area and scroll of the reference
  143. ref.selectionEnd = before.length + settings.prefix.length + selected.length;
  144. if (selectionStart === selectionEnd) {
  145. ref.selectionStart = ref.selectionEnd;
  146. } else {
  147. ref.selectionStart = before.length + settings.prefix.length;
  148. }
  149. ref.scrollTop = scrollTop;
  150. };
  151. self.build_toolbars = function(){
  152. if (localStorage.formatText_toolbar == 'true'){
  153. // remove existing toolbars
  154. if ($('.format-text').length > 0) $('.format-text').remove();
  155. // Place toolbar above each textarea input
  156. var name, options = '', rules = JSON.parse(localStorage.formatText_rules);
  157. for (var index in rules) {
  158. if (!rules.hasOwnProperty(index)) continue;
  159. name = rules[index].text;
  160. //add hint if key exists
  161. if (rules[index].key) {
  162. name += ' (CTRL + '+ rules[index].key.toUpperCase() +')';
  163. }
  164. options += '<option value="'+ index +'">'+ name +'</option>';
  165. }
  166. $('[name="body"]').before('<div class="format-text"><a href="javascript:;" onclick="formatText.toolbar_wrap(this);">Wrap</a><select>'+ options +'</select></div>');
  167. $('body').append('<style>#quick-reply .format-text>a{width:15%;display:inline-block;text-align:center;}#quick-reply .format-text>select{width:85%;};</style>');
  168. }
  169. };
  170. self.add_rule = function(rule, index){
  171. if (rule === undefined) rule = {
  172. text: 'New Rule',
  173. key: '',
  174. multiline:false,
  175. exclusiveline:false,
  176. prefix:'',
  177. suffix:''
  178. }
  179. // generate an id for the rule
  180. if (index === undefined) {
  181. var rules = JSON.parse(localStorage.formatText_rules);
  182. while (rules[index] || index === undefined) {
  183. index = ''
  184. index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1);
  185. index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1);
  186. index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1);
  187. }
  188. }
  189. if (window.Options && Options.get_tab('formatting')){
  190. var html = $('<div class="format_rule" name="'+ index +'"></div>').html('\
  191. <input type="text" name="text" class="format_option" size="10" value=\"'+ rule.text.replace(/"/g, '&quot;') +'\">\
  192. <input type="checkbox" name="multiline" class="format_option" '+ (rule.multiline ? 'checked' : '') +'>\
  193. <input type="checkbox" name="exclusiveline" class="format_option" '+ (rule.exclusiveline ? 'checked' : '') +'>\
  194. <input type="text" name="prefix" class="format_option" size="8" value=\"'+ (rule.prefix ? rule.prefix.replace(/"/g, '&quot;') : '') +'\">\
  195. <input type="text" name="suffix" class="format_option" size="8" value=\"'+ (rule.suffix ? rule.suffix.replace(/"/g, '&quot;') : '') +'\">\
  196. <input type="text" name="key" class="format_option" size="2" maxlength="1" value=\"'+ rule.key +'\">\
  197. <input type="button" value="X" onclick="if(confirm(\'Do you wish to remove the '+ rule.text +' formatting rule?\'))$(this).parent().remove();">\
  198. ');
  199. if ($('.format_rule').length > 0) {
  200. $('.format_rule').last().after(html);
  201. } else {
  202. Options.extend_tab('formatting', html);
  203. }
  204. }
  205. };
  206. self.save_rules = function(){
  207. var rule, newrules = {}, rules = $('.format_rule');
  208. for (var index=0;rules[index];index++) {
  209. rule = $(rules[index]);
  210. newrules[rule.attr('name')] = {
  211. text: rule.find('[name="text"]').val(),
  212. key: rule.find('[name="key"]').val(),
  213. prefix: rule.find('[name="prefix"]').val(),
  214. suffix: rule.find('[name="suffix"]').val(),
  215. multiline: rule.find('[name="multiline"]').is(':checked'),
  216. exclusiveline: rule.find('[name="exclusiveline"]').is(':checked')
  217. };
  218. }
  219. localStorage.formatText_rules = JSON.stringify(newrules);
  220. self.build_toolbars();
  221. };
  222. self.reset_rules = function(to_default) {
  223. $('.format_rule').remove();
  224. var rules;
  225. if (to_default) rules = self.rules;
  226. else rules = JSON.parse(localStorage.formatText_rules);
  227. for (var index in rules){
  228. if (!rules.hasOwnProperty(index)) continue;
  229. self.add_rule(rules[index], index);
  230. }
  231. };
  232. // setup default rules for customizing
  233. if (!localStorage.formatText_rules) localStorage.formatText_rules = JSON.stringify(self.rules);
  234. // setup code to be ran when page is ready (work around for main.js compilation).
  235. $(document).ready(function(){
  236. // Add settings to Options panel general tab
  237. if (window.Options && Options.get_tab('general')) {
  238. var s1 = '#formatText_keybinds>input', s2 = '#formatText_toolbar>input', e = 'change';
  239. Options.extend_tab('general', '\
  240. <fieldset>\
  241. <legend>Formatting Options</legend>\
  242. <label id="formatText_keybinds"><input type="checkbox">' + _('Enable formatting keybinds') + '</label>\
  243. <label id="formatText_toolbar"><input type="checkbox">' + _('Show formatting toolbar') + '</label>\
  244. </fieldset>\
  245. ');
  246. } else {
  247. var s1 = '#formatText_keybinds', s2 = '#formatText_toolbar', e = 'click';
  248. $('hr:first').before('<div id="formatText_keybinds" style="text-align:right"><a class="unimportant" href="javascript:void(0)">'+ _('Enable formatting keybinds') +'</a></div>');
  249. $('hr:first').before('<div id="formatText_toolbar" style="text-align:right"><a class="unimportant" href="javascript:void(0)">'+ _('Show formatting toolbar') +'</a></div>');
  250. }
  251. // add the tab for customizing the format settings
  252. if (window.Options && !Options.get_tab('formatting')) {
  253. Options.add_tab('formatting', 'angle-right', _('Customize Formatting'));
  254. Options.extend_tab('formatting', '\
  255. <style>\
  256. .format_option{\
  257. margin-right:5px;\
  258. overflow:initial;\
  259. font-size:15px;\
  260. }\
  261. .format_option[type="text"]{\
  262. text-align:center;\
  263. padding-bottom: 2px;\
  264. padding-top: 2px;\
  265. }\
  266. .format_option:last-child{\
  267. margin-right:0;\
  268. }\
  269. fieldset{\
  270. margin-top:5px;\
  271. }\
  272. </style>\
  273. ');
  274. // Data control row
  275. Options.extend_tab('formatting', '\
  276. <button onclick="formatText.add_rule();">'+_('Add Rule')+'</button>\
  277. <button onclick="formatText.save_rules();">'+_('Save Rules')+'</button>\
  278. <button onclick="formatText.reset_rules(false);">'+_('Revert')+'</button>\
  279. <button onclick="formatText.reset_rules(true);">'+_('Reset to Default')+'</button>\
  280. ');
  281. // Descriptor row
  282. Options.extend_tab('formatting', '\
  283. <span class="format_option" style="margin-left:25px;">Name</span>\
  284. <span class="format_option" style="margin-left:45px;" title="Multi-line: Allow formatted area to contain linebreaks.">ML</span>\
  285. <span class="format_option" style="margin-left:0px;" title="Exclusive-line: Require formatted area to start after and end before a linebreak.">EL</span>\
  286. <span class="format_option" style="margin-left:25px;" title="Text injected at the start of a format area.">Prefix</span>\
  287. <span class="format_option" style="margin-left:60px;" title="Text injected at the end of a format area.">Suffix</span>\
  288. <span class="format_option" style="margin-left:40px;" title="Optional keybind value to allow keyboard shortcut access.">Key</span>\
  289. ');
  290. // Rule rows
  291. var rules = JSON.parse(localStorage.formatText_rules);
  292. for (var index in rules){
  293. if (!rules.hasOwnProperty(index)) continue;
  294. self.add_rule(rules[index], index);
  295. }
  296. }
  297. // setting for enabling formatting keybinds
  298. $(s1).on(e, function(e) {
  299. console.log('Keybind');
  300. if (!localStorage.formatText_keybinds || localStorage.formatText_keybinds == 'false') {
  301. localStorage.formatText_keybinds = 'true';
  302. if (window.Options && Options.get_tab('general')) e.target.checked = true;
  303. } else {
  304. localStorage.formatText_keybinds = 'false';
  305. if (window.Options && Options.get_tab('general')) e.target.checked = false;
  306. }
  307. });
  308. // setting for toolbar injection
  309. $(s2).on(e, function(e) {
  310. console.log('Toolbar');
  311. if (!localStorage.formatText_toolbar || localStorage.formatText_toolbar == 'false') {
  312. localStorage.formatText_toolbar = 'true';
  313. if (window.Options && Options.get_tab('general')) e.target.checked = true;
  314. formatText.build_toolbars();
  315. } else {
  316. localStorage.formatText_toolbar = 'false';
  317. if (window.Options && Options.get_tab('general')) e.target.checked = false;
  318. $('.format-text').remove();
  319. }
  320. });
  321. // make sure the tab settings are switch properly at loadup
  322. if (window.Options && Options.get_tab('general')) {
  323. if (localStorage.formatText_keybinds == 'true') $(s1)[0].checked = true;
  324. else $(s1)[0].checked = false;
  325. if (localStorage.formatText_toolbar == 'true') $(s2)[0].checked = true;
  326. else $(s2)[0].checked = false;
  327. }
  328. // Initial toolbar injection
  329. formatText.build_toolbars();
  330. //attach listener to <body> so it also works on quick-reply box
  331. $('body').on('keydown', '[name="body"]', function(e) {
  332. if (!localStorage.formatText_keybinds || localStorage.formatText_keybinds == 'false') return;
  333. var key = String.fromCharCode(e.which).toLowerCase();
  334. var rules = JSON.parse(localStorage.formatText_rules);
  335. for (var index in rules) {
  336. if (!rules.hasOwnProperty(index)) continue;
  337. if (key === rules[index].key && e.ctrlKey) {
  338. e.preventDefault();
  339. if (e.shiftKey) {
  340. formatText.wrap(e.target, 'textarea[name="body"]', index, true);
  341. } else {
  342. formatText.wrap(e.target, 'textarea[name="body"]', index, false);
  343. }
  344. }
  345. }
  346. });
  347. // Signal that comment-toolbar loading has completed.
  348. $(document).trigger('formatText');
  349. });
  350. return self;
  351. })(jQuery);
  352. }