-
Notifications
You must be signed in to change notification settings - Fork 28
[Mission4/김규란] - Project_Notion_Vanilla_JS #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 3/#4_gxxrxn
Are you sure you want to change the base?
Changes from all commits
5c7931d
0da15c1
5e74b67
9a7f4b2
f591bf4
35bb3b3
c0f809c
5decc3d
7454bd5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| .vscode | ||
| .env.js |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="ko"> | ||
|
|
||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <title>Fry's Notion</title> | ||
| <link rel="stylesheet" href="/style.css" type="text/css" /> | ||
| </head> | ||
|
|
||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/main.js"></script> | ||
| </body> | ||
|
|
||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import Router from "./Router.js"; | ||
| import { routes } from "./constants/routes.js"; | ||
| import { navigate } from "./utils/navigate.js"; | ||
|
|
||
| export default function App({ $target }) { | ||
| const findMatchedRoute = (pathname) => routes.find((route) => route.path.test(pathname)); | ||
|
|
||
| this.$target = $target; | ||
| this.state = { currentPage: null }; | ||
|
|
||
| this.init = () => { | ||
| new Router({ onRoute: this.route.bind(this) }); | ||
| }; | ||
|
|
||
| this.route = () => { | ||
| const { pathname } = location; | ||
| const { currentPage } = this.state; | ||
| const nextPage = findMatchedRoute(pathname)?.page; | ||
|
|
||
| const documentIdFromPath = pathname.split("/documents/")[1]; | ||
| const documentIdFromHistory = history.state?.documentId || null; | ||
|
|
||
| if (documentIdFromPath != documentIdFromHistory || !nextPage) { | ||
| navigate("/404", true); | ||
| return; | ||
| } | ||
|
|
||
| const needNewPage = | ||
| !currentPage || !(currentPage instanceof nextPage) || !documentIdFromHistory; | ||
|
|
||
| if (needNewPage) { | ||
| this.setState({ | ||
| currentPage: new nextPage({ | ||
| $target: this.$target, | ||
| initialState: { documentId: documentIdFromHistory }, | ||
| }), | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| this.state.currentPage.setState({ documentId: documentIdFromHistory }); | ||
| }; | ||
|
|
||
| this.setState = (newState) => { | ||
| this.state = { ...this.state, ...newState }; | ||
| }; | ||
|
|
||
| this.init(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { HISTORY_CHANGE, POP_STATE } from "./constants/routes.js"; | ||
|
|
||
| export default function Router({ onRoute }) { | ||
| this.init = () => { | ||
| window.addEventListener(HISTORY_CHANGE, ({ detail }) => { | ||
| const { to, isReplace, state } = detail; | ||
|
|
||
| if (isReplace || to === location.pathname) { | ||
| history.replaceState(state, "", to); | ||
| } else { | ||
| history.pushState(state, "", to); | ||
| } | ||
|
|
||
| onRoute(); | ||
| }); | ||
|
|
||
| window.addEventListener(POP_STATE, () => { | ||
| onRoute(); | ||
| }); | ||
|
|
||
| onRoute(); | ||
| }; | ||
|
|
||
| this.init(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| export default function Component({ $target, initialState }) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p5) 현재 사용되지 않는 파일 같네요! 아마 클래스로 짜시다가 중간에 함수형으로 바꾸셨다고 했는데 그 과정에서 남기신게 아닐까.. 추측해봅니다! |
||
| this.$target = $target; | ||
| this.state = { ...initialState }; | ||
|
|
||
| this.init = () => { | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.setState = () => { | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| this.$target.innerHTML = ` | ||
| `; | ||
| this.mounted(); | ||
| }; | ||
|
|
||
| this.mounted = () => {}; | ||
|
|
||
| this.setEvent = () => {}; | ||
|
|
||
| this.init(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import DocumentHeader from "./DocumentHeader.js"; | ||
| import DocumentContent from "./DocumentContent.js"; | ||
|
|
||
| import API from "../../utils/api.js"; | ||
| import { setItemToStorage, getItemFromStorage } from "../../utils/storage.js"; | ||
| import { debounce } from "../../utils/index.js"; | ||
| import { navigate } from "../../utils/navigate.js"; | ||
|
|
||
| export default function Document({ | ||
| $target, | ||
| initialState = { | ||
| documentId: null, | ||
| }, | ||
| }) { | ||
| const fetchDocument = async (documentId) => { | ||
| const response = await API.getDocuments(documentId); | ||
|
Comment on lines
+15
to
+16
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. API를 객체로 선언하시다니...! 이렇게 작성하면 실수를 줄일 수 있겠네요!😲 |
||
|
|
||
| if (!response) { | ||
| navigate("/", true); | ||
| return; | ||
| } | ||
|
|
||
| return [{ title: response.title, content: response.content }, response]; | ||
| }; | ||
|
|
||
| const handleDocumentEdit = async (text, section = "title") => { | ||
| const storedItem = getItemFromStorage("notion", { currentDocument: {} }); | ||
|
|
||
| storedItem.currentDocument = { | ||
| ...storedItem.currentDocument, | ||
| [section]: text, | ||
| tempSavedAt: new Date(), | ||
| }; | ||
| setItemToStorage("notion", { ...storedItem }); | ||
|
|
||
| const { title, content } = storedItem.currentDocument; | ||
| await API.updateDocument(this.state.documentId, { title, content }); | ||
| }; | ||
|
|
||
| const renderDocumentById = async ($header, $body) => { | ||
| const { documentId } = this.state; | ||
| const [{ title, content }, response] = await fetchDocument(documentId); | ||
|
|
||
| setItemToStorage("notion", { currentDocument: response }); | ||
|
|
||
| new DocumentHeader({ | ||
| $target: $header, | ||
| initialState: { title }, | ||
| onEdit: debounce(handleDocumentEdit, 300), | ||
| }); | ||
| new DocumentContent({ | ||
| $target: $body, | ||
| initialState: { content }, | ||
| onEdit: debounce(handleDocumentEdit, 300), | ||
| }); | ||
| }; | ||
|
|
||
| const renderNewDocument = ($header, $body) => { | ||
| new DocumentHeader({ $target: $header, onEdit: debounce(handleDocumentEdit, 300) }); | ||
| new DocumentContent({ $target: $body, onEdit: debounce(handleDocumentEdit, 300) }); | ||
| }; | ||
|
|
||
| const renderNoDocument = ($container) => { | ||
| $container.innerHTML = ` | ||
| <h1 style="color: rgb(102, 75, 63, 0.7); font-weight: 800;">Notion에 오신 것을 환영해요!</h1> | ||
| <img src="https://media3.giphy.com/media/KjuQizGwJCsgoYdziS/giphy.gif?cid=ecf05e47ly3czt6iu86gd916h6oqna0t6wnb0e95ldri599i&rid=giphy.gif&ct=s" /> | ||
| `; | ||
| }; | ||
|
|
||
| this.$target = $target; | ||
| this.state = initialState; | ||
|
|
||
| this.init = () => { | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| this.$target.innerHTML = ` | ||
| <div id="document"> | ||
| <div id="document-header"></div> | ||
| <div id="document-body"></div> | ||
| </div> | ||
| `; | ||
|
|
||
| this.mounted(); | ||
| }; | ||
|
|
||
| this.mounted = async () => { | ||
| const $container = this.$target.querySelector("#document"); | ||
| const $header = this.$target.querySelector("#document-header"); | ||
| const $body = this.$target.querySelector("#document-body"); | ||
|
|
||
| const { documentId } = this.state; | ||
|
|
||
| if (documentId === "new") { | ||
| renderNewDocument($header, $body); | ||
| return; | ||
| } else if (!!documentId) { | ||
| renderDocumentById($header, $body); | ||
| } else { | ||
| renderNoDocument($container); | ||
| } | ||
| }; | ||
|
|
||
| this.init(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { addEvent } from "../../utils/event.js"; | ||
| import { parseNewline } from "../../utils/index.js"; | ||
|
|
||
| export default function DocumentContent({ | ||
| $target, | ||
| initialState = { | ||
| content: "", | ||
| }, | ||
| onEdit, | ||
| }) { | ||
| this.$target = $target; | ||
| this.state = initialState; | ||
|
|
||
| this.init = () => { | ||
| this.setEvent(); | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| const { content } = this.state; | ||
|
|
||
| this.$target.innerHTML = ` | ||
| <div id="document-editor" name="content" contenteditable="true" placeholder="Type for creating new document">${parseNewline( | ||
| content ?? "" | ||
| )}</div> | ||
| `; | ||
| }; | ||
|
|
||
| this.setEvent = () => { | ||
| addEvent(this.$target, "keyup", "[name=content]", (event) => { | ||
| onEdit(event.target.innerText, "content"); | ||
| }); | ||
| }; | ||
|
Comment on lines
+29
to
+33
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이벤트 리스너를 따로 함수로 만드신 건가요?! 이렇게 코드를 작성할 수도 있군요... 😮 |
||
|
|
||
| this.setState = (newState) => { | ||
| this.state = { ...this.state, ...newState }; | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.init(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { DEFAULT } from "../../constants/config.js"; | ||
| import { addEvent } from "../../utils/event.js"; | ||
|
|
||
| export default function DocumentHeader({ | ||
| $target, | ||
| initialState = { | ||
| title: "", | ||
| }, | ||
| onEdit, | ||
| }) { | ||
| this.$target = $target; | ||
| this.state = initialState; | ||
|
|
||
| this.init = () => { | ||
| this.setEvent(); | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| const { title } = this.state; | ||
|
|
||
| this.$target.innerHTML = ` | ||
| <div class="title" name="title" placeholder=${DEFAULT.DOCUMENT_NAME} contenteditable="true">${ | ||
| title === DEFAULT.DOCUMENT_NAME ? "" : title | ||
| }</div>`; | ||
| }; | ||
|
|
||
| this.setEvent = () => { | ||
| addEvent(this.$target, "keyup", "[name=title]", (event) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P5 : 저의 경우에는 event type을 keyup으로 해두면 화살표 키나 shift, caps lock 등 입력 값이 변하지 않았음에도 이벤트가 발생해서 유효하지 않은 api가 날라가더라구요. 그래서 input으로 바꿔서 입력값이 변할 때에만 api가 날라갈 수 있게 했었습니다. 그리고 이전에 React에서 겪은 적이 있는데, event type을 keyup으로 해두고 한글을 입력하면 이벤트가 중복해서 2번 발생하는 현상이 있었습니다. 실제로 검색해보니 javascript 자체의 문제더라구요. 한글 관련 에러여서 공식 자료도 찾기 어려웠었습니다. 이런 현상을 피하기 위해서 다른 event type을 고려해보시는 것도 좋을 것 같습니다! p.s 과제에서 확인해보니, 여기선 keyup으로 해도 이벤트가 중복 발생하진 않네요! |
||
| onEdit(event.target.innerText, "title"); | ||
| }); | ||
| }; | ||
|
|
||
| this.init(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import DocumentItem from "./DocumentItem.js"; | ||
| import { createDocumentsListElement, findDocumentElement } from "../../utils/helper.js"; | ||
|
|
||
| export default function Details({ | ||
| $target, | ||
| initialState = { | ||
| id: null, | ||
| title: "", | ||
| documents: [], | ||
| }, | ||
| onAddButtonClick, | ||
| }) { | ||
| this.$target = $target; | ||
| this.state = initialState; | ||
|
|
||
| this.init = () => { | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| const { documents } = this.state; | ||
|
|
||
| this.$target.innerHTML = ` | ||
| <details id="document-details"> | ||
| <summary></summary> | ||
| <ul>${createDocumentsListElement(documents)}</ul> | ||
| </details> | ||
| `; | ||
|
|
||
| this.mounted(); | ||
| }; | ||
|
|
||
| this.mounted = () => { | ||
| const $rootDocument = this.$target.querySelector("summary"); | ||
| const { id, title, documents } = this.state; | ||
|
|
||
| new DocumentItem({ | ||
| $target: $rootDocument, | ||
| initialState: { id, title }, | ||
| onAddButtonClick, | ||
| }); | ||
|
|
||
| documents.forEach(({ id, title, documents }) => { | ||
| const $target = findDocumentElement(id); | ||
|
|
||
| new Details({ | ||
| $target, | ||
| initialState: { title, id, documents }, | ||
| onAddButtonClick, | ||
| }); | ||
| }); | ||
| }; | ||
|
|
||
| this.setState = (nextState) => { | ||
| this.state = { ...this.state, ...nextState }; | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.init(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ask) 코드를 읽어보니 모두 this.$target = $target 으로 초기 설정을 하셨는데 이유가 뭔가요????? 궁금합니다.. 뭔가 규란님이라면 이유가 있어서 하셨을 것 같아서요 !!