import { addClass, elt, rmClass } from "../util/dom.js" import { on } from "../util/event.js" import { scrollGap, paddingVert } from "../measurement/position_measurement.js" import { ie, ie_version, mac, mac_geMountainLion } from "../util/browser.js" import { updateHeightsInViewport } from "./update_lines.js" import { Delayed } from "../util/misc.js" import { setScrollLeft, updateScrollTop } from "./scrolling.js" // SCROLLBARS // Prepare DOM reads needed to update the scrollbars. Done in one // shot to minimize update/measure roundtrips. export function measureForScrollbars(cm) { let d = cm.display, gutterW = d.gutters.offsetWidth let docH = Math.round(cm.doc.height + paddingVert(cm.display)) return { 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 } } class NativeScrollbars { constructor(place, scroll, cm) { this.cm = cm let vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar") let horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar") vert.tabIndex = horiz.tabIndex = -1 place(vert); place(horiz) on(vert, "scroll", () => { if (vert.clientHeight) scroll(vert.scrollTop, "vertical") }) on(horiz, "scroll", () => { if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal") }) this.checkedZeroWidth = 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" } update(measure) { let needsH = measure.scrollWidth > measure.clientWidth + 1 let needsV = measure.scrollHeight > measure.clientHeight + 1 let sWidth = measure.nativeBarWidth if (needsV) { this.vert.style.display = "block" this.vert.style.bottom = needsH ? sWidth + "px" : "0" let 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" let totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0) this.horiz.firstChild.style.width = Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px" } else { this.horiz.style.display = "" this.horiz.firstChild.style.width = "0" } if (!this.checkedZeroWidth && measure.clientHeight > 0) { if (sWidth == 0) this.zeroWidthHack() this.checkedZeroWidth = true } return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} } setScrollLeft(pos) { if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz") } setScrollTop(pos) { if (this.vert.scrollTop != pos) this.vert.scrollTop = pos if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert, "vert") } zeroWidthHack() { let w = mac && !mac_geMountainLion ? "12px" : "18px" this.horiz.style.height = this.vert.style.width = w this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none" this.disableHoriz = new Delayed this.disableVert = new Delayed } enableZeroWidthBar(bar, delay, type) { bar.style.pointerEvents = "auto" function maybeDisable() { // To find out whether the scrollbar is still visible, we // check whether the element under the pixel in the bottom // right corner of the scrollbar box is the scrollbar box // itself (when the bar is still visible) or its filler child // (when the bar is hidden). If it is still visible, we keep // it enabled, if it's hidden, we disable pointer events. let box = bar.getBoundingClientRect() let elt = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1) if (elt != bar) bar.style.pointerEvents = "none" else delay.set(1000, maybeDisable) } delay.set(1000, maybeDisable) } clear() { let parent = this.horiz.parentNode parent.removeChild(this.horiz) parent.removeChild(this.vert) } } class NullScrollbars { update() { return {bottom: 0, right: 0} } setScrollLeft() {} setScrollTop() {} clear() {} } export function updateScrollbars(cm, measure) { if (!measure) measure = measureForScrollbars(cm) let startWidth = cm.display.barWidth, startHeight = cm.display.barHeight updateScrollbarsInner(cm, measure) for (let 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) { let d = cm.display let sizes = d.scrollbars.update(measure) d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px" d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px" d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent" if (sizes.right && sizes.bottom) { d.scrollbarFiller.style.display = "block" d.scrollbarFiller.style.height = sizes.bottom + "px" d.scrollbarFiller.style.width = sizes.right + "px" } else d.scrollbarFiller.style.display = "" if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { d.gutterFiller.style.display = "block" d.gutterFiller.style.height = sizes.bottom + "px" d.gutterFiller.style.width = measure.gutterWidth + "px" } else d.gutterFiller.style.display = "" } export let scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars} export 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 scrollbarModel[cm.options.scrollbarStyle](node => { cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller) // Prevent clicks in the scrollbars from killing focus on(node, "mousedown", () => { if (cm.state.focused) setTimeout(() => cm.display.input.focus(), 0) }) node.setAttribute("cm-not-content", "true") }, (pos, axis) => { if (axis == "horizontal") setScrollLeft(cm, pos) else updateScrollTop(cm, pos) }, cm) if (cm.display.scrollbars.addClass) addClass(cm.display.wrapper, cm.display.scrollbars.addClass) }