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