384 lines
14 KiB
JavaScript
384 lines
14 KiB
JavaScript
/*
|
|
* comment-toolbar.js
|
|
* - Adds a toolbar above the commenting area containing most of 8Chan's formatting options
|
|
* - Press Esc to close quick-reply window when it's in focus
|
|
*
|
|
* Usage:
|
|
* $config['additional_javascript'][] = 'js/jquery.min.js';
|
|
* $config['additional_javascript'][] = 'js/comment-toolbar.js';
|
|
*/
|
|
if (active_page == 'thread' || active_page == 'index') {
|
|
var formatText = (function($){
|
|
"use strict";
|
|
var self = {};
|
|
self.rules = {
|
|
spoiler: {
|
|
text: _('Spoiler'),
|
|
key: 's',
|
|
multiline: false,
|
|
exclusiveline: false,
|
|
prefix:'**',
|
|
suffix:'**'
|
|
},
|
|
italics: {
|
|
text: _('Italics'),
|
|
key: 'i',
|
|
multiline: false,
|
|
exclusiveline: false,
|
|
prefix: "''",
|
|
suffix: "''"
|
|
},
|
|
bold: {
|
|
text: _('Bold'),
|
|
key: 'b',
|
|
multiline: false,
|
|
exclusiveline: false,
|
|
prefix: "'''",
|
|
suffix: "'''"
|
|
},
|
|
underline: {
|
|
text: _('Underline'),
|
|
key: 'u',
|
|
multiline: false,
|
|
exclusiveline: false,
|
|
prefix:'__',
|
|
suffix:'__'
|
|
},
|
|
code: {
|
|
text: _('Code'),
|
|
key: 'f',
|
|
multiline: true,
|
|
exclusiveline: false,
|
|
prefix: '[code]',
|
|
suffix: '[/code]'
|
|
},
|
|
strike: {
|
|
text: _('Strike'),
|
|
key: 'd',
|
|
multiline:false,
|
|
exclusiveline:false,
|
|
prefix:'~~',
|
|
suffix:'~~'
|
|
},
|
|
heading: {
|
|
text: _('Heading'),
|
|
key: 'r',
|
|
multiline:false,
|
|
exclusiveline:true,
|
|
prefix:'==',
|
|
suffix:'=='
|
|
}
|
|
};
|
|
|
|
self.toolbar_wrap = function(node) {
|
|
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) {
|
|
// 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);
|
|
}
|
|
}
|
|
target = res;
|
|
//record scroll top to restore it later.
|
|
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 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 = before.length -1;
|
|
while (cursor >= 0 && breakSpace.indexOf(before.charAt(cursor)) == -1) {
|
|
cursor--;
|
|
}
|
|
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 < after.length && breakSpace.indexOf(after.charAt(cursor)) == -1) {
|
|
cursor++;
|
|
}
|
|
selected += after.substring(0, cursor);
|
|
after = after.substring(cursor);
|
|
}
|
|
|
|
// 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) {
|
|
ref.selectionStart = ref.selectionEnd;
|
|
} else {
|
|
ref.selectionStart = before.length + settings.prefix.length;
|
|
}
|
|
ref.scrollTop = scrollTop;
|
|
};
|
|
|
|
self.build_toolbars = function(){
|
|
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;
|
|
|
|
//add hint if key exists
|
|
if (rules[index].key) {
|
|
name += ' (CTRL + '+ rules[index].key.toUpperCase() +')';
|
|
}
|
|
options += '<option value="'+ index +'">'+ name +'</option>';
|
|
}
|
|
$('[name="body"]').before('<div class="format-text"><a href="javascript:;" onclick="formatText.toolbar_wrap(this);">Wrap</a><select>'+ options +'</select></div>');
|
|
$('body').append('<style>#quick-reply .format-text>a{width:15%;display:inline-block;text-align:center;}#quick-reply .format-text>select{width:85%;};</style>');
|
|
}
|
|
};
|
|
|
|
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 = $('<div class="format_rule" name="'+ index +'"></div>').html('\
|
|
<input type="text" name="text" class="format_option" size="10" value=\"'+ rule.text.replace(/"/g, '"') +'\">\
|
|
<input type="checkbox" name="multiline" class="format_option" '+ (rule.multiline ? 'checked' : '') +'>\
|
|
<input type="checkbox" name="exclusiveline" class="format_option" '+ (rule.exclusiveline ? 'checked' : '') +'>\
|
|
<input type="text" name="prefix" class="format_option" size="8" value=\"'+ (rule.prefix ? rule.prefix.replace(/"/g, '"') : '') +'\">\
|
|
<input type="text" name="suffix" class="format_option" size="8" value=\"'+ (rule.suffix ? rule.suffix.replace(/"/g, '"') : '') +'\">\
|
|
<input type="text" name="key" class="format_option" size="2" maxlength="1" value=\"'+ rule.key +'\">\
|
|
<input type="button" value="X" onclick="if(confirm(\'Do you wish to remove the '+ rule.text +' formatting rule?\'))$(this).parent().remove();">\
|
|
');
|
|
|
|
if ($('.format_rule').length > 0) {
|
|
$('.format_rule').last().after(html);
|
|
} else {
|
|
Options.extend_tab('formatting', html);
|
|
}
|
|
}
|
|
};
|
|
|
|
self.save_rules = function(){
|
|
var rule, newrules = {}, rules = $('.format_rule');
|
|
for (var index=0;rules[index];index++) {
|
|
rule = $(rules[index]);
|
|
newrules[rule.attr('name')] = {
|
|
text: rule.find('[name="text"]').val(),
|
|
key: rule.find('[name="key"]').val(),
|
|
prefix: rule.find('[name="prefix"]').val(),
|
|
suffix: rule.find('[name="suffix"]').val(),
|
|
multiline: rule.find('[name="multiline"]').is(':checked'),
|
|
exclusiveline: rule.find('[name="exclusiveline"]').is(':checked')
|
|
};
|
|
}
|
|
localStorage.formatText_rules = JSON.stringify(newrules);
|
|
self.build_toolbars();
|
|
};
|
|
|
|
self.reset_rules = function(to_default) {
|
|
$('.format_rule').remove();
|
|
var rules;
|
|
if (to_default) rules = self.rules;
|
|
else rules = JSON.parse(localStorage.formatText_rules);
|
|
for (var index in rules){
|
|
if (!rules.hasOwnProperty(index)) continue;
|
|
self.add_rule(rules[index], index);
|
|
}
|
|
};
|
|
|
|
// setup default rules for customizing
|
|
if (!localStorage.formatText_rules) localStorage.formatText_rules = JSON.stringify(self.rules);
|
|
|
|
// setup code to be ran when page is ready (work around for main.js compilation).
|
|
$(document).ready(function(){
|
|
// Add settings to Options panel general tab
|
|
if (window.Options && Options.get_tab('general')) {
|
|
var s1 = '#formatText_keybinds>input', s2 = '#formatText_toolbar>input', e = 'change';
|
|
Options.extend_tab('general', '\
|
|
<fieldset>\
|
|
<legend>Formatting Options</legend>\
|
|
<label id="formatText_keybinds"><input type="checkbox">' + _('Enable formatting keybinds') + '</label>\
|
|
<label id="formatText_toolbar"><input type="checkbox">' + _('Show formatting toolbar') + '</label>\
|
|
</fieldset>\
|
|
');
|
|
} else {
|
|
var s1 = '#formatText_keybinds', s2 = '#formatText_toolbar', e = 'click';
|
|
$('hr:first').before('<div id="formatText_keybinds" style="text-align:right"><a class="unimportant" href="javascript:void(0)">'+ _('Enable formatting keybinds') +'</a></div>');
|
|
$('hr:first').before('<div id="formatText_toolbar" style="text-align:right"><a class="unimportant" href="javascript:void(0)">'+ _('Show formatting toolbar') +'</a></div>');
|
|
}
|
|
|
|
// 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', '\
|
|
<style>\
|
|
.format_option{\
|
|
margin-right:5px;\
|
|
overflow:initial;\
|
|
font-size:15px;\
|
|
}\
|
|
.format_option[type="text"]{\
|
|
text-align:center;\
|
|
padding-bottom: 2px;\
|
|
padding-top: 2px;\
|
|
}\
|
|
.format_option:last-child{\
|
|
margin-right:0;\
|
|
}\
|
|
fieldset{\
|
|
margin-top:5px;\
|
|
}\
|
|
</style>\
|
|
');
|
|
|
|
// Data control row
|
|
Options.extend_tab('formatting', '\
|
|
<button onclick="formatText.add_rule();">'+_('Add Rule')+'</button>\
|
|
<button onclick="formatText.save_rules();">'+_('Save Rules')+'</button>\
|
|
<button onclick="formatText.reset_rules(false);">'+_('Revert')+'</button>\
|
|
<button onclick="formatText.reset_rules(true);">'+_('Reset to Default')+'</button>\
|
|
');
|
|
|
|
// Descriptor row
|
|
Options.extend_tab('formatting', '\
|
|
<span class="format_option" style="margin-left:25px;">Name</span>\
|
|
<span class="format_option" style="margin-left:45px;" title="Multi-line: Allow formatted area to contain linebreaks.">ML</span>\
|
|
<span class="format_option" style="margin-left:0px;" title="Exclusive-line: Require formatted area to start after and end before a linebreak.">EL</span>\
|
|
<span class="format_option" style="margin-left:25px;" title="Text injected at the start of a format area.">Prefix</span>\
|
|
<span class="format_option" style="margin-left:60px;" title="Text injected at the end of a format area.">Suffix</span>\
|
|
<span class="format_option" style="margin-left:40px;" title="Optional keybind value to allow keyboard shortcut access.">Key</span>\
|
|
');
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// setting for enabling formatting keybinds
|
|
$(s1).on(e, function(e) {
|
|
console.log('Keybind');
|
|
if (!localStorage.formatText_keybinds || localStorage.formatText_keybinds == 'false') {
|
|
localStorage.formatText_keybinds = 'true';
|
|
if (window.Options && Options.get_tab('general')) e.target.checked = true;
|
|
} else {
|
|
localStorage.formatText_keybinds = 'false';
|
|
if (window.Options && Options.get_tab('general')) e.target.checked = false;
|
|
}
|
|
});
|
|
|
|
// setting for toolbar injection
|
|
$(s2).on(e, function(e) {
|
|
console.log('Toolbar');
|
|
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_keybinds == 'true') $(s1)[0].checked = true;
|
|
else $(s1)[0].checked = false;
|
|
if (localStorage.formatText_toolbar == 'true') $(s2)[0].checked = true;
|
|
else $(s2)[0].checked = false;
|
|
}
|
|
|
|
// Initial toolbar injection
|
|
formatText.build_toolbars();
|
|
|
|
//attach listener to <body> so it also works on quick-reply box
|
|
$('body').on('keydown', '[name="body"]', function(e) {
|
|
if (!localStorage.formatText_keybinds || localStorage.formatText_keybinds == '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);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Signal that comment-toolbar loading has completed.
|
|
$(document).trigger('formatText');
|
|
});
|
|
|
|
return self;
|
|
})(jQuery);
|
|
}
|