diff --git a/lib/codemirror.css b/lib/codemirror.css index b962b38374..5ef92dfaf0 100644 --- a/lib/codemirror.css +++ b/lib/codemirror.css @@ -27,6 +27,10 @@ background-color: #f7f7f7; white-space: nowrap; } +.CodeMirror-rtl .CodeMirror-gutters { + border-right: none; + border-left: 1px solid #ddd; +} .CodeMirror-linenumbers {} .CodeMirror-linenumber { padding: 0 3px 0 5px; @@ -165,10 +169,22 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} outline: none; /* Prevent dragging from highlighting the element */ position: relative; } + +.CodeMirror-rtl .CodeMirror-scroll { + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-left: -30px; + margin-right: 0; +} + .CodeMirror-sizer { position: relative; border-right: 30px solid transparent; } +.CodeMirror-rtl .CodeMirror-sizer { + border-right: 0; + border-left: 30px solid transparent; +} /* The fake, visible scrollbars. Used to force redraw during scrolling before actual scrolling happens, thus preventing shaking and @@ -183,6 +199,9 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} overflow-x: hidden; overflow-y: scroll; } +.CodeMirror-rtl .CodeMirror-vscrollbar { + left: 0; right: initial; +} .CodeMirror-hscrollbar { bottom: 0; left: 0; overflow-y: hidden; @@ -191,15 +210,22 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} .CodeMirror-scrollbar-filler { right: 0; bottom: 0; } +.CodeMirror-rtl .CodeMirror-scrollbar-filler { + right: initial; left: 0; +} .CodeMirror-gutter-filler { left: 0; bottom: 0; } +.CodeMirror-rtl .CodeMirror-gutter-filler { + left: initial; right: 0; +} .CodeMirror-gutters { position: absolute; left: 0; top: 0; min-height: 100%; z-index: 3; } +.CodeMirror-rtl .CodeMirror-gutters { left: initial } .CodeMirror-gutter { white-space: normal; height: 100%; @@ -269,7 +295,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} .CodeMirror-widget {} -.CodeMirror-rtl pre { direction: rtl; } +.CodeMirror-rtl { direction: rtl; } .CodeMirror-code { outline: none; diff --git a/src/display/line_numbers.js b/src/display/line_numbers.js index c48f2204d5..01642ce842 100644 --- a/src/display/line_numbers.js +++ b/src/display/line_numbers.js @@ -9,21 +9,52 @@ import { updateGutterSpace } from "./update_display" export function alignHorizontally(cm) { let display = cm.display, view = display.view if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return - let comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft - let gutterW = display.gutters.offsetWidth, left = comp + "px" + let isLtr = cm.doc.direction == "ltr" + let scroll = display.scroller.scrollLeft - cm.doc.scrollLeft + let comp = compensateForHScroll(display, isLtr) + (isLtr ? -scroll : scroll) + let offset = comp + "px" + let side = isLtr ? "left" : "right" + let otherSide = isLtr ? "right" : "left" for (let i = 0; i < view.length; i++) if (!view[i].hidden) { if (cm.options.fixedGutter) { - if (view[i].gutter) - view[i].gutter.style.left = left - if (view[i].gutterBackground) - view[i].gutterBackground.style.left = left + if (view[i].gutter) { + view[i].gutter.style[side] = offset + view[i].gutter.style[otherSide] = null + } + if (view[i].gutterBackground) { + view[i].gutterBackground.style[side] = offset + view[i].gutterBackground.style[otherSide] = null + } } let align = view[i].alignable - if (align) for (let j = 0; j < align.length; j++) - align[j].style.left = left + if (align) for (let j = 0; j < align.length; j++) { + align[j].style[side] = offset + align[j].style[otherSide] = null + } } - if (cm.options.fixedGutter) - display.gutters.style.left = (comp + gutterW) + "px" + setGutterOffset(cm) +} + +function setGutterOffset(cm, fixed = cm.options.fixedGutter, alsoIfNotFixed = false) { + let isLtr = cm.doc.direction == "ltr" + let side = isLtr ? "left" : "right" + let display = cm.display + if (!fixed) { + if (alsoIfNotFixed) { + display.gutters.style[side] = "0" + display.gutters.style[isLtr ? "right" : "left"] = null + } + return + } + let scroll = display.scroller.scrollLeft - cm.doc.scrollLeft + let comp = compensateForHScroll(display, isLtr) + (isLtr ? -scroll : scroll) + let gutterW = display.gutters.offsetWidth + display.gutters.style[side] = (comp + gutterW) + "px" + display.gutters.style[isLtr ? "right" : "left"] = null +} + +export function updateFixedGutter(cm, val) { + setGutterOffset(cm, val, true) } // Used to ensure that the line number gutter is still the right diff --git a/src/display/operations.js b/src/display/operations.js index 6c4a0e1392..b3a9be5a3a 100644 --- a/src/display/operations.js +++ b/src/display/operations.js @@ -1,6 +1,7 @@ import { clipPos } from "../line/pos" import { findMaxLine } from "../line/spans" import { displayWidth, measureChar, scrollGap } from "../measurement/position_measurement" +import { gecko } from "../util/browser" import { signal } from "../util/event" import { activeElt } from "../util/dom" import { finishOperation, pushOperation } from "../util/operation_group" @@ -100,7 +101,7 @@ function endOperation_R2(op) { cm.display.sizerWidth = op.adjustWidthTo op.barMeasure.scrollWidth = Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth) - op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)) + op.maxScrollLeft = Math[gecko && cm.doc.direction == "rtl" ? "min" : "max"](0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)) } if (op.updatedDisplay || op.selectionChanged) diff --git a/src/display/scrollbars.js b/src/display/scrollbars.js index d03850be0f..cb5921ed21 100644 --- a/src/display/scrollbars.js +++ b/src/display/scrollbars.js @@ -65,8 +65,8 @@ class NativeScrollbars { if (needsH) { this.horiz.style.display = "block" - this.horiz.style.right = needsV ? sWidth + "px" : "0" - this.horiz.style.left = measure.barLeft + "px" + this.horiz.style[this.cm.doc.direction == "ltr" ? "right" : "left"] = needsV ? sWidth + "px" : "0" + this.horiz.style[this.cm.doc.direction == "ltr" ? "left" : "right"] = 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" @@ -150,7 +150,8 @@ 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[cm.doc.direction == "ltr" ? "paddingRight" : "paddingLeft"] = (d.barWidth = sizes.right) + "px" + d.sizer.style[cm.doc.direction == "ltr" ? "paddingLeft" : "paddingRight"] = 0 d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px" d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent" diff --git a/src/display/scrolling.js b/src/display/scrolling.js index b2deb2ed64..110a62c934 100644 --- a/src/display/scrolling.js +++ b/src/display/scrolling.js @@ -86,10 +86,11 @@ export function calculateScrollPos(cm, rect) { let screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0) let tooWide = rect.right - rect.left > screenw if (tooWide) rect.right = rect.left + screenw - if (rect.left < 10) + let rtl = cm.doc.direction == "rtl" + if (Math.abs(rect.left) < 10 || (rtl ? rect.left > 0 : rect.left < 0)) result.scrollLeft = 0 else if (rect.left < screenleft) - result.scrollLeft = Math.max(0, rect.left - (tooWide ? 0 : 10)) + result.scrollLeft = Math[rtl ? "min" : "max"](0, rect.left - (tooWide ? 0 : 10)) else if (rect.right > screenw + screenleft - 3) result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw return result diff --git a/src/display/selection.js b/src/display/selection.js index d9b69d4921..9b69e625b8 100644 --- a/src/display/selection.js +++ b/src/display/selection.js @@ -92,7 +92,7 @@ function drawSelectionRange(cm, range, output) { start = leftPos if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) end = rightPos - if (left < leftSide + 1) left = leftSide + if (left < leftSide + 1 && cm.doc.direction != "rtl") left = leftSide add(left, rightPos.top, right - left, rightPos.bottom) }) return {start: start, end: end} diff --git a/src/display/update_display.js b/src/display/update_display.js index 17d5a069e5..c4ee8fb8f6 100644 --- a/src/display/update_display.js +++ b/src/display/update_display.js @@ -49,7 +49,8 @@ export function maybeClipScrollbars(cm) { display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth display.heightForcer.style.height = scrollGap(cm) + "px" display.sizer.style.marginBottom = -display.nativeBarWidth + "px" - display.sizer.style.borderRightWidth = scrollGap(cm) + "px" + display.sizer.style[cm.doc.direction == "ltr" ? "borderLeftWidth" : "borderRightWidth"] = null + display.sizer.style[cm.doc.direction == "ltr" ? "borderRightWidth" : "borderLeftWidth"] = scrollGap(cm) + "px" display.scrollbarsClipped = true } } @@ -219,7 +220,8 @@ function patchDisplay(cm, updateNumbersFrom, dims) { export function updateGutterSpace(cm) { let width = cm.display.gutters.offsetWidth - cm.display.sizer.style.marginLeft = width + "px" + cm.display.sizer.style[cm.doc.direction == "ltr" ? "marginRight" : "marginLeft"] = null + cm.display.sizer.style[cm.doc.direction == "ltr" ? "marginLeft" : "marginRight"] = width + "px" } export function setDocumentHeight(cm, measure) { diff --git a/src/display/update_line.js b/src/display/update_line.js index 15a2394257..2db1bac459 100644 --- a/src/display/update_line.js +++ b/src/display/update_line.js @@ -93,17 +93,18 @@ function updateLineGutter(cm, lineView, lineN, dims) { lineView.node.removeChild(lineView.gutterBackground) lineView.gutterBackground = null } + let side = (cm.doc.direction == "ltr" ? "left" : "right") if (lineView.line.gutterClass) { let wrap = ensureLineWrapped(lineView) lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, - `left: ${cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth}px; width: ${dims.gutterTotalWidth}px`) + `${side}: ${cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth}px; width: ${dims.gutterTotalWidth}px`) cm.display.input.setUneditable(lineView.gutterBackground) wrap.insertBefore(lineView.gutterBackground, lineView.text) } let markers = lineView.line.gutterMarkers if (cm.options.lineNumbers || markers) { let wrap = ensureLineWrapped(lineView) - let gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", `left: ${cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth}px`) + let gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", `${side}: ${cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth}px`) cm.display.input.setUneditable(gutterWrap) wrap.insertBefore(gutterWrap, lineView.text) if (lineView.line.gutterClass) @@ -112,12 +113,12 @@ function updateLineGutter(cm, lineView, lineN, dims) { lineView.lineNumber = gutterWrap.appendChild( elt("div", lineNumberFor(cm.options, lineN), "CodeMirror-linenumber CodeMirror-gutter-elt", - `left: ${dims.gutterLeft["CodeMirror-linenumbers"]}px; width: ${cm.display.lineNumInnerWidth}px`)) + `${side}: ${dims.gutterLeft["CodeMirror-linenumbers"]}px; width: ${cm.display.lineNumInnerWidth}px`)) if (markers) for (let k = 0; k < cm.options.gutters.length; ++k) { let id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id] if (found) gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", - `left: ${dims.gutterLeft[id]}px; width: ${dims.gutterWidth[id]}px`)) + `${side}: ${dims.gutterLeft[id]}px; width: ${dims.gutterWidth[id]}px`)) } } } diff --git a/src/edit/mouse_events.js b/src/edit/mouse_events.js index 52131bc5cd..e790ff8d6e 100644 --- a/src/edit/mouse_events.js +++ b/src/edit/mouse_events.js @@ -278,9 +278,10 @@ function leftButtonSelect(cm, e, start, type, addNew) { // handlers for the corresponding event. function gutterEvent(cm, e, type, prevent) { let mX, mY + let check = cm.doc.direction == "ltr" ? dom => mX >= Math.floor(dom.getBoundingClientRect().right) : dom => mX <= Math.floor(dom.getBoundingClientRect().left) try { mX = e.clientX; mY = e.clientY } catch(e) { return false } - if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false + if (check(cm.display.gutters)) return false if (prevent) e_preventDefault(e) let display = cm.display @@ -291,7 +292,7 @@ function gutterEvent(cm, e, type, prevent) { for (let i = 0; i < cm.options.gutters.length; ++i) { let g = display.gutters.childNodes[i] - if (g && g.getBoundingClientRect().right >= mX) { + if (g && check(g)) { let line = lineAtHeight(cm.doc, mY) let gutter = cm.options.gutters[i] signal(cm, type, cm, line, gutter, e) diff --git a/src/edit/options.js b/src/edit/options.js index 930c41dd21..fcedda143c 100644 --- a/src/edit/options.js +++ b/src/edit/options.js @@ -1,15 +1,16 @@ import { onBlur } from "../display/focus" import { setGuttersForLineNumbers, updateGutters } from "../display/gutters" -import { alignHorizontally } from "../display/line_numbers" +import { alignHorizontally, updateFixedGutter } from "../display/line_numbers" import { loadMode, resetModeState } from "../display/mode_state" import { initScrollbars, updateScrollbars } from "../display/scrollbars" +import { setScrollLeft } from "../display/scroll_events" import { updateSelection } from "../display/selection" import { regChange } from "../display/view_tracking" import { getKeyMap } from "../input/keymap" import { defaultSpecialCharPlaceholder } from "../line/line_data" import { Pos } from "../line/pos" import { findMaxLine } from "../line/spans" -import { clearCaches, compensateForHScroll, estimateLineHeights } from "../measurement/position_measurement" +import { clearCaches, estimateLineHeights } from "../measurement/position_measurement" import { replaceRange } from "../model/changes" import { mobile, windows } from "../util/browser" import { addClass, rmClass } from "../util/dom" @@ -99,7 +100,7 @@ export function defineOptions(CodeMirror) { guttersChanged(cm) }, true) option("fixedGutter", true, (cm, val) => { - cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0" + updateFixedGutter(cm, val) cm.refresh() }, true) option("coverGutterNextToScrollbar", false, cm => updateScrollbars(cm), true) @@ -153,7 +154,12 @@ export function defineOptions(CodeMirror) { option("tabindex", null, (cm, val) => cm.display.input.getField().tabIndex = val || "") option("autofocus", null) - option("direction", "ltr", (cm, val) => cm.doc.setDirection(val), true) + option("direction", "ltr", (cm, val) => { + cm.doc.setDirection(val) + alignHorizontally(cm) + setScrollLeft(cm, 0) + guttersChanged(cm) + }, true) } function guttersChanged(cm) { diff --git a/src/measurement/position_measurement.js b/src/measurement/position_measurement.js index 5fb6a6534b..c10ffcc548 100644 --- a/src/measurement/position_measurement.js +++ b/src/measurement/position_measurement.js @@ -419,7 +419,7 @@ export function coordsChar(cm, x, y) { let lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1 if (lineN > last) return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1) - if (x < 0) x = 0 + if (x < 0 && cm.doc.direction != "rtl") x = 0 let lineObj = getLine(doc, lineN) for (;;) { @@ -537,7 +537,7 @@ export function getDimensions(cm) { left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft width[cm.options.gutters[i]] = n.clientWidth } - return {fixedPos: compensateForHScroll(d), + return {fixedPos: compensateForHScroll(d, cm.doc.direction == "ltr"), gutterTotalWidth: d.gutters.offsetWidth, gutterLeft: left, gutterWidth: width, @@ -547,8 +547,10 @@ export function getDimensions(cm) { // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, // but using getBoundingClientRect to get a sub-pixel-accurate // result. -export function compensateForHScroll(display) { - return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left +export function compensateForHScroll(display, isLtr) { + let side = isLtr ? "left" : "right" + let diff = display.scroller.getBoundingClientRect()[side] - display.sizer.getBoundingClientRect()[side] + return isLtr ? diff : -diff } // Returns a function that estimates the height of a line, to use as diff --git a/src/model/document_data.js b/src/model/document_data.js index 7f6e3367d1..4501c2288b 100644 --- a/src/model/document_data.js +++ b/src/model/document_data.js @@ -100,7 +100,7 @@ export function attachDoc(cm, doc) { } function setDirectionClass(cm) { - ;(cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl") + ;(cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.wrapper, "CodeMirror-rtl") } export function directionChanged(cm) {