oj-ace-editor
Version:
Ace Editor plugin for OJ
703 lines (607 loc) • 24 kB
JavaScript
// oj.AceEditor.js
;(function(root, factory){
// Export to Node
if (typeof module === 'object' && module.exports)
module.exports = factory(root)
// Export to RequireJS
else if (typeof define === 'function' && define.amd)
define(function(){return factory(root)})
// Export to OJ
else
factory(root, root.oj)
}(this, function(root, oj){
var debounce;
var plugin = function(oj,settings){
// Client side the AceEditor must be included by <script> tag. Help people understand.
if (oj.isClient && !oj.isDefined(ace))
throw new Error('oj.AceEditor: ace not found')
if (typeof settings !== 'object')
settings = {}
var AceEditor = oj.createType('AceEditor', {
base: oj.ModelKeyView,
constructor: function(){
var union = oj.unionArguments(arguments),
options = union.options,
args = union.args,
defaults = {
width: 400, // Default the height
height: 200, // Default the width
fontSize: 14, // Default font size
showFoldWidgets: false, // Hide fold widgets
showPrintMargin: false, // Hide print margin
useSoftTabs: true, // Change tabs to spaces
behaviorsEnabled: true, // Enable quote and paren matching
foldStyle: 'markbegin', // Default fold style when folds are unhidden
hScrollBarAlwaysVisible: false, // Prevent ugly scroll bars
vScrollBarAlwaysVisible: false, // Ace doesn't have this property. This solution is imperfect.
// Disable workers on local files because ace doesn't support this
useWorker: false// window.location.protocol != 'file:'
};
// Default options if unspecified
for (k in defaults) {
if (options[k] == null)
options[k] = defaults[k];
}
var ver = this._getVersion();
// ver.major = 10;
// Create el as relatively positioned div
var This = this;
this.el = oj(function(){
oj.div(function(){
if(ver.major == -1)
oj.div({c:'oj-AceEditor-editor', style:{position:'absolute',width:options.width, height:options.height}});
else
oj.TextArea({c:'oj-AceEditor-editor',
change:function(){
if (typeof This.viewChanged == 'function') {
This.viewChanged();
}
if (typeof This.change == 'function') {
This.change();
}
},
style:{
backgroundColor:'#FEFAF3',
color:'#586E75',
border:'none',
position:'absolute',
fontSize: options.fontSize,
width:options.width,
fontFamily:"Monaco,Menlo,Ubuntu Mono,Consolas,source-code-pro,monospace",
height:options.height}});
},{
style:{
position:'relative',
width:options.width,
height:options.height
}
}
);
});
this.$editor = this.$('.oj-AceEditor-editor');
if (ver.major == -1) {
// Create editor
if (oj.isClient && typeof ace != 'undefined') {
this.editor = ace.edit(this.$editor.get(0));
this.editor.resize()
// Register for editor changes
// Use debounce to ensure cut and paste only fires one event change
var This = this;
this.session.doc.on('change', debounce(50, function(){
if (typeof This.viewChanged == 'function')
This.viewChanged();
if (typeof This.change == 'function')
This.change();
}));
}
}
else
{
this._editor = {
width:function(){},
height:function(){},
getSession:function(){return {
doc:{'on':function(){}},
getValue:function(){},
setValue:function(){},
getMode:function(){return {'$id':'js'}},
setMode:function(){},
getTabSize:function(){return 2;},
setTabSize:function(){},
setFoldStyle:function(){},
getUseSoftTabs:function(){return true},
setUseSoftTabs:function(){},
getUseWrapMode:function(){},
setUseWrapMode:function(){},
getWrapLimitRange:function(){},
setWrapLimitRange:function(){},
getUseWorker:function(){},
setUseWorker:function(){}
}},
setSession:function(){},
renderer:{
getShowGutter:function(){},
setShowGutter:function(){},
getPrintMarginColumn:function(){},
setPrintMarginColumn:function(){},
getHScrollBarAlwaysVisible:function(){},
setHScrollBarAlwaysVisible:function(){}
},
getTheme:function(){},
setTheme:function(){},
resize:function(){},
getReadOnly:function(){},
setReadOnly:function(){},
setFontSize:function(){},
getCursorPosition:function(){},
moveCursorToPosition:function(){},
getShowPrintMargin:function(){},
setShowPrintMargin:function(){},
getShowInvisibles:function(){},
setShowInvisibles:function(){},
getDisplayIndentGuides:function(){},
setDisplayIndentGuides:function(){},
getShowFoldWidgets:function(){},
setShowFoldWidgets:function(){},
getHighlightSelectedWord:function(){},
setHighlightSelectedWord:function(){},
getHighlightActiveLine:function(){},
setHighlightActiveLine:function(){},
getBehavioursEnabled:function(){},
setBehavioursEnabled:function(){}
}
}
// Shift editor properties
var props = [
'theme',
'mode',
'width',
'height',
'wrapLimit',
'showPrintMargin',
'readOnly',
'fontSize',
'tabSize',
'foldStyle',
'selectionStyle',
'showPrintMargin',
'showInvisibles',
'showGutter',
'showIndentGuides',
'showFoldWidgets',
'highlightSelectedWord',
'highlightActiveLine',
'useSoftTabs',
'useWrapMode',
'wrapLimitRange',
'printMarginColumn',
'animatedScroll',
'useWorker',
'hScrollBarAlwaysVisible',
'vScrollBarAlwaysVisible',
'fadeFoldWidgets',
'behaviorsEnabled'
];
for (var i = 0; i < props.length; i++) {
var prop = props[i];
if (options[prop] != null)
this[prop] = oj.argumentShift(options, prop);
}
// Value is property or first argument
value = oj.argumentShift(options, 'value') || args.join('\n');
// Pass on options. Args have been handled at this level.
AceEditor.base.constructor.apply(this, [options]);
// Hide vertical scroll bar
this.$scrollbar = this.$('.ace_scrollbar');
this.$scroller = this.$('.ace_scroller');
this.$content = this.$('.ace_content');
if (value)
this.value = value;
},
properties: {
// Accessing Properties
// ----------------------------------------------------------------------
value: {
get: function(){
if(this._isVer) {
return this._verEditor.value;
}
if(this.session)
return this.session.getValue(); },
set: function(v){
if(this.session) {
// Save the location of the cursor
var pos = this.cursorPosition;
this.session.setValue(v)
// Restore the location of the cursor
this.cursorPosition = pos;
if (this._isVer) {
this._verEditor.value = v
}
}
}
},
editor: {
get: function(){ return this._editor; },
set: function(v){ this._editor = v; }
},
session: {
get: function(){ if(this.editor) return this.editor.getSession(); },
set: function(v){ if(this.editor) this.editor.setSession(v); }
},
renderer: {
get: function(){ if(this.editor) return this.editor.renderer; }
},
change: {
get: function(){ return this._change; },
set: function(v){ this._change = v; }
},
// Wrap ace event handling object
eventHandler: {
get: function(){
if (typeof 'ace' == 'undefined')
return;
return this._eventHandler || (this._eventHandler = ace.require("ace/lib/event"));
}
},
// Wrap ace container element
containerEl: {
get: function(){
return this._container || (this._container = this.$('.editor-container')[0]);
}
},
// Custom Properties
// ----------------------------------------------------------------------
// Set/get theme and automatically add ace/theme prefix
theme: {
get: function(){
if (!this.editor) return;
var theme = this.editor.getTheme();
var prefix = 'ace/theme/';
if (theme && theme.indexOf(prefix) === 0)
theme = theme.slice(prefix.length);
return theme;
},
set: function(v){
if (!this.editor) return;
var prefix = 'ace/theme/';
if (v && v.indexOf(prefix) != 0)
v = prefix + v;
this.editor.setTheme(v);
}
},
// Set/get mode and automatically add ace/mode prefix
mode: {
get: function(){
if (!this.session) return;
// Get mode string from Mode object
var mode = this.session.getMode().$id;
var prefix = 'ace/mode/';
if (mode && mode.indexOf(prefix) === 0)
mode = mode.slice(prefix.length);
return mode;
},
set: function(v){
if (!this.session) return;
var prefix = 'ace/mode/';
if (v && v.indexOf(prefix) != 0)
v = prefix + v;
this.session.setMode(v);
}
},
// Change width
width: {
get: function(){ return this.$el.width(); },
set: function(v){
this.$el.width(v)
this.updateWidth()
}
},
// Change height
height: {
get: function(){ return this.$el.height(); },
set: function(v){
this.$el.height(v);
this.$editor.height(v);
if (this.editor)
this.editor.resize();
}
},
// Meta property that sets wrapLimitRange, printMarginColumn, and useWrapMode all at once
// wrapLimit: 40 (limit of 40 characters)
// wrapLimit: 'off' (no limit, creates scroll bar)
// wrapLimit: 'auto' (limit to size of buffer)
wrapLimit: {
get: function(){ if(this.session) return this.wrapLimitRange; },
set: function(v){
if(this.session) {
// Turn off wrapping if false or set to 'off'
if(!v || v === 'off') {
this.useWrapMode = false;
this.printMarginColumn = 80;
}
// Wrap to region if set to 'auto'
else if (v === 'auto') {
this.useWrapMode = true;
this.wrapLimitRange = null;
this.printMarginColumn = 80;
}
// Otherwise wrap to specified character count
else if (typeof v === 'number') {
this.wrapLimitRange = v;
this.printMarginColumn = v;
this.useWrapMode = true;
}
}
}
},
// TODO: Enable Drag and drop as a property
// useDragAndDrop: {
// get: function(){},
// set: function(v){}
// },
// https://github.com/ajaxorg/ace/blob/master/demo/kitchen-sink/demo.js#L437
// event.addListener(container, "drop", function(e) {
// var file;
// try {
// file = e.dataTransfer.files[0];
// if (window.FileReader) {
// var reader = new FileReader();
// reader.onload = function() {
// var mode = modelist.getModeFromPath(file.name);
// env.editor.session.doc.setValue(reader.result);
// modeEl.value = mode.name;
// env.editor.session.setMode(mode.mode);
// env.editor.session.modeName = mode.name;
// };
// reader.readAsText(file);
// }
// return event.preventDefault(e);
// } catch(err) {
// return event.stopEvent(e);
// }
// });
// Editor Configuration Properties
// ----------------------------------------------------------------------
// Make the editor read only
readOnly: {
get: function(){ if(this.editor) return this.editor.getReadOnly(); },
set: function(v){ if(this.editor) this.editor.setReadOnly(v); }
},
// Change font size
fontSize: {
get: function(){ if(this.editor) return this._fontSize; },
set: function(v){
if(this.editor) {
this.editor.setFontSize(v);
this._fontSize = v;
}
}
},
// Change tab size
tabSize: {
get: function(){ if(this.editor) return this.session.getTabSize(); },
set: function(v){ if(this.editor) this.session.setTabSize(v); }
},
// Change cursorPosition as object: {row:4, column:25}
cursorPosition: {
get: function(){ if(this.editor) return this.editor.getCursorPosition(); },
set: function(v){ if(this.editor) this.editor.moveCursorToPosition(v); }
},
// Editor Show Properties
// ----------------------------------------------------------------------
// Show print margin
showPrintMargin: {
get: function(){ if(this.editor) return this.editor.getShowPrintMargin(); },
set: function(v){ if(this.editor) this.editor.setShowPrintMargin(v); }
},
// Show invisible characters
showInvisibles: {
get: function(){ if(this.editor) return this.editor.getShowInvisibles(); },
set: function(v){ if(this.editor) this.editor.setShowInvisibles(v); }
},
// Show gutter
showGutter: {
get: function(){ if(this.editor) return this.renderer.getShowGutter(); },
set: function(v){ if(this.editor) this.renderer.setShowGutter(v); }
},
// Show Indent guides
showIndentGuides: {
get: function(){ if(this.editor) return this.editor.getDisplayIndentGuides(); },
set: function(v){ if(this.editor) this.editor.setDisplayIndentGuides(v); }
},
// Show fold widgets that collapse / expand code blocks
showFoldWidgets: {
get: function(){ if(this.editor) return this.editor.getShowFoldWidgets(); },
set: function(v){ if(this.editor) this.editor.setShowFoldWidgets(v); }
},
// Editor Highlight Properties
// ----------------------------------------------------------------------
// Highlight selected word elsewhere in the editor
highlightSelectedWord: {
get: function(){ if(this.editor) return this.editor.getHighlightSelectedWord(); },
set: function(v){ if(this.editor) this.editor.setHighlightSelectedWord(v); }
},
// Highlight active line
highlightActiveLine: {
get: function(){ if(this.editor) return this.editor.getHighlightActiveLine(); },
set: function(v){ if(this.editor) this.editor.setHighlightActiveLine(v); }
},
// Editor Style Properties
// ----------------------------------------------------------------------
// Selection style options: 'line' or 'text'
selectionStyle: {
get: function(){ if(this.editor) return this.editor.getSelectionStyle() || 'line';
},
set: function(v){
if(this.editor) {
if (v !== 'line' && v !== 'text')
throw new Error("oj.AceEditor: selectionStyle expects 'line' or 'text'")
this.editor.setSelectionStyle(v);
}
}
},
// Fold style options: 'manual', markbegin' or 'markbeginend'
foldStyle: {
get: function(){ if(this.session) return this._foldStyle; },
set: function(v){
if(this.session) {
this._foldStyle = v;
this.session.setFoldStyle(v);
}
}
},
// Enable ace editor behaviors to auto match quotes, parens, curly braces, and square brackets
behaviorsEnabled: {
get: function(){ if(this.editor) return this.editor.getBehavioursEnabled(); },
set: function(v){ if(this.editor) this.editor.setBehavioursEnabled(v); }
},
// Editor Not-Very-Important Properties
// ----------------------------------------------------------------------------------
useSoftTabs: {
get: function(){ if(this.session) return this.session.getUseSoftTabs(); },
set: function(v){ if(this.session) this.session.setUseSoftTabs(v); }
},
// Set whether wrapping should be on (true) or off (false)
useWrapMode: {
get: function(){ if(this.session) return this.session.getUseWrapMode(); },
set: function(v){ if(this.session) this.session.setUseWrapMode(v); }
},
// Set the wrap limit character count
wrapLimitRange: {
get: function(){ if(this.session) return this.session.getWrapLimitRange(); },
set: function(v){ if(this.session) this.session.setWrapLimitRange(v, v); },
},
// Set the number of characters the margin should appear at.
printMarginColumn: {
get: function(){ if(this.renderer) return this.renderer.getPrintMarginColumn(); },
set: function(v){ if(this.renderer) this.renderer.setPrintMarginColumn(v); }
},
// Animates scrolling for find and goto line
animatedScroll: {
get: function(){ if(this.editor) return this.editor.getAnimatedScroll(); },
set: function(v){ if(this.editor) this.editor.setAnimatedScroll(v); }
},
// Use or disable worker threads in ace editor
useWorker: {
get: function(){ if(this.session) return this.session.getUseWorker(); },
set: function(v){ if(this.session) this.session.setUseWorker(v); }
},
// Turn horizontal scrollbar on permanently
hScrollBarAlwaysVisible: {
get: function(){ if(this.editor) return this.renderer.getHScrollBarAlwaysVisible(); },
set: function(v){ if(this.editor) this.renderer.setHScrollBarAlwaysVisible(v); }
},
// Turn horizontal scrollbar on permanently
vScrollBarAlwaysVisible: {
get: function(){ return this._vScrollBarAlwaysVisible || false; },
set: function(v){
this._vScrollBarAlwaysVisible = v;
if (this.$scrollbar != null) {
if (v) {
this.$scrollbar.show();
this.updateWidth();
} else {
this.$scrollbar.hide()
this.updateWidth();
}
}
}
},
// Fade fold widgets that allow code blocks to be collapsed
fadeFoldWidgets: {
get: function(){ if(this.editor) return this.editor.getFadeFoldWidgets(); },
set: function(v){ if(this.editor) this.editor.setFadeFoldWidgets(v); }
},
$scrollbar: null,
$scroller: null,
$content: null,
_isVer:{
get:function(){
return this._getVersion().major != -1
}
},
_verEditor:{
get:function(){
if(!this._isVer)
return null;
return this.$editor.get(0).oj;
}
}
},
methods: {
// When the view changes
viewChanged: function(){
AceEditor.base.inserted.apply(this, arguments);
},
// When inserted in the dom
inserted: function(){
AceEditor.base.inserted.apply(this, arguments);
this.value = this.value
// Scroll visiblility can only be triggered once inserted
this.vScrollBarAlwaysVisible = this.vScrollBarAlwaysVisible
},
// The width is dependent on if vertical scroll is turned off
updateWidth: function(){
// Get the width
var w = this.width;
// The editor width is bigger if there is no scroll bar
widthScroll = this.vScrollBarAlwaysVisible ? 0 : this.scrollWidth();
this.$editor.width(w + widthScroll);
// Trigger the editor to change size
if (this.editor)
this.editor.resize();
},
// Dynamically calculate the scroll width by measuring the difference
// Cache the result
scrollWidth: function(){
if (this._scrollWidth != null)
return this._scrollWidth;
if (this.isInserted) {
var div = $('<div style="width:50px;height:50px;overflow:hidden;position:absolute;top:-200px;left:-200px;"><div style="height:100px;"></div>');
// Append our div, do our calculation and then remove it
$('body').append(div);
var w1 = $('div', div).innerWidth();
div.css('overflow-y', 'scroll');
var w2 = $('div', div).innerWidth();
$(div).remove();
return this._scrollWidth = (w1 - w2);
}
},
_getVersion: function(){
if (oj.isClient) {
var agent = navigator.userAgent;
var reg = /MSIE\s?(\d+)(?:\.(\d+))?/i;
var matches = agent.match(reg);
if (matches != null) {
return { major: matches[1], minor: matches[2] };
}
}
return { major: "-1", minor: "-1" };
}
}
});
return {AceEditor:AceEditor};
};
// Debounce from underscore to remove the only underscore dependency
// http://underscorejs.org/#debounce
debounce = function(wait, func, immediate) {
var timeout, result;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) result = func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(context, args);
return result;
};
};
// Export to OJ
if (typeof oj != 'undefined')
oj.use(plugin);
return plugin;
}));