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-weight: 500;
|
||||
color: #5a5a5a;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
|
|
|
|||
55
src/App.js
55
src/App.js
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Component } from 'react'
|
||||
import logo from './logo.svg'
|
||||
import ContentEditable from './components/ContentEditable'
|
||||
import './App.css'
|
||||
|
||||
class App extends Component {
|
||||
|
|
@ -30,7 +31,7 @@ class App extends Component {
|
|||
return false
|
||||
}
|
||||
|
||||
// reset input
|
||||
// reset input to empty
|
||||
this.inputElement.value = ''
|
||||
|
||||
// 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() {
|
||||
const { todos } = this.state
|
||||
return todos.map((todo, i) => {
|
||||
|
|
@ -124,7 +168,14 @@ class App extends Component {
|
|||
return (
|
||||
<div key={i} className='todo-item'>
|
||||
<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>
|
||||
{deleteButton}
|
||||
</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