@@ -16,47 +16,96 @@ package org.modelix.editor
16
16
import kotlinx.html.TagConsumer
17
17
import kotlinx.html.classes
18
18
import kotlinx.html.div
19
+ import kotlinx.html.style
19
20
import org.w3c.dom.HTMLElement
21
+ import org.w3c.dom.asList
22
+ import kotlin.math.max
23
+ import kotlin.math.min
20
24
21
25
class JSCaretSelectionView (selection : CaretSelection , val editor : JsEditorComponent ) : SelectionView<CaretSelection>(selection) {
22
26
27
+ private fun hasRange () = selection.start != selection.end
28
+
23
29
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
+ }
34
51
}
35
52
}
36
53
}
37
54
38
55
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
+ }
43
72
}
44
73
45
74
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()
53
83
val leftEnd = caretPos == 0
54
- val rightEnd = caretPos == textLength
84
+ val rightEnd = caretPos == text.length
55
85
val caretOffsetX = if (rightEnd && ! leftEnd) - 4 else - 1
56
86
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"
60
90
}
61
91
}
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
+ }
62
111
}
0 commit comments