mirror of
https://github.com/atom/atom.git
synced 2026-01-14 17:38:03 -05:00
Render hidden input and handle focus and blur
This commit is contained in:
committed by
Antonio Scandurra
parent
9487c1cd00
commit
c52d66377f
@@ -151,6 +151,51 @@ describe('TextEditorComponent', () => {
|
||||
cursorNodes = Array.from(element.querySelectorAll('.cursor'))
|
||||
expect(cursorNodes.length).toBe(0)
|
||||
})
|
||||
|
||||
it('places the hidden input element at the location of the last cursor if it is visible', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 60, width: 120, rowsPerTile: 2})
|
||||
const {hiddenInput} = component.refs
|
||||
component.refs.scroller.scrollTop = 100
|
||||
component.refs.scroller.scrollLeft = 40
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
expect(component.getRenderedStartRow()).toBe(4)
|
||||
expect(component.getRenderedEndRow()).toBe(12)
|
||||
|
||||
// When out of view, the hidden input is positioned at 0, 0
|
||||
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
|
||||
console.log(hiddenInput.offsetParent);
|
||||
console.log(hiddenInput.offsetTop);
|
||||
expect(hiddenInput.offsetTop).toBe(0)
|
||||
expect(hiddenInput.offsetLeft).toBe(0)
|
||||
|
||||
// Otherwise it is positioned at the last cursor position
|
||||
editor.addCursorAtScreenPosition([7, 4])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(hiddenInput.getBoundingClientRect().top).toBe(clientTopForLine(component, 7))
|
||||
expect(Math.round(hiddenInput.getBoundingClientRect().left)).toBe(clientLeftForCharacter(component, 7, 4))
|
||||
})
|
||||
|
||||
it('focuses the hidden input elemnent and adds the is-focused class when focused', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {hiddenInput} = component.refs
|
||||
|
||||
expect(document.activeElement).not.toBe(hiddenInput)
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(hiddenInput)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(element.classList.contains('is-focused')).toBe(true)
|
||||
|
||||
element.focus() // focusing back to the element does not blur
|
||||
expect(document.activeElement).toBe(hiddenInput)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(element.classList.contains('is-focused')).toBe(true)
|
||||
|
||||
document.body.focus()
|
||||
expect(document.activeElement).not.toBe(hiddenInput)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(element.classList.contains('is-focused')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
function verifyCursorPosition (component, cursorNode, row, column) {
|
||||
@@ -180,10 +225,10 @@ function clientLeftForCharacter (component, row, column) {
|
||||
|
||||
function lineNodeForScreenRow (component, row) {
|
||||
const screenLine = component.getModel().screenLineForScreenRow(row)
|
||||
return component.lineNodesByScreenLine.get(screenLine)
|
||||
return component.lineNodesByScreenLineId.get(screenLine.id)
|
||||
}
|
||||
|
||||
function textNodesForScreenRow (component, row) {
|
||||
const screenLine = component.getModel().screenLineForScreenRow(row)
|
||||
return component.textNodesByScreenLine.get(screenLine)
|
||||
return component.textNodesByScreenLineId.get(screenLine.id)
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ class TextEditorComponent {
|
||||
this.measurements = null
|
||||
this.visible = false
|
||||
this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure
|
||||
this.horizontalPixelPositionsByScreenLine = new WeakMap() // Values are maps from column to horiontal pixel positions
|
||||
this.lineNodesByScreenLine = new WeakMap()
|
||||
this.textNodesByScreenLine = new WeakMap()
|
||||
this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions
|
||||
this.lineNodesByScreenLineId = new Map()
|
||||
this.textNodesByScreenLineId = new Map()
|
||||
this.cursorsToRender = []
|
||||
|
||||
if (this.props.model) this.observeModel()
|
||||
@@ -87,8 +87,18 @@ class TextEditorComponent {
|
||||
style = {contain: 'strict'}
|
||||
}
|
||||
|
||||
return $('atom-text-editor', {style},
|
||||
$.div({ref: 'scroller', onScroll: this.didScroll, className: 'scroll-view'},
|
||||
let className = 'editor'
|
||||
if (this.focused) {
|
||||
className += ' is-focused'
|
||||
}
|
||||
|
||||
return $('atom-text-editor', {
|
||||
className,
|
||||
style,
|
||||
tabIndex: -1,
|
||||
on: {focus: this.didFocus}
|
||||
},
|
||||
$.div({ref: 'scroller', on: {scroll: this.didScroll}, className: 'scroll-view'},
|
||||
$.div({
|
||||
style: {
|
||||
isolate: 'content',
|
||||
@@ -212,7 +222,7 @@ class TextEditorComponent {
|
||||
style.width = width
|
||||
style.height = height
|
||||
children = [
|
||||
this.renderCursors(width, height),
|
||||
this.renderCursorsAndInput(width, height),
|
||||
this.renderLineTiles(width, height)
|
||||
]
|
||||
} else {
|
||||
@@ -230,7 +240,7 @@ class TextEditorComponent {
|
||||
renderLineTiles (width, height) {
|
||||
if (!this.measurements) return []
|
||||
|
||||
const {lineNodesByScreenLine, textNodesByScreenLine} = this
|
||||
const {lineNodesByScreenLineId, textNodesByScreenLineId} = this
|
||||
|
||||
const firstTileStartRow = this.getFirstTileStartRow()
|
||||
const visibleTileCount = this.getVisibleTileCount()
|
||||
@@ -250,8 +260,8 @@ class TextEditorComponent {
|
||||
key: screenLine.id,
|
||||
screenLine,
|
||||
displayLayer,
|
||||
lineNodesByScreenLine,
|
||||
textNodesByScreenLine
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
}))
|
||||
if (screenLine === this.longestLineToMeasure) {
|
||||
this.longestLineToMeasure = null
|
||||
@@ -280,8 +290,8 @@ class TextEditorComponent {
|
||||
key: this.longestLineToMeasure.id,
|
||||
screenLine: this.longestLineToMeasure,
|
||||
displayLayer,
|
||||
lineNodesByScreenLine,
|
||||
textNodesByScreenLine
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
}))
|
||||
this.longestLineToMeasure = null
|
||||
}
|
||||
@@ -297,9 +307,23 @@ class TextEditorComponent {
|
||||
}, tileNodes)
|
||||
}
|
||||
|
||||
renderCursors (width, height) {
|
||||
renderCursorsAndInput (width, height) {
|
||||
const cursorHeight = this.measurements.lineHeight + 'px'
|
||||
|
||||
const children = [this.renderHiddenInput()]
|
||||
|
||||
for (let i = 0; i < this.cursorsToRender.length; i++) {
|
||||
const {pixelLeft, pixelTop, pixelWidth} = this.cursorsToRender[i]
|
||||
children.push($.div({
|
||||
className: 'cursor',
|
||||
style: {
|
||||
height: cursorHeight,
|
||||
width: pixelWidth + 'px',
|
||||
transform: `translate(${pixelLeft}px, ${pixelTop}px)`
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return $.div({
|
||||
key: 'cursors',
|
||||
className: 'cursors',
|
||||
@@ -308,18 +332,37 @@ class TextEditorComponent {
|
||||
contain: 'strict',
|
||||
width, height
|
||||
}
|
||||
},
|
||||
this.cursorsToRender.map(({pixelLeft, pixelTop, pixelWidth}) =>
|
||||
$.div({
|
||||
className: 'cursor',
|
||||
style: {
|
||||
height: cursorHeight,
|
||||
width: pixelWidth + 'px',
|
||||
transform: `translate(${pixelLeft}px, ${pixelTop}px)`
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
}, children)
|
||||
}
|
||||
|
||||
renderHiddenInput () {
|
||||
let top, left
|
||||
const hiddenInputState = this.getHiddenInputState()
|
||||
if (hiddenInputState) {
|
||||
top = hiddenInputState.pixelTop
|
||||
left = hiddenInputState.pixelLeft
|
||||
} else {
|
||||
top = 0
|
||||
left = 0
|
||||
}
|
||||
|
||||
return $.input({
|
||||
ref: 'hiddenInput',
|
||||
key: 'hiddenInput',
|
||||
className: 'hidden-input',
|
||||
on: {blur: this.didBlur},
|
||||
tabIndex: -1,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
width: '1px',
|
||||
height: this.measurements.lineHeight + 'px',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
opacity: 0,
|
||||
padding: 0,
|
||||
border: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
queryCursorsToRender () {
|
||||
@@ -330,10 +373,14 @@ class TextEditorComponent {
|
||||
this.getRenderedEndRow() - 1,
|
||||
]
|
||||
})
|
||||
const lastCursorMarker = model.getLastCursor().getMarker()
|
||||
|
||||
this.cursorsToRender.length = cursorMarkers.length
|
||||
this.lastCursorIndex = -1
|
||||
for (let i = 0; i < cursorMarkers.length; i++) {
|
||||
const screenPosition = cursorMarkers[i].getHeadScreenPosition()
|
||||
const cursorMarker = cursorMarkers[i]
|
||||
if (cursorMarker === lastCursorMarker) this.lastCursorIndex = i
|
||||
const screenPosition = cursorMarker.getHeadScreenPosition()
|
||||
const {row, column} = screenPosition
|
||||
this.requestHorizontalMeasurement(row, column)
|
||||
let columnWidth = 0
|
||||
@@ -367,6 +414,12 @@ class TextEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
getHiddenInputState () {
|
||||
if (this.lastCursorIndex >= 0) {
|
||||
return this.cursorsToRender[this.lastCursorIndex]
|
||||
}
|
||||
}
|
||||
|
||||
didAttach () {
|
||||
this.intersectionObserver = new IntersectionObserver((entries) => {
|
||||
const {intersectionRect} = entries[entries.length - 1]
|
||||
@@ -396,6 +449,37 @@ class TextEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
didFocus () {
|
||||
const {hiddenInput} = this.refs
|
||||
|
||||
// Ensure the input is in the visible part of the scrolled content to avoid
|
||||
// the browser trying to auto-scroll to the form-field.
|
||||
hiddenInput.style.top = this.measurements.scrollTop + 'px'
|
||||
hiddenInput.style.left = this.measurements.scrollLeft + 'px'
|
||||
|
||||
hiddenInput.focus()
|
||||
this.focused = true
|
||||
|
||||
// Restore the previous position of the field now that it is focused.
|
||||
const currentHiddenInputState = this.getHiddenInputState()
|
||||
if (currentHiddenInputState) {
|
||||
hiddenInput.style.top = currentHiddenInputState.pixelTop + 'px'
|
||||
hiddenInput.style.left = currentHiddenInputState.pixelLeft + 'px'
|
||||
} else {
|
||||
hiddenInput.style.top = 0
|
||||
hiddenInput.style.left = 0
|
||||
}
|
||||
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
|
||||
didBlur (event) {
|
||||
if (this.element !== event.relatedTarget && !this.element.contains(event.relatedTarget)) {
|
||||
this.focused = false
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
didScroll () {
|
||||
this.measureScrollPosition()
|
||||
this.updateSync()
|
||||
@@ -417,13 +501,18 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
measureEditorDimensions () {
|
||||
let dimensionsChanged = false
|
||||
const scrollerHeight = this.refs.scroller.offsetHeight
|
||||
const scrollerWidth = this.refs.scroller.offsetWidth
|
||||
if (scrollerHeight !== this.measurements.scrollerHeight) {
|
||||
this.measurements.scrollerHeight = this.refs.scroller.offsetHeight
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
this.measurements.scrollerHeight = scrollerHeight
|
||||
dimensionsChanged = true
|
||||
}
|
||||
if (scrollerWidth !== this.measurements.scrollerWidth) {
|
||||
this.measurements.scrollerWidth = scrollerWidth
|
||||
dimensionsChanged = true
|
||||
}
|
||||
return dimensionsChanged
|
||||
}
|
||||
|
||||
measureScrollPosition () {
|
||||
@@ -440,7 +529,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
measureLongestLineWidth (screenLine) {
|
||||
this.measurements.scrollWidth = this.lineNodesByScreenLine.get(screenLine).firstChild.offsetWidth
|
||||
this.measurements.scrollWidth = this.lineNodesByScreenLineId.get(screenLine.id).firstChild.offsetWidth
|
||||
}
|
||||
|
||||
measureGutterDimensions () {
|
||||
@@ -461,12 +550,12 @@ class TextEditorComponent {
|
||||
columnsToMeasure.sort((a, b) => a - b)
|
||||
|
||||
const screenLine = this.getModel().displayLayer.getScreenLine(row)
|
||||
const lineNode = this.lineNodesByScreenLine.get(screenLine)
|
||||
const textNodes = this.textNodesByScreenLine.get(screenLine)
|
||||
let positionsForLine = this.horizontalPixelPositionsByScreenLine.get(screenLine)
|
||||
const lineNode = this.lineNodesByScreenLineId.get(screenLine.id)
|
||||
const textNodes = this.textNodesByScreenLineId.get(screenLine.id)
|
||||
let positionsForLine = this.horizontalPixelPositionsByScreenLineId.get(screenLine.id)
|
||||
if (positionsForLine == null) {
|
||||
positionsForLine = new Map()
|
||||
this.horizontalPixelPositionsByScreenLine.set(screenLine, positionsForLine)
|
||||
this.horizontalPixelPositionsByScreenLineId.set(screenLine.id, positionsForLine)
|
||||
}
|
||||
|
||||
this.measureHorizontalPositionsOnLine(lineNode, textNodes, columnsToMeasure, positionsForLine)
|
||||
@@ -478,6 +567,8 @@ class TextEditorComponent {
|
||||
let textNodeStartColumn = 0
|
||||
let textNodesIndex = 0
|
||||
|
||||
if (!textNodes) debugger
|
||||
|
||||
columnLoop:
|
||||
for (let columnsIndex = 0; columnsIndex < columnsToMeasure.length; columnsIndex++) {
|
||||
while (textNodesIndex < textNodes.length) {
|
||||
@@ -519,8 +610,11 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
pixelLeftForScreenRowAndColumn (row, column) {
|
||||
if (column === 0) return 0
|
||||
const screenLine = this.getModel().displayLayer.getScreenLine(row)
|
||||
return this.horizontalPixelPositionsByScreenLine.get(screenLine).get(column)
|
||||
|
||||
if (!this.horizontalPixelPositionsByScreenLineId.has(screenLine.id)) debugger
|
||||
return this.horizontalPixelPositionsByScreenLineId.get(screenLine.id).get(column)
|
||||
}
|
||||
|
||||
getModel () {
|
||||
@@ -622,13 +716,15 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
class LineComponent {
|
||||
constructor ({displayLayer, screenLine, lineNodesByScreenLine, textNodesByScreenLine}) {
|
||||
constructor (props) {
|
||||
const {displayLayer, screenLine, lineNodesByScreenLineId, textNodesByScreenLineId} = props
|
||||
this.props = props
|
||||
this.element = document.createElement('div')
|
||||
this.element.classList.add('line')
|
||||
lineNodesByScreenLine.set(screenLine, this.element)
|
||||
lineNodesByScreenLineId.set(screenLine.id, this.element)
|
||||
|
||||
const textNodes = []
|
||||
textNodesByScreenLine.set(screenLine, textNodes)
|
||||
textNodesByScreenLineId.set(screenLine.id, textNodes)
|
||||
|
||||
const {lineText, tagCodes} = screenLine
|
||||
let startIndex = 0
|
||||
@@ -671,6 +767,11 @@ class LineComponent {
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
destroy () {
|
||||
this.props.lineNodesByScreenLineId.delete(this.props.screenLine.id)
|
||||
this.props.textNodesByScreenLineId.delete(this.props.screenLine.id)
|
||||
}
|
||||
}
|
||||
|
||||
const classNamesByScopeName = new Map()
|
||||
|
||||
@@ -146,17 +146,6 @@ atom-text-editor {
|
||||
box-shadow: inset 1px 0;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
|
||||
Reference in New Issue
Block a user