diff --git a/doc/manual.html b/doc/manual.html index 42fc03ebbf73eb413da50c980632c8fc8b2a4182..3d5ad6e69285eea3f558c49a47cec616c417b3ec 100644 --- a/doc/manual.html +++ b/doc/manual.html @@ -318,6 +318,14 @@ horizontally (false) or whether it stays fixed during horizontal scrolling (true, the default).</dd> + <dt id="option_scrollbarStyle"><code><strong>scrollbarStyle</strong>: string</code></dt> + <dd>Chooses a scrollbar implementation. The default + is <code>"native"</code>, showing native scrollbars. The core + library also provides the <code>"null"</code> style, which + completely hides the + scrollbars. <a href="addon_simplescrollbars">Addons</a> can + implement additional scrollbar models.</dd> + <dt id="option_coverGutterNextToScrollbar"><code><strong>coverGutterNextToScrollbar</strong>: boolean</code></dt> <dd>When <a href="#option_fixedGutter"><code>fixedGutter</code></a> is on, and there is a horizontal scrollbar, by default the diff --git a/lib/codemirror.js b/lib/codemirror.js index 591f992964611c270074b70d81dca80075507328..cee9abccdd7d5091d0c7430c1eddd796a637846f 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -77,6 +77,7 @@ if (options.lineWrapping) this.display.wrapper.className += " CodeMirror-wrap"; if (options.autofocus && !mobile) focusInput(this); + initScrollbars(this); this.state = { keyMaps: [], // stores maps added by addKeyMap @@ -137,14 +138,13 @@ // Wraps and hides input textarea d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); - // The fake scrollbar elements. - d.scrollbarH = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); - d.scrollbarV = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); // Covers bottom-right square when both scrollbars are present. d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("not-content", "true"); // Covers bottom of gutter when coverGutterNextToScrollbar is on // and h scrollbar is present. d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("not-content", "true"); // Will contain the actual code, positioned to cover the viewport. d.lineDiv = elt("div", null, "CodeMirror-code"); // Elements are added to these to represent selection and cursors. @@ -172,8 +172,7 @@ d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); d.scroller.setAttribute("tabIndex", "-1"); // The element in which the editor lives. - d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV, - d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + d.wrapper = elt("div", [d.inputDiv, d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } @@ -182,8 +181,6 @@ if (!webkit) d.scroller.draggable = true; // Needed to handle Tab key in KHTML if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } - // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). - if (ie && ie_version < 8) d.scrollbarH.style.minHeight = d.scrollbarV.style.minWidth = "18px"; if (place) { if (place.appendChild) place.appendChild(d.wrapper); @@ -339,7 +336,6 @@ function updateGutterSpace(cm) { var width = cm.display.gutters.offsetWidth; cm.display.sizer.style.marginLeft = width + "px"; - cm.display.scrollbarH.style.left = cm.options.fixedGutter ? width + "px" : 0; } // Compute the character length of a line, taking into account @@ -395,83 +391,163 @@ // Prepare DOM reads needed to update the scrollbars. Done in one // shot to minimize update/measure roundtrips. function measureForScrollbars(cm) { - var scroll = cm.display.scroller; + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); return { - clientHeight: scroll.clientHeight, - barHeight: cm.display.scrollbarV.clientHeight, - scrollWidth: scroll.scrollWidth, clientWidth: scroll.clientWidth, - barWidth: cm.display.scrollbarH.clientWidth, - docHeight: Math.round(cm.doc.height + paddingVert(cm.display)) + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW }; } + function NativeScrollbars(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + place(vert); place(horiz); + + on(vert, "scroll", function() { + if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); + }); + on(horiz, "scroll", function() { + if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); + }); + + this.checkedOverlay = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; + } + + NativeScrollbars.prototype = copyObj({ + update: function(measure) { // FIXME move left positioning into this + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedOverlay && measure.clientHeight > 0) { + if (sWidth == 0) this.overlayHack(); + this.checkedOverlay = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; + }, + setScrollLeft: function(pos) { + if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; + }, + setScrollTop: function(pos) { + if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; + }, + overlayHack: function() { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.minWidth = this.vert.style.minHeight = w; + var self = this; + var barMouseDown = function(e) { + if (e_target(e) != self.vert && e_target(e) != self.horiz) + operation(self.cm, onMouseDown)(e); + }; + on(this.vert, "mousedown", barMouseDown); + on(this.horiz, "mousedown", barMouseDown); + }, + clear: function() { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + } + }, NativeScrollbars.prototype); + + function NullScrollbars() {} + + NullScrollbars.prototype = copyObj({ + update: function() { return {bottom: 0, right: 0}; }, + setScrollLeft: function() {}, + setScrollTop: function() {}, + clear: function() {} + }, NullScrollbars.prototype); + + CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + on(node, "mousedown", function() { + if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); + }); + node.setAttribute("not-content", "true"); + }, function(pos, axis) { + if (axis == "horizontal") setScrollLeft(cm, pos); + else setScrollTop(cm, pos); + }, cm); + if (cm.display.scrollbars.addClass) + addClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + function updateScrollbars(cm, measure) { if (!measure) measure = measureForScrollbars(cm); var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; updateScrollbarsInner(cm, measure); - // FIXME optimize by somehow detecting when the situation is okay? - // (Revisit after implementing custom scrollbars) - if (startWidth != cm.display.barWidth || startHeight != cm.display.barHeight) { + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { if (startWidth != cm.display.barWidth && cm.options.lineWrapping) updateHeightsInViewport(cm); updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; } } // Re-synchronize the fake scrollbars with the actual size of the // content. function updateScrollbarsInner(cm, measure) { - var d = cm.display, sWidth = scrollbarWidth(d.measure); - var needsH = measure.scrollWidth > measure.clientWidth; - var scrollHeight = measure.docHeight + scrollGap(cm) + d.barHeight; - var needsV = scrollHeight > measure.clientHeight; - - if (needsV) { - d.scrollbarV.style.display = "block"; - d.scrollbarV.style.bottom = needsH ? sWidth + "px" : "0"; - // A bug in IE8 can cause this value to be negative, so guard it. - d.scrollbarV.firstChild.style.height = - Math.max(0, scrollHeight - measure.clientHeight + (measure.barHeight || d.scrollbarV.clientHeight)) + "px"; - } else { - d.scrollbarV.style.display = ""; - d.scrollbarV.firstChild.style.height = "0"; - } - d.sizer.style.paddingRight = (d.barWidth = needsV ? sWidth : 0) + "px"; + var d = cm.display; + var sizes = d.scrollbars.update(measure); - if (needsH) { - d.scrollbarH.style.display = "block"; - d.scrollbarH.style.right = needsV ? sWidth + "px" : "0"; - d.scrollbarH.firstChild.style.width = - (measure.scrollWidth - measure.clientWidth + (measure.barWidth || d.scrollbarH.clientWidth)) + "px"; - } else { - d.scrollbarH.style.display = ""; - d.scrollbarH.firstChild.style.width = "0"; - } - d.sizer.style.paddingBottom = (d.barHeight = needsH ? sWidth : 0) + "px"; + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; - if (needsH && needsV) { + if (sizes.right && sizes.bottom) { d.scrollbarFiller.style.display = "block"; - d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = sWidth + "px"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; } else d.scrollbarFiller.style.display = ""; - if (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { d.gutterFiller.style.display = "block"; - d.gutterFiller.style.height = sWidth + "px"; - d.gutterFiller.style.width = d.gutters.offsetWidth + "px"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; } else d.gutterFiller.style.display = ""; - - if (!cm.state.checkedOverlayScrollbar && measure.clientHeight > 0) { - if (sWidth === 0) { - var w = mac && !mac_geMountainLion ? "12px" : "18px"; - d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = w; - var barMouseDown = function(e) { - if (e_target(e) != d.scrollbarV && e_target(e) != d.scrollbarH) - operation(cm, onMouseDown)(e); - }; - on(d.scrollbarV, "mousedown", barMouseDown); - on(d.scrollbarH, "mousedown", barMouseDown); - } - cm.state.checkedOverlayScrollbar = true; - } } // Compute the lines that are visible in a given viewport (defaults @@ -2113,12 +2189,14 @@ // Propagate the scroll position to the actual DOM scroller if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { - var top = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); - display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = top; + doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); + display.scrollbars.setScrollTop(doc.scrollTop); + display.scroller.scrollTop = doc.scrollTop; } if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { - var left = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); - display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = left; + doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); + display.scrollbars.setScrollLeft(doc.scrollLeft); + display.scroller.scrollLeft = doc.scrollLeft; alignHorizontally(cm); } // If we need to scroll a specific position into view, do so. @@ -2563,21 +2641,11 @@ signal(cm, "scroll", cm); } }); - on(d.scrollbarV, "scroll", function() { - if (d.scroller.clientHeight) setScrollTop(cm, d.scrollbarV.scrollTop); - }); - on(d.scrollbarH, "scroll", function() { - if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft); - }); // Listen to wheel events in order to try and update the viewport on time. on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); - // Prevent clicks in the scrollbars from killing focus - function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); } - on(d.scrollbarH, "mousedown", reFocus); - on(d.scrollbarV, "mousedown", reFocus); // Prevent wrapper from ever scrolling on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); @@ -2670,6 +2738,7 @@ return; // Might be a text scaling operation, clear size caches. d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; cm.setSize(); } @@ -2689,11 +2758,8 @@ // coordinates beyond the right of the text. function posFromMouse(cm, e, liberal, forRect) { var display = cm.display; - if (!liberal) { - var target = e_target(e); - if (target == display.scrollbarH || target == display.scrollbarV || - target == display.scrollbarFiller || target == display.gutterFiller) return null; - } + if (!liberal && e_target(e).getAttribute("not-content") == "true") return null; + var x, y, space = display.lineSpace.getBoundingClientRect(); // Fails unpredictably on IE[67] when mouse is dragged around quickly. try { x = e.clientX - space.left; y = e.clientY - space.top; } @@ -3051,7 +3117,7 @@ cm.doc.scrollTop = val; if (!gecko) updateDisplaySimple(cm, {top: val}); if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; - if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); if (gecko) updateDisplaySimple(cm); startWorker(cm, 100); } @@ -3063,7 +3129,7 @@ cm.doc.scrollLeft = val; alignHorizontally(cm); if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; - if (cm.display.scrollbarH.scrollLeft != val) cm.display.scrollbarH.scrollLeft = val; + cm.display.scrollbars.setScrollLeft(val); } // Since the delta values reported on mouse wheel events are @@ -3382,7 +3448,7 @@ function rehide() { display.inputDiv.style.position = "relative"; display.input.style.cssText = oldCSS; - if (ie && ie_version < 9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos; + if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); slowPoll(cm); // Try to detect the user choosing select-all @@ -4536,7 +4602,13 @@ cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; cm.refresh(); }, true); - option("coverGutterNextToScrollbar", false, updateScrollbars, true); + option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); + option("scrollbarStyle", "native", function(cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); option("lineNumbers", false, function(cm) { setGuttersForLineNumbers(cm.options); guttersChanged(cm); @@ -7519,7 +7591,6 @@ on(window, "resize", function() { if (resizeTimer == null) resizeTimer = setTimeout(function() { resizeTimer = null; - knownScrollbarWidth = null; forEachCodeMirror(onResize); }, 100); }); @@ -7540,16 +7611,6 @@ return "draggable" in div || "dragDrop" in div; }(); - var knownScrollbarWidth; - function scrollbarWidth(measure) { - if (knownScrollbarWidth != null) return knownScrollbarWidth; - var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll"); - removeChildrenAndAdd(measure, test); - if (test.offsetWidth) - knownScrollbarWidth = test.offsetHeight - test.clientHeight; - return knownScrollbarWidth || 0; - } - var zwspSupported; function zeroWidthElement(measure) { if (zwspSupported == null) {