420 lines
10 KiB
JavaScript
420 lines
10 KiB
JavaScript
/**
|
||
* Layer that binds Zen Coding's actions to editArea
|
||
*
|
||
* @author Sergey Chikuyonok (serge.che@gmail.com)
|
||
* @link http://chikuyonok.ru
|
||
*
|
||
* @include "zen_coding.js"
|
||
* @include "html_matcher.js"
|
||
*/
|
||
|
||
var EditArea_zencoding = (function() {
|
||
/** @type {EditArea} Current editor's instance */
|
||
var editor = null,
|
||
editor_id = 0,
|
||
is_mac = (/mac\s+os/i.test(navigator.userAgent));
|
||
|
||
/**
|
||
* Return true if Alt key is pressed
|
||
* @param {Event} evt
|
||
* @return {Boolean}
|
||
*/
|
||
function AltPressed(e) {
|
||
if (window.event)
|
||
return (window.event.altKey);
|
||
else
|
||
return e.modifiers ? (e.altKey || (e.modifiers % 2)) : e.altKey;
|
||
};
|
||
|
||
/**
|
||
* Return true if Ctrl key is pressed
|
||
* @return {Boolean}
|
||
*/
|
||
function CtrlPressed(e) {
|
||
// as usual, Opera brings few "suprises" here
|
||
if (window.event && !is_mac) {
|
||
return (window.event.ctrlKey);
|
||
} else {
|
||
return (e.ctrlKey || (e.metaKey && window.opera) || (e.modifiers==2) || (e.modifiers==3) || (e.modifiers>5));
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Return true if Shift key is pressed
|
||
* @return {Boolean}
|
||
*/
|
||
function ShiftPressed(e) {
|
||
if (window.event) {
|
||
return (window.event.shiftKey);
|
||
} else {
|
||
return (e.shiftKey || (e.modifiers>3));
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Finds abbreviation in cucrrent editor and returns it
|
||
* @return {String|null}
|
||
*/
|
||
function findAbbreviation() {
|
||
var range = editor.getSelectionRange(editor_id);
|
||
if (range.start != range.end) {
|
||
// abbreviation is selected by user
|
||
return editor.getSelectedText(editor_id);
|
||
} else {
|
||
var content = editor.getValue(editor_id);
|
||
return zen_coding.extractAbbreviation(content.substring(0, range.start));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns full line on text for passed character position
|
||
*/
|
||
function getLine(char_pos) {
|
||
var content = editor.getValue(editor_id),
|
||
start_ix = char_pos,
|
||
end_ix,
|
||
ch;
|
||
|
||
function isNewline(ch) {
|
||
return ch == '\n' || ch == '\r';
|
||
}
|
||
|
||
// find the beginnig of the line
|
||
while (start_ix--) {
|
||
if (isNewline(content.charAt(start_ix))) {
|
||
start_ix++;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// find the end of the line
|
||
for (end_ix = char_pos; end_ix < content.length; end_ix++) {
|
||
if (isNewline(content.charAt(end_ix)))
|
||
break;
|
||
}
|
||
|
||
return content.substring(start_ix, end_ix);
|
||
}
|
||
|
||
/**
|
||
* Returns padding of current editor's line
|
||
* @return {String}
|
||
*/
|
||
function getCurrentLinePadding() {
|
||
var range = editor.getSelectionRange(editor_id),
|
||
cur_line = getLine(range.start);
|
||
|
||
return (cur_line.match(/^(\s+)/) || [''])[0];
|
||
}
|
||
|
||
/**
|
||
* Replaces current editor's substring with new content. Multiline content
|
||
* will be automatically padded
|
||
*
|
||
* @param {String} editor_str Current editor's substring
|
||
* @param {String} content New content
|
||
*/
|
||
function replaceEditorContent(editor_str, content) {
|
||
if (!content)
|
||
return;
|
||
|
||
// add padding for current line
|
||
content = zen_coding.padString(content, getCurrentLinePadding());
|
||
|
||
// get char index where we need to place cursor
|
||
var range = editor.getSelectionRange(editor_id);
|
||
var start_pos = range.end - editor_str.length;
|
||
var cursor_pos = content.indexOf('|');
|
||
content = content.replace(/\|/g, '');
|
||
|
||
// replace content in editor
|
||
editor.setSelectionRange(editor_id, start_pos, start_pos + editor_str.length);
|
||
editor.setSelectedText(editor_id, content);
|
||
|
||
// place cursor
|
||
if (cursor_pos != -1)
|
||
editor.setSelectionRange(editor_id, start_pos + cursor_pos, start_pos + cursor_pos);
|
||
}
|
||
|
||
/**
|
||
* Search for the new edit point
|
||
* @param {Number} Search direction: -1 — left, 1 — right
|
||
* @param {Number} Initial offset from the current caret position
|
||
* @return {Number} Returns -1 if edit point wasn't found
|
||
*/
|
||
function findNewEditPoint(inc, offset) {
|
||
inc = inc || 1;
|
||
offset = offset || 0;
|
||
var content = editor.getValue(editor_id),
|
||
cur_point = editor.getSelectionRange(editor_id).start + offset,
|
||
max_len = content.length,
|
||
next_point = -1;
|
||
|
||
function ch(ix) {
|
||
return content.charAt(ix);
|
||
}
|
||
|
||
while (cur_point < max_len && cur_point > 0) {
|
||
cur_point += inc;
|
||
var cur_char = ch(cur_point),
|
||
next_char = ch(cur_point + 1),
|
||
prev_char = ch(cur_point - 1);
|
||
|
||
switch (cur_char) {
|
||
case '"':
|
||
case '\'':
|
||
if (next_char == cur_char && prev_char == '=') {
|
||
// empty attribute
|
||
next_point = cur_point + 1;
|
||
}
|
||
break;
|
||
case '>':
|
||
if (next_char == '<') {
|
||
// between tags
|
||
next_point = cur_point + 1;
|
||
}
|
||
break;
|
||
}
|
||
|
||
if (next_point != -1)
|
||
break;
|
||
}
|
||
|
||
return next_point;
|
||
}
|
||
|
||
/**
|
||
* Unindent content, thus preparing text for tag wrapping
|
||
* @param {String} text
|
||
* @return {String}
|
||
*/
|
||
function unindent(text) {
|
||
var pad = getCurrentLinePadding();
|
||
var lines = zen_coding.splitByLines(text);
|
||
for (var i = 0; i < lines.length; i++) {
|
||
if (lines[i].search(pad) == 0)
|
||
lines[i] = lines[i].substr(pad.length);
|
||
}
|
||
|
||
return lines.join(zen_coding.getNewline());
|
||
}
|
||
|
||
/**
|
||
* Wraps content with abbreviation
|
||
* @param {String} editor_type
|
||
* @param {String} profile_name
|
||
*/
|
||
function mainWrapWithAbbreviation(editor_type, profile_name) {
|
||
profile_name = profile_name || 'xhtml';
|
||
|
||
var range = editor.getSelectionRange(editor_id),
|
||
content = editor.getValue(editor_id),
|
||
start_offset = range.start,
|
||
end_offset = range.end,
|
||
|
||
abbr = prompt('Enter abbreviation');
|
||
|
||
if (!abbr)
|
||
return null;
|
||
|
||
if (start_offset == end_offset) {
|
||
// no selection, find tag pair
|
||
var range = HTMLPairMatcher(content, Math.max(start_offset, end_offset));
|
||
|
||
if (!range || range[0] == -1) // nothing to wrap
|
||
return null;
|
||
|
||
start_offset = range[0];
|
||
end_offset = range[1];
|
||
|
||
// narrow down selection until first non-space character
|
||
var re_space = /\s|\n|\r/;
|
||
function isSpace(ch) {
|
||
return re_space.test(ch);
|
||
}
|
||
|
||
while (start_offset < end_offset) {
|
||
if (!isSpace(content.charAt(start_offset)))
|
||
break;
|
||
|
||
start_offset++;
|
||
}
|
||
|
||
while (end_offset > start_offset) {
|
||
end_offset--;
|
||
if (!isSpace(content.charAt(end_offset))) {
|
||
end_offset++;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
var content = content.substring(start_offset, end_offset),
|
||
result = zen_coding.wrapWithAbbreviation(abbr, unindent(content), editor_type, profile_name);
|
||
|
||
if (result) {
|
||
editor.setSelectionRange(editor_id, end_offset, end_offset);
|
||
replaceEditorContent(content, result);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Performs Zen Coding action on keydown event
|
||
* @param {Event} evt
|
||
*/
|
||
function keyDown(evt) {
|
||
evt = evt || window.event;
|
||
var letter = String.fromCharCode(evt.keyCode).toLowerCase(),
|
||
stop_event = false;
|
||
|
||
if (CtrlPressed(evt) && !AltPressed(evt) && !ShiftPressed(evt)) {
|
||
switch (evt.keyCode) {
|
||
case 188: // Ctrl+, — expand abbreviation
|
||
case 44:
|
||
var abbr = findAbbreviation();
|
||
if (abbr) {
|
||
var profile_name = 'xhtml',
|
||
syntax = (editArea.current_code_lang in zen_settings) ? editArea.current_code_lang : 'html';
|
||
|
||
var content = zen_coding.expandAbbreviation(abbr, syntax, profile_name);
|
||
replaceEditorContent(abbr, content);
|
||
}
|
||
stop_event = true;
|
||
break;
|
||
case 77: // Ctrl+M — match pair
|
||
case 109:
|
||
var selection = editor.getSelectionRange(editor_id),
|
||
range = HTMLPairMatcher(editor.getValue(editor_id), Math.max(selection.start, selection.end));
|
||
|
||
if (range && range[0] != -1)
|
||
editor.setSelectionRange(editor_id, range[0], range[1]);
|
||
|
||
stop_event = true;
|
||
break;
|
||
case 72: // Ctrl+H — wrap with abbreviation
|
||
mainWrapWithAbbreviation('html', 'xhtml');
|
||
|
||
stop_event = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (CtrlPressed(evt) && !AltPressed(evt) && ShiftPressed(evt)) {
|
||
switch (evt.keyCode) {
|
||
case 37: // Ctrl+Shift+LEFT_ARROW – prev edit point
|
||
var new_point = findNewEditPoint(-1),
|
||
range = editor.getSelectionRange(editor_id);
|
||
|
||
|
||
if (new_point == range.start)
|
||
// returned to the current position, start searching from the new one
|
||
new_point = findNewEditPoint(-1, -2);
|
||
|
||
if (new_point != -1)
|
||
editor.setSelectionRange(editor_id, new_point, new_point);
|
||
|
||
stop_event = true;
|
||
break;
|
||
|
||
case 39: // Shift+Ctrl+RIGHT_ARROW – next edit point
|
||
var new_point = findNewEditPoint(1);
|
||
if (new_point != -1)
|
||
editor.setSelectionRange(editor_id, new_point, new_point);
|
||
|
||
stop_event = true;
|
||
break;
|
||
|
||
case 38: // Shift+Ctrl+UP_ARROW – go to matching pair
|
||
var caret_pos = editor.getSelectionRange(editor_id).start,
|
||
content = editor.getValue(editor_id);
|
||
|
||
if (content.charAt(caret_pos) == '<')
|
||
// looks like caret is outside of tag pair
|
||
caret_pos++;
|
||
|
||
var range = HTMLPairMatcher(content, caret_pos);
|
||
|
||
if (range && range[0] != -1) {
|
||
// match found
|
||
var open_tag = HTMLPairMatcher.last_match.opening_tag,
|
||
close_tag = HTMLPairMatcher.last_match.closing_tag;
|
||
|
||
if (close_tag) { // exclude unary tags
|
||
var new_pos = -1;
|
||
if (open_tag.start <= caret_pos && open_tag.end >= caret_pos)
|
||
new_pos = close_tag.start
|
||
else if (close_tag.start <= caret_pos && close_tag.end >= caret_pos)
|
||
new_pos = open_tag.start;
|
||
|
||
if (new_pos != -1)
|
||
editor.setSelectionRange(editor_id, new_pos, new_pos);
|
||
}
|
||
}
|
||
|
||
stop_event = true;
|
||
break;
|
||
|
||
case 77: // Shift+Ctrl+M — merge lines
|
||
var range = editor.getSelectionRange(editor_id),
|
||
content = editor.getValue(editor_id),
|
||
start_ix = range.start,
|
||
end_ix = range.end;
|
||
|
||
if (start_ix == end_ix) {
|
||
// find matching tag
|
||
var pair = HTMLPairMatcher(content, start_ix);
|
||
if (pair) {
|
||
start_ix = pair[0];
|
||
end_ix = pair[1];
|
||
}
|
||
}
|
||
|
||
if (start_ix != end_ix) {
|
||
// got range, merge lines
|
||
var text = content.substring(start_ix, end_ix),
|
||
old_text = text;
|
||
|
||
var lines = text.split(/(\r|\n)/);
|
||
|
||
for (var i = 1; i < lines.length; i++) {
|
||
lines[i] = lines[i].replace(/^\s+/, '');
|
||
}
|
||
|
||
text = lines.join('').replace(/\s{2,}/, ' ');
|
||
|
||
editor.setSelectionRange(editor_id, end_ix, end_ix);
|
||
replaceEditorContent(old_text, text);
|
||
}
|
||
|
||
stop_event = true;
|
||
break;
|
||
|
||
}
|
||
}
|
||
|
||
if(stop_event){
|
||
// in case of a control that sould'nt be used by IE but that is used => THROW a javascript error that will stop key action
|
||
if(window.event) evt.keyCode = 0;
|
||
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
return {
|
||
init: function() {
|
||
editArea.load_script(this.baseURL+"core.js");
|
||
},
|
||
|
||
onkeydown: function(evt) {
|
||
editor = parent.editAreaLoader;
|
||
editor_id = editArea.id;
|
||
|
||
return keyDown(evt);
|
||
}
|
||
}
|
||
})();
|
||
|
||
editArea.add_plugin("zencoding", EditArea_zencoding); |