add update on blur
This commit is contained in:
parent
4640eb9494
commit
894293837f
5 changed files with 230 additions and 2 deletions
|
|
@ -63,6 +63,7 @@
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #5a5a5a;
|
color: #5a5a5a;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
@keyframes App-logo-spin {
|
||||||
|
|
|
||||||
55
src/App.js
55
src/App.js
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import logo from './logo.svg'
|
import logo from './logo.svg'
|
||||||
|
import ContentEditable from './components/ContentEditable'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
|
@ -30,7 +31,7 @@ class App extends Component {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset input
|
// reset input to empty
|
||||||
this.inputElement.value = ''
|
this.inputElement.value = ''
|
||||||
|
|
||||||
// Optimistically add todo to UI
|
// Optimistically add todo to UI
|
||||||
|
|
@ -109,6 +110,49 @@ class App extends Component {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
updateTodo = (id, todoValue) => {
|
||||||
|
// Make API request to update todo
|
||||||
|
fetch(`/.netlify/functions/todos-update/${id}`, {
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: todoValue
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
// mode: 'cors', // no-cors, cors, *same-origin
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
console.log(json)
|
||||||
|
// fin
|
||||||
|
}).catch((e) => {
|
||||||
|
console.log('An API error occurred', e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
handleDataChange = (event, currentValue) => {
|
||||||
|
// save on change debounced?
|
||||||
|
}
|
||||||
|
handleBlur = (event, currentValue) => {
|
||||||
|
let isDifferent = false
|
||||||
|
const key = event.target.dataset.key
|
||||||
|
|
||||||
|
const updatedTodos = this.state.todos.map((todo, i) => {
|
||||||
|
const { data, ref } = todo
|
||||||
|
const id = getTodoId(todo)
|
||||||
|
if (id === key && todo.data.title !== currentValue) {
|
||||||
|
todo.data.title = currentValue
|
||||||
|
isDifferent = true
|
||||||
|
}
|
||||||
|
return todo
|
||||||
|
})
|
||||||
|
|
||||||
|
// only set state if input different
|
||||||
|
if (isDifferent) {
|
||||||
|
this.setState({
|
||||||
|
todos: updatedTodos
|
||||||
|
}, () => {
|
||||||
|
this.updateTodo(key, currentValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
renderTodos() {
|
renderTodos() {
|
||||||
const { todos } = this.state
|
const { todos } = this.state
|
||||||
return todos.map((todo, i) => {
|
return todos.map((todo, i) => {
|
||||||
|
|
@ -124,7 +168,14 @@ class App extends Component {
|
||||||
return (
|
return (
|
||||||
<div key={i} className='todo-item'>
|
<div key={i} className='todo-item'>
|
||||||
<div className='todo-list-title'>
|
<div className='todo-list-title'>
|
||||||
{data.title}
|
<ContentEditable
|
||||||
|
tagName='span'
|
||||||
|
editKey={id}
|
||||||
|
onChange={this.handleDataChange} // handle innerHTML change
|
||||||
|
onBlur={this.handleBlur} // handle innerHTML change
|
||||||
|
html={data.title}
|
||||||
|
>
|
||||||
|
</ContentEditable>
|
||||||
</div>
|
</div>
|
||||||
{deleteButton}
|
{deleteButton}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
9
src/components/ContentEditable/ContentEditable.css
Normal file
9
src/components/ContentEditable/ContentEditable.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.editable {
|
||||||
|
cursor: text;
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
.editable[contenteditable="true"] {
|
||||||
|
outline: 3px solid #efefef;
|
||||||
|
}
|
||||||
66
src/components/ContentEditable/Editable.js
Normal file
66
src/components/ContentEditable/Editable.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/* fork of https://github.com/lovasoa/react-contenteditable */
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class Editable extends React.Component {
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
// We need not rerender if the change of props simply reflects the user's
|
||||||
|
// edits. Rerendering in this case would make the cursor/caret jump.
|
||||||
|
return (
|
||||||
|
// Rerender if there is no element yet...
|
||||||
|
!this.htmlEl
|
||||||
|
// ...or if html really changed... (programmatically, not by user edit)
|
||||||
|
|| (nextProps.html !== this.htmlEl.innerHTML
|
||||||
|
&& nextProps.html !== this.props.html)
|
||||||
|
// ...or if editing is enabled or disabled.
|
||||||
|
|| this.props.disabled !== nextProps.disabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.htmlEl && this.props.html !== this.htmlEl.innerHTML) {
|
||||||
|
// Perhaps React (whose VDOM gets outdated because we often prevent
|
||||||
|
// rerendering) did not update the DOM. So we update it manually now.
|
||||||
|
this.htmlEl.innerHTML = this.props.html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preventEnter = (evt) => {
|
||||||
|
if (evt.which === 13) {
|
||||||
|
evt.preventDefault()
|
||||||
|
if (!this.htmlEl) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.htmlEl.blur()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitChange = (evt) => {
|
||||||
|
if (!this.htmlEl) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const html = this.htmlEl.innerHTML
|
||||||
|
if (this.props.onChange && html !== this.lastHtml) {
|
||||||
|
evt.target.value = html
|
||||||
|
this.props.onChange(evt, html)
|
||||||
|
}
|
||||||
|
this.lastHtml = html
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { tagName, html, onChange, ...props } = this.props
|
||||||
|
|
||||||
|
const domNodeType = tagName || 'div'
|
||||||
|
const elementProps = {
|
||||||
|
...props,
|
||||||
|
ref: (e) => this.htmlEl = e,
|
||||||
|
onKeyDown: this.preventEnter,
|
||||||
|
onInput: this.emitChange,
|
||||||
|
onBlur: this.props.onBlur || this.emitChange,
|
||||||
|
contentEditable: !this.props.disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
let children = this.props.children
|
||||||
|
if (html) {
|
||||||
|
elementProps.dangerouslySetInnerHTML = { __html: html }
|
||||||
|
children = null
|
||||||
|
}
|
||||||
|
return React.createElement(domNodeType, elementProps, children)
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/components/ContentEditable/index.js
Normal file
101
src/components/ContentEditable/index.js
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Editable from './Editable'
|
||||||
|
import './ContentEditable.css'
|
||||||
|
|
||||||
|
export default class ContentEditable extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
this.hasFocused = false
|
||||||
|
}
|
||||||
|
handleClick = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const event = e || window.event
|
||||||
|
// hacks to give the contenteditable block a better UX
|
||||||
|
event.persist()
|
||||||
|
if (!this.hasFocused) {
|
||||||
|
const caretRange = getMouseEventCaretRange(event)
|
||||||
|
window.setTimeout(() => {
|
||||||
|
selectRange(caretRange)
|
||||||
|
this.hasFocused = true
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
// end hacks to give the contenteditable block a better UX
|
||||||
|
this.setState({
|
||||||
|
disabled: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
handleClickOutside = (evt) => {
|
||||||
|
const event = evt || window.event
|
||||||
|
// presist blur event for react
|
||||||
|
event.persist()
|
||||||
|
const value = evt.target.value || evt.target.innerText
|
||||||
|
this.setState({
|
||||||
|
disabled: true
|
||||||
|
}, () => {
|
||||||
|
this.hasFocused = false // reset single click functionality
|
||||||
|
if (this.props.onBlur) {
|
||||||
|
this.props.onBlur(evt, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { onChange, children, html, editKey, tagName } = this.props
|
||||||
|
const content = html || children
|
||||||
|
return (
|
||||||
|
<Editable
|
||||||
|
tagName={tagName}
|
||||||
|
data-key={editKey}
|
||||||
|
className={'editable'}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onBlur={this.handleClickOutside}
|
||||||
|
html={content}
|
||||||
|
disabled={this.state.disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMouseEventCaretRange(event) {
|
||||||
|
const x = event.clientX
|
||||||
|
const y = event.clientY
|
||||||
|
let range
|
||||||
|
|
||||||
|
if (document.body.createTextRange) {
|
||||||
|
// IE
|
||||||
|
range = document.body.createTextRange()
|
||||||
|
range.moveToPoint(x, y)
|
||||||
|
} else if (typeof document.createRange !== 'undefined') {
|
||||||
|
// Try Firefox rangeOffset + rangeParent properties
|
||||||
|
if (typeof event.rangeParent !== 'undefined') {
|
||||||
|
range = document.createRange()
|
||||||
|
range.setStart(event.rangeParent, event.rangeOffset)
|
||||||
|
range.collapse(true)
|
||||||
|
} else if (document.caretPositionFromPoint) {
|
||||||
|
// Try the standards-based way next
|
||||||
|
const pos = document.caretPositionFromPoint(x, y)
|
||||||
|
range = document.createRange()
|
||||||
|
range.setStart(pos.offsetNode, pos.offset)
|
||||||
|
range.collapse(true)
|
||||||
|
} else if (document.caretRangeFromPoint) {
|
||||||
|
// WebKit
|
||||||
|
range = document.caretRangeFromPoint(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRange(range) {
|
||||||
|
if (range) {
|
||||||
|
if (typeof range.select !== 'undefined') {
|
||||||
|
range.select()
|
||||||
|
} else if (typeof window.getSelection !== 'undefined') {
|
||||||
|
const sel = window.getSelection()
|
||||||
|
sel.removeAllRanges()
|
||||||
|
sel.addRange(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue