diff --git a/src/App.css b/src/App.css
index 60ff282..8c0c969 100644
--- a/src/App.css
+++ b/src/App.css
@@ -63,6 +63,7 @@
font-size: 17px;
font-weight: 500;
color: #5a5a5a;
+ flex-grow: 1;
}
@keyframes App-logo-spin {
diff --git a/src/App.js b/src/App.js
index 5032472..2590a7f 100644
--- a/src/App.js
+++ b/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 (
- {data.title}
+
+
{deleteButton}
diff --git a/src/components/ContentEditable/ContentEditable.css b/src/components/ContentEditable/ContentEditable.css
new file mode 100644
index 0000000..262dead
--- /dev/null
+++ b/src/components/ContentEditable/ContentEditable.css
@@ -0,0 +1,9 @@
+.editable {
+ cursor: text;
+ display: block;
+ padding: 10px;
+ width: 90%;
+}
+.editable[contenteditable="true"] {
+ outline: 3px solid #efefef;
+}
diff --git a/src/components/ContentEditable/Editable.js b/src/components/ContentEditable/Editable.js
new file mode 100644
index 0000000..4d8657e
--- /dev/null
+++ b/src/components/ContentEditable/Editable.js
@@ -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)
+ }
+}
diff --git a/src/components/ContentEditable/index.js b/src/components/ContentEditable/index.js
new file mode 100644
index 0000000..dbebbf2
--- /dev/null
+++ b/src/components/ContentEditable/index.js
@@ -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 (
+
+ )
+ }
+}
+
+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)
+ }
+ }
+}