Skip to content

Commit ddea10f

Browse files
committed
Support gutters at the end side of the content
FEATURE: Gutters can now specify that they should be displayed after the content (which would be to the right in a left-to-right layout). Issue codemirror/dev#1577
1 parent c5e9086 commit ddea10f

File tree

2 files changed

+50
-12
lines changed

2 files changed

+50
-12
lines changed

src/gutter.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ interface GutterConfig {
7474
updateSpacer?: null | ((spacer: GutterMarker, update: ViewUpdate) => GutterMarker)
7575
/// Supply event handlers for DOM events on this gutter.
7676
domEventHandlers?: Handlers,
77+
/// By default, gutters are shown horizontally before the editor
78+
/// content (to the left in a left-to-right layout). Set this to
79+
/// `"after"` to show a gutter on the other side of the content.
80+
side?: "before" | "after"
7781
}
7882

7983
const defaults = {
@@ -86,7 +90,8 @@ const defaults = {
8690
lineMarkerChange: null,
8791
initialSpacer: null,
8892
updateSpacer: null,
89-
domEventHandlers: {}
93+
domEventHandlers: {},
94+
side: "before" as const
9095
}
9196

9297
const activeGutters = Facet.define<Required<GutterConfig>>()
@@ -120,18 +125,22 @@ export function gutters(config?: {fixed?: boolean}): Extension {
120125
const gutterView = ViewPlugin.fromClass(class {
121126
gutters: SingleGutterView[]
122127
dom: HTMLElement
128+
domAfter: HTMLElement | null = null
123129
fixed: boolean
124130
prevViewport: {from: number, to: number}
125131

126132
constructor(readonly view: EditorView) {
127133
this.prevViewport = view.viewport
128134
this.dom = document.createElement("div")
129-
this.dom.className = "cm-gutters"
135+
this.dom.className = "cm-gutters cm-gutters-before"
130136
this.dom.setAttribute("aria-hidden", "true")
131137
this.dom.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px"
132138
this.gutters = view.state.facet(activeGutters).map(conf => new SingleGutterView(view, conf))
133-
for (let gutter of this.gutters) this.dom.appendChild(gutter.dom)
134139
this.fixed = !view.state.facet(unfixGutters)
140+
for (let gutter of this.gutters) {
141+
if (gutter.config.side == "after") this.getDOMAfter().appendChild(gutter.dom)
142+
else this.dom.appendChild(gutter.dom)
143+
}
135144
if (this.fixed) {
136145
// FIXME IE11 fallback, which doesn't support position: sticky,
137146
// by using position: relative + event handlers that realign the
@@ -142,6 +151,18 @@ const gutterView = ViewPlugin.fromClass(class {
142151
view.scrollDOM.insertBefore(this.dom, view.contentDOM)
143152
}
144153

154+
getDOMAfter() {
155+
if (!this.domAfter) {
156+
this.domAfter = document.createElement("div")
157+
this.domAfter.className = "cm-gutters cm-gutters-after"
158+
this.domAfter.setAttribute("aria-hidden", "true")
159+
this.domAfter.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px"
160+
this.domAfter.style.position = this.fixed ? "sticky" : ""
161+
this.view.scrollDOM.appendChild(this.domAfter)
162+
}
163+
return this.domAfter
164+
}
165+
145166
update(update: ViewUpdate) {
146167
if (this.updateGutters(update)) {
147168
// Detach during sync when the viewport changed significantly
@@ -152,18 +173,24 @@ const gutterView = ViewPlugin.fromClass(class {
152173
this.syncGutters(vpOverlap < (vpB.to - vpB.from) * 0.8)
153174
}
154175
if (update.geometryChanged) {
155-
this.dom.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px"
176+
let min = (this.view.contentHeight / this.view.scaleY) + "px"
177+
this.dom.style.minHeight = min
178+
if (this.domAfter) this.domAfter.style.minHeight = min
156179
}
157180
if (this.view.state.facet(unfixGutters) != !this.fixed) {
158181
this.fixed = !this.fixed
159182
this.dom.style.position = this.fixed ? "sticky" : ""
183+
if (this.domAfter) this.domAfter.style.position = this.fixed ? "sticky" : ""
160184
}
161185
this.prevViewport = update.view.viewport
162186
}
163187

164188
syncGutters(detach: boolean) {
165189
let after = this.dom.nextSibling
166-
if (detach) this.dom.remove()
190+
if (detach) {
191+
this.dom.remove()
192+
if (this.domAfter) this.domAfter.remove()
193+
}
167194
let lineClasses = RangeSet.iter(this.view.state.facet(gutterLineClass), this.view.viewport.from)
168195
let classSet: GutterMarker[] = []
169196
let contexts = this.gutters.map(gutter => new UpdateContext(gutter, this.view.viewport, -this.view.documentPadding.top))
@@ -188,7 +215,10 @@ const gutterView = ViewPlugin.fromClass(class {
188215
}
189216
}
190217
for (let cx of contexts) cx.finish()
191-
if (detach) this.view.scrollDOM.insertBefore(this.dom, after)
218+
if (detach) {
219+
this.view.scrollDOM.insertBefore(this.dom, after)
220+
if (this.domAfter) this.view.scrollDOM.appendChild(this.domAfter)
221+
}
192222
}
193223

194224
updateGutters(update: ViewUpdate) {
@@ -214,7 +244,10 @@ const gutterView = ViewPlugin.fromClass(class {
214244
g.dom.remove()
215245
if (gutters.indexOf(g) < 0) g.destroy()
216246
}
217-
for (let g of gutters) this.dom.appendChild(g.dom)
247+
for (let g of gutters) {
248+
if (g.config.side == "after") this.getDOMAfter().appendChild(g.dom)
249+
else this.dom.appendChild(g.dom)
250+
}
218251
this.gutters = gutters
219252
}
220253
return change
@@ -223,14 +256,16 @@ const gutterView = ViewPlugin.fromClass(class {
223256
destroy() {
224257
for (let view of this.gutters) view.destroy()
225258
this.dom.remove()
259+
if (this.domAfter) this.domAfter.remove()
226260
}
227261
}, {
228262
provide: plugin => EditorView.scrollMargins.of(view => {
229263
let value = view.plugin(plugin)
230264
if (!value || value.gutters.length == 0 || !value.fixed) return null
265+
let before = value.dom.offsetWidth * view.scaleX, after = value.domAfter ? value.domAfter.offsetWidth * view.scaleX : 0
231266
return view.textDirection == Direction.LTR
232-
? {left: value.dom.offsetWidth * view.scaleX}
233-
: {right: value.dom.offsetWidth * view.scaleX}
267+
? {left: before, right: after}
268+
: {right: before, left: after}
234269
})
235270
})
236271

src/theme.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,17 @@ export const baseTheme = buildTheme("." + baseThemeID, {
164164
display: "flex",
165165
height: "100%",
166166
boxSizing: "border-box",
167-
insetInlineStart: 0,
168-
zIndex: 200
167+
zIndex: 200,
169168
},
169+
".cm-gutters-before": { insetInlineStart: 0 },
170+
".cm-gutters-after": { insetInlineEnd: 0 },
170171

171172
"&light .cm-gutters": {
172173
backgroundColor: "#f5f5f5",
173174
color: "#6c6c6c",
174-
borderRight: "1px solid #ddd"
175+
border: "0px solid #ddd",
176+
"&.cm-gutters-before": { borderRightWidth: "1px" },
177+
"&.cm-gutters-after": { borderLeftWidth: "1px" },
175178
},
176179

177180
"&dark .cm-gutters": {

0 commit comments

Comments
 (0)