mirror of https://github.com/sunface/rust-course
parent
63752a0e79
commit
a53ffd4fde
@ -0,0 +1,63 @@
|
||||
import useCaretPosition from 'components/markdown-editor/position'
|
||||
import TestStyles from 'theme/caret.styles'
|
||||
import React, { useRef, useState, useEffect, Fragment } from 'react'
|
||||
import { render } from 'react-dom'
|
||||
|
||||
|
||||
const App = () => {
|
||||
const triggerRef = useRef(null)
|
||||
const [showTrigger, setShowTrigger] = useState(false)
|
||||
const {
|
||||
x: triggerX,
|
||||
y: triggerY,
|
||||
getPosition: getPositionTrigger,
|
||||
} = useCaretPosition(triggerRef)
|
||||
|
||||
const handleCustomUI = (e) => {
|
||||
const previousCharacter = e.target.value
|
||||
.charAt(triggerRef.current.selectionStart - 2)
|
||||
.trim()
|
||||
const character = e.target.value
|
||||
.charAt(triggerRef.current.selectionStart - 1)
|
||||
.trim()
|
||||
if (character === '@' && previousCharacter === '') {
|
||||
setShowTrigger(true)
|
||||
}
|
||||
if (character === '' && showTrigger) {
|
||||
setShowTrigger(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerRef.current) {
|
||||
getPositionTrigger(triggerRef)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<TestStyles />
|
||||
<section>
|
||||
<textarea
|
||||
ref={triggerRef}
|
||||
placeholder="Type the @ symbol to trigger UI"
|
||||
spellCheck="false"
|
||||
onKeyUp={handleCustomUI}
|
||||
onInput={() => getPositionTrigger(triggerRef)}
|
||||
/>
|
||||
<span
|
||||
className="marker marker--trigger"
|
||||
style={{
|
||||
display: showTrigger ? 'block' : 'none',
|
||||
//@ts-ignore
|
||||
'--y': triggerY,
|
||||
'--x': triggerX,
|
||||
}}>
|
||||
Triggered UI! <span role="img">😎</span>
|
||||
</span>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* returns x, y coordinates for absolute positioning of a span within a given text input
|
||||
* at a given selection point
|
||||
* @param {object} input - the input element to obtain coordinates for
|
||||
* @param {number} selectionPoint - the selection point for the input
|
||||
*/
|
||||
const getCaretPosition = (input, selection = 'selectionStart') => {
|
||||
const { scrollLeft, scrollTop } = input
|
||||
// This provides a hook for getSelection to reuse getCaretPosition.
|
||||
const selectionPoint = input[selection] || input.selectionStart
|
||||
const { height, width, left, top } = input.getBoundingClientRect()
|
||||
// create a dummy element that will be a clone of our input
|
||||
const div = document.createElement('div')
|
||||
// get the computed style of the input and clone it onto the dummy element
|
||||
const copyStyle = getComputedStyle(input)
|
||||
for (const prop of copyStyle) {
|
||||
div.style[prop] = copyStyle[prop]
|
||||
}
|
||||
// we need a character that will replace whitespace when filling our dummy element if it's a single line <input/>
|
||||
const swap = '.'
|
||||
const inputValue =
|
||||
input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value
|
||||
// set the div content to that of the textarea up until selection
|
||||
const textContent = inputValue.substr(0, selectionPoint)
|
||||
// set the text content of the dummy element div
|
||||
div.textContent = textContent
|
||||
if (input.tagName === 'TEXTAREA') div.style.height = 'auto'
|
||||
// if a single line input then the div needs to be single line and not break out like a text area
|
||||
if (input.tagName === 'INPUT') div.style.width = 'auto'
|
||||
// Apply absolute positioning to account for textarea resize, etc.
|
||||
div.style.position = 'absolute'
|
||||
// create a marker element to obtain caret position
|
||||
const span = document.createElement('span')
|
||||
// give the span the textContent of remaining content so that the recreated dummy element is as close as possible
|
||||
span.textContent = inputValue.substr(selectionPoint) || '.'
|
||||
// append the span marker to the div
|
||||
div.appendChild(span)
|
||||
// append the dummy element to the body
|
||||
document.body.appendChild(div)
|
||||
// get the marker position, this is the caret position top and left relative to the input
|
||||
const { offsetLeft: spanX, offsetTop: spanY } = span
|
||||
// lastly, remove that dummy element
|
||||
// NOTE:: can comment this out for debugging purposes if you want to see where that span is rendered
|
||||
document.body.removeChild(div)
|
||||
// return an object with the x and y of the caret. account for input positioning so that you don't need to wrap the input
|
||||
let x = left + spanX
|
||||
let y = top + spanY
|
||||
const { lineHeight, paddingRight } = copyStyle
|
||||
x = Math.min(x - scrollLeft, left + width - parseInt(paddingRight, 10))
|
||||
// Need to account for any scroll position for the window.
|
||||
y = Math.min(y - scrollTop, top + height - parseInt(lineHeight, 10)) + window.scrollY
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
}
|
||||
}
|
||||
|
||||
const getSelectionPosition = (input) => {
|
||||
const { y: startY, x: startX } = getCaretPosition(input, 'selectionStart')
|
||||
const { x: endX } = getCaretPosition(input, 'selectionEnd')
|
||||
// Gives you a basic left position for where to put it and the starting position.
|
||||
const x = startX + ((endX - startX) / 2)
|
||||
const y = startY
|
||||
return {
|
||||
x, y
|
||||
}
|
||||
}
|
||||
|
||||
const useCaretPosition = (element) => {
|
||||
const [x, setX] = useState(null)
|
||||
const [y, setY] = useState(null)
|
||||
|
||||
const getPosition = (element) => {
|
||||
if (element.current) {
|
||||
const { x, y } = getCaretPosition(element.current)
|
||||
setX(x)
|
||||
setY(y)
|
||||
}
|
||||
}
|
||||
|
||||
const getSelection = (element) => {
|
||||
if (element.current) {
|
||||
const { x, y } = getSelectionPosition(element.current)
|
||||
setX(x)
|
||||
setY(y)
|
||||
}
|
||||
}
|
||||
|
||||
return { x, y, getPosition, getSelection }
|
||||
}
|
||||
|
||||
export default useCaretPosition
|
@ -0,0 +1,51 @@
|
||||
import { Global, css } from "@emotion/react"
|
||||
|
||||
const CaretStyles = () => (
|
||||
<Global
|
||||
styles={(theme: any) => css`
|
||||
.repo-link {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.repo-link path {
|
||||
fill: hsl(0, 0%, 10%);
|
||||
cursor: pointer;
|
||||
transition: fill 0.1s;
|
||||
}
|
||||
|
||||
.repo-link:hover path {
|
||||
fill: hsl(0, 0%, 40%);
|
||||
}
|
||||
|
||||
.marker {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: calc(var(--x, 0) * 1px);
|
||||
top: calc(var(--y, 0) * 1px);
|
||||
background: hsl(0, 0%, 10%);
|
||||
color: hsl(0, 0%, 98%);
|
||||
z-index: 9999;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
transform: translate(10px, -25%);
|
||||
transition: top 0.1s, left 0.1s;
|
||||
}
|
||||
|
||||
.marker--selection {
|
||||
transform: translate(-50%, -120%);
|
||||
}
|
||||
|
||||
/* For debugging purposes */
|
||||
textarea:focus ~ .marker--basic,
|
||||
textarea:focus ~ .marker--selection {
|
||||
display: block;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
)
|
||||
|
||||
export default CaretStyles
|
@ -0,0 +1 @@
|
||||
Subproject commit 1b85f4a981c21a4e6823527302c6cb82b02f26bd
|
Loading…
Reference in new issue