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) + } + } +}