From 86ddb4ecbbca36739da1993497376fb309c6b79f Mon Sep 17 00:00:00 2001 From: ForklessAnon Date: Fri, 31 Oct 2014 21:53:02 -0500 Subject: [PATCH] Update formatting toolbar to include user definable settings and customized options. --- js/comment-toolbar.js | 528 +++++++++++++++++++++++++----------------- 1 file changed, 316 insertions(+), 212 deletions(-) diff --git a/js/comment-toolbar.js b/js/comment-toolbar.js index 7ae11a73..2c8b9ca8 100644 --- a/js/comment-toolbar.js +++ b/js/comment-toolbar.js @@ -8,253 +8,357 @@ * $config['additional_javascript'][] = 'js/comment-toolbar.js'; */ if (active_page == 'thread' || active_page == 'index') { - $(document).ready(function () { - 'use strict'; - var formats = { - bold: { - displayText: 'B', - altText: 'bold', - styleCSS: 'font-weight: bold;', - options: { - prefix: "'''", - suffix: "'''" - }, - edit: function (box, options) { - wrapSelection(box, options); - }, - shortcutKey: 'b' + var formatText = (function($){ + "use strict"; + var self = {}; + self.rules = { + spoiler: { + text: 'Spoiler', + key: 's', + multiline: false, + exclusiveline: false, + prefix:'**', + suffix:'**' }, italics: { - displayText: 'i', - altText: 'italics', - styleCSS: 'font-style: italic;', - options: { - prefix: "''", - suffix: "''" - }, - edit: function (box, options) { - wrapSelection(box, options); - }, - shortcutKey: 'i' + text: 'Italics', + key: 'i', + multiline: false, + exclusiveline: false, + prefix: "''", + suffix: "''" }, - under: { - displayText: 'U', - altText: 'underline', - styleCSS: 'text-decoration: underline;', - options: { - prefix: '__', - suffix: '__' - }, - edit: function (box, options) { - wrapSelection(box, options); - }, - shortcutKey: 'u' + bold: { + text: 'Bold', + key: 'b', + multiline: false, + exclusiveline: false, + prefix: "'''", + suffix: "'''" }, - spoiler: { - displayText: 'spoiler', - altText: 'mark as spoiler', - styleCSS: '', - options: { - prefix: '[spoiler]', - suffix: '[/spoiler]' - }, - edit: function (box, options) { - wrapSelection(box, options); - }, - shortcutKey: 's' + underline: { + text: 'Underline', + key: 'u', + multiline: false, + exclusiveline: false, + prefix:'__', + suffix:'__' }, code: { - displayText: 'code', - altText: "code formatting", - styleCSS: 'font-family: "Courier New", Courier, monospace;', - options: { - prefix: '[code]', - suffix: '[/code]', - multiline: true - }, - edit: function (box, options) { - wrapSelection(box, options); - }, - shortcutKey: 'd' + text: 'Code', + key: 'f', + multiline: true, + exclusiveline: false, + prefix: '[code]', + suffix: '[/code]' }, strike: { - displayText: 'strike', - altText: 'strikethrough', - styleCSS: 'text-decoration: line-through;', - options: { - prefix: '~~', - suffix: '~~' - }, - edit: function (box, options) { - wrapSelection(box, options); - } + text: 'Strike', + key: 'd', + multiline:false, + exclusiveline:false, + prefix:'~~', + suffix:'~~' }, heading: { - displayText: 'heading', - altText: 'redtext', - styleCSS: 'color: #AF0A0F; font-weight: bold;', - options: { - prefix: '==', - suffix: '==', - exclusiveLine: true - }, - edit: function (box, options) { - wrapSelection(box, options); - } + text: 'Heading', + key: 'r', + multiline:false, + exclusiveline:true, + prefix:'==', + suffix:'==' } }; - - var key, name, altText, ele; - var strBuilder = []; - var subStr = ''; - var styleRules = ''; - - //not exactly mine - var wrapSelection = function (box, options) { - if (box == null) { - return; + + self.toolbar_wrap = function(node) { + if (!localStorage.formatText_enable || localStorage.formatText_enable == 'false') return; + var parent = $(node).parents('form[name="post"]'); + self.wrap(parent.find('#body')[0],'textarea[name="body"]', parent.find('.format-text > select')[0].value, false); + }; + + self.wrap = function(ref, target, option, expandedwrap) { + if (!localStorage.formatText_enable || localStorage.formatText_enable == 'false') return; + // clean and validate arguments + if (ref == null) return; + var settings = {multiline: false, exclusiveline: false, prefix:'', suffix: null}; + $.extend(settings,JSON.parse(localStorage.formatText_rules)[option]); + + // resolve targets into array of proper node elements + // yea, this is overly verbose, oh well. + var res = []; + if (target instanceof Array) { + for (var indexa in target) { + if (target.hasOwnProperty(indexa)) { + if (typeof target[indexa] == 'string') { + var nodes = $(target[indexa]); + for (var indexb in nodes) { + if (indexa.hasOwnProperty(indexb)) res.push(nodes[indexb]); + } + } else { + res.push(target[indexa]); + } + } + } + } else { + if (typeof target == 'string') { + var nodes = $(target); + for (var index in nodes) { + if (nodes.hasOwnProperty(index)) res.push(nodes[index]); + } + } else { + res.push(target); + } } - var prefix = options.prefix; - var suffix = options.suffix; - var multiline = options.multiline || false; - var exclusiveLine = options.exclusiveLine || false; - + target = res; //record scroll top to restore it later. - var scrollTop = box.scrollTop; - var selectionStart = box.selectionStart; - var selectionEnd = box.selectionEnd; - var text = box.value; - var beforeSelection = text.substring(0, selectionStart); - var selectedText = text.substring(selectionStart, selectionEnd); - var afterSelection = text.substring(selectionEnd); + var scrollTop = ref.scrollTop; + //We will restore the selection later, so record the current selection + var selectionStart = ref.selectionStart; + var selectionEnd = ref.selectionEnd; + + var text = ref.value; + var before = text.substring(0, selectionStart); + var selected = text.substring(selectionStart, selectionEnd); + var after = text.substring(selectionEnd); + var whiteSpace = [" ","\t"]; var breakSpace = ["\r","\n"]; - var trailingSpace = ""; - var cursor = selectedText.length - 1; - - //remove trailing space - while (cursor > 0 && selectedText[cursor] === " ") { - trailingSpace += " "; - cursor--; - } - selectedText = selectedText.substring(0, cursor + 1); - - if (!multiline) - selectedText = selectedText.replace(/(\r|\n|\r\n)/g, suffix +"$1"+ prefix); - - if (exclusiveLine) { + var cursor; + + // handles multiline selections on formatting that doesn't support spanning over multiple lines + if (!settings.multiline) selected = selected.replace(/(\r|\n|\r\n)/g,settings.suffix +"$1"+ settings.prefix); + + // 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 + if (settings.exclusiveline || expandedwrap) { // buffer the begining of the selection until a linebreak - cursor = beforeSelection.length -1; - while (cursor >= 0 && breakSpace.indexOf(beforeSelection.charAt(cursor)) == -1) { + cursor = before.length -1; + while (cursor >= 0 && breakSpace.indexOf(before.charAt(cursor)) == -1) { cursor--; } - selectedText = beforeSelection.substring(cursor +1) + selectedText; - beforeSelection = beforeSelection.substring(0, cursor +1); + selected = before.substring(cursor +1) + selected; + before = before.substring(0, cursor +1); // buffer the end of the selection until a linebreak cursor = 0; - while (cursor < afterSelection.length && breakSpace.indexOf(afterSelection.charAt(cursor)) == -1) { + while (cursor < after.length && breakSpace.indexOf(after.charAt(cursor)) == -1) { cursor++; } - selectedText += afterSelection.substring(0, cursor); - afterSelection = afterSelection.substring(cursor); + selected += after.substring(0, cursor); + after = after.substring(cursor); } - - box.value = beforeSelection + prefix + selectedText + suffix + trailingSpace + afterSelection; - - box.selectionEnd = beforeSelection.length + prefix.length + selectedText.length; + + // set values + var res = before + settings.prefix + selected + settings.suffix + after; + $(target).val(res); + + // restore the selection area and scroll of the reference + ref.selectionEnd = before.length + settings.prefix.length + selected.length; if (selectionStart === selectionEnd) { - box.selectionStart = box.selectionEnd; + ref.selectionStart = ref.selectionEnd; } else { - box.selectionStart = beforeSelection.length + prefix.length; + ref.selectionStart = before.length + settings.prefix.length; } - box.scrollTop = scrollTop; + ref.scrollTop = scrollTop; }; + + self.build_toolbars = function(){ + if (!localStorage.formatText_enable || localStorage.formatText_enable == 'false') return; + if (localStorage.formatText_toolbar == 'true'){ + // remove existing toolbars + if ($('.format-text').length > 0) $('.format-text').remove(); + + // Place toolbar above each textarea input + var name, options = '', rules = JSON.parse(localStorage.formatText_rules); + for (var index in rules) { + if (!rules.hasOwnProperty(index)) continue; + name = rules[index].text; - /* Generate the HTML for the toolbar - */ - for (ele in formats) { - if (formats.hasOwnProperty(ele) && formats[ele].displayText != null) { - name = formats[ele].displayText; - altText = formats[ele].altText || ''; - key = formats[ele].shortcutKey; - - //add tooltip text - if (altText) { - if (key) { - altText += ' (ctrl+'+ key +')'; + //add hint if key exists + if (rules[index].key) { + name += ' (CTRL + '+ rules[index].key.toUpperCase() +')'; } - altText = 'title="'+ altText +'"'; + options += ''; } - - subStr = ''+ name +''; - strBuilder.push(subStr); + $('[name="body"]').before('
Wrap
'); + $('body').append(''); + } + }; + + self.add_rule = function(rule, index){ + if (rule === undefined) rule = { + text: 'New Rule', + key: '', + multiline:false, + exclusiveline:false, + prefix:'', + suffix:'' + } + + // generate an id for the rule + if (index === undefined) { + var rules = JSON.parse(localStorage.formatText_rules); + while (rules[index] || index === undefined) { + index = '' + index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1); + index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1); + index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1); + } + } + if (window.Options && Options.get_tab('formatting')){ + var html = $('
').html('\ + \ + \ + \ + \ + \ + \ + \ + '); + + if ($('.format_rule').length > 0) { + $('.format_rule').last().after(html); + } else { + Options.extend_tab('formatting', html); + } + } + }; + + self.save_rules = function(){ + var newrules = {}, rules = $('.format_rule'); + for (var index=0;rules[index];index++) { + console.log(rules[index]); + newrules[$(rules[index]).attr('name')] = { + text: $(rules[index]).find('[name="text"]').val(), + key: $(rules[index]).find('[name="key"]').val(), + prefix: $(rules[index]).find('[name="prefix"]').val(), + suffix: $(rules[index]).find('[name="suffix"]').val(), + multiline: $(rules[index]).find('[name="multiline"]').is(':checked'), + exclusiveline: $(rules[index]).find('[name="exclusiveline"]').is(':checked') + }; + } + console.log(newrules); + localStorage.formatText_rules = JSON.stringify(newrules); + self.build_toolbars(); + }; + + self.reset_rules = function() { + localStorage.formatText_rules = JSON.stringify(self.rules); + }; + + // store default rules for customizing + if (!localStorage.formatText_rules) self.reset_rules(); + + // Add settings to Options panel general tab + if (window.Options && Options.get_tab('general')) { + var s1 = '#formatText_enable>input', s2 = '#formatText_toolbar>input', e = 'change'; + Options.extend_tab('general', ''); + Options.extend_tab('general', ''); + } else { + var s1 = '#formatText_enable', s2 = '#formatText_toolbar', e = 'click'; + $('hr:first').before('
'+ _('Enable post formatting') +'
'); + $('hr:first').before('
'+ _('Show formatting toolbar') +'
'); + } + + // setting for enableing text formatting + $(s1).on(e, function(e) { + if (!localStorage.formatText_enable || localStorage.formatText_enable == 'false') { + localStorage.formatText_enable = 'true'; + if (window.Options && Options.get_tab('general')) e.target.checked = true; } else { - continue; + localStorage.formatText_enable = 'false'; + if (window.Options && Options.get_tab('general')) e.target.checked = false; + } + }); + + // setting for toolbar injection + $(s2).on(e, function(e) { + if (!localStorage.formatText_toolbar || localStorage.formatText_toolbar == 'false') { + localStorage.formatText_toolbar = 'true'; + if (window.Options && Options.get_tab('general')) e.target.checked = true; + formatText.build_toolbars(); + } else { + localStorage.formatText_toolbar = 'false'; + if (window.Options && Options.get_tab('general')) e.target.checked = false; + $('.format-text').remove(); + } + }); + + // make sure the tab settings are switch properly at loadup + if (window.Options && Options.get_tab('general')) { + if (localStorage.formatText_enable == 'true') $(s1)[0].checked = true; + else $(s1)[0].checked = false; + if (localStorage.formatText_toolbar == 'true') $(s2)[0].checked = true; + else $(s2)[0].checked = false; + } + + // add the tab for customizing the format settings + if (window.Options && !Options.get_tab('formatting')) { + Options.add_tab('formatting', 'angle-right', 'Customize Formatting'); + Options.extend_tab('formatting', '\ + \ + '); + + // Data control row + Options.extend_tab('formatting', '\ + \ + \ + \ + '); + + // Descriptor row + Options.extend_tab('formatting', '\ + Name\ + ML\ + EL\ + Prefix\ + Suffix\ + Key\ + '); + + // Rule rows + var rules = JSON.parse(localStorage.formatText_rules); + for (var index in rules){ + if (!rules.hasOwnProperty(index)) continue; + self.add_rule(rules[index], index); } } - - $( 'textarea[name="body"]' ).before( '
' ); - $( '.tf-toolbar' ).html( strBuilder.join(' | ') ); - - /* Sets the CSS style - */ - styleRules = '\n/* generated by 8chan Formatting Tools */'+ - '\n.tf-toolbar {padding: 0px 5px 1px 5px;}'+ - '\n.tf-toolbar :link {text-decoration: none;}'; - for (ele in formats) { - if (formats.hasOwnProperty(ele) && formats[ele].styleCSS) { - styleRules += ' \n#tf-' + ele + ' {' + formats[ele].styleCSS + '}'; - } - } - //add CSS rule to user's custom CSS if it exist - if ($( '.user-css' ).length !== 0) { - $( '.user-css' ).append( styleRules ); - } else { - $( 'body' ).append( '' ); - } - - /* Attach event listeners - */ - $( 'body' ).on( 'keydown', 'textarea[name="body"]', {formats: formats}, function (e) { - //shortcuts - if (e.ctrlKey) { - var ch = String.fromCharCode(e.which).toLowerCase(); - var box = e.target; - var formats = e.data.formats; - for (var ele in formats) { - if (formats.hasOwnProperty(ele) && (ch === formats[ele].shortcutKey)) { - formats[ele].edit(box, formats[ele].options); - e.preventDefault(); - } - } - } - }); - $( 'body' ).on( 'keydown', '#quick-reply textarea[name="body"]', {formats: formats}, function (e) { - //close quick reply when esc is prssed - if (e.which === 27) { - $( '.close-btn' ).trigger( 'click' ); - } - }); - $( 'body' ).on( 'click', '.tf-toolbar a[id]', {formats: formats}, function (e) { - //toolbar buttons - var formats = e.data.formats; - var box = $(e.target).parent().next()[0]; - - for (var ele in formats) { - if (formats.hasOwnProperty(ele) && (e.target.id === 'tf-' + ele)) { - formats[ele].edit(box, formats[ele].options); - } - } - }); - // $( 'body' ).on( 'keydown', function (e) { - // if (e.which === 67 && - // e.target.nodeName !== 'INPUT' && //The C, the whole C, and nothing but the C - // e.target.nodeName !== 'TEXTAREA' && - // !(e.ctrlKey || e.altKey || e.shiftKey)) { - // document.location.href = '//'+ document.location.host +'/'+ board_name +'/catalog.html'; - // } - // }); + + return self; + })(jQuery); + // run initial toolbar injection + formatText.build_toolbars(); + + //attach listeners to so it also works on quick-reply box + $('body').on('keydown', '#body, #quick-reply #body', function(e) { + if (!localStorage.formatText_enable || localStorage.formatText_enable == 'false') return; + var key = String.fromCharCode(e.which).toLowerCase(); + var rules = JSON.parse(localStorage.formatText_rules); + for (var index in rules) { + if (!rules.hasOwnProperty(index)) continue; + if (key === rules[index].key && e.ctrlKey) { + e.preventDefault(); + if (e.shiftKey) { + formatText.wrap(e.target, 'textarea[name="body"]', index, true); + } else { + formatText.wrap(e.target, 'textarea[name="body"]', index, false); + } + } + } }); + $(document).trigger('formatText'); }