diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index 4bc1c35e..08c5098c 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -360,31 +360,32 @@ bindCheckbox("read_only", function(checked) { env.editor.setReadOnly(checked); }); -var secondSession = null; bindDropdown("split", function(value) { var sp = env.split; - if (value == "none") { - if (sp.getSplits() == 2) { - secondSession = sp.getEditor(1).session; - } + if (value == "none") { sp.setSplits(1); } else { var newEditor = (sp.getSplits() == 1); - if (value == "below") { - sp.setOrientation(sp.BELOW); - } else { - sp.setOrientation(sp.BESIDE); - } + sp.setOrientation(value == "below" ? sp.BELOW : sp.BESIDE); sp.setSplits(2); if (newEditor) { - var session = secondSession || sp.getEditor(0).session; + var session = sp.getEditor(0).session; var newSession = sp.setSession(session, 1); newSession.name = session.name; } } }); +function synchroniseScrolling() { + var s1 = env.split.$editors[0].session; + var s2 = env.split.$editors[1].session; + s1.on('changeScrollTop', function(pos) {s2.setScrollTop(pos)}); + s2.on('changeScrollTop', function(pos) {s1.setScrollTop(pos)}); + s1.on('changeScrollLeft', function(pos) {s2.setScrollLeft(pos)}); + s2.on('changeScrollLeft', function(pos) {s1.setScrollLeft(pos)}); +} + bindCheckbox("highlight_token", function(checked) { var editor = env.editor; if (editor.tokenTooltip && !checked) { diff --git a/demo/scrollable-page.html b/demo/scrollable-page.html index 308208ed..dfc48eba 100644 --- a/demo/scrollable-page.html +++ b/demo/scrollable-page.html @@ -88,6 +88,7 @@ require("ace/commands/default_commands").commands.push({ exec: function(editor) { dom.toggleCssClass(document.body, "fullScreen") dom.toggleCssClass(editor.container, "fullScreen") + editor.setAutoScrollEditorIntoView() editor.resize() } }) @@ -96,7 +97,7 @@ require("ace/commands/default_commands").commands.push({ var editor = ace.edit("editor"); editor.setTheme("ace/theme/twilight"); editor.session.setMode("ace/mode/javascript"); - +editor.setAutoScrollEditorIntoView(); var count = 1; function add() { @@ -113,6 +114,8 @@ function add() { editor = ace.edit(el) editor.setTheme(theme) editor.session.setMode("ace/mode/javascript") + + editor.setAutoScrollEditorIntoView() editor.setValue([ "this is editor number: ", count, "\n", diff --git a/lib/ace/commands/command_manager.js b/lib/ace/commands/command_manager.js index 86a668e5..b3e0fc6d 100644 --- a/lib/ace/commands/command_manager.js +++ b/lib/ace/commands/command_manager.js @@ -8,7 +8,7 @@ var EventEmitter = require("../lib/event_emitter").EventEmitter; /** * @class CommandManager * - * + * **/ /** @@ -16,7 +16,7 @@ var EventEmitter = require("../lib/event_emitter").EventEmitter; * @param {String} platform Identifier for the platform; must be either `'mac'` or `'win'` * @param {Array} commands A list of commands * - * + * * * **/ @@ -27,7 +27,7 @@ var CommandManager = function(platform, commands) { this.commmandKeyBinding = {}; this.addCommands(commands); - + this.setDefaultHandler("exec", function(e) { return e.command.exec(e.editor, e.args || {}); }); @@ -49,11 +49,9 @@ oop.inherits(CommandManager, HashHandler); if (editor && editor.$readOnly && !command.readOnly) return false; - var retvalue = this._emit("exec", { - editor: editor, - command: command, - args: args - }); + var e = {editor: editor, command: command, args: args}; + var retvalue = this._emit("exec", e); + this._signal("afterExec", e); return retvalue === false ? false : true; }; diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index 24a654dd..72a70034 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -717,9 +717,7 @@ var EditSession = function(text, mode) { clazz = "ace_step"; var range = new Range(startRow, 0, endRow, Infinity); - - var id = this.addMarker(range, clazz, "fullLine", inFront); - range.id = id; + range.id = this.addMarker(range, clazz, "fullLine", inFront); return range; }; @@ -999,11 +997,11 @@ var EditSession = function(text, mode) { **/ this.setScrollTop = function(scrollTop) { scrollTop = Math.round(Math.max(0, scrollTop)); - if (this.$scrollTop === scrollTop) + if (this.$scrollTop === scrollTop || isNaN(scrollTop)) return; this.$scrollTop = scrollTop; - this._emit("changeScrollTop", scrollTop); + this._signal("changeScrollTop", scrollTop); }; /** @@ -1020,11 +1018,11 @@ var EditSession = function(text, mode) { **/ this.setScrollLeft = function(scrollLeft) { scrollLeft = Math.round(Math.max(0, scrollLeft)); - if (this.$scrollLeft === scrollLeft) + if (this.$scrollLeft === scrollLeft || isNaN(scrollLeft)) return; this.$scrollLeft = scrollLeft; - this._emit("changeScrollLeft", scrollLeft); + this._signal("changeScrollLeft", scrollLeft); }; /** diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 5e4b7967..4039e0b7 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -507,10 +507,13 @@ var Editor = function(renderer, session) { session.removeMarker(session.$highlightLineMarker.id); session.$highlightLineMarker = null; } else if (!session.$highlightLineMarker && highlight) { - session.$highlightLineMarker = session.highlightLines(highlight.row, highlight.row, "ace_active-line"); + var range = new Range(highlight.row, highlight.column, highlight.row, Infinity); + range.id = session.addMarker(range, "ace_active-line", "screenLine"); + session.$highlightLineMarker = range; } else if (highlight) { session.$highlightLineMarker.start.row = highlight.row; session.$highlightLineMarker.end.row = highlight.row; + session.$highlightLineMarker.start.column = highlight.column; session._emit("changeBackMarker"); } }; @@ -2107,6 +2110,62 @@ var Editor = function(renderer, session) { this.renderer.destroy(); this._emit("destroy", this); }; + + /** + * Enables automatic scrolling of the cursor into view when editor itself is inside scrollable element + * @param {Boolean} enable default true + **/ + this.setAutoScrollEditorIntoView = function(enable) { + if (enable === true) + return; + var rect; + var self = this; + var shouldScroll = false; + if (!this.$scrollAnchor) + this.$scrollAnchor = document.createElement("div"); + var scrollAnchor = this.$scrollAnchor; + scrollAnchor.style.cssText = "position:absolute"; + this.container.insertBefore(scrollAnchor, this.container.firstChild); + var onChangeSelection = this.on("changeSelection", function() { + shouldScroll = true; + }); + // needed to not trigger sync reflow + var onBeforeRender = this.renderer.on("beforeRender", function() { + if (shouldScroll) + rect = self.renderer.container.getBoundingClientRect(); + }); + var onAfterRender = this.renderer.on("afterRender", function() { + if (shouldScroll && rect && self.isFocused()) { + var renderer = self.renderer; + var pos = renderer.$cursorLayer.$pixelPos; + var config = renderer.layerConfig; + var top = pos.top - config.offset; + if (pos.top >= 0 && top + rect.top < 0) { + shouldScroll = true; + } else if (pos.top < config.height && + pos.top + rect.top + config.lineHeight > window.innerHeight) { + shouldScroll = false; + } else { + shouldScroll = null; + } + if (shouldScroll != null) { + scrollAnchor.style.top = top + "px"; + scrollAnchor.style.left = pos.left + "px"; + scrollAnchor.style.height = config.lineHeight + "px"; + scrollAnchor.scrollIntoView(shouldScroll); + } + shouldScroll = rect = null; + } + }); + this.setAutoScrollEditorIntoView = function(enable) { + if (enable === false) + return; + delete this.setAutoScrollEditorIntoView; + this.removeEventListener("changeSelection", onChangeSelection); + this.renderer.removeEventListener("afterRender", onAfterRender); + this.renderer.removeEventListener("beforeRender", onBeforeRender); + }; + }; }).call(Editor.prototype); diff --git a/lib/ace/keyboard/textinput.js b/lib/ace/keyboard/textinput.js index e4def8b1..397a0917 100644 --- a/lib/ace/keyboard/textinput.js +++ b/lib/ace/keyboard/textinput.js @@ -374,9 +374,12 @@ var TextInput = function(parentNode, host) { resetSelection(host.selection.isEmpty()); host._emit("nativecontextmenu", {target: host}); var rect = host.container.getBoundingClientRect(); + var style = dom.computedStyle(host.container); + var top = rect.top + (parseInt(style.borderTopWidth) || 0); + var left = rect.left + (parseInt(rect.borderLeftWidth) || 0); var move = function(e) { - text.style.left = e.clientX - rect.left - 2 + "px"; - text.style.top = e.clientY - rect.top - 2 + "px"; + text.style.left = e.clientX - left - 2 + "px"; + text.style.top = e.clientY - top - 2 + "px"; }; move(e); diff --git a/lib/ace/layer/cursor.js b/lib/ace/layer/cursor.js index bd0219a4..26ade52b 100644 --- a/lib/ace/layer/cursor.js +++ b/lib/ace/layer/cursor.js @@ -170,7 +170,7 @@ var Cursor = function(parentEl) { selections = [{cursor: null}]; } - for (var i = selections.length; i--; ) { + for (var i = 0, n = selections.length; i < n; i++) { var pixelPos = this.getPixelPosition(selections[i].cursor, true); if ((pixelPos.top > config.height + config.offset || pixelPos.top < -config.offset) && i > 1) { diff --git a/lib/ace/layer/marker.js b/lib/ace/layer/marker.js index c68d3964..9162a501 100644 --- a/lib/ace/layer/marker.js +++ b/lib/ace/layer/marker.js @@ -82,6 +82,8 @@ var Marker = function(parentEl) { marker.renderer(html, range, left, top, config); } else if (marker.type == "fullLine") { this.drawFullLineMarker(html, range, marker.clazz, config); + } else if (marker.type == "screenLine") { + this.drawScreenLineMarker(html, range, marker.clazz, config); } else if (range.isMultiLine()) { if (marker.type == "text") this.drawTextMarker(html, range, marker.clazz, config); @@ -194,7 +196,19 @@ var Marker = function(parentEl) { "top:", top, "px;", "left:0;right:0;'>" ); - } + }; + + this.drawScreenLineMarker = function(stringBuilder, range, clazz, config) { + var top = this.$getTop(range.start.row, config); + var height = config.lineHeight; + + stringBuilder.push( + "
" + ); + }; }).call(Marker.prototype); diff --git a/lib/ace/lib/event_emitter.js b/lib/ace/lib/event_emitter.js index 5f4bec86..a359a9a7 100644 --- a/lib/ace/lib/event_emitter.js +++ b/lib/ace/lib/event_emitter.js @@ -32,11 +32,13 @@ define(function(require, exports, module) { "use strict"; var EventEmitter = {}; +var stopPropagation = function() { this.propagationStopped = true; }; +var preventDefault = function() { this.defaultPrevented = true; }; EventEmitter._emit = EventEmitter._dispatchEvent = function(eventName, e) { - this._eventRegistry = this._eventRegistry || {}; - this._defaultHandlers = this._defaultHandlers || {}; + this._eventRegistry || (this._eventRegistry = {}); + this._defaultHandlers || (this._defaultHandlers = {}); var listeners = this._eventRegistry[eventName] || []; var defaultHandler = this._defaultHandlers[eventName]; @@ -48,18 +50,12 @@ EventEmitter._dispatchEvent = function(eventName, e) { if (!e.type) e.type = eventName; - - if (!e.stopPropagation) { - e.stopPropagation = function() { - this.propagationStopped = true; - }; - } - - if (!e.preventDefault) { - e.preventDefault = function() { - this.defaultPrevented = true; - }; - } + if (!e.stopPropagation) + e.stopPropagation = stopPropagation; + if (!e.preventDefault) + e.preventDefault = preventDefault; + if (!e.target) + e.target = this; for (var i=0; i= 0) { var c = fw[i]; if (c == null) - c = fw[i] = session.getFoldWidget(); + c = fw[i] = session.getFoldWidget(i); if (c == "start") { var range = session.getFoldWidgetRange(i); diff --git a/lib/ace/multi_select.js b/lib/ace/multi_select.js index 52137ddb..9ae3b847 100644 --- a/lib/ace/multi_select.js +++ b/lib/ace/multi_select.js @@ -66,7 +66,7 @@ var EditSession = require("./edit_session").EditSession; // automatically sorted list of ranges this.rangeList = null; - /** + /** * Adds a range to a selection by entering multiselect mode, if necessary. * @param {Range} range The new range to add * @param {Boolean} $blockChangeEvents Whether or not to block changing events @@ -218,7 +218,7 @@ var EditSession = require("./edit_session").EditSession; var start = range.end, end = range.start; else var start = range.start, end = range.end; - + this.addRange(Range.fromPoints(end, end)); this.addRange(Range.fromPoints(start, start)); return; @@ -261,9 +261,9 @@ var EditSession = require("./edit_session").EditSession; }; /** - * + * * Gets list of ranges composing rectangular block on the screen - * + * * @param {Cursor} screenCursor The cursor to use * @param {Anchor} screenAnchor The anchor to use * @param {Boolean} includeEmptyLines If true, this includes ranges inside the block which are empty due to clipping @@ -339,8 +339,8 @@ var EditSession = require("./edit_session").EditSession; var Editor = require("./editor").Editor; (function() { - /** - * + /** + * * Updates the cursor and marker layers. * @method Editor.updateSelectionMarkers * @@ -350,7 +350,7 @@ var Editor = require("./editor").Editor; this.renderer.updateBackMarkers(); }; - /** + /** * Adds the selection and cursor. * @param {Range} orientedRange A range containing a cursor * @returns {Range} @@ -368,7 +368,7 @@ var Editor = require("./editor").Editor; return orientedRange; }; - /** + /** * Removes the selection marker. * @param {Range} The selection range added with [[Editor.addSelectionMarker `addSelectionMarker()`]]. * @method Editor.removeSelectionMarker @@ -455,12 +455,12 @@ var Editor = require("./editor").Editor; e.preventDefault(); }; - /** + /** * Executes a command for each selection range. * @param {String} cmd The command to execute * @param {String} args Any arguments for the command * @method Editor.forEachSelection - **/ + **/ this.forEachSelection = function(cmd, args) { if (this.inVirtualSelectionMode) return; @@ -491,7 +491,7 @@ var Editor = require("./editor").Editor; this.onSelectionChange(); }; - /** + /** * Removes all the selections except the last added one. * @method Editor.exitMultiSelectMode **/ @@ -522,7 +522,7 @@ var Editor = require("./editor").Editor; if (this.$readOnly) return; - this._emit("paste", text); + this._signal("paste", text); if (!this.inMultiSelectMode || this.inVirtualSelectionMode) return this.insert(text); @@ -547,7 +547,7 @@ var Editor = require("./editor").Editor; * @param {Object} The search options * @param {Boolean} keeps * - * @returns {Number} The cumulative count of all found matches + * @returns {Number} The cumulative count of all found matches * @method Editor.findAll **/ this.findAll = function(needle, options, additive) { @@ -575,11 +575,11 @@ var Editor = require("./editor").Editor; /** * Adds a cursor above or below the active cursor. - * + * * @param {Number} dir The direction of lines to select: -1 for up, 1 for down * @param {Boolean} skip If `true`, removes the active selection range * - * @method Editor.selectMoreLines + * @method Editor.selectMoreLines */ this.selectMoreLines = function(dir, skip) { var range = this.selection.toOrientedRange(); @@ -619,7 +619,7 @@ var Editor = require("./editor").Editor; this.selection.substractPoint(toRemove); }; - /** + /** * Transposes the selected ranges. * @param {Number} dir The direction to rotate selections * @method Editor.transposeSelections @@ -661,7 +661,7 @@ var Editor = require("./editor").Editor; } }; - /** + /** * Finds the next occurence of text in an active selection and adds it to the selections. * @param {Number} dir The direction of lines to select: -1 for up, 1 for down * @param {Boolean} skip If `true`, removes the active selection range @@ -688,7 +688,7 @@ var Editor = require("./editor").Editor; this.multiSelect.substractPoint(range.cursor); }; - /** + /** * Aligns the cursors or selected text. * @method Editor.alignCursors **/ diff --git a/lib/ace/virtual_renderer.js b/lib/ace/virtual_renderer.js index 1a9e0721..a6a61653 100644 --- a/lib/ace/virtual_renderer.js +++ b/lib/ace/virtual_renderer.js @@ -465,8 +465,16 @@ var VirtualRenderer = function(container, theme) { }; this.$updateGutterLineHighlight = function() { - this.$gutterLineHighlight.style.top = this.$cursorLayer.$pixelPos.top - this.layerConfig.offset + "px"; - this.$gutterLineHighlight.style.height = this.layerConfig.lineHeight + "px"; + var pos = this.$cursorLayer.$pixelPos; + var height = this.layerConfig.lineHeight; + if (this.session.getUseWrapMode()) { + var cursor = this.session.selection.getCursor(); + cursor.column = 0; + pos = this.$cursorLayer.getPixelPosition(cursor, true); + height *= this.session.getRowLength(cursor.row); + } + this.$gutterLineHighlight.style.top = pos.top - this.layerConfig.offset + "px"; + this.$gutterLineHighlight.style.height = height + "px"; }; this.$updatePrintMargin = function() { @@ -632,6 +640,7 @@ var VirtualRenderer = function(container, theme) { if (!force && (!changes || !this.session || !this.container.offsetWidth)) return; + this._signal("beforeRender"); // text, scrolling and resize changes can cause the view port size to change if (changes & this.CHANGE_FULL || changes & this.CHANGE_SIZE || @@ -666,12 +675,12 @@ var VirtualRenderer = function(container, theme) { this.$cursorLayer.update(this.layerConfig); this.$moveTextAreaToCursor(); this.$highlightGutterLine && this.$updateGutterLineHighlight(); + this._signal("afterRender"); return; } // scrolling if (changes & this.CHANGE_SCROLL) { - this.$updateScrollBar(); if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES) this.$textLayer.update(this.layerConfig); else @@ -682,8 +691,10 @@ var VirtualRenderer = function(container, theme) { this.$markerBack.update(this.layerConfig); this.$markerFront.update(this.layerConfig); this.$cursorLayer.update(this.layerConfig); - this.$moveTextAreaToCursor(); this.$highlightGutterLine && this.$updateGutterLineHighlight(); + this.$moveTextAreaToCursor(); + this.$updateScrollBar(); + this._signal("afterRender"); return; } @@ -717,6 +728,8 @@ var VirtualRenderer = function(container, theme) { if (changes & this.CHANGE_SIZE) this.$updateScrollBar(); + + this._signal("afterRender"); }; this.$computeLayerConfig = function() { @@ -1122,9 +1135,9 @@ var VirtualRenderer = function(container, theme) { * @returns {Boolean} **/ this.isScrollableBy = function(deltaX, deltaY) { - if (deltaY < 0 && this.session.getScrollTop() > 0) + if (deltaY < 0 && this.session.getScrollTop() >= 1) return true; - if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight < this.layerConfig.maxHeight) + if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight - this.layerConfig.maxHeight < -1) return true; // todo: handle horizontal scrolling }; @@ -1231,10 +1244,6 @@ var VirtualRenderer = function(container, theme) { this.$composition = null; }; - this._loadTheme = function(name, callback) { - - }; - /** * [Sets a new theme for the editor. `theme` should exist, and be a directory path, like `ace/theme/textmate`.]{: #VirtualRenderer.setTheme} * @param {String} theme The path to a theme