mirror of
https://github.com/anteo/redmine_custom_workflows.git
synced 2026-01-25 15:54:19 +00:00
353 lines
14 KiB
JavaScript
353 lines
14 KiB
JavaScript
/* encoding: utf-8
|
|
*
|
|
* Redmine plugin for Custom Workflows
|
|
*
|
|
* Copyright Anton Argirov
|
|
* Copyright Karel Pičman <karel.picman@kontron.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
(function($) {
|
|
$.fn.taboverride = function(tabSize, autoIndent) {
|
|
this.each(function() {
|
|
$(this).data('taboverride', new TabOverride(this, tabSize, autoIndent));
|
|
});
|
|
};
|
|
|
|
function TabOverride(element, tabSize, autoIndent) {
|
|
let ta = document.createElement('textarea');
|
|
ta.value = '\n';
|
|
|
|
this.newline = ta.value;
|
|
this.newlineLen = this.newline.length;
|
|
this.autoIndent = autoIndent;
|
|
this.inWhitespace = false;
|
|
this.element = element;
|
|
this.setTabSize(tabSize);
|
|
|
|
$(element).on('keypress', $.proxy(this.overrideKeyPress, this));
|
|
$(element).on('keydown', $.proxy(this.overrideKeyDown, this));
|
|
}
|
|
|
|
TabOverride.prototype = {
|
|
/**
|
|
* Returns the current tab size. 0 represents the tab character.
|
|
*
|
|
* @return {Number} the size (length) of the tab string or 0 for the tab character
|
|
*
|
|
* @name getTabSize
|
|
* @function
|
|
*/
|
|
getTabSize:function () {
|
|
return this.aTab === '\t' ? 0 : this.aTab.length;
|
|
},
|
|
|
|
/**
|
|
* Sets the tab size for all elements that have Tab Override enabled.
|
|
* 0 represents the tab character.
|
|
*
|
|
* @param {Number} size the tab size (default = 0)
|
|
*
|
|
* @name setTabSize
|
|
* @function
|
|
*/
|
|
setTabSize:function (size) {
|
|
if (!size) { // size is 0 or not specified (or falsy)
|
|
this.aTab = '\t';
|
|
} else if (typeof size === 'number' && size > 0) {
|
|
this.aTab = '';
|
|
for (let i = 0; i < size; i += 1) {
|
|
this.aTab += ' ';
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Prevents the default action for the keyPress event when tab or enter are
|
|
* pressed. Opera (and Firefox) also fire a keypress event when the tab or
|
|
* enter key is pressed. Opera requires that the default action be prevented
|
|
* on this event or the textarea will lose focus.
|
|
*
|
|
* @param {Event} e the event object
|
|
* @private
|
|
*/
|
|
overrideKeyPress:function (e) {
|
|
let key = e.keyCode;
|
|
if ((key === 9 || (key === 13 && this.autoIndent && !this.inWhitespace)) && !e.ctrlKey && !e.altKey) {
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Inserts / removes tabs and newlines on the keyDown event for the tab or enter key.
|
|
*
|
|
* @param {Event} e the event object
|
|
*
|
|
* @private
|
|
*/
|
|
overrideKeyDown:function (e) {
|
|
let key = e.keyCode, // the key code for the key that was pressed
|
|
tab, // the string representing a tab
|
|
tabLen, // the length of a tab
|
|
text, // initial text in the textarea
|
|
range, // the IE TextRange object
|
|
tempRange, // used to calculate selection start and end positions in IE
|
|
preNewlines, // the number of newline character sequences before the selection start (for IE)
|
|
selNewlines, // the number of newline character sequences within the selection (for IE)
|
|
initScrollTop, // initial scrollTop value used to fix scrolling in Firefox
|
|
selStart, // the selection start position
|
|
selEnd, // the selection end position
|
|
sel, // the selected text
|
|
startLine, // for multi-line selections, the first character position of the first line
|
|
endLine, // for multi-line selections, the last character position of the last line
|
|
numTabs, // the number of tabs inserted / removed in the selection
|
|
startTab, // if a tab was removed from the start of the first line
|
|
preTab, // if a tab was removed before the start of the selection
|
|
whitespace, // the whitespace at the beginning of the first selected line
|
|
whitespaceLen; // the length of the whitespace at the beginning of the first selected line
|
|
|
|
// don't do any unnecessary work
|
|
if ((key !== 9 && (key !== 13 || !this.autoIndent)) || e.ctrlKey || e.altKey) {
|
|
return;
|
|
}
|
|
|
|
// initialize variables used for tab and enter keys
|
|
this.inWhitespace = false; // this will be set to true if enter is pressed in the leading whitespace
|
|
text = this.element.value;
|
|
|
|
// this is really just for Firefox, but will be used by all browsers that support
|
|
// selectionStart and selectionEnd - whenever the textarea value property is reset,
|
|
// Firefox scrolls back to the top - this is used to set it back to the original value
|
|
// scrollTop is nonstandard, but supported by all modern browsers
|
|
initScrollTop = this.element.scrollTop;
|
|
|
|
// get the text selection
|
|
// prefer the nonstandard document.selection way since it allows for
|
|
// automatic scrolling to the cursor via the range.select() method
|
|
if (document.selection) { // IE
|
|
range = document.selection.createRange();
|
|
sel = range.text;
|
|
tempRange = range.duplicate();
|
|
tempRange.moveToElementText(this.element);
|
|
tempRange.setEndPoint('EndToEnd', range);
|
|
selEnd = tempRange.text.length;
|
|
selStart = selEnd - sel.length;
|
|
|
|
// whenever the value of the textarea is changed, the range needs to be reset
|
|
// IE <9 (and Opera) use both \r and \n for newlines - this adds an extra character
|
|
// that needs to be accounted for when doing position calculations with ranges
|
|
// these values are used to offset the selection start and end positions
|
|
if (this.newlineLen > 1) {
|
|
preNewlines = text.slice(0, selStart).split(this.newline).length - 1;
|
|
selNewlines = sel.split(this.newline).length - 1;
|
|
} else {
|
|
preNewlines = selNewlines = 0;
|
|
}
|
|
} else if (typeof this.element.selectionStart !== 'undefined') {
|
|
selStart = this.element.selectionStart;
|
|
selEnd = this.element.selectionEnd;
|
|
sel = text.slice(selStart, selEnd);
|
|
} else {
|
|
return; // cannot access textarea selection - do nothing
|
|
}
|
|
|
|
// tab key - insert / remove tab
|
|
if (key === 9) {
|
|
// initialize tab variables
|
|
tab = this.aTab;
|
|
tabLen = tab.length;
|
|
numTabs = 0;
|
|
startTab = 0;
|
|
preTab = 0;
|
|
|
|
// multi-line selection
|
|
if (selStart !== selEnd && sel.indexOf('\n') !== -1) {
|
|
if (text.charAt(selEnd - 1) === '\n') {
|
|
selEnd = selEnd - this.newlineLen;
|
|
sel = text.slice(selStart, selEnd);
|
|
}
|
|
// for multiple lines, only insert / remove tabs from the beginning of each line
|
|
|
|
// find the start of the first selected line
|
|
if (selStart === 0 || text.charAt(selStart - 1) === '\n') {
|
|
// the selection starts at the beginning of a line
|
|
startLine = selStart;
|
|
} else {
|
|
// the selection starts after the beginning of a line
|
|
// set startLine to the beginning of the first partially selected line
|
|
// subtract 1 from selStart in case the cursor is at the newline character,
|
|
// for instance, if the very end of the previous line was selected
|
|
// add 1 to get the next character after the newline
|
|
// if there is none before the selection, lastIndexOf returns -1
|
|
// when 1 is added to that it becomes 0 and the first character is used
|
|
startLine = text.lastIndexOf('\n', selStart - 1) + 1;
|
|
}
|
|
|
|
// find the end of the last selected line
|
|
if (selEnd === text.length || text.charAt(selEnd) === '\n') {
|
|
// the selection ends at the end of a line
|
|
endLine = selEnd;
|
|
} else {
|
|
// the selection ends before the end of a line
|
|
// set endLine to the end of the last partially selected line
|
|
endLine = text.indexOf('\n', selEnd);
|
|
if (endLine === -1) {
|
|
endLine = text.length;
|
|
}
|
|
}
|
|
// if the shift key was pressed, remove tabs instead of inserting them
|
|
if (e.shiftKey) {
|
|
if (text.slice(startLine).indexOf(tab) === 0) {
|
|
// is this tab part of the selection?
|
|
if (startLine === selStart) {
|
|
// it is, remove it
|
|
sel = sel.slice(tabLen);
|
|
} else {
|
|
// the tab comes before the selection
|
|
preTab = tabLen;
|
|
}
|
|
startTab = tabLen;
|
|
}
|
|
|
|
this.element.value = text.slice(0, startLine) + text.slice(startLine + preTab, selStart) +
|
|
sel.replace(new RegExp('\n' + tab, 'g'), function () {
|
|
numTabs += 1;
|
|
return '\n';
|
|
}) + text.slice(selEnd);
|
|
|
|
// set start and end points
|
|
if (range) { // IE
|
|
// setting end first makes calculations easier
|
|
range.collapse();
|
|
range.moveEnd('character', selEnd - startTab - (numTabs * tabLen) - selNewlines - preNewlines);
|
|
range.moveStart('character', selStart - preTab - preNewlines);
|
|
range.select();
|
|
} else {
|
|
// set start first for Opera
|
|
this.element.selectionStart = selStart - preTab; // preTab is 0 or tabLen
|
|
// move the selection end over by the total number of tabs removed
|
|
this.element.selectionEnd = selEnd - startTab - (numTabs * tabLen);
|
|
}
|
|
} else { // no shift key
|
|
numTabs = 1; // for the first tab
|
|
// insert tabs at the beginning of each line of the selection
|
|
this.element.value = text.slice(0, startLine) + tab + text.slice(startLine, selStart) +
|
|
sel.replace(/\n/g, function () {
|
|
numTabs += 1;
|
|
return '\n' + tab;
|
|
}) + text.slice(selEnd);
|
|
|
|
// set start and end points
|
|
if (range) { // IE
|
|
range.collapse();
|
|
range.moveEnd('character', selEnd + (numTabs * tabLen) - selNewlines - preNewlines);
|
|
range.moveStart('character', selStart + tabLen - preNewlines);
|
|
range.select();
|
|
} else {
|
|
// the selection start is always moved by 1 character
|
|
this.element.selectionStart = selStart + (selStart == startLine ? 0 : tabLen);
|
|
// move the selection end over by the total number of tabs inserted
|
|
this.element.selectionEnd = selEnd + (numTabs * tabLen);
|
|
this.element.scrollTop = initScrollTop;
|
|
}
|
|
}
|
|
} else { // single line selection
|
|
// if the shift key was pressed, remove a tab instead of inserting one
|
|
if (e.shiftKey) {
|
|
// if the character before the selection is a tab, remove it
|
|
if (text.slice(selStart - tabLen).indexOf(tab) === 0) {
|
|
this.element.value = text.slice(0, selStart - tabLen) + text.slice(selStart);
|
|
|
|
// set start and end points
|
|
if (range) { // IE
|
|
// collapses range and moves it by -1 tab
|
|
range.move('character', selStart - tabLen - preNewlines);
|
|
range.select();
|
|
} else {
|
|
this.element.selectionEnd = this.element.selectionStart = selStart - tabLen;
|
|
this.element.scrollTop = initScrollTop;
|
|
}
|
|
}
|
|
} else { // no shift key - insert a tab
|
|
if (range) { // IE
|
|
range.text = tab;
|
|
range.select();
|
|
} else {
|
|
this.element.value = text.slice(0, selStart) + tab + text.slice(selEnd);
|
|
this.element.selectionEnd = this.element.selectionStart = selStart + tabLen;
|
|
this.element.scrollTop = initScrollTop;
|
|
}
|
|
}
|
|
}
|
|
} else if (this.autoIndent) { // Enter key
|
|
// insert a newline and copy the whitespace from the beginning of the line
|
|
// find the start of the first selected line
|
|
if (selStart === 0 || text.charAt(selStart - 1) === '\n') {
|
|
// the selection starts at the beginning of a line
|
|
// do nothing special
|
|
this.inWhitespace = true;
|
|
return;
|
|
} else {
|
|
// see explanation under "multi-line selection" above
|
|
startLine = text.lastIndexOf('\n', selStart - 1) + 1;
|
|
}
|
|
|
|
// find the end of the first selected line
|
|
endLine = text.indexOf('\n', selStart);
|
|
|
|
// if no newline is found, set endLine to the end of the text
|
|
if (endLine === -1) {
|
|
endLine = text.length;
|
|
}
|
|
|
|
// get the whitespace at the beginning of the first selected line (spaces and tabs only)
|
|
whitespace = text.slice(startLine, endLine).match(/^[ \t]*/)[0];
|
|
whitespaceLen = whitespace.length;
|
|
|
|
// the cursor (selStart) is in the whitespace at beginning of the line
|
|
// do nothing special
|
|
if (selStart < startLine + whitespaceLen) {
|
|
this.inWhitespace = true;
|
|
return;
|
|
}
|
|
|
|
if (range) { // IE
|
|
// insert the newline and whitespace
|
|
range.text = '\n' + whitespace;
|
|
range.select();
|
|
} else {
|
|
// insert the newline and whitespace
|
|
this.element.value = text.slice(0, selStart) + '\n' + whitespace + text.slice(selEnd);
|
|
// Opera uses \r\n for a newline, instead of \n,
|
|
// so use newlineLen instead of a hard-coded value
|
|
this.element.selectionEnd = this.element.selectionStart = selStart + this.newlineLen + whitespaceLen;
|
|
this.element.scrollTop = initScrollTop;
|
|
}
|
|
}
|
|
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
})(jQuery);
|
|
|
|
function checkAndDisable(id, checked) {
|
|
if (checked) {
|
|
$('#' + id).find('input[type=checkbox]').attr('checked', true).attr('disabled', true);
|
|
} else {
|
|
$('#' + id).find('input[type=checkbox]').removeAttr('checked').removeAttr('disabled');
|
|
}
|
|
} |