mediawiki-extensions-Visual.../modules/syntaxhighlight/ve.ui.MWSyntaxHighlightSimpleSurface.js
Tongbo Sui 62ec9d92e2 SyntaxHighlight node support
Update: Switched to generic ve.ui.Toolbar and ve.ui.Tool for consistency
with VE in general. Changed searchbox to floating and hidden by default.
Call for search using shortcut key. Overriden ESC key.

Node handler for <syntaxhighlight>'s Rdfa type.
Embedded in VisualEditor.php with its own ResourceLoader module.
Use $wgVisualEditorEnableExperimentalCode in LocalSettings.php
to load the module.

Supported languages (for testing): text, javascript

Features:
(1) Internal mechanisms:
	(1.1) Tokenizer. Tokenize input code string.
	(1.2) Highlighter. Highlight based on predefined rules.
	(1.3) Syntax checker. Validate based on predefined rules.
(2) SimpleSurface:
	(2.1) Auto indenter. Works with (){}[] blocks.
	(2.2) Reformatter. Reformats spaces and remove trailing
	whitespaces.
	(2.3) Language selection dropdown.
	(2.4) Basic editing. Insert, deletion, selection.
	(2.5) Clipboard & edit history support.
		(2.5.1) Undo (Ctrl + Z), redo (Ctrl + Y)
		(2.5.2) Copy (Ctrl + C), cut (Ctrl + X), paste (Ctrl + V)
	(2.6) Search & replace. Ctrl + F to quickly move focus to search
	box.

Bug 47742

Change-Id: I4adede9e05fd2236cee50ce03f597e8ff6b1914d
2013-10-02 11:01:06 -07:00

1371 lines
39 KiB
JavaScript

/*!
* VisualEditor user interface MWSyntaxHighlightDialog class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* SimpleSurface is designed for MWSyntaxHighlight nodes which contains node model data and view.
* Not compatible with other nodes.
*
* @class
* @extends ve.Element
*
* @constructor
* @param {String} data MWSyntaxHighlight model data
* @param {String} lang The language being edited
*/
ve.ui.MWSyntaxHighlightSimpleSurface = function VeUiMWSyntaxHighlightSimpleSurface( data, lang ) {
// Parent constructor
ve.Element.call( this );
// Surface properties
this.lang = lang;
// Character dimension; in px
this.charDimension = { 'height' : 0, 'width' : 0 };
// Node model data and tokenized data
this.model = data;
this.tokens = null;
// Helpers
this.tokenizer = null;
this.highlighter = null;
// Edit stack
this.undoStack = [];
this.redoStack = [];
// Mouse input properties
this.mouseDown = false;
this.mouseMove = false;
this.cursorInterval = null; // Cursor blink timer
this.selection = {
'view' : {
'start' : { 'x' : 0, 'y' : 0 },
'end' : { 'x' : 0, 'y' : 0 }
},
'model' : { 'start': 0, 'end' : 0 },
'modelSorted' : { 'start': 0, 'end' : 0 }
};
this.modelUnseleted = { 'left': '', 'right':'' };
// Key input properties
this.input = {
'start' : -1,
'old' : '',
'newer' : '',
'pushReady' : false
};
this.inputPushDelimiter = /\W/;
// Cache
this.cache = {
'editboxHeight': Infinity,
'editboxScroll': 0
};
// Element properties
// Toolbar
this.$toolbarLayer = this.$$( '<div>' );
this.$toolbar = this.$$( '<div>' );
this.$buttonUndo = this.$$( '<a>' );
this.$buttonRedo = this.$$( '<a>' );
this.$buttonIndent = this.$$( '<a>' );
this.$buttonBeautify = this.$$( '<a>' );
this.$langDropdown = this.$$( '<select>' );
// Editbox
this.$editboxLayer = this.$$( '<div>' );
this.$lineNumberLayer = this.$$( '<div>' );
this.$lineNumber = this.$$( '<div>' );
this.$textLayer = this.$$( '<div>' );
this.$text = this.$$( '<div>' );
this.$lineWrap = this.$$( '<div>' );
// Cursor
this.$cursorLineOverlay = this.$$( '<div>' );
this.$cursor = this.$$( '<div>' );
// Selection & clipboard
this.$selectionOverlay = this.$$( '<div>' );
this.$selectionTextarea = this.$$( '<textarea>' );
// Search & replace
this.$searchContainer = this.$$( '<div>' );
this.$searchBox = this.$$( '<input>' );
this.$replaceBox = this.$$( '<input>' );
this.$buttonSearch = this.$$( '<a>' );
this.$buttonReplace = this.$$( '<a>' );
this.searchRegex = null;
this.searchHasResult = false;
this.toolbar = new ve.ui.Toolbar({ '$$': this.$$ });
this.toolbar.setup([{
'include':[ 'synhiUndo', 'synhiRedo', 'synhiIndent', 'synhiBeautify' ]
}]);
this.toolbar.context = this;
// Initialization
this.$
.addClass( 've-ui-simplesurface' );
this.$toolbarLayer
.addClass( 've-ui-simplesurface-container-toolbar' )
.append(
this.$toolbar
.addClass( 've-ui-simplesurface-toolbar' )/*
.append(
this.$buttonUndo.$
.addClass('ve-ui-simplesurface-toolbar-button')
.addClass('ve-ui-icon-undo') )
)
.append(
this.$buttonRedo
.addClass('ve-ui-simplesurface-toolbar-button')
.addClass('ve-ui-icon-redo') )
.append(
this.$buttonIndent
.addClass('ve-ui-simplesurface-toolbar-button')
.addClass('ve-ui-icon-indent-list') )
.append(
this.$buttonBeautify
.addClass('ve-ui-simplesurface-toolbar-button')
.addClass('ve-ui-icon-reformat') )*/
.append(
this.$langDropdown
.addClass('ve-ui-simplesurface-toolbar-dropdown') )
);
this.$editboxLayer
.addClass( 've-ui-simplesurface-container-editbox' )
.append( this.$lineNumberLayer
.addClass( 've-ui-simplesurface-container-editbox-lineNumber' )
.append( this.$lineNumber
.addClass( 've-ui-simplesurface-lineNumber' ) ) )
.append(
this.$textLayer
.addClass( 've-ui-simplesurface-container-editbox-text' )
.append(
this.$lineWrap
.addClass( 've-ui-simplesurface-text-linewrap' ) )
.append(
this.$selectionOverlay
.addClass( 've-ui-simplesurface-container-text-selection' ) )
.append(
this.$cursorLineOverlay
.addClass( 've-ui-simplesurface-cursor-line' )
.append(
this.$cursor
.addClass( 've-ui-simplesurface-cursor' ) ) )
.append(
this.$text
.addClass( 've-ui-simplesurface-text' ) ) )
.append(
this.$selectionTextarea
.addClass( 've-ui-simplesurface-textarea' ) );
this.$searchContainer
.addClass( 've-ui-simplesurface-container-search' )
.append( this.$searchBox )
.append( this.$buttonSearch
.addClass('ve-ui-simplesurface-toolbar-button')
.addClass('ve-ui-icon-search') )
.append( this.$replaceBox )
.append( this.$buttonReplace
.addClass('ve-ui-simplesurface-toolbar-button')
.addClass('ve-ui-icon-replace') );
// Make instance globally accessible for debugging
ve.instances.push( this );
};
/* Inheritance */
ve.inheritClass( ve.ui.MWSyntaxHighlightSimpleSurface , ve.Element );
/* Methods */
/**
* Initialization
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.initialize = function () {
// Initialize helpers
this.tokenizer = new ve.dm.MWSyntaxHighlightTokenizer();
this.highlighter = new ve.ce.MWSyntaxHighlightHighlighter(this.lang);
// Check language support
if ( this.highlighter.isSupportedLanguage( this.lang ) ){
if ( !this.highlighter.isEnabledLanguage( this.lang ) ){
this.$.append( this.$$( '<div>' ).text( 'Cannot edit: ' + this.lang + '\nSupport has been disabled.' ) );
} else {
var langList = this.highlighter.getSupportedLanguages(), key, options = '',
self;
// Language dropdown
for (key in langList){
if (key === this.lang){
options += '<option value="'+key+'" selected>'+key.charAt(0).toUpperCase() + key.slice(1)+'</option>';
} else {
options += '<option value="'+key+'">'+key.charAt(0).toUpperCase() + key.slice(1)+'</option>';
}
}
this.$langDropdown.html(options);
// Main editor interface
this.$
.append( this.$toolbarLayer )
.append( this.toolbar.$ )
.append( this.$editboxLayer.append( this.$searchContainer ) );
// Event handlers
this.$textLayer
.on( 'mousedown', ve.bind(this.onMousedown, this))
.on( 'mousemove mouseleave', ve.bind(this.onMousemove, this))
.on( 'mouseup', ve.bind(this.onMouseup, this))
.on( 'keypress', ve.bind(this.onKeypress, this))
.on( 'keydown', ve.bind(this.onKeydown, this));
this.$text.on( 'blur', ve.bind(this.hideCursor, this));
this.$lineNumber.on( 'click', 'pre', ve.bind(this.selectLine, this));
/*this.$buttonUndo.on( 'click',
ve.bind(function(e){
if (!this.$buttonUndo.hasClass('ve-ui-simplesurface-toolbar-button-disabled')){
e.preventDefault();
e.which = 90;
e.ctrlKey = true;
this.onKeydown(e);
}
}, this));
this.$buttonRedo.on( 'click',
ve.bind(function(e){
if (!this.$buttonRedo.hasClass('ve-ui-simplesurface-toolbar-button-disabled')){
e.preventDefault();
e.which = 89;
e.ctrlKey = true;
this.onKeydown(e);
}
}, this));
this.$buttonIndent.on( 'click', ve.bind(function(e){
e.preventDefault();
this.indent();
return false;
}, this));
this.$buttonBeautify.on( 'click', ve.bind(function(e){
e.preventDefault();
this.doBeautify();
return false;
}, this) );
*/
this.$editboxLayer.on( 'scroll', ve.bind(function(){
this.cacheUpdate();
if (this.$searchContainer.css('display') !== 'none' ){
this.$searchContainer
.stop(true, true)
.animate({top:this.cache.editboxScroll}, 100);
}
}, this) );
this.$langDropdown.on( 'change', ve.bind(function(){
this.reloadRules();
}, this));
this.$searchBox.on(
'keydown mouseup cut paste change input select',
ve.bind( this.onSearchInput, this )
);
this.$buttonSearch.on( 'click', ve.bind(this.searchStep, this) );
this.$buttonReplace.on( 'click', ve.bind(this.searchReplace, this) );
this.$selectionTextarea.on( 'copy paste cut', ve.bind(this.onClipboard, this) );
$(window).bind('resize', ve.bind(this.cacheUpdate, this)); // Used to update cached css values
// Initialize helpers
this.highlighter.initialize();
this.tokenize();
this.refresh();
// Measure character metric
this.charDimension.height = parseInt(this.$lineNumber.find('pre:first').find('span').css('font-size'), 10) * 2;
this.charDimension.width = this.$lineNumber.find('pre:first').find('span').width();
// Line wrap position
this.$lineWrap.css({'left':80 * this.charDimension.width});
// Make focusable
this.$text.attr('tabindex','-1');
this.$selectionTextarea.attr('tabindex','-1');
// Caching
self = this;
setTimeout(function(){
self.cache.editboxScroll = self.$editboxLayer.scrollTop();
self.cache.editboxHeight = self.$editboxLayer.outerHeight();
}, 500); // <500 ms not working
// Initialize undo/redo buttons; show cursor
this.checkStack();
this.createSelection();
}
} else {
this.$.append( this.$$( '<div>' ).text( 'Cannot edit: ' + this.lang + '\nLanguage not supported.') );
}
};
/**
* Destroy the surface, removing all DOM elements and event listeners.
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.destroy = function () {
ve.instances.splice( ve.instances.indexOf( this ), 1 );
// Turn off event handlers
this.$textLayer.off( 'mousedown mousemove mouseup mouseleave keypress keydown' );
this.$text.off( 'blur' );
this.$lineNumber.off( 'click' );/*
this.$buttonUndo.off( 'click' );
this.$buttonRedo.off( 'click' );
this.$buttonIndent.off( 'click' );
this.$buttonBeautify.off( 'click' );*/
this.$editboxLayer.off( 'scroll' );
this.$langDropdown.off( 'change' );
this.$searchBox.off( 'keydown mouseup cut paste change input select' );
this.$buttonSearch.off( 'click' );
this.$buttonReplace.off( 'click' );
this.$selectionTextarea.off( 'copy paste cut' );
$(window).unbind('resize', this.cacheUpdate);
// Clean up
this.$.empty();
this.stopBlinkCursor();
};
/**
* Get the surface's model
*
* @method
* @returns {string} Node model data
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.getModel = function () {
return this.model;
};
/**
* Update cache
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.cacheUpdate = function () {
this.cache.editboxScroll = this.$editboxLayer.scrollTop();
this.cache.editboxHeight = this.$editboxLayer.outerHeight();
};
/**
* Reload rules
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.reloadRules = function () {
this.lang = this.$langDropdown.val();
this.highlighter.loadRules(this.lang);
this.tokenize();
this.refresh();
};
/**
* Get the language that this surface is working on
*
* @method
* @returns {string} Language name
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.getLang = function () {
return this.lang;
};
/**
* Tokenize model data;
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.tokenize = function () {
this.tokens = this.tokenizer.tokenize(this.model);
};
/**
* Display tokenized data
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.refresh = function () {
// Highlight
this.tokens = this.highlighter.mark( this.tokens, this.model, false );
// Validate
this.tokens = this.highlighter.mark( this.tokens, this.model, true );
var marked = this.highlighter.display( this.tokens );
this.$text.html( marked.tokenDisplay );
this.$lineNumber.html( marked.lineNumber );
};
/**
* Auto indentation
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.indent = function () {
this.pushStack();
var r = [
[/\{/, /\}/],
[/\[/, /\]/],
[/\(/, /\)/]
],
i;
for ( i = 0; i < r.length; i++ ){
this.doIndent(r[i]);
}
// Reset cursor
this.selection.model.start = 0;
this.selection.model.end = 0;
this.createSelection();
};
/**
* Helper method for auto indentation
*
* @method
* @param {Array} regexGroup RegExp objects
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.doIndent = function ( regexGroup ) {
var a = [], c,
l = [],
match,
A = regexGroup[0],
B = regexGroup[1],
regex = new RegExp('['+A.source + B.source+']', 'g'),
i, j,
lineArray = this.model.split('\n');
// Stack based search; record block levels
while ( (match = regex.exec( this.model ) ) !== null ){
if (A.test(match[0])){
a.push(match.index);
} else {
if (a.length !== 0){
c = a.pop();
l.push({
'idx': [this.modelToView(c).y, this.modelToView(match.index).y],
'lvl': a.length + 1
});
}
}
}
l.sort(function(a, b){return a.idx[0] - b.idx[0];});
// Replace whitespaces within each block
for ( i = 0; i < l.length; i++ ){
for ( j = l[i].idx[0] + 1; j < l[i].idx[1]; j++ ){
lineArray[j] = lineArray[j].replace(/^[ \t]*/, new Array( l[i].lvl + 1 ).join('\t'));
}
}
// Treat changes as one edit
this.selection.model.start = 0;
this.selection.model.end = this.model.length;
this.createSelection();
this.input.newer = lineArray.join('\n');
this.edit();
this.clearStack();
};
/**
* Beautify code
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.doBeautify = function () {
this.pushStack();
// Head & tail spaces
var headTail = /(\w *?)([\+\-\*\/\=><\|\&\%\!\:\?]+)( *?\w)/g,
// Inbetween spaces
between = /(\( *?.+ *?\)|\[ *?.+ *?\]|\{ *?.+ *?\}|< *?.+ *?>)/g,
// Tail only spaces
tail = /(\w *?)([,;])(?=\w)/g,
// Tab spaces
tabSpace = /( {4}| {8})/g,
// Compress duplicate spaces
dupSpace = /( {2,})/g,
// Delete trailing whitespaces
trailSpace = /([\t ]+)(?=$)/gm,
// Exclusion
exclude = [ new RegExp('(\"[^]*?\")','g'), /('[^]*?')/g ],
match, exclusions = [], i,
modelCache = [],
// Helper for inbetween spaces
betweenFunc = function(match){
return match.slice(0,1) + ' '+match.slice(1,match.length-1)+' '+match.slice(match.length-1);
};
// Calculate exclusions
for ( i = 0; i < exclude.length; i++ ){
while ( (match = exclude[i].exec( this.model ) ) !== null ){
exclusions.push({'a': match.index, 'b': match.index + match[1].length});
}
}
exclusions.sort(function(a, b){ return a.a - b.a; });
// Slice model
if (exclusions.length === 0){
modelCache.push({
'text':this.model,
'after':''
});
} else {
for ( i = 0; i < exclusions.length; i++ ){
if ( i >= 1 ){
modelCache.push({
'text':this.model.slice(exclusions[i-1].b, exclusions[i].a),
'after':this.model.slice(exclusions[i].a, exclusions[i].b)
});
} else {
modelCache.push({
'text':this.model.slice(0, exclusions[i].a),
'after':this.model.slice(exclusions[i].a, exclusions[i].b)
});
}
if ( i === exclusions.length - 1){
if (exclusions[i].b < this.model.length){
modelCache.push({
'text':this.model.slice(exclusions[i].b),
'after':''
});
}
}
}
}
// Reformat
for ( i = 0; i < modelCache.length; i++ ){
modelCache[i].text = modelCache[i].text.replace( headTail, '$1 $2 $3');
modelCache[i].text = modelCache[i].text.replace( between, betweenFunc);
modelCache[i].text = modelCache[i].text.replace( tail, '$1$2 ');
modelCache[i].text = modelCache[i].text.replace( tabSpace, '\t');
modelCache[i].text = modelCache[i].text.replace( dupSpace, ' ');
modelCache[i].text = modelCache[i].text.replace( trailSpace, '');
modelCache[i] = modelCache[i].text + modelCache[i].after;
}
// Treat changes as one edit
this.selection.model.start = 0;
this.selection.model.end = this.model.length;
this.createSelection();
this.input.newer = modelCache.join('');
this.edit();
this.clearStack();
// Auto indent
this.indent();
};
/**
* Toolbar button handler
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onButtonUndo = function () {
var e = jQuery.Event();
e.which = 90;
e.ctrlKey = true;
this.onKeydown(e);
};
/**
* Toolbar button handler
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onButtonRedo = function () {
var e = jQuery.Event();
e.which = 89;
e.ctrlKey = true;
this.onKeydown(e);
};
/**
* Mouse down handler
*
* @method
* @param {jQuery.Event} event
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onMousedown = function ( event ) {
event.preventDefault();
this.stopBlinkCursor();
this.pushStack();
this.mouseDown = true;
var model = this.viewToModel(
this.checkViewBounding( this.findLocalView(event, this.$textLayer) )
);
if (this.mouseMove){
this.selection.model.end = model;
this.createSelection();
this.showSelection();
} else {
this.hideSelection();
this.selection.model.start = model;
this.selection.model.end = model;
this.createSelection();
}
return false;
};
/**
* Mouse move and mouse leave handler
*
* @method
* @param {jQuery.Event} event
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onMousemove = function ( event ) {
event.preventDefault();
if (this.mouseDown){
if (event.type === 'mousemove'){
this.selection.model.end = this.viewToModel(
this.checkViewBounding( this.findLocalView(event, this.$textLayer) )
);
this.mouseMove = true;
this.createSelection();
this.showSelection();
}
}
return false;
};
/**
* Mouse up handler
*
* @method
* @param {jQuery.Event} event
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onMouseup = function ( event ) {
event.preventDefault();
this.mouseDown = false;
this.mouseMove = false;
return false;
};
/**
* Select a line
*
* @method
* @param {jQuery.Event} event
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.selectLine = function ( event ) {
var lineIndex = this.findLocalView(event, this.$lineNumber).y;
event.preventDefault();
this.pushStack();
this.selection.model.start = this.viewToModel({ 'x': 0 , 'y': lineIndex });
this.selection.model.end = this.viewToModel(this.checkViewBounding({ 'x' : 0 , 'y' : lineIndex + 1}));
if (this.selection.model.start === this.selection.model.end){
this.selection.model.end = this.viewToModel(this.checkViewBounding({ 'x' : Infinity, 'y': lineIndex}));
}
this.createSelection();
this.showSelection();
if (this.mouseMove){ this.mouseMove = false; }
if (this.mouseDown){ this.mouseDown = false; }
return false;
};
/**
* User input handler; characters
*
* @method
* @param {jQuery.Event} event
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onKeypress = function ( event ) {
var charIn = String.fromCharCode(event.which);
if ( event.which === 13 ){
charIn = '\n';
}
this.input.newer += charIn;
this.input.pushReady = true;
this.edit(false);
if (this.inputPushDelimiter.test(charIn)){
this.pushStack();
}
return false;
};
/**
* User input handler; non-characters
*
* @method
* @param {jQuery.Event} event
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onKeydown = function ( event ) {
var key = event.which,
ctrl = event.ctrlKey,
shift = event.shiftKey;
this.stopBlinkCursor();
if ( key === 9 ){ // Tab
event.preventDefault();
// Previous operation
this.pushStack();
if (this.selection.model.start === this.selection.model.end){
// Simple insertion
event.which = 9;
this.onKeypress(event);
} else {
// Initialize selection range
this.selection.modelSorted.start = this.selection.view.start.y > this.selection.view.end.y ?
parseInt(this.$text.find('pre:eq('+this.selection.view.end.y+')').find('span:first').attr('idx'),10) :
parseInt(this.$text.find('pre:eq('+this.selection.view.start.y+')').find('span:first').attr('idx'),10);
this.input.start = this.selection.modelSorted.start;
this.input.old = this.model.slice(this.selection.modelSorted.start, this.selection.modelSorted.end);
this.modelUnseleted.left = this.model.slice(0, this.selection.modelSorted.start);
this.modelUnseleted.right = this.model.slice( this.selection.modelSorted.end );
// Insert / remove tabs
if (shift){
this.input.newer = this.input.old.replace(/^\t/gm, '');
} else {
this.input.newer = this.input.old.replace(/^/gm, '\t');
}
this.hideSelection();
// Edit
this.model = [this.modelUnseleted.left,
this.input.newer,
this.modelUnseleted.right].join('');
this.input.pushReady = true;
// Refresh
this.pushStack();
this.tokenize();
this.refresh();
this.selection.model.end = this.modelUnseleted.left.length + this.input.newer.length;
this.createSelection();
}
} else if ( key === 8 ){ // Backspace
event.preventDefault();
this.pushStack(); // Previous operation
if (this.selection.model.start === this.selection.model.end){
// Delete one character
this.selection.model.start = this.checkViewBounding(this.selection.model.start - 1);
this.createSelection();
}
this.input.newer = '';
this.edit(false);
// This operation
this.input.pushReady = true;
this.pushStack();
} else if ( key === 46 ){ // Delete
event.preventDefault();
this.pushStack(); // Previous operation
if (this.selection.model.start === this.selection.model.end){
// Delete one character
this.selection.model.end = this.checkModelBounding(this.selection.model.end + 1);
this.createSelection();
}
this.input.newer = '';
this.edit(false);
// This operation
this.input.pushReady = true;
this.pushStack();
} else if ( key === 90 && ctrl ){ // Ctrl + Z, undo
event.preventDefault();
this.pushStack(); // Undo the current unstacked operation, if any
this.undo();
} else if ( key === 89 && ctrl ){ // Ctrl + Y, redo
event.preventDefault();
this.redo();
} else if ( key === 65 && ctrl ){ // Ctrl + A, select all
event.preventDefault();
this.selection.model.start = 0;
this.selection.model.end = this.checkModelBounding(Infinity);
this.createSelection();
this.showSelection();
} else if ( key >= 37 && key <= 40 ){ // Arrow keys
event.preventDefault();
this.pushStack(); // Previous operation
this.hideSelection();
if ( key === 37 ){ // Left
this.selection.model.end = this.checkModelBounding( this.selection.model.end - 1);
} else if ( key === 39 ){ // Right
this.selection.model.end = this.checkModelBounding( this.selection.model.end + 1);
} else if ( key === 38 ){ // Up
this.selection.model.end = this.viewToModel(
this.checkViewBounding({
'x': this.selection.view.end.x,
'y': this.selection.view.end.y - 1 })
);
} else if ( key === 40 ){ // Down
this.selection.model.end = this.viewToModel(
this.checkViewBounding({
'x': this.selection.view.end.x,
'y': this.selection.view.end.y + 1 })
);
}
if ( !shift ){
this.selection.model.start = this.selection.model.end;
}
this.createSelection();
if (shift){
this.showSelection();
}
} else if ( key === 70 && ctrl ){ // Ctrl + F, search & replace
event.preventDefault();
this.pushStack();
this.$searchContainer.css('display','inline-block');
this.$searchBox.focus();
} else if ( key === 67 && ctrl ){ // Ctrl + C, copy; preliminary actions
this.pushStack();
if (this.selection.model.start !== this.selection.model.end){
this.$selectionTextarea.focus();
}
} else if ( key === 86 && ctrl ){ // Ctrl + V, paste; preliminary actions
this.pushStack(); // Previous operation
this.$selectionTextarea.focus();
} else if ( key === 88 && ctrl ){ // Ctrl + X, cut; preliminary actions
this.pushStack();
if (this.selection.model.start !== this.selection.model.end){
this.$selectionTextarea.focus();
}
} else if ( key === 27 ){ // Esc
this.pushStack();
return false;
}
};
/**
* User input handler; clipboard events
*
* @method
* @param {jQuery.Event} event
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onClipboard = function ( event ) {
var which = event.type,
self = this;
if ( which === 'copy' ){
this.$selectionTextarea.get(0).value = this.model.slice(this.selection.modelSorted.start, this.selection.modelSorted.end);
this.$selectionTextarea.select();
// Callback after event is handled by browser
setTimeout(function(){
self.$text.focus();
self.showCursor();
self.startBlinkCursor();
},0);
} else if ( which === 'paste' ){
this.$selectionTextarea.select();
// Callback after event is handled by browser
setTimeout(function(){
self.input.newer = self.$selectionTextarea.get(0).value;
self.edit(false);
// This operation
self.input.pushReady = true;
self.pushStack();
},0);
} else if ( which === 'cut' ){
this.$selectionTextarea.get(0).value = this.model.slice(this.selection.modelSorted.start, this.selection.modelSorted.end);
this.$selectionTextarea.select();
// Callback after event is handled by browser
setTimeout(function(){
event.which = 8;
self.onKeydown(event);
},0);
}
};
/**
* Edit model data based on current operation
*
* @method
* @param {boolean} setOld (optional) Parameter passed to createSelection()
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.edit = function ( setOld ) {
this.hideSelection();
this.model = [this.modelUnseleted.left,
this.input.newer,
this.modelUnseleted.right].join('');
this.tokenize();
this.refresh();
this.selection.model.start = this.modelUnseleted.left.length;
this.selection.model.end = this.modelUnseleted.left.length + this.input.newer.length;
this.createSelection(setOld);
};
/**
* Push operation to history
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.pushStack = function () {
if (this.input.pushReady){
this.input.pushReady = false;
this.undoStack.push(this.copyStack(this.input));
this.clearStack();
this.checkStack();
}
};
/**
* Clear & reset current operation
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.clearStack = function () {
this.input.start = -1;
this.input.old = '';
this.input.newer = '';
this.input.pushReady = false;
this.selection.model.start = this.selection.model.end;
this.createSelection();
};
/**
* Reverse operation
*
* @method
* @param {Object} stack Operation
* @returns {Object} Reversed operation
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.reverseStack = function ( stack ) {
return {
'start':stack.start,
'old':stack.newer,
'newer':stack.old,
'pushReady':stack.pushReady
};
};
/**
* Deep copy operation
*
* @method
* @param {Object} stack Operation
* @returns {Object} Deep copy of the operation
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.copyStack = function ( stack ) {
return {
'start':stack.start,
'old':stack.old,
'newer':stack.newer,
'pushReady':stack.pushReady
};
};
/**
* Redo operations; one at each time
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.redo = function () {
if (this.redoStack.length !== 0){
this.input = this.redoStack.pop();
this.selection.model.start = this.input.start;
this.selection.model.end = this.input.start + this.input.old.length;
this.createSelection();
this.edit(false);
this.input.pushReady = true;
this.pushStack();
}
};
/**
* Undo operations; one at each time
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.undo = function () {
var toDo;
if (this.undoStack.length !== 0){
toDo = this.undoStack.pop();
this.input = this.reverseStack(toDo);
this.selection.model.start = this.input.start;
this.selection.model.end = this.input.start + this.input.old.length;
this.createSelection();
this.edit();
this.redoStack.push(this.copyStack(toDo));
this.clearStack();
this.checkStack();
}
};
/**
* Check stack height and style undo, redo buttons
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.checkStack = function () {
this.toolbar.emit( 'updateState' );
/*
if ( this.undoStack.length === 0 ){ // Grey out
this.$buttonUndo.addClass('ve-ui-simplesurface-toolbar-button-disabled');
} else {
this.$buttonUndo.removeClass('ve-ui-simplesurface-toolbar-button-disabled');
}
if ( this.redoStack.length === 0 ){ // Grey out
this.$buttonRedo.addClass('ve-ui-simplesurface-toolbar-button-disabled');
} else {
this.$buttonRedo.removeClass('ve-ui-simplesurface-toolbar-button-disabled');
}
*/
};
/**
* Check rounded view index against view borders
*
* @method
* @param {Object} roundedIndex Rounded line / column index
* @returns {Object} Bounded line / column index
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.checkViewBounding = function ( roundedIndex ){
var lineLength, docHeight,
tmp;
// vertical bound
docHeight = this.$text.find('div').children('pre').length;
if (roundedIndex.y >= docHeight){
roundedIndex.y = docHeight - 1;
} else if (roundedIndex.y < 0){
roundedIndex.y = 0;
}
// horizontal bound
tmp = this.$text.find('div pre:eq('+roundedIndex.y+')');
// ignore line breaks & phantom
lineLength =
tmp.find('span:last').text() === '\n' || tmp.find('span:last').attr('ph') ?
parseInt(tmp.attr('length'), 10) - 1 : parseInt(tmp.attr('length'), 10);
if (roundedIndex.x < 0){
roundedIndex.x = 0;
} else {
roundedIndex.x = roundedIndex.x > lineLength ? lineLength : roundedIndex.x;
}
return roundedIndex;
};
/**
* Check model index against model borders
*
* @method
* @param {int} model Model index
* @returns {int} Bounded model index
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.checkModelBounding = function ( model ){
if (model < 0){
model = 0;
} else if (model > this.model.length){
model = this.model.length;
}
return model;
};
/**
* Convert mouse coordinate to local view coordinate
*
* @method
* @param {jQuery.Event} event
* @param {jQuery.Object} localTarget Editor element object
* @returns {Object} Line / column coordinate
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.findLocalView = function ( event, localTarget ){
var x = (event.pageX - localTarget.offset().left) / this.charDimension.width,
y = (event.pageY - localTarget.offset().top) / this.charDimension.height;
if ( y >= Math.ceil(y) - 0.1 ){ // Advance to next line
y = Math.ceil(y);
} else { // Keep within current line
y = Math.floor(y);
}
x = Math.round(x);
return {
'x' : x,
'y' : y
};
};
/**
* Start blinking cursor
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.startBlinkCursor = function () {
if (!this.cursorInterval){
var self = this;
this.cursorInterval = setInterval( function(){
if (self.$cursor.css('display') === 'none'){
self.$cursor.css('display','block');
} else {
self.$cursor.css('display','none');
}
} , 500);
}
};
/**
* Stop blinking cursor
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.stopBlinkCursor = function () {
if (this.cursorInterval){
clearInterval( this.cursorInterval );
this.cursorInterval = null;
}
};
/**
* Sort selection model indices, initialize an operation, show cursor view
*
* @method
* @param {boolean} setOld (optional) Whether to affect operation's .old value; default is true
* @param {boolean} suppressFocus (optional) Whether to prevent focusing editing area; default is false
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.createSelection = function ( setOld, suppressFocus ) {
if (setOld === undefined ){
setOld = true;
}
if (suppressFocus === undefined ){
suppressFocus = false;
}
this.selection.view.start = this.modelToView( this.selection.model.start );
this.selection.view.end = this.modelToView( this.selection.model.end );
this.selection.modelSorted.start = this.selection.model.start > this.selection.model.end ?
this.selection.model.end : this.selection.model.start;
this.selection.modelSorted.end = this.selection.model.start > this.selection.model.end ?
this.selection.model.start : this.selection.model.end;
this.input.start = this.selection.modelSorted.start;
if (setOld){
this.input.old = this.model.slice(this.selection.modelSorted.start, this.selection.modelSorted.end);
}
this.modelUnseleted.left = this.model.slice(0, this.selection.modelSorted.start);
this.modelUnseleted.right = this.model.slice( this.selection.modelSorted.end );
if (!suppressFocus){
this.$text.focus();
}
this.showCursor();
};
/**
* Show cursor; scroll editor window if necessary
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.showCursor = function () {
this.$cursorLineOverlay.css({
'left':0,
'top':this.selection.view.end.y * this.charDimension.height,
'height':this.charDimension.height,
'width':this.$editboxLayer.width() - parseInt(this.$textLayer.css('margin-left'), 10) - 1,
'display':'block'
});
this.$cursor.css({
'left': this.selection.view.end.x * this.charDimension.width,
'width':this.charDimension.width,
'display':'block'
});
this.startBlinkCursor();
var bottomDiff = (this.selection.view.end.y + 1) * this.charDimension.height - this.cache.editboxScroll - this.cache.editboxHeight,
topDiff = this.cache.editboxScroll - this.selection.view.end.y * this.charDimension.height;
if ( bottomDiff > 0 ){
this.$editboxLayer
.stop(true, true)
.animate({scrollTop:this.cache.editboxScroll + bottomDiff}, 100);
} else if ( topDiff > 0 ){
this.$editboxLayer
.stop(true, true)
.animate({scrollTop: this.selection.view.end.y * this.charDimension.height}, 100);
}
};
/**
* Hide cursor
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.hideCursor = function () {
this.stopBlinkCursor();
this.$cursorLineOverlay.css({
'display':'none'
});
this.$cursor.css({
'display':'none'
});
};
/**
* Show selection highlight
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.showSelection = function () {
this.$selectionOverlay.empty();
var startLineIdx = this.selection.view.start.y,
endLineIdx = this.selection.view.end.y,
startCharIdx = this.selection.view.start.x,
endCharIdx = this.selection.view.end.x,
i;
if (startLineIdx === endLineIdx){ // Same line
if ( startCharIdx > endCharIdx ){
startCharIdx = [endCharIdx, endCharIdx = startCharIdx][0];
}
this.$selectionOverlay
.append(
this.$$('<div>').addClass('ve-ui-simplesurface-text-selected')
.css({
'left':startCharIdx * this.charDimension.width,
'top':startLineIdx * this.charDimension.height,
'width':(endCharIdx - startCharIdx) * this.charDimension.width,
'height':this.charDimension.height
}) );
} else { // Different lines
if ( startLineIdx > endLineIdx ) {
startLineIdx = [endLineIdx, endLineIdx = startLineIdx][0];
startCharIdx = [endCharIdx, endCharIdx = startCharIdx][0];
}
// First & last line
this.$selectionOverlay
.append(
this.$$('<div>').addClass('ve-ui-simplesurface-text-selected')
.css({
'left':startCharIdx * this.charDimension.width,
'top':startLineIdx * this.charDimension.height,
'width':
(this.checkViewBounding( {'x':Infinity,'y':startLineIdx } ).x + 1 - startCharIdx) *
this.charDimension.width,
'height':this.charDimension.height
}) )
.append(
this.$$('<div>').addClass('ve-ui-simplesurface-text-selected')
.css({
'left':0,
'top':endLineIdx * this.charDimension.height,
'width': endCharIdx * this.charDimension.width,
'height':this.charDimension.height
}) );
// Middle lines
for ( i = startLineIdx + 1; i < endLineIdx; i++ ){
this.$selectionOverlay
.append(
this.$$('<div>').addClass('ve-ui-simplesurface-text-selected')
.css({
'left': 0,
'top': i * this.charDimension.height,
'width': (this.checkViewBounding( {'x':Infinity, 'y':i } ).x + 1) * this.charDimension.width,
'height':this.charDimension.height
})
);
}
}
// Display to view
this.$selectionOverlay.css('display','block');
};
/**
* Hide selection highlight
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.hideSelection = function () {
this.$selectionOverlay.empty();
this.$selectionOverlay.css('display','none');
};
/**
* Convert model index to view index
*
* @method
* @param {int} model Model index
* @returns {Object} View index
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.modelToView = function ( model ) {
var match = [],
$line = this.$text.find('pre'),
i;
for ( i = 0; i < $line.length; i++ ){
if (parseInt($line.eq(i).attr('m'),10) <= model &&
parseInt($line.eq(i).attr('n'),10) > model){
break;
}
}
$line = $line.eq(i).find('span');
match = jQuery.grep($line,function(e){
return (parseInt(e.getAttribute('idx'),10) <= model &&
parseInt(e.getAttribute('idx'),10) + e.innerText.length > model);
});
return {
'x': parseInt(match[match.length-1].getAttribute('col'), 10) +
(model - parseInt(match[match.length-1].getAttribute('idx'), 10)),
'y': i
};
};
/**
* Convert view index to model index
*
* @method
* @param {Object} view View index
* @returns {int} Model index
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.viewToModel = function ( view ) {
var $lineChildren = this.$text.find('pre:eq('+view.y+')').find('span'),
match = jQuery.grep( $lineChildren, function(e){
return (
parseInt(e.getAttribute('col'), 10) <= view.x &&
parseInt(e.getAttribute('col'), 10) + e.innerText.length > view.x
);
}),
model = match[match.length-1].hasAttribute('tb') ?
parseInt(match[match.length-1].getAttribute('idx'), 10) :
parseInt(match[match.length-1].getAttribute('idx'), 10) +
view.x - parseInt(match[match.length-1].getAttribute('col'),10);
return model;
};
/**
* Search
*
* @method
* @param {string} value Value to search
* @returns {boolean} Whether there is a match
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.search = function ( value ) {
var regex = new RegExp( value, 'g' );
if ( !regex.test( this.model ) ){
return false;
} else {
return true;
}
};
/**
* Search input handler
*
* @method
* @param {jQuery.Event} event
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.onSearchInput = function ( event ) {
if ( event.which === 27 ){ // Esc
this.$searchContainer.css('display','none');
this.$text.focus();
return false;
} else {
setTimeout( ve.bind( function () {
var value = this.$searchBox.get(0).value;
if ( event === undefined ){
event = {'which':0};
}
if ( event.which === 13 ){ // Enter key
this.searchStep();
} else if ( value !== '' ){
if ( !this.search( value ) ){ // No match
this.searchHasResult = false;
this.$searchBox.css({
'background':'red'
});
} else {
this.searchRegex = new RegExp( this.$searchBox.get(0).value, 'g' );
this.searchRegex.lastIndex = 0;
this.searchHasResult = true;
this.$searchBox.css({
'background':'transparent'
});
this.searchStep();
}
} else {
this.$searchBox.css({
'background':'transparent'
});
this.hideSelection();
}
}, this ) );
}
};
/**
* Step search results
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.searchStep = function () {
var match;
if ( this.searchRegex && (match = this.searchRegex.exec( this.model )) !== null ){
this.selection.model.start = match.index;
this.selection.model.end = match.index + match[0].length;
this.createSelection(undefined, true);
this.showSelection();
} else {
this.onSearchInput();
}
};
/**
* Replace search result
*
* @method
*/
ve.ui.MWSyntaxHighlightSimpleSurface.prototype.searchReplace = function () {
if ( this.searchHasResult ){
this.input.newer = this.$replaceBox.get(0).value;
this.input.pushReady = true;
this.edit();
this.pushStack();
this.searchStep();
}
};