Skip to content

Commit ee6c65e

Browse files
committed
shift+left/right to select a range of characters
1 parent 6896985 commit ee6c65e

File tree

3 files changed

+102
-27
lines changed

3 files changed

+102
-27
lines changed

editor-runtime/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In
3535
when (knownKey) {
3636
KnownKeys.ArrowLeft -> {
3737
if (end > 0) {
38-
editor.changeSelection(CaretSelection(layoutable, end - 1))
38+
if (event.modifiers.shift) {
39+
editor.changeSelection(CaretSelection(layoutable, start, end - 1))
40+
} else {
41+
editor.changeSelection(CaretSelection(layoutable, end - 1))
42+
}
3943
} else {
4044
val previous = layoutable.getSiblingsInText(next = false)
4145
.filterIsInstance<LayoutableCell>()
@@ -47,7 +51,11 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In
4751
}
4852
KnownKeys.ArrowRight -> {
4953
if (end < (layoutable.cell.getSelectableText()?.length ?: 0)) {
50-
editor.changeSelection(CaretSelection(layoutable, end + 1))
54+
if (event.modifiers.shift) {
55+
editor.changeSelection(CaretSelection(layoutable, start, end + 1))
56+
} else {
57+
editor.changeSelection(CaretSelection(layoutable, end + 1))
58+
}
5159
} else {
5260
val next = layoutable.getSiblingsInText(next = true)
5361
.filterIsInstance<LayoutableCell>()

editor-runtime/src/jsMain/kotlin/org/modelix/editor/DomUtils.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ data class Bounds(val x: Double, val y: Double, val width: Double, val height: D
1919
fun HTMLElement.getAbsoluteBounds(): Bounds {
2020
return getBoundingClientRect().toBounds()
2121
}
22+
23+
fun HTMLElement.setBounds(bounds: Bounds) {
24+
with(style) {
25+
left = "${bounds.x}px"
26+
top = "${bounds.y}px"
27+
width = "${bounds.width}px"
28+
height = "${bounds.height}px"
29+
}
30+
}
31+
2232
fun HTMLElement.getAbsoluteInnerBounds(): Bounds {
2333
return (getClientRects().asSequence().firstOrNull()?.toBounds() ?: ZERO_BOUNDS)
2434
}
@@ -47,6 +57,14 @@ fun Bounds.union(other: Bounds?): Bounds {
4757
return Bounds(minX, minY, maxX - minX, maxY - minY)
4858
}
4959

60+
fun Bounds.translated(deltaX: Double, deltaY: Double) = copy(x = x + deltaX, y = y + deltaY)
61+
fun Bounds.expanded(delta: Double) = copy(
62+
x = x - delta,
63+
y = y - delta,
64+
width = width + delta * 2.0,
65+
height = height + delta * 2.0
66+
)
67+
5068
private fun getBodyAbsoluteBounds() = document.body?.getBoundingClientRect()?.toBounds() ?: ZERO_BOUNDS
5169
fun MouseEvent.getAbsolutePositionX() = clientX - getBodyAbsoluteBounds().x
5270
fun MouseEvent.getAbsolutePositionY() = clientY - getBodyAbsoluteBounds().y

editor-runtime/src/jsMain/kotlin/org/modelix/editor/JSCaretSelectionView.kt

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,96 @@ package org.modelix.editor
1616
import kotlinx.html.TagConsumer
1717
import kotlinx.html.classes
1818
import kotlinx.html.div
19+
import kotlinx.html.style
1920
import org.w3c.dom.HTMLElement
21+
import org.w3c.dom.asList
22+
import kotlin.math.max
23+
import kotlin.math.min
2024

2125
class JSCaretSelectionView(selection: CaretSelection, val editor: JsEditorComponent) : SelectionView<CaretSelection>(selection) {
2226

27+
private fun hasRange() = selection.start != selection.end
28+
2329
override fun <T> produceHtml(consumer: TagConsumer<T>) {
24-
consumer.div("caret own") {
25-
val textLength = selection.layoutable.cell.getVisibleText()?.length ?: 0
26-
if (textLength == 0) {
27-
// A typical case is a StringLiteral editor for an empty string.
28-
// There is no space around the empty text cell.
29-
// 'leftend' or 'rightend' styles would look like the caret is set into one of the '"' cells.
30-
} else if (selection.end == 0) {
31-
classes += "leftend"
32-
} else if (selection.end == textLength) {
33-
classes += "rightend"
30+
with(consumer) {
31+
div("caret-selection") {
32+
style = "position: absolute"
33+
if (hasRange()) {
34+
div("selected-word") {
35+
style = "position: absolute; background-color:hsla(196, 67%, 45%, 0.3)"
36+
}
37+
}
38+
div("caret own") {
39+
style = "position: absolute"
40+
val textLength = selection.layoutable.cell.getVisibleText()?.length ?: 0
41+
if (textLength == 0) {
42+
// A typical case is a StringLiteral editor for an empty string.
43+
// There is no space around the empty text cell.
44+
// 'leftend' or 'rightend' styles would look like the caret is set into one of the '"' cells.
45+
} else if (selection.end == 0) {
46+
classes += "leftend"
47+
} else if (selection.end == textLength) {
48+
classes += "rightend"
49+
}
50+
}
3451
}
3552
}
3653
}
3754

3855
override fun update() {
39-
val layoutable = selection.layoutable
40-
val textElement = GeneratedHtmlMap.getOutput(selection.layoutable) ?: return
41-
val caretElement = GeneratedHtmlMap.getOutput(this) ?: return
42-
updateCaretBounds(textElement, selection.end, editor.getMainLayer(), caretElement)
56+
val textDom = GeneratedHtmlMap.getOutput(selection.layoutable) ?: return
57+
val mainLayerBounds = editor.getMainLayer()?.getAbsoluteBounds() ?: ZERO_BOUNDS
58+
val textBoundsUtil = TextBoundsUtil(textDom)
59+
val selectionDom = GeneratedHtmlMap.getOutput(this) ?: return
60+
val selectionBounds = textBoundsUtil.getTextBounds().expanded(1.0)
61+
selectionDom.setBounds(selectionBounds.relativeTo(mainLayerBounds))
62+
val caretDom = selectionDom.childNodes.asList().filterIsInstance<HTMLElement>().lastOrNull() ?: return
63+
updateCaretBounds(textDom, selection.end, selectionBounds, caretDom)
64+
65+
if (hasRange()) {
66+
val rangeDom = selectionDom.childNodes.asList().filterIsInstance<HTMLElement>().firstOrNull() ?: return
67+
val minPos = min(selection.start, selection.end)
68+
val maxPos = max(selection.start, selection.end)
69+
val substringBounds = textBoundsUtil.getSubstringBounds(minPos until maxPos)
70+
rangeDom.setBounds(substringBounds.relativeTo(selectionBounds))
71+
}
4372
}
4473

4574
companion object {
46-
fun updateCaretBounds(textElement: HTMLElement, caretPos: Int, coordinatesElement: HTMLElement?, caretElement: HTMLElement) {
47-
val text = textElement.innerText
48-
val textLength = text.length
49-
val cellAbsoluteBounds = textElement.getAbsoluteInnerBounds()
50-
val cellRelativeBounds = cellAbsoluteBounds.relativeTo(coordinatesElement?.getAbsoluteBounds() ?: ZERO_BOUNDS)
51-
val characterWidth = if (textLength == 0) 0.0 else cellAbsoluteBounds.width / textLength
52-
val caretX = cellRelativeBounds.x + caretPos * characterWidth
75+
fun updateCaretBounds(textElement: HTMLElement, caretPos: Int, coordinatesElement: HTMLElement?, caretDom: HTMLElement) {
76+
updateCaretBounds(textElement, caretPos, coordinatesElement?.getAbsoluteBounds() ?: ZERO_BOUNDS, caretDom)
77+
}
78+
79+
fun updateCaretBounds(textElement: HTMLElement, caretPos: Int, relativeTo: Bounds, caretDom: HTMLElement) {
80+
val textBoundsUtil = TextBoundsUtil(textElement, relativeTo)
81+
val textBounds = textBoundsUtil.getTextBounds()
82+
val text = textBoundsUtil.getText()
5383
val leftEnd = caretPos == 0
54-
val rightEnd = caretPos == textLength
84+
val rightEnd = caretPos == text.length
5585
val caretOffsetX = if (rightEnd && !leftEnd) -4 else -1
5686
val caretOffsetY = if (leftEnd || rightEnd) -1 else 0
57-
caretElement.style.height = "${cellRelativeBounds.height}px"
58-
caretElement.style.left = "${caretX + caretOffsetX}px"
59-
caretElement.style.top = "${cellRelativeBounds.y + caretOffsetY}px"
87+
caretDom.style.height = "${textBounds.height}px"
88+
caretDom.style.left = "${textBoundsUtil.getCaretX(caretPos) + caretOffsetX}px"
89+
caretDom.style.top = "${textBounds.y + caretOffsetY}px"
6090
}
6191
}
92+
}
93+
94+
private class TextBoundsUtil(val dom: HTMLElement, val relativeTo: Bounds = ZERO_BOUNDS) {
95+
fun getText(): String = dom.innerText
96+
fun getTextLength() = getText().length
97+
fun getTextBounds() = dom.getAbsoluteInnerBounds().relativeTo(relativeTo)
98+
fun getTextWidth() = getTextBounds().width
99+
fun getTextHeight() = getTextBounds().height
100+
fun getCharWidth() = getTextWidth() / getTextLength()
101+
fun getCaretX(pos: Int) = getTextBounds().let {
102+
val charWidth = it.width / getTextLength()
103+
it.x + pos * charWidth
104+
}
105+
fun getSubstringBounds(range: IntRange) = getTextBounds().let {
106+
val charWidth = it.width / getTextLength()
107+
val minX = it.x + range.first * charWidth
108+
val maxX = it.x + (range.last + 1) * charWidth
109+
it.copy(x = minX, width = maxX - minX)
110+
}
62111
}

0 commit comments

Comments
 (0)