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
|
@ -1,4 +1,12 @@
|
|||||||
import {User} from 'src/types/session'
|
import {User} from 'src/types/session'
|
||||||
export function getUserName(user:User) {
|
export function getUserName(user:User) {
|
||||||
return user.nickname === "" ? user.username : user.nickname
|
return user.nickname === "" ? user.username : user.nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUsernameChar(c) {
|
||||||
|
if ((c >= "a" && "c<=z") || (c >= "0" && c <= "9") || (c === "-")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
@ -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
|
@ -1,28 +1,45 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
"allowJs": true,
|
"dom",
|
||||||
"skipLibCheck": true,
|
"dom.iterable",
|
||||||
"strict": false,
|
"esnext"
|
||||||
"forceConsistentCasingInFileNames": true,
|
],
|
||||||
"noEmit": true,
|
"allowJs": true,
|
||||||
"esModuleInterop": true,
|
"skipLibCheck": true,
|
||||||
"module": "esnext",
|
"strict": false,
|
||||||
"moduleResolution": "node",
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"noEmit": true,
|
||||||
"isolatedModules": true,
|
"esModuleInterop": true,
|
||||||
"jsx": "preserve",
|
"module": "esnext",
|
||||||
"baseUrl": ".",
|
"moduleResolution": "node",
|
||||||
"paths": {
|
"resolveJsonModule": true,
|
||||||
"hooks/*": ["src/hooks/*"],
|
"isolatedModules": true,
|
||||||
"components/*": ["src/components/*"],
|
"jsx": "preserve",
|
||||||
"utils/*": ["src/utils/*"],
|
"baseUrl": ".",
|
||||||
"analytics/*": ["src/analytics/*"]
|
"paths": {
|
||||||
},
|
"hooks/*": [
|
||||||
"downlevelIteration": true
|
"src/hooks/*"
|
||||||
|
],
|
||||||
|
"components/*": [
|
||||||
|
"src/components/*"
|
||||||
|
],
|
||||||
|
"utils/*": [
|
||||||
|
"src/utils/*"
|
||||||
|
],
|
||||||
|
"analytics/*": [
|
||||||
|
"src/analytics/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"downlevelIteration": true
|
||||||
"exclude": ["node_modules"]
|
},
|
||||||
}
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 1b85f4a981c21a4e6823527302c6cb82b02f26bd
|
Loading…
Reference in new issue