├── .babelrc ├── .eslintrc ├── .github └── workflows │ └── linters.yml ├── .gitignore ├── .hintrc ├── .stylelintrc ├── README.md ├── dist ├── 6731ef40f4416b5c9069.ttf ├── a0d3b87095ec6aee15f3.png ├── index.html └── main.js ├── package-lock.json ├── package.json ├── src ├── assets │ ├── css │ │ └── style.css │ ├── fonts │ │ └── cocogoose-pro-regular.ttf │ └── img │ │ └── todo-list-desktop-v-snapshot.png ├── components │ ├── ClearAllButton.js │ ├── CreateNewTaskItem.js │ ├── HeadingTaskListItem.js │ ├── TaskListItem.js │ └── TaskListItem.test.js ├── index.html ├── index.js └── modules │ ├── DataStore.js │ ├── Task.js │ └── Task.test.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "@babel/plugin-transform-modules-commonjs" 6 | ] 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "extends": [ 13 | "airbnb-base" 14 | ], 15 | "rules": { 16 | "no-shadow": "off", 17 | "no-param-reassign": "off", 18 | "eol-last": "off", 19 | "import/extensions": [ 20 | 1, 21 | { 22 | "js": "always", 23 | "json": "always" 24 | } 25 | ] 26 | }, 27 | "ignorePatterns": [ 28 | "dist/", 29 | "build/" 30 | ] 31 | } -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | lighthouse: 10 | name: Lighthouse 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "12.x" 17 | - name: Setup Lighthouse 18 | run: npm install -g @lhci/cli@0.7.x 19 | - name: Lighthouse Report 20 | run: lhci autorun --upload.target=temporary-public-storage --collect.staticDistDir=. 21 | webhint: 22 | name: Webhint 23 | runs-on: ubuntu-18.04 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: "12.x" 29 | - name: Setup Webhint 30 | run: | 31 | npm install --save-dev hint@6.x 32 | [ -f .hintrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/html-css-js/.hintrc 33 | - name: Webhint Report 34 | run: npx hint . 35 | stylelint: 36 | name: Stylelint 37 | runs-on: ubuntu-18.04 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: "12.x" 43 | - name: Setup Stylelint 44 | run: | 45 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 46 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/html-css-js/.stylelintrc.json 47 | - name: Stylelint Report 48 | run: npx stylelint "**/*.{css,scss}" 49 | eslint: 50 | name: ESLint 51 | runs-on: ubuntu-18.04 52 | steps: 53 | - uses: actions/checkout@v2 54 | - uses: actions/setup-node@v1 55 | with: 56 | node-version: "12.x" 57 | - name: Setup ESLint 58 | run: | 59 | npm install --save-dev eslint@7.x eslint-config-airbnb-base@14.x eslint-plugin-import@2.x babel-eslint@10.x 60 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/html-css-js/.eslintrc.json 61 | - name: ESLint Report 62 | run: npx eslint . -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | 4 | coverage/ -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "connector": { 3 | "name": "local", 4 | "options": { 5 | "pattern": [ 6 | "**", 7 | "!.git/**", 8 | "!node_modules/**" 9 | ] 10 | } 11 | }, 12 | "extends": [ 13 | "development" 14 | ], 15 | "formatters": [ 16 | "stylish" 17 | ], 18 | "hints": [ 19 | "button-type", 20 | "disown-opener", 21 | "html-checker", 22 | "meta-charset-utf-8", 23 | "meta-viewport", 24 | "no-inline-styles:error" 25 | ] 26 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": null, 6 | "scss/at-rule-no-unknown": true, 7 | "csstree/validator": true 8 | }, 9 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css", "**/*.js", "**/*.jsx"] 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![](https://img.shields.io/badge/Microverse-blueviolet) 3 | 4 | # todo-list 5 | "To-do list" is a tool that helps to organize your day. It simply lists the things that you need to do and allows you to mark them as complete. This simple website allows you to that, and it is built using ES6 and Webpack! 6 | 7 | ### Desktop Version 8 | 9 | ![Desktop - Books List Page](./src/assets/img/todo-list-desktop-v-snapshot.png) 10 | 11 | In this project, you will get a simple HTML list of To Do tasks. It is built using webpack and served by a webpack dev server. The list is styled according to the specifications listed by the [minimalist](https://web.archive.org/web/20180320194056/http://www.getminimalist.com:80/) website. 12 | 13 | The CRUD (create, update, delete) methods for the todo tasks are implemented. All the elements of the user interface are fully functional and the application is completed.You will also be able to mark task completion by selecting the corresponding checkbox (or undo it by unchecking the checkbox). The updated tasks list will be stored in local storage. 14 | 15 | ## Built With 16 | 17 | - HTML, CSS and JavaScript (ES6) 18 | 19 | ## Live Demo 20 | 21 | - [Live Demo Link](https://teks-todo-list.netlify.app) 22 | 23 | ## Getting Started 24 | - Install Node.js ^12.13 25 | - clone the repository by running\ 26 | `git clone https://github.com/gtekle/todo-list.git` 27 | - navigate to the folder\ 28 | `cd todo-list` 29 | - Install packages\ 30 | `npm install` 31 | - To run application using webpack-dev-server\ 32 | `npm start` 33 | 34 | ## Author 35 | 36 | 👤 **Tekle Gebreyohannes** 37 | 38 | - GitHub: [@githubhandle](https://github.com/gtekle) 39 | - LinkedIn: [LinkedIn](www.linkedin.com/in/tekle-gebreyohannes-kidanemariam-7605752b) 40 | 41 | ## 🤝 Contributing 42 | 43 | Contributions, issues, and feature requests are welcome! 44 | 45 | Feel free to check the [issues page](../../issues/). 46 | 47 | ## Show your support 48 | 49 | Give a ⭐️ if you like this project! 50 | 51 | ## Acknowledgments 52 | 53 | - [Microverse Inc](https://www.microverse.org/) 54 | - [minimalist](https://web.archive.org/web/20180320194056/http://www.getminimalist.com:80/) 55 | 56 | ## 📝 License 57 | 58 | This project is [MIT](./MIT.md) licensed. 59 | -------------------------------------------------------------------------------- /dist/6731ef40f4416b5c9069.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtekle/todo-list/66daf5675b29b0ae9965acaef53895127ddec5e8/dist/6731ef40f4416b5c9069.ttf -------------------------------------------------------------------------------- /dist/a0d3b87095ec6aee15f3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtekle/todo-list/66daf5675b29b0ae9965acaef53895127ddec5e8/dist/a0d3b87095ec6aee15f3.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | To Do List 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

TO DO LIST

17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). 3 | * This devtool is neither made for production nor for readable output files. 4 | * It uses "eval()" calls to create a separate source file in the browser devtools. 5 | * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) 6 | * or disable the default devtool with "devtool: false". 7 | * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). 8 | */ 9 | /******/ (() => { // webpackBootstrap 10 | /******/ "use strict"; 11 | /******/ var __webpack_modules__ = ({ 12 | 13 | /***/ "./node_modules/css-loader/dist/cjs.js!./src/assets/css/style.css": 14 | /*!************************************************************************!*\ 15 | !*** ./node_modules/css-loader/dist/cjs.js!./src/assets/css/style.css ***! 16 | \************************************************************************/ 17 | /***/ ((module, __webpack_exports__, __webpack_require__) => { 18 | 19 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_getUrl_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/css-loader/dist/runtime/getUrl.js */ \"./node_modules/css-loader/dist/runtime/getUrl.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_getUrl_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_getUrl_js__WEBPACK_IMPORTED_MODULE_2__);\n// Imports\n\n\n\nvar ___CSS_LOADER_URL_IMPORT_0___ = new URL(/* asset import */ __webpack_require__(/*! ../fonts/cocogoose-pro-regular.ttf */ \"./src/assets/fonts/cocogoose-pro-regular.ttf\"), __webpack_require__.b);\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\nvar ___CSS_LOADER_URL_REPLACEMENT_0___ = _node_modules_css_loader_dist_runtime_getUrl_js__WEBPACK_IMPORTED_MODULE_2___default()(___CSS_LOADER_URL_IMPORT_0___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"*,\\n*::after,\\n*::before {\\n box-sizing: border-box;\\n}\\n\\n@font-face {\\n font-family: cocogoose;\\n src: url(\" + ___CSS_LOADER_URL_REPLACEMENT_0___ + \");\\n}\\n\\nbody {\\n background-color: #ddd;\\n margin: 0;\\n font-family: Arial, Helvetica, sans-serif;\\n}\\n\\n.btn {\\n cursor: pointer;\\n}\\n\\n#header,\\n#footer {\\n background-color: darkgray;\\n width: 100%;\\n height: 5px;\\n padding: 0;\\n margin: 0;\\n}\\n\\n#main {\\n max-width: 94%;\\n min-height: calc(100vh - 64px);\\n margin: 12px auto;\\n text-align: center;\\n}\\n\\n.main-section-title {\\n color: darkslategray;\\n font-family: cocogoose, sans-serif;\\n}\\n\\n.todo-list {\\n list-style: none;\\n text-align: center;\\n padding: 0;\\n background-color: #d3d3d3;\\n margin: 0;\\n box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, 0.2);\\n}\\n\\n.todo-heading {\\n display: flex;\\n justify-content: space-between;\\n}\\n\\n.todo-heading h2 {\\n color: darkgray;\\n font-size: 20px;\\n line-height: 24px;\\n font-weight: 700;\\n}\\n\\n.todo-heading .btn-refresh {\\n border: none;\\n background-color: #fff;\\n text-align: right;\\n padding: 0;\\n margin: 0;\\n margin-right: 10px;\\n}\\n\\n.todo-heading i {\\n color: darkgray;\\n font-size: 22px;\\n font-weight: 500;\\n}\\n\\n.btn i:hover {\\n color: black;\\n}\\n\\n.btn i:active {\\n color: blueviolet;\\n}\\n\\n#form-add-task {\\n display: flex;\\n width: 100%;\\n text-align: justify;\\n background-color: #fff;\\n border-bottom: 1px solid gray;\\n}\\n\\n#form-add-task input {\\n width: calc(100% - 43px);\\n margin: 0;\\n font-size: 17px;\\n line-height: 32px;\\n padding: 3px 10px;\\n font-style: italic;\\n border: none;\\n outline: none;\\n}\\n\\n#form-add-task .btn-add-task {\\n display: flex;\\n flex-direction: column;\\n justify-content: center;\\n align-items: center;\\n color: grey;\\n border: none;\\n margin: 0;\\n font-weight: 700;\\n vertical-align: text-top;\\n font-size: 16px;\\n line-height: 24px;\\n background-color: #fff;\\n}\\n\\n.todo-list li {\\n display: flex;\\n border-bottom: 1px solid #bbb;\\n justify-content: space-between;\\n align-items: center;\\n background-color: #fff;\\n padding-left: 10px;\\n font-size: 16px;\\n font-weight: 500;\\n cursor: text;\\n}\\n\\ninput[name=\\\"isCompleted\\\"] {\\n cursor: pointer;\\n}\\n\\n.item-description {\\n display: flex;\\n align-items: center;\\n font-size: 18px;\\n width: 100%;\\n}\\n\\n.todo-list li .task-description {\\n color: rgba(0, 0, 0, 0.8);\\n width: 100%;\\n margin: 0;\\n padding: 10px;\\n font-size: 16px;\\n font-weight: 500;\\n text-align: left;\\n}\\n\\n.todo-list .fa-ellipsis-v {\\n color: grey;\\n font-size: 16px;\\n padding: 5px;\\n cursor: move;\\n z-index: 10;\\n}\\n\\n.todo-list .btn-v-ellipsis {\\n border: none;\\n background-color: transparent;\\n cursor: pointer;\\n z-index: 100;\\n}\\n\\n.todo-list .btn-trash {\\n border: none;\\n background-color: transparent;\\n cursor: pointer;\\n z-index: 110;\\n}\\n\\n.todo-list .fa-trash {\\n color: red;\\n font-size: 16px;\\n padding: 5px;\\n}\\n\\n.todo-list .fa-ellipsis-v:hover {\\n color: black;\\n}\\n\\n.todo-list .over {\\n background-color: #cababa;\\n}\\n\\n.todo-list li > * {\\n margin-right: 10px;\\n}\\n\\n.btn-clear-completed {\\n border: none;\\n padding: 6px;\\n margin: 10px;\\n font-size: 16px;\\n background-color: transparent;\\n font-weight: 700;\\n color: darkslategray;\\n}\\n\\n.d-off {\\n display: none;\\n}\\n\\n@media screen and (min-width: 768px) {\\n #main {\\n max-width: 40%;\\n min-height: calc(100vh - 88px);\\n margin: 24px auto;\\n }\\n}\\n\", \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://todo-list/./src/assets/css/style.css?./node_modules/css-loader/dist/cjs.js"); 20 | 21 | /***/ }), 22 | 23 | /***/ "./node_modules/css-loader/dist/runtime/api.js": 24 | /*!*****************************************************!*\ 25 | !*** ./node_modules/css-loader/dist/runtime/api.js ***! 26 | \*****************************************************/ 27 | /***/ ((module) => { 28 | 29 | eval("\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\nmodule.exports = function (cssWithMappingToString) {\n var list = []; // return the list of modules as css string\n\n list.toString = function toString() {\n return this.map(function (item) {\n var content = \"\";\n var needLayer = typeof item[5] !== \"undefined\";\n\n if (item[4]) {\n content += \"@supports (\".concat(item[4], \") {\");\n }\n\n if (item[2]) {\n content += \"@media \".concat(item[2], \" {\");\n }\n\n if (needLayer) {\n content += \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\");\n }\n\n content += cssWithMappingToString(item);\n\n if (needLayer) {\n content += \"}\";\n }\n\n if (item[2]) {\n content += \"}\";\n }\n\n if (item[4]) {\n content += \"}\";\n }\n\n return content;\n }).join(\"\");\n }; // import a list of modules into the list\n\n\n list.i = function i(modules, media, dedupe, supports, layer) {\n if (typeof modules === \"string\") {\n modules = [[null, modules, undefined]];\n }\n\n var alreadyImportedModules = {};\n\n if (dedupe) {\n for (var k = 0; k < this.length; k++) {\n var id = this[k][0];\n\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n }\n\n for (var _k = 0; _k < modules.length; _k++) {\n var item = [].concat(modules[_k]);\n\n if (dedupe && alreadyImportedModules[item[0]]) {\n continue;\n }\n\n if (typeof layer !== \"undefined\") {\n if (typeof item[5] === \"undefined\") {\n item[5] = layer;\n } else {\n item[1] = \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\").concat(item[1], \"}\");\n item[5] = layer;\n }\n }\n\n if (media) {\n if (!item[2]) {\n item[2] = media;\n } else {\n item[1] = \"@media \".concat(item[2], \" {\").concat(item[1], \"}\");\n item[2] = media;\n }\n }\n\n if (supports) {\n if (!item[4]) {\n item[4] = \"\".concat(supports);\n } else {\n item[1] = \"@supports (\".concat(item[4], \") {\").concat(item[1], \"}\");\n item[4] = supports;\n }\n }\n\n list.push(item);\n }\n };\n\n return list;\n};\n\n//# sourceURL=webpack://todo-list/./node_modules/css-loader/dist/runtime/api.js?"); 30 | 31 | /***/ }), 32 | 33 | /***/ "./node_modules/css-loader/dist/runtime/getUrl.js": 34 | /*!********************************************************!*\ 35 | !*** ./node_modules/css-loader/dist/runtime/getUrl.js ***! 36 | \********************************************************/ 37 | /***/ ((module) => { 38 | 39 | eval("\n\nmodule.exports = function (url, options) {\n if (!options) {\n options = {};\n }\n\n if (!url) {\n return url;\n }\n\n url = String(url.__esModule ? url.default : url); // If url is already wrapped in quotes, remove them\n\n if (/^['\"].*['\"]$/.test(url)) {\n url = url.slice(1, -1);\n }\n\n if (options.hash) {\n url += options.hash;\n } // Should url be wrapped?\n // See https://drafts.csswg.org/css-values-3/#urls\n\n\n if (/[\"'() \\t\\n]|(%20)/.test(url) || options.needQuotes) {\n return \"\\\"\".concat(url.replace(/\"/g, '\\\\\"').replace(/\\n/g, \"\\\\n\"), \"\\\"\");\n }\n\n return url;\n};\n\n//# sourceURL=webpack://todo-list/./node_modules/css-loader/dist/runtime/getUrl.js?"); 40 | 41 | /***/ }), 42 | 43 | /***/ "./node_modules/css-loader/dist/runtime/noSourceMaps.js": 44 | /*!**************************************************************!*\ 45 | !*** ./node_modules/css-loader/dist/runtime/noSourceMaps.js ***! 46 | \**************************************************************/ 47 | /***/ ((module) => { 48 | 49 | eval("\n\nmodule.exports = function (i) {\n return i[1];\n};\n\n//# sourceURL=webpack://todo-list/./node_modules/css-loader/dist/runtime/noSourceMaps.js?"); 50 | 51 | /***/ }), 52 | 53 | /***/ "./src/assets/css/style.css": 54 | /*!**********************************!*\ 55 | !*** ./src/assets/css/style.css ***! 56 | \**********************************/ 57 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 58 | 59 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! !../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ \"./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! !../../../node_modules/style-loader/dist/runtime/styleDomAPI.js */ \"./node_modules/style-loader/dist/runtime/styleDomAPI.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! !../../../node_modules/style-loader/dist/runtime/insertBySelector.js */ \"./node_modules/style-loader/dist/runtime/insertBySelector.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! !../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js */ \"./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! !../../../node_modules/style-loader/dist/runtime/insertStyleElement.js */ \"./node_modules/style-loader/dist/runtime/insertStyleElement.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! !../../../node_modules/style-loader/dist/runtime/styleTagTransform.js */ \"./node_modules/style-loader/dist/runtime/styleTagTransform.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! !!../../../node_modules/css-loader/dist/cjs.js!./style.css */ \"./node_modules/css-loader/dist/cjs.js!./src/assets/css/style.css\");\n\n \n \n \n \n \n \n \n \n \n\nvar options = {};\n\noptions.styleTagTransform = (_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default());\noptions.setAttributes = (_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default());\n\n options.insert = _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default().bind(null, \"head\");\n \noptions.domAPI = (_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default());\noptions.insertStyleElement = (_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default());\n\nvar update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"], options);\n\n\n\n\n /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"] && _node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"].locals ? _node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"].locals : undefined);\n\n\n//# sourceURL=webpack://todo-list/./src/assets/css/style.css?"); 60 | 61 | /***/ }), 62 | 63 | /***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js": 64 | /*!****************************************************************************!*\ 65 | !*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***! 66 | \****************************************************************************/ 67 | /***/ ((module) => { 68 | 69 | eval("\n\nvar stylesInDOM = [];\n\nfunction getIndexByIdentifier(identifier) {\n var result = -1;\n\n for (var i = 0; i < stylesInDOM.length; i++) {\n if (stylesInDOM[i].identifier === identifier) {\n result = i;\n break;\n }\n }\n\n return result;\n}\n\nfunction modulesToDom(list, options) {\n var idCountMap = {};\n var identifiers = [];\n\n for (var i = 0; i < list.length; i++) {\n var item = list[i];\n var id = options.base ? item[0] + options.base : item[0];\n var count = idCountMap[id] || 0;\n var identifier = \"\".concat(id, \" \").concat(count);\n idCountMap[id] = count + 1;\n var indexByIdentifier = getIndexByIdentifier(identifier);\n var obj = {\n css: item[1],\n media: item[2],\n sourceMap: item[3],\n supports: item[4],\n layer: item[5]\n };\n\n if (indexByIdentifier !== -1) {\n stylesInDOM[indexByIdentifier].references++;\n stylesInDOM[indexByIdentifier].updater(obj);\n } else {\n var updater = addElementStyle(obj, options);\n options.byIndex = i;\n stylesInDOM.splice(i, 0, {\n identifier: identifier,\n updater: updater,\n references: 1\n });\n }\n\n identifiers.push(identifier);\n }\n\n return identifiers;\n}\n\nfunction addElementStyle(obj, options) {\n var api = options.domAPI(options);\n api.update(obj);\n\n var updater = function updater(newObj) {\n if (newObj) {\n if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap && newObj.supports === obj.supports && newObj.layer === obj.layer) {\n return;\n }\n\n api.update(obj = newObj);\n } else {\n api.remove();\n }\n };\n\n return updater;\n}\n\nmodule.exports = function (list, options) {\n options = options || {};\n list = list || [];\n var lastIdentifiers = modulesToDom(list, options);\n return function update(newList) {\n newList = newList || [];\n\n for (var i = 0; i < lastIdentifiers.length; i++) {\n var identifier = lastIdentifiers[i];\n var index = getIndexByIdentifier(identifier);\n stylesInDOM[index].references--;\n }\n\n var newLastIdentifiers = modulesToDom(newList, options);\n\n for (var _i = 0; _i < lastIdentifiers.length; _i++) {\n var _identifier = lastIdentifiers[_i];\n\n var _index = getIndexByIdentifier(_identifier);\n\n if (stylesInDOM[_index].references === 0) {\n stylesInDOM[_index].updater();\n\n stylesInDOM.splice(_index, 1);\n }\n }\n\n lastIdentifiers = newLastIdentifiers;\n };\n};\n\n//# sourceURL=webpack://todo-list/./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js?"); 70 | 71 | /***/ }), 72 | 73 | /***/ "./node_modules/style-loader/dist/runtime/insertBySelector.js": 74 | /*!********************************************************************!*\ 75 | !*** ./node_modules/style-loader/dist/runtime/insertBySelector.js ***! 76 | \********************************************************************/ 77 | /***/ ((module) => { 78 | 79 | eval("\n\nvar memo = {};\n/* istanbul ignore next */\n\nfunction getTarget(target) {\n if (typeof memo[target] === \"undefined\") {\n var styleTarget = document.querySelector(target); // Special case to return head of iframe instead of iframe itself\n\n if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {\n try {\n // This will throw an exception if access to iframe is blocked\n // due to cross-origin restrictions\n styleTarget = styleTarget.contentDocument.head;\n } catch (e) {\n // istanbul ignore next\n styleTarget = null;\n }\n }\n\n memo[target] = styleTarget;\n }\n\n return memo[target];\n}\n/* istanbul ignore next */\n\n\nfunction insertBySelector(insert, style) {\n var target = getTarget(insert);\n\n if (!target) {\n throw new Error(\"Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.\");\n }\n\n target.appendChild(style);\n}\n\nmodule.exports = insertBySelector;\n\n//# sourceURL=webpack://todo-list/./node_modules/style-loader/dist/runtime/insertBySelector.js?"); 80 | 81 | /***/ }), 82 | 83 | /***/ "./node_modules/style-loader/dist/runtime/insertStyleElement.js": 84 | /*!**********************************************************************!*\ 85 | !*** ./node_modules/style-loader/dist/runtime/insertStyleElement.js ***! 86 | \**********************************************************************/ 87 | /***/ ((module) => { 88 | 89 | eval("\n\n/* istanbul ignore next */\nfunction insertStyleElement(options) {\n var element = document.createElement(\"style\");\n options.setAttributes(element, options.attributes);\n options.insert(element, options.options);\n return element;\n}\n\nmodule.exports = insertStyleElement;\n\n//# sourceURL=webpack://todo-list/./node_modules/style-loader/dist/runtime/insertStyleElement.js?"); 90 | 91 | /***/ }), 92 | 93 | /***/ "./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js": 94 | /*!**********************************************************************************!*\ 95 | !*** ./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js ***! 96 | \**********************************************************************************/ 97 | /***/ ((module, __unused_webpack_exports, __webpack_require__) => { 98 | 99 | eval("\n\n/* istanbul ignore next */\nfunction setAttributesWithoutAttributes(styleElement) {\n var nonce = true ? __webpack_require__.nc : 0;\n\n if (nonce) {\n styleElement.setAttribute(\"nonce\", nonce);\n }\n}\n\nmodule.exports = setAttributesWithoutAttributes;\n\n//# sourceURL=webpack://todo-list/./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js?"); 100 | 101 | /***/ }), 102 | 103 | /***/ "./node_modules/style-loader/dist/runtime/styleDomAPI.js": 104 | /*!***************************************************************!*\ 105 | !*** ./node_modules/style-loader/dist/runtime/styleDomAPI.js ***! 106 | \***************************************************************/ 107 | /***/ ((module) => { 108 | 109 | eval("\n\n/* istanbul ignore next */\nfunction apply(styleElement, options, obj) {\n var css = \"\";\n\n if (obj.supports) {\n css += \"@supports (\".concat(obj.supports, \") {\");\n }\n\n if (obj.media) {\n css += \"@media \".concat(obj.media, \" {\");\n }\n\n var needLayer = typeof obj.layer !== \"undefined\";\n\n if (needLayer) {\n css += \"@layer\".concat(obj.layer.length > 0 ? \" \".concat(obj.layer) : \"\", \" {\");\n }\n\n css += obj.css;\n\n if (needLayer) {\n css += \"}\";\n }\n\n if (obj.media) {\n css += \"}\";\n }\n\n if (obj.supports) {\n css += \"}\";\n }\n\n var sourceMap = obj.sourceMap;\n\n if (sourceMap && typeof btoa !== \"undefined\") {\n css += \"\\n/*# sourceMappingURL=data:application/json;base64,\".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), \" */\");\n } // For old IE\n\n /* istanbul ignore if */\n\n\n options.styleTagTransform(css, styleElement, options.options);\n}\n\nfunction removeStyleElement(styleElement) {\n // istanbul ignore if\n if (styleElement.parentNode === null) {\n return false;\n }\n\n styleElement.parentNode.removeChild(styleElement);\n}\n/* istanbul ignore next */\n\n\nfunction domAPI(options) {\n var styleElement = options.insertStyleElement(options);\n return {\n update: function update(obj) {\n apply(styleElement, options, obj);\n },\n remove: function remove() {\n removeStyleElement(styleElement);\n }\n };\n}\n\nmodule.exports = domAPI;\n\n//# sourceURL=webpack://todo-list/./node_modules/style-loader/dist/runtime/styleDomAPI.js?"); 110 | 111 | /***/ }), 112 | 113 | /***/ "./node_modules/style-loader/dist/runtime/styleTagTransform.js": 114 | /*!*********************************************************************!*\ 115 | !*** ./node_modules/style-loader/dist/runtime/styleTagTransform.js ***! 116 | \*********************************************************************/ 117 | /***/ ((module) => { 118 | 119 | eval("\n\n/* istanbul ignore next */\nfunction styleTagTransform(css, styleElement) {\n if (styleElement.styleSheet) {\n styleElement.styleSheet.cssText = css;\n } else {\n while (styleElement.firstChild) {\n styleElement.removeChild(styleElement.firstChild);\n }\n\n styleElement.appendChild(document.createTextNode(css));\n }\n}\n\nmodule.exports = styleTagTransform;\n\n//# sourceURL=webpack://todo-list/./node_modules/style-loader/dist/runtime/styleTagTransform.js?"); 120 | 121 | /***/ }), 122 | 123 | /***/ "./src/components/ClearAllButton.js": 124 | /*!******************************************!*\ 125 | !*** ./src/components/ClearAllButton.js ***! 126 | \******************************************/ 127 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 128 | 129 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nconst btnClearAllCompleted = () => {\n const btnClearCompleted = document.createElement('button');\n btnClearCompleted.setAttribute('type', 'button');\n btnClearCompleted.setAttribute('name', 'btnClearCompleted');\n btnClearCompleted.innerText = 'Clear all completed';\n btnClearCompleted.classList.add('btn', 'btn-clear-completed');\n\n return btnClearCompleted;\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (btnClearAllCompleted);\n\n//# sourceURL=webpack://todo-list/./src/components/ClearAllButton.js?"); 130 | 131 | /***/ }), 132 | 133 | /***/ "./src/components/CreateNewTaskItem.js": 134 | /*!*********************************************!*\ 135 | !*** ./src/components/CreateNewTaskItem.js ***! 136 | \*********************************************/ 137 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 138 | 139 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _modules_Task_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../modules/Task.js */ \"./src/modules/Task.js\");\n/* harmony import */ var _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../modules/DataStore.js */ \"./src/modules/DataStore.js\");\n/* harmony import */ var _TaskListItem_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./TaskListItem.js */ \"./src/components/TaskListItem.js\");\n\n\n\n\n\nconst formAddTask = () => {\n const form = document.createElement('form');\n form.id = 'form-add-task';\n form.innerHTML = `\n `;\n\n form.addEventListener('submit', (event) => {\n event.preventDefault();\n\n if (localStorage.getItem('tasks') === 'undefined') {\n localStorage.setItem('tasks', JSON.stringify([]));\n }\n\n _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"].tasks = JSON.parse(localStorage.getItem('tasks'));\n\n const { newTask } = form.elements;\n const task = new _modules_Task_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"](newTask.value, false, _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"].tasks.length + 1);\n task.addTask();\n (0,_TaskListItem_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(task);\n (0,_TaskListItem_js__WEBPACK_IMPORTED_MODULE_2__.refreshTaskList)();\n newTask.value = '';\n newTask.style.focus = false;\n });\n return form;\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (formAddTask);\n\n//# sourceURL=webpack://todo-list/./src/components/CreateNewTaskItem.js?"); 140 | 141 | /***/ }), 142 | 143 | /***/ "./src/components/HeadingTaskListItem.js": 144 | /*!***********************************************!*\ 145 | !*** ./src/components/HeadingTaskListItem.js ***! 146 | \***********************************************/ 147 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 148 | 149 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"checkRefreshButtonEvent\": () => (/* binding */ checkRefreshButtonEvent),\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _TaskListItem_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./TaskListItem.js */ \"./src/components/TaskListItem.js\");\n\n\nconst todoListHeading = () => {\n const todoHeading = document.createElement('li');\n todoHeading.innerHTML = `

Today's Tasks

\n `;\n todoHeading.classList.add('todo-heading');\n\n return todoHeading;\n};\n\nconst checkRefreshButtonEvent = () => {\n const btnRefreshList = document.querySelector('.btn-refresh');\n\n btnRefreshList.addEventListener('click', (event) => {\n event.stopPropagation();\n\n (0,_TaskListItem_js__WEBPACK_IMPORTED_MODULE_0__.refreshTaskList)();\n });\n\n (0,_TaskListItem_js__WEBPACK_IMPORTED_MODULE_0__.clearAllCompletedTasksEvent)();\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (todoListHeading);\n\n//# sourceURL=webpack://todo-list/./src/components/HeadingTaskListItem.js?"); 150 | 151 | /***/ }), 152 | 153 | /***/ "./src/components/TaskListItem.js": 154 | /*!****************************************!*\ 155 | !*** ./src/components/TaskListItem.js ***! 156 | \****************************************/ 157 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 158 | 159 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"refreshTaskList\": () => (/* binding */ refreshTaskList),\n/* harmony export */ \"isTaskCompleted\": () => (/* binding */ isTaskCompleted),\n/* harmony export */ \"isTaskClicked\": () => (/* binding */ isTaskClicked),\n/* harmony export */ \"checkTaskDragEvents\": () => (/* binding */ checkTaskDragEvents),\n/* harmony export */ \"clearAllCompletedTasksEvent\": () => (/* binding */ clearAllCompletedTasksEvent),\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../modules/DataStore.js */ \"./src/modules/DataStore.js\");\n/* harmony import */ var _modules_Task_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../modules/Task.js */ \"./src/modules/Task.js\");\n\n\n\nconst refreshTaskList = () => {\n const tasks = document.querySelectorAll('.task');\n tasks.forEach((prevTask) => {\n prevTask.parentNode.removeChild(prevTask);\n });\n\n _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"].tasks = _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"].updateTaskIndex();\n _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"].tasks.forEach((task) => {\n // eslint-disable-next-line\n renderTaskItem(task);\n });\n};\n\nconst isTaskCompleted = () => {\n const completedTasks = document.querySelectorAll('input[name=\"isCompleted\"]');\n completedTasks.forEach((btnIsCompleted) => {\n btnIsCompleted.addEventListener('click', () => {\n if (btnIsCompleted.checked) {\n btnIsCompleted.checked = true;\n btnIsCompleted.parentNode.nextSibling.nextSibling.style.textDecoration = 'line-through';\n } else {\n btnIsCompleted.checked = false;\n btnIsCompleted.parentNode.nextSibling.nextSibling.style.textDecoration = '';\n }\n });\n });\n};\n\nconst isTaskClicked = () => {\n const allTasks = document.querySelectorAll('.task');\n\n allTasks.forEach((task) => {\n const newTaskDescription = ['', ''];\n const taskDescription = task.childNodes[0].childNodes[2];\n const btnEllipsis = task.childNodes[2];\n const btnDeleteTask = task.childNodes[4];\n\n task.addEventListener('click', () => {\n newTaskDescription[0] = taskDescription.innerText;\n newTaskDescription.push(parseInt(task.id, 10));\n taskDescription.contentEditable = true;\n taskDescription.focus();\n\n // The following two lines of code are used to move the cursor\n // to the end of existing text in contentEditable HTML5 elements.\n document.execCommand('selectAll', false, null);\n document.getSelection().collapseToEnd();\n\n taskDescription.style.outline = 'none';\n task.style.backgroundColor = 'lightgreen';\n taskDescription.style.color = 'blue';\n btnEllipsis.classList.add('d-off');\n btnDeleteTask.classList.remove('d-off');\n\n if (!btnDeleteTask.classList.contains('d-off')) {\n btnDeleteTask.addEventListener('click', (event) => {\n event.stopImmediatePropagation();\n\n const task = btnDeleteTask.parentNode;\n const targetTask = new _modules_Task_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"]();\n targetTask.removeTask(parseInt(task.id, 10));\n task.parentNode.removeChild(task);\n refreshTaskList();\n });\n }\n\n allTasks.forEach((inactiveTask) => {\n if (task !== inactiveTask) {\n inactiveTask.childNodes[0].childNodes[2].contentEditable = false;\n inactiveTask.style.backgroundColor = '#fff';\n inactiveTask.childNodes[0].childNodes[2].style.color = '#000';\n inactiveTask.childNodes[0].childNodes[2].style.opacity = '0.8';\n inactiveTask.childNodes[0].childNodes[2].style.border = 'none';\n inactiveTask.childNodes[4].classList.add('d-off');\n inactiveTask.childNodes[2].classList.remove('d-off');\n }\n });\n });\n\n taskDescription.addEventListener('input', () => {\n newTaskDescription[1] = task.childNodes[0].childNodes[2].innerText;\n });\n\n taskDescription.addEventListener('focusout', () => {\n if (newTaskDescription[0] !== newTaskDescription[1] && newTaskDescription[1] !== '') {\n const targetTask = new _modules_Task_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"]();\n targetTask.editTask(newTaskDescription[1], parseInt(task.id, 10) - 1);\n newTaskDescription.length = 2;\n }\n });\n });\n};\n\nconst dragDropIndices = [];\nconst checkTaskDragEvents = () => {\n const taskItems = document.querySelectorAll('.task');\n\n taskItems.forEach((task) => {\n task.addEventListener('dragstart', () => {\n if (dragDropIndices.length === 0) {\n dragDropIndices.push(parseInt(task.id, 10) - 1);\n }\n });\n\n task.addEventListener('dragover', (event) => {\n event.preventDefault();\n\n task.classList.add('over');\n });\n\n task.addEventListener('drop', (event) => {\n event.stopImmediatePropagation();\n\n dragDropIndices.push(parseInt(task.id, 10) - 1);\n task.classList.remove('over');\n _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"].swapTasks(dragDropIndices[0], dragDropIndices[1]);\n refreshTaskList();\n }, (function runAfterAll() {\n dragDropIndices.length = 0;\n }()));\n\n task.addEventListener('dragleave', () => {\n task.classList.remove('over');\n });\n });\n};\n\nconst clearAllCompletedTasksEvent = () => {\n const btnClearAllCompleted = document.querySelector('.btn-clear-completed');\n\n btnClearAllCompleted.addEventListener('click', (event) => {\n event.stopPropagation();\n\n let completedTasksArray = [];\n const completedTasks = document.querySelectorAll('input[name=\"isCompleted\"]');\n [...completedTasks].filter((btnChecked) => btnChecked.checked === true);\n completedTasks.forEach((btnCheck) => {\n if (btnCheck.checked === true) {\n const targetTaskItem = btnCheck.parentNode.parentNode.parentNode;\n completedTasksArray.push(targetTaskItem);\n }\n });\n\n completedTasksArray.forEach((item) => {\n const targetTask = new _modules_Task_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"]();\n targetTask.removeTask(parseInt(item.id, 10));\n item.parentNode.removeChild(item);\n }, (function runAfterAll() {\n completedTasksArray = [];\n refreshTaskList();\n }()));\n });\n};\n\nconst renderTaskItem = (task) => {\n if (!task) return;\n\n const tasksContainer = document.querySelector('.todo-list');\n const taskItem = document.createElement('li');\n taskItem.innerHTML = `
\n
\n

${task.description}

\n \n `;\n taskItem.id = task.index;\n taskItem.setAttribute('draggable', 'true');\n taskItem.classList.add('task');\n\n tasksContainer.appendChild(taskItem);\n\n isTaskClicked();\n checkTaskDragEvents();\n isTaskCompleted();\n clearAllCompletedTasksEvent();\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (renderTaskItem);\n\n//# sourceURL=webpack://todo-list/./src/components/TaskListItem.js?"); 160 | 161 | /***/ }), 162 | 163 | /***/ "./src/index.js": 164 | /*!**********************!*\ 165 | !*** ./src/index.js ***! 166 | \**********************/ 167 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 168 | 169 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _assets_css_style_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/css/style.css */ \"./src/assets/css/style.css\");\n/* harmony import */ var _assets_img_todo_list_desktop_v_snapshot_png__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./assets/img/todo-list-desktop-v-snapshot.png */ \"./src/assets/img/todo-list-desktop-v-snapshot.png\");\n/* harmony import */ var _modules_Task_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./modules/Task.js */ \"./src/modules/Task.js\");\n/* harmony import */ var _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./modules/DataStore.js */ \"./src/modules/DataStore.js\");\n/* harmony import */ var _components_HeadingTaskListItem_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./components/HeadingTaskListItem.js */ \"./src/components/HeadingTaskListItem.js\");\n/* harmony import */ var _components_TaskListItem_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./components/TaskListItem.js */ \"./src/components/TaskListItem.js\");\n/* harmony import */ var _components_CreateNewTaskItem_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./components/CreateNewTaskItem.js */ \"./src/components/CreateNewTaskItem.js\");\n/* harmony import */ var _components_ClearAllButton_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./components/ClearAllButton.js */ \"./src/components/ClearAllButton.js\");\n\n\n\n\n\n\n\n\n\n\nwindow.addEventListener('DOMContentLoaded', () => {\n const todoList = document.querySelector('.todo-list');\n todoList.appendChild((0,_components_HeadingTaskListItem_js__WEBPACK_IMPORTED_MODULE_4__[\"default\"])());\n todoList.appendChild((0,_components_CreateNewTaskItem_js__WEBPACK_IMPORTED_MODULE_6__[\"default\"])());\n\n if (localStorage.getItem('tasks') === 'undefined' || localStorage.getItem('tasks') === null) {\n localStorage.setItem('tasks', JSON.stringify([]));\n }\n\n _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_3__[\"default\"].tasks = JSON.parse(localStorage.getItem('tasks'));\n\n _modules_DataStore_js__WEBPACK_IMPORTED_MODULE_3__[\"default\"].tasks.forEach((task) => {\n if (task?.description === '') {\n const taskToBeRemoved = new _modules_Task_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"]();\n taskToBeRemoved.removeTask(parseInt(task.index, 10));\n (0,_components_TaskListItem_js__WEBPACK_IMPORTED_MODULE_5__.refreshTaskList)();\n } else (0,_components_TaskListItem_js__WEBPACK_IMPORTED_MODULE_5__[\"default\"])(task);\n });\n\n todoList.parentNode.appendChild((0,_components_ClearAllButton_js__WEBPACK_IMPORTED_MODULE_7__[\"default\"])());\n\n (0,_components_HeadingTaskListItem_js__WEBPACK_IMPORTED_MODULE_4__.checkRefreshButtonEvent)();\n});\n\n//# sourceURL=webpack://todo-list/./src/index.js?"); 170 | 171 | /***/ }), 172 | 173 | /***/ "./src/modules/DataStore.js": 174 | /*!**********************************!*\ 175 | !*** ./src/modules/DataStore.js ***! 176 | \**********************************/ 177 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 178 | 179 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ DataStore)\n/* harmony export */ });\nclass DataStore {\n static constructor() {\n this.tasks = [];\n }\n\n static getTasks() {\n this.tasks = JSON.parse(localStorage.getItem('tasks'));\n return JSON.parse(localStorage.getItem('tasks'));\n }\n\n static saveTasks(task) {\n this.tasks.push(task);\n localStorage.setItem('tasks', JSON.stringify(this.tasks));\n }\n\n static editTask(newDescription, index) {\n this.tasks[index].description = newDescription;\n localStorage.setItem('tasks', JSON.stringify(this.tasks));\n }\n\n static deleteTask(index) {\n this.tasks = this.tasks.filter((task) => task.index !== index);\n localStorage.setItem('tasks', JSON.stringify(this.tasks));\n }\n\n static updateTaskIndex() {\n for (let i = 0; i < this.tasks.length; i += 1) {\n this.tasks[i].index = i + 1;\n }\n\n localStorage.setItem('tasks', JSON.stringify(this.tasks));\n\n return this.tasks;\n }\n\n static swapTasks(taskOneIndex, taskTwoIndex) {\n const temp = this.tasks[taskTwoIndex];\n this.tasks[taskTwoIndex] = this.tasks[taskOneIndex];\n this.tasks[taskOneIndex] = temp;\n\n DataStore.updateTaskIndex();\n }\n}\n\n//# sourceURL=webpack://todo-list/./src/modules/DataStore.js?"); 180 | 181 | /***/ }), 182 | 183 | /***/ "./src/modules/Task.js": 184 | /*!*****************************!*\ 185 | !*** ./src/modules/Task.js ***! 186 | \*****************************/ 187 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 188 | 189 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Task)\n/* harmony export */ });\n/* harmony import */ var _DataStore_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./DataStore.js */ \"./src/modules/DataStore.js\");\n\n\nclass Task {\n constructor(description, isCompleted = false, index) {\n this.description = description;\n this.isCompleted = isCompleted;\n this.index = index;\n }\n\n addTask() {\n _DataStore_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"].saveTasks(this);\n }\n\n removeTask(index) {\n this.index = index;\n _DataStore_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"].deleteTask(this.index);\n }\n\n editTask(description, index) {\n this.index = index;\n _DataStore_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"].editTask(description, this.index);\n }\n}\n\n//# sourceURL=webpack://todo-list/./src/modules/Task.js?"); 190 | 191 | /***/ }), 192 | 193 | /***/ "./src/assets/fonts/cocogoose-pro-regular.ttf": 194 | /*!****************************************************!*\ 195 | !*** ./src/assets/fonts/cocogoose-pro-regular.ttf ***! 196 | \****************************************************/ 197 | /***/ ((module, __unused_webpack_exports, __webpack_require__) => { 198 | 199 | eval("module.exports = __webpack_require__.p + \"6731ef40f4416b5c9069.ttf\";\n\n//# sourceURL=webpack://todo-list/./src/assets/fonts/cocogoose-pro-regular.ttf?"); 200 | 201 | /***/ }), 202 | 203 | /***/ "./src/assets/img/todo-list-desktop-v-snapshot.png": 204 | /*!*********************************************************!*\ 205 | !*** ./src/assets/img/todo-list-desktop-v-snapshot.png ***! 206 | \*********************************************************/ 207 | /***/ ((module, __unused_webpack_exports, __webpack_require__) => { 208 | 209 | eval("module.exports = __webpack_require__.p + \"a0d3b87095ec6aee15f3.png\";\n\n//# sourceURL=webpack://todo-list/./src/assets/img/todo-list-desktop-v-snapshot.png?"); 210 | 211 | /***/ }) 212 | 213 | /******/ }); 214 | /************************************************************************/ 215 | /******/ // The module cache 216 | /******/ var __webpack_module_cache__ = {}; 217 | /******/ 218 | /******/ // The require function 219 | /******/ function __webpack_require__(moduleId) { 220 | /******/ // Check if module is in cache 221 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 222 | /******/ if (cachedModule !== undefined) { 223 | /******/ return cachedModule.exports; 224 | /******/ } 225 | /******/ // Create a new module (and put it into the cache) 226 | /******/ var module = __webpack_module_cache__[moduleId] = { 227 | /******/ id: moduleId, 228 | /******/ // no module.loaded needed 229 | /******/ exports: {} 230 | /******/ }; 231 | /******/ 232 | /******/ // Execute the module function 233 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 234 | /******/ 235 | /******/ // Return the exports of the module 236 | /******/ return module.exports; 237 | /******/ } 238 | /******/ 239 | /******/ // expose the modules object (__webpack_modules__) 240 | /******/ __webpack_require__.m = __webpack_modules__; 241 | /******/ 242 | /************************************************************************/ 243 | /******/ /* webpack/runtime/compat get default export */ 244 | /******/ (() => { 245 | /******/ // getDefaultExport function for compatibility with non-harmony modules 246 | /******/ __webpack_require__.n = (module) => { 247 | /******/ var getter = module && module.__esModule ? 248 | /******/ () => (module['default']) : 249 | /******/ () => (module); 250 | /******/ __webpack_require__.d(getter, { a: getter }); 251 | /******/ return getter; 252 | /******/ }; 253 | /******/ })(); 254 | /******/ 255 | /******/ /* webpack/runtime/define property getters */ 256 | /******/ (() => { 257 | /******/ // define getter functions for harmony exports 258 | /******/ __webpack_require__.d = (exports, definition) => { 259 | /******/ for(var key in definition) { 260 | /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { 261 | /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); 262 | /******/ } 263 | /******/ } 264 | /******/ }; 265 | /******/ })(); 266 | /******/ 267 | /******/ /* webpack/runtime/global */ 268 | /******/ (() => { 269 | /******/ __webpack_require__.g = (function() { 270 | /******/ if (typeof globalThis === 'object') return globalThis; 271 | /******/ try { 272 | /******/ return this || new Function('return this')(); 273 | /******/ } catch (e) { 274 | /******/ if (typeof window === 'object') return window; 275 | /******/ } 276 | /******/ })(); 277 | /******/ })(); 278 | /******/ 279 | /******/ /* webpack/runtime/hasOwnProperty shorthand */ 280 | /******/ (() => { 281 | /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) 282 | /******/ })(); 283 | /******/ 284 | /******/ /* webpack/runtime/make namespace object */ 285 | /******/ (() => { 286 | /******/ // define __esModule on exports 287 | /******/ __webpack_require__.r = (exports) => { 288 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 289 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 290 | /******/ } 291 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 292 | /******/ }; 293 | /******/ })(); 294 | /******/ 295 | /******/ /* webpack/runtime/publicPath */ 296 | /******/ (() => { 297 | /******/ var scriptUrl; 298 | /******/ if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + ""; 299 | /******/ var document = __webpack_require__.g.document; 300 | /******/ if (!scriptUrl && document) { 301 | /******/ if (document.currentScript) 302 | /******/ scriptUrl = document.currentScript.src 303 | /******/ if (!scriptUrl) { 304 | /******/ var scripts = document.getElementsByTagName("script"); 305 | /******/ if(scripts.length) scriptUrl = scripts[scripts.length - 1].src 306 | /******/ } 307 | /******/ } 308 | /******/ // When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration 309 | /******/ // or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic. 310 | /******/ if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser"); 311 | /******/ scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/"); 312 | /******/ __webpack_require__.p = scriptUrl; 313 | /******/ })(); 314 | /******/ 315 | /******/ /* webpack/runtime/jsonp chunk loading */ 316 | /******/ (() => { 317 | /******/ __webpack_require__.b = document.baseURI || self.location.href; 318 | /******/ 319 | /******/ // object to store loaded and loading chunks 320 | /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched 321 | /******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded 322 | /******/ var installedChunks = { 323 | /******/ "main": 0 324 | /******/ }; 325 | /******/ 326 | /******/ // no chunk on demand loading 327 | /******/ 328 | /******/ // no prefetching 329 | /******/ 330 | /******/ // no preloaded 331 | /******/ 332 | /******/ // no HMR 333 | /******/ 334 | /******/ // no HMR manifest 335 | /******/ 336 | /******/ // no on chunks loaded 337 | /******/ 338 | /******/ // no jsonp function 339 | /******/ })(); 340 | /******/ 341 | /************************************************************************/ 342 | /******/ 343 | /******/ // startup 344 | /******/ // Load entry module and return exports 345 | /******/ // This entry module can't be inlined because the eval devtool is used. 346 | /******/ var __webpack_exports__ = __webpack_require__("./src/index.js"); 347 | /******/ 348 | /******/ })() 349 | ; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-list", 3 | "jest": { 4 | "verbose": true, 5 | "resetMocks": false, 6 | "setupFiles": [ 7 | "jest-localstorage-mock" 8 | ], 9 | "testEnvironment": "jsdom" 10 | }, 11 | "version": "1.0.0", 12 | "description": "", 13 | "private": true, 14 | "scripts": { 15 | "test": "jest --coverage", 16 | "watch": "jest --watch *.js", 17 | "start": "webpack serve --open", 18 | "build": "webpack", 19 | "predeploy": "npm run build", 20 | "deploy": "gh-pages -d dist" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/gtekle/todo-list.git" 25 | }, 26 | "keywords": [], 27 | "author": "", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/gtekle/todo-list/issues" 31 | }, 32 | "homepage": "https://github.com/gtekle/todo-list#readme", 33 | "devDependencies": { 34 | "@babel/plugin-transform-modules-commonjs": "^7.16.8", 35 | "babel-eslint": "^10.1.0", 36 | "css-loader": "^6.5.1", 37 | "enzyme": "^3.11.0", 38 | "enzyme-adapter-react-16": "^1.15.6", 39 | "eslint": "^7.32.0", 40 | "eslint-config-airbnb-base": "^14.2.1", 41 | "eslint-plugin-import": "^2.25.4", 42 | "hint": "^6.1.9", 43 | "html-webpack-plugin": "^5.5.0", 44 | "jest": "^27.4.7", 45 | "jest-localstorage-mock": "^2.4.18", 46 | "style-loader": "^3.3.1", 47 | "stylelint": "^14.2.0", 48 | "stylelint-config-standard": "^21.0.0", 49 | "stylelint-csstree-validator": "^1.9.0", 50 | "stylelint-scss": "^3.21.0", 51 | "webpack": "^5.65.0", 52 | "webpack-cli": "^4.9.1", 53 | "webpack-dev-server": "^4.7.3" 54 | }, 55 | "dependencies": { 56 | "gh-pages": "^3.2.3", 57 | "jquery": "^3.6.0" 58 | } 59 | } -------------------------------------------------------------------------------- /src/assets/css/style.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | @font-face { 8 | font-family: cocogoose; 9 | src: url('../fonts/cocogoose-pro-regular.ttf'); 10 | } 11 | 12 | body { 13 | background-color: #ddd; 14 | margin: 0; 15 | font-family: Arial, Helvetica, sans-serif; 16 | } 17 | 18 | .btn { 19 | cursor: pointer; 20 | border: none; 21 | background-color: #fff; 22 | margin: 0; 23 | } 24 | 25 | #header, 26 | #footer { 27 | background-color: darkgray; 28 | width: 100%; 29 | height: 5px; 30 | padding: 0; 31 | margin: 0; 32 | } 33 | 34 | #main { 35 | max-width: 94%; 36 | min-height: calc(100vh - 64px); 37 | margin: 12px auto; 38 | text-align: center; 39 | } 40 | 41 | .main-section-title { 42 | color: darkslategray; 43 | font-family: cocogoose, sans-serif; 44 | } 45 | 46 | .todo-list { 47 | list-style: none; 48 | text-align: center; 49 | padding: 0; 50 | background-color: #d3d3d3; 51 | margin: 0; 52 | box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, 0.2); 53 | } 54 | 55 | .todo-heading { 56 | display: flex; 57 | justify-content: space-between; 58 | } 59 | 60 | .todo-heading h2 { 61 | color: darkgray; 62 | font-size: 20px; 63 | line-height: 24px; 64 | font-weight: 700; 65 | } 66 | 67 | .todo-heading .btn-refresh { 68 | text-align: right; 69 | padding: 0; 70 | margin-right: 10px; 71 | } 72 | 73 | .todo-heading i { 74 | color: darkgray; 75 | font-size: 22px; 76 | font-weight: 500; 77 | } 78 | 79 | .btn i:hover { 80 | color: black; 81 | } 82 | 83 | .btn i:active { 84 | color: blueviolet; 85 | } 86 | 87 | #form-add-task { 88 | display: flex; 89 | width: 100%; 90 | text-align: justify; 91 | background-color: #fff; 92 | border-bottom: 1px solid gray; 93 | } 94 | 95 | #form-add-task input { 96 | width: calc(100% - 43px); 97 | margin: 0; 98 | font-size: 17px; 99 | line-height: 32px; 100 | padding: 3px 10px; 101 | font-style: italic; 102 | border: none; 103 | outline: none; 104 | } 105 | 106 | #form-add-task .btn-add-task { 107 | display: flex; 108 | flex-direction: column; 109 | justify-content: center; 110 | align-items: center; 111 | color: grey; 112 | font-weight: 700; 113 | vertical-align: text-top; 114 | font-size: 16px; 115 | line-height: 24px; 116 | } 117 | 118 | .todo-list li { 119 | display: flex; 120 | border-bottom: 1px solid #bbb; 121 | justify-content: space-between; 122 | align-items: center; 123 | background-color: #fff; 124 | padding-left: 10px; 125 | font-size: 16px; 126 | font-weight: 500; 127 | cursor: text; 128 | } 129 | 130 | input[name="isCompleted"] { 131 | cursor: pointer; 132 | } 133 | 134 | .item-description { 135 | display: flex; 136 | align-items: center; 137 | font-size: 18px; 138 | width: 100%; 139 | } 140 | 141 | .todo-list li .task-description { 142 | color: rgba(0, 0, 0, 0.8); 143 | width: 100%; 144 | margin: 0; 145 | padding: 10px; 146 | font-size: 16px; 147 | font-weight: 500; 148 | text-align: left; 149 | } 150 | 151 | .todo-list .fa-ellipsis-v { 152 | color: grey; 153 | font-size: 16px; 154 | padding: 5px; 155 | cursor: move; 156 | z-index: 10; 157 | } 158 | 159 | .todo-list .btn-v-ellipsis { 160 | background-color: transparent; 161 | z-index: 100; 162 | } 163 | 164 | .todo-list .btn-trash { 165 | background-color: transparent; 166 | z-index: 110; 167 | } 168 | 169 | .todo-list .fa-trash { 170 | color: red; 171 | font-size: 16px; 172 | padding: 5px; 173 | } 174 | 175 | .todo-list .fa-ellipsis-v:hover { 176 | color: black; 177 | } 178 | 179 | .todo-list .over { 180 | background-color: #cababa; 181 | } 182 | 183 | .todo-list li > * { 184 | margin-right: 10px; 185 | } 186 | 187 | .btn-clear-completed { 188 | padding: 6px; 189 | margin: 10px; 190 | font-size: 16px; 191 | background-color: transparent; 192 | font-weight: 700; 193 | color: darkslategray; 194 | } 195 | 196 | .d-off { 197 | display: none; 198 | } 199 | 200 | @media screen and (min-width: 768px) { 201 | #main { 202 | max-width: 40%; 203 | min-height: calc(100vh - 88px); 204 | margin: 24px auto; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/assets/fonts/cocogoose-pro-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtekle/todo-list/66daf5675b29b0ae9965acaef53895127ddec5e8/src/assets/fonts/cocogoose-pro-regular.ttf -------------------------------------------------------------------------------- /src/assets/img/todo-list-desktop-v-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtekle/todo-list/66daf5675b29b0ae9965acaef53895127ddec5e8/src/assets/img/todo-list-desktop-v-snapshot.png -------------------------------------------------------------------------------- /src/components/ClearAllButton.js: -------------------------------------------------------------------------------- 1 | const btnClearAllCompleted = () => { 2 | const btnClearCompleted = document.createElement('button'); 3 | btnClearCompleted.setAttribute('type', 'button'); 4 | btnClearCompleted.setAttribute('name', 'btnClearCompleted'); 5 | btnClearCompleted.innerText = 'Clear all completed'; 6 | btnClearCompleted.classList.add('btn', 'btn-clear-completed'); 7 | 8 | return btnClearCompleted; 9 | }; 10 | 11 | export default btnClearAllCompleted; -------------------------------------------------------------------------------- /src/components/CreateNewTaskItem.js: -------------------------------------------------------------------------------- 1 | import Task from '../modules/Task.js'; 2 | import DataStore from '../modules/DataStore.js'; 3 | 4 | import renderTaskItem, { refreshTaskList } from './TaskListItem.js'; 5 | 6 | const formAddTask = () => { 7 | const form = document.createElement('form'); 8 | form.id = 'form-add-task'; 9 | form.innerHTML = ` 10 | `; 13 | 14 | form.addEventListener('submit', (event) => { 15 | event.preventDefault(); 16 | 17 | if (localStorage.getItem('tasks') === 'undefined') { 18 | localStorage.setItem('tasks', JSON.stringify([])); 19 | } 20 | 21 | DataStore.tasks = JSON.parse(localStorage.getItem('tasks')); 22 | 23 | const { newTask } = form.elements; 24 | const task = new Task(newTask.value, false, DataStore.tasks.length + 1); 25 | task.addTask(); 26 | renderTaskItem(task); 27 | refreshTaskList(); 28 | newTask.value = ''; 29 | newTask.style.focus = false; 30 | }); 31 | return form; 32 | }; 33 | 34 | export default formAddTask; -------------------------------------------------------------------------------- /src/components/HeadingTaskListItem.js: -------------------------------------------------------------------------------- 1 | import { refreshTaskList, clearAllCompletedTasksEvent } from './TaskListItem.js'; 2 | 3 | const todoListHeading = () => { 4 | const todoHeading = document.createElement('li'); 5 | todoHeading.innerHTML = `

Today's Tasks

6 | `; 8 | todoHeading.classList.add('todo-heading'); 9 | 10 | return todoHeading; 11 | }; 12 | 13 | export const checkRefreshButtonEvent = () => { 14 | const btnRefreshList = document.querySelector('.btn-refresh'); 15 | 16 | btnRefreshList.addEventListener('click', (event) => { 17 | event.stopPropagation(); 18 | 19 | refreshTaskList(); 20 | }); 21 | 22 | clearAllCompletedTasksEvent(); 23 | }; 24 | 25 | export default todoListHeading; -------------------------------------------------------------------------------- /src/components/TaskListItem.js: -------------------------------------------------------------------------------- 1 | import DataStore from '../modules/DataStore.js'; 2 | import Task from '../modules/Task.js'; 3 | 4 | export const refreshTaskList = () => { 5 | const tasks = document.querySelectorAll('.task'); 6 | tasks.forEach((prevTask) => { 7 | prevTask.parentNode.removeChild(prevTask); 8 | }); 9 | 10 | DataStore.tasks = DataStore.updateTaskIndex(); 11 | DataStore.tasks.forEach((task) => { 12 | // eslint-disable-next-line 13 | renderTaskItem(task); 14 | }); 15 | }; 16 | 17 | export const isTaskCompleted = () => { 18 | const completedTasks = document.querySelectorAll('input[name="isCompleted"]'); 19 | completedTasks.forEach((btnIsCompleted) => { 20 | btnIsCompleted.addEventListener('click', () => { 21 | if (btnIsCompleted.checked) { 22 | btnIsCompleted.checked = true; 23 | btnIsCompleted.parentNode.nextSibling.nextSibling.style.textDecoration = 'line-through'; 24 | } else { 25 | btnIsCompleted.checked = false; 26 | btnIsCompleted.parentNode.nextSibling.nextSibling.style.textDecoration = ''; 27 | } 28 | }); 29 | }); 30 | }; 31 | 32 | export const isTaskClicked = () => { 33 | const allTasks = document.querySelectorAll('.task'); 34 | 35 | allTasks.forEach((task) => { 36 | const newTaskDescription = ['', '']; 37 | 38 | const [descriptionDiv, , btnMove, , btnDelete] = task.childNodes; 39 | const [, , description] = descriptionDiv.childNodes; 40 | 41 | task.addEventListener('click', () => { 42 | newTaskDescription[0] = description.innerText; 43 | newTaskDescription.push(parseInt(task.id, 10)); 44 | description.contentEditable = true; 45 | description.focus(); 46 | 47 | // The following two lines of code are used to move the cursor 48 | // to the end of existing text in contentEditable HTML5 elements. 49 | document.execCommand('selectAll', false, null); 50 | document.getSelection().collapseToEnd(); 51 | 52 | task.style.backgroundColor = 'lightgreen'; 53 | description.style.outline = 'none'; 54 | description.style.color = 'blue'; 55 | btnMove.classList.add('d-off'); 56 | btnDelete.classList.remove('d-off'); 57 | 58 | if (!btnDelete.classList.contains('d-off')) { 59 | btnDelete.addEventListener('click', (event) => { 60 | event.stopImmediatePropagation(); 61 | 62 | const task = btnDelete.parentNode; 63 | const targetTask = new Task(); 64 | targetTask.removeTask(parseInt(task.id, 10)); 65 | task.parentNode.removeChild(task); 66 | refreshTaskList(); 67 | }); 68 | } 69 | 70 | allTasks.forEach((inactiveTask) => { 71 | if (task !== inactiveTask) { 72 | const [descriptionDiv, , btnMove, , btnDelete] = inactiveTask.childNodes; 73 | const [, , description] = descriptionDiv.childNodes; 74 | 75 | inactiveTask.style.backgroundColor = '#fff'; 76 | description.contentEditable = false; 77 | description.style.color = '#000'; 78 | description.style.opacity = '0.8'; 79 | description.style.border = 'none'; 80 | btnDelete.classList.add('d-off'); 81 | btnMove.classList.remove('d-off'); 82 | } 83 | }); 84 | }); 85 | 86 | description.addEventListener('input', () => { 87 | newTaskDescription[1] = description.innerText; 88 | }); 89 | 90 | description.addEventListener('focusout', () => { 91 | if (newTaskDescription[0] !== newTaskDescription[1] && newTaskDescription[1] !== '') { 92 | const targetTask = new Task(); 93 | targetTask.editTask(newTaskDescription[1], parseInt(task.id, 10) - 1); 94 | newTaskDescription.length = 2; 95 | } 96 | }); 97 | }); 98 | }; 99 | 100 | const dragDropIndices = []; 101 | export const checkTaskDragEvents = () => { 102 | const taskItems = document.querySelectorAll('.task'); 103 | 104 | taskItems.forEach((task) => { 105 | task.addEventListener('dragstart', () => { 106 | if (dragDropIndices.length === 0) { 107 | dragDropIndices.push(parseInt(task.id, 10) - 1); 108 | } 109 | }); 110 | 111 | task.addEventListener('dragover', (event) => { 112 | event.preventDefault(); 113 | 114 | task.classList.add('over'); 115 | }); 116 | 117 | task.addEventListener('drop', (event) => { 118 | event.stopImmediatePropagation(); 119 | 120 | dragDropIndices.push(parseInt(task.id, 10) - 1); 121 | task.classList.remove('over'); 122 | DataStore.swapTasks(dragDropIndices[0], dragDropIndices[1]); 123 | refreshTaskList(); 124 | }, (function runAfterAll() { 125 | dragDropIndices.length = 0; 126 | }())); 127 | 128 | task.addEventListener('dragleave', () => { 129 | task.classList.remove('over'); 130 | }); 131 | }); 132 | }; 133 | 134 | export const clearAllCompletedTasksEvent = () => { 135 | const btnClearAllCompleted = document.querySelector('.btn-clear-completed'); 136 | 137 | btnClearAllCompleted.addEventListener('click', (event) => { 138 | event.stopImmediatePropagation(); 139 | 140 | let completedTasksArray = []; 141 | const completedTasks = document.querySelectorAll('input[name="isCompleted"]'); 142 | [...completedTasks].filter((btnChecked) => btnChecked.checked === true); 143 | completedTasks.forEach((btnCheck) => { 144 | if (btnCheck.checked === true) { 145 | const targetTaskItem = btnCheck.parentNode.parentNode.parentNode; 146 | completedTasksArray.push(targetTaskItem); 147 | } 148 | }); 149 | 150 | completedTasksArray.forEach((item) => { 151 | const targetListItem = document.getElementById(item.id); 152 | const targetTask = new Task(); 153 | targetListItem.parentNode.removeChild(targetListItem); 154 | targetTask.removeTask(parseInt(item.id, 10)); 155 | }, (function runAfterAll() { 156 | completedTasksArray = []; 157 | refreshTaskList(); 158 | }())); 159 | }); 160 | }; 161 | 162 | const renderTaskItem = (task) => { 163 | if (!task) return; 164 | 165 | const tasksContainer = document.querySelector('.todo-list'); 166 | const taskItem = document.createElement('li'); 167 | taskItem.innerHTML = `
168 |
169 |

${task.description}

170 | 171 | `; 172 | taskItem.id = task.index; 173 | taskItem.setAttribute('draggable', 'true'); 174 | taskItem.classList.add('task'); 175 | 176 | tasksContainer.appendChild(taskItem); 177 | 178 | isTaskClicked(); 179 | checkTaskDragEvents(); 180 | isTaskCompleted(); 181 | clearAllCompletedTasksEvent(); 182 | }; 183 | 184 | export default renderTaskItem; -------------------------------------------------------------------------------- /src/components/TaskListItem.test.js: -------------------------------------------------------------------------------- 1 | import 'jest-localstorage-mock'; 2 | import Task from '../modules/Task.js'; 3 | import DataStore from '../modules/DataStore.js'; 4 | import renderTaskItem, { isTaskCompleted, clearAllCompletedTasksEvent } from './TaskListItem.js'; 5 | import btnClearAllCompleted from './ClearAllButton.js'; 6 | 7 | const $ = require('jquery'); 8 | 9 | const task = new Task('task bone', false, 1); 10 | 11 | beforeAll(() => { 12 | const todoList = document.createElement('ul'); 13 | todoList.classList.add('todo-list'); 14 | document.body.appendChild(todoList); 15 | document.body.appendChild(btnClearAllCompleted()); 16 | DataStore.tasks = []; 17 | }); 18 | 19 | describe('completed status', () => { 20 | test('completed status', () => { 21 | task.addTask(task); 22 | renderTaskItem(task); 23 | const taskStatus = document.getElementById(`chkcompleted-${task.index}`); 24 | const prevTaskStatus = taskStatus.checked; 25 | $(taskStatus).click(); 26 | isTaskCompleted(); 27 | expect(taskStatus.checked).toBe(!prevTaskStatus); 28 | }); 29 | }); 30 | 31 | describe('clear all completed tasks', () => { 32 | test('clear all completed tasks', () => { 33 | task.addTask(task); 34 | renderTaskItem(task); 35 | const taskStatus = document.getElementById(`chkcompleted-${task.index}`); 36 | $(taskStatus).click(); 37 | clearAllCompletedTasksEvent(); 38 | const deletedTask = document.querySelectorAll('li .task'); 39 | expect(deletedTask).toHaveLength(0); 40 | }); 41 | }); -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | To Do List With Webpack 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

TO DO LIST

17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './assets/css/style.css'; 2 | import './assets/img/todo-list-desktop-v-snapshot.png'; 3 | import Task from './modules/Task.js'; 4 | import DataStore from './modules/DataStore.js'; 5 | 6 | import todoListHeading, 7 | { 8 | checkRefreshButtonEvent, 9 | } from './components/HeadingTaskListItem.js'; 10 | import renderTaskItem, { refreshTaskList } from './components/TaskListItem.js'; 11 | import formAddTask from './components/CreateNewTaskItem.js'; 12 | import btnClearAllCompleted from './components/ClearAllButton.js'; 13 | 14 | window.addEventListener('DOMContentLoaded', () => { 15 | const todoList = document.querySelector('.todo-list'); 16 | todoList.appendChild(todoListHeading()); 17 | todoList.appendChild(formAddTask()); 18 | todoList.parentNode.appendChild(btnClearAllCompleted()); 19 | 20 | if (localStorage.getItem('tasks') === 'undefined' || localStorage.getItem('tasks') === null) { 21 | localStorage.setItem('tasks', JSON.stringify([])); 22 | } 23 | 24 | DataStore.tasks = JSON.parse(localStorage.getItem('tasks')); 25 | 26 | DataStore.tasks.forEach((task) => { 27 | if (task?.description === '') { 28 | const taskToBeRemoved = new Task(); 29 | taskToBeRemoved.removeTask(parseInt(task.index, 10)); 30 | refreshTaskList(); 31 | } else renderTaskItem(task); 32 | }); 33 | 34 | checkRefreshButtonEvent(); 35 | }); -------------------------------------------------------------------------------- /src/modules/DataStore.js: -------------------------------------------------------------------------------- 1 | export default class DataStore { 2 | static constructor() { 3 | this.tasks = []; 4 | } 5 | 6 | static getTasks() { 7 | this.tasks = JSON.parse(localStorage.getItem('tasks')); 8 | return JSON.parse(localStorage.getItem('tasks')); 9 | } 10 | 11 | static saveTasks(task) { 12 | this.tasks.push(task); 13 | localStorage.setItem('tasks', JSON.stringify(this.tasks)); 14 | } 15 | 16 | static editTask(newDescription, index) { 17 | this.tasks[index].description = newDescription; 18 | localStorage.setItem('tasks', JSON.stringify(this.tasks)); 19 | } 20 | 21 | static deleteTask(index) { 22 | this.tasks = this.tasks.filter((task) => task.index !== index); 23 | localStorage.setItem('tasks', JSON.stringify(this.tasks)); 24 | DataStore.updateTaskIndex(); 25 | } 26 | 27 | static updateTaskIndex() { 28 | for (let i = 0; i < this.tasks.length; i += 1) { 29 | this.tasks[i].index = i + 1; 30 | } 31 | 32 | localStorage.setItem('tasks', JSON.stringify(this.tasks)); 33 | 34 | return this.tasks; 35 | } 36 | 37 | static swapTasks(taskOneIndex, taskTwoIndex) { 38 | let prevTask = this.tasks[taskOneIndex]; 39 | let i = taskTwoIndex; 40 | 41 | if (taskOneIndex > taskTwoIndex) { 42 | while (i <= taskOneIndex) { 43 | const temp = this.tasks[i]; 44 | this.tasks[i] = prevTask; 45 | prevTask = temp; 46 | i += 1; 47 | } 48 | } else { 49 | while (i >= taskOneIndex) { 50 | const temp = this.tasks[i]; 51 | this.tasks[i] = prevTask; 52 | prevTask = temp; 53 | i -= 1; 54 | } 55 | } 56 | 57 | DataStore.updateTaskIndex(); 58 | } 59 | } -------------------------------------------------------------------------------- /src/modules/Task.js: -------------------------------------------------------------------------------- 1 | import DataStore from './DataStore.js'; 2 | 3 | export default class Task { 4 | constructor(description, isCompleted = false, index) { 5 | this.description = description; 6 | this.isCompleted = isCompleted; 7 | this.index = index; 8 | } 9 | 10 | addTask() { 11 | DataStore.saveTasks(this); 12 | } 13 | 14 | removeTask(index) { 15 | this.index = index; 16 | DataStore.deleteTask(this.index); 17 | } 18 | 19 | editTask(description, index) { 20 | this.index = index; 21 | DataStore.editTask(description, this.index - 1); 22 | } 23 | } -------------------------------------------------------------------------------- /src/modules/Task.test.js: -------------------------------------------------------------------------------- 1 | import Task from './Task.js'; 2 | import DataStore from './DataStore.js'; 3 | import 'jest-localstorage-mock'; 4 | import renderTaskItem from '../components/TaskListItem.js'; 5 | import btnClearAllCompleted from '../components/ClearAllButton.js'; 6 | 7 | const task = new Task('task one', false, 1); 8 | 9 | beforeAll(() => { 10 | const todoList = document.createElement('ul'); 11 | todoList.classList.add('todo-list'); 12 | document.body.appendChild(todoList); 13 | document.body.appendChild(btnClearAllCompleted()); 14 | DataStore.tasks = []; 15 | }); 16 | 17 | describe('add Task', () => { 18 | test('add method', () => { 19 | task.addTask(task); 20 | expect(DataStore.tasks.length).toBe(1); 21 | }); 22 | 23 | test('render task item to the list', () => { 24 | renderTaskItem(task); 25 | const list = document.querySelectorAll('.task'); 26 | expect(list).toHaveLength(1); 27 | }); 28 | }); 29 | 30 | describe('edit Task', () => { 31 | test('editTask method', () => { 32 | task.editTask('task one edited', task.index); 33 | expect(DataStore.tasks[task.index - 1].description).toBe('task one edited'); 34 | }); 35 | 36 | test('render task with edit description in DOM', () => { 37 | const targetTask = document.getElementById(task.index); 38 | targetTask.parentNode.removeChild(targetTask); 39 | renderTaskItem(DataStore.tasks[task.index - 1]); 40 | const updateTaskDescription = document.querySelector('.task .task-description'); 41 | expect(updateTaskDescription.innerHTML).toBe('task one edited'); 42 | }); 43 | }); 44 | 45 | describe('remove Task', () => { 46 | test('removeTask method', () => { 47 | task.removeTask(task.index); 48 | expect(DataStore.tasks.length).toBe(0); 49 | }); 50 | 51 | test('remove task from DOM', () => { 52 | const targetTask = document.getElementById(task.index); 53 | targetTask.parentNode.removeChild(targetTask); 54 | const deletedTask = document.querySelectorAll('li .task'); 55 | expect(deletedTask).toHaveLength(0); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/index.js', 7 | output: { 8 | filename: 'main.js', 9 | path: path.resolve(__dirname, 'dist'), 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.css$/i, 15 | use: ['style-loader', 'css-loader'], 16 | }, 17 | { 18 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 19 | type: 'asset/resource', 20 | }, 21 | { 22 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 23 | type: 'asset/resource', 24 | }, 25 | ], 26 | }, 27 | devServer: { 28 | static: './dist', 29 | }, 30 | plugins: [ 31 | new HtmlWebpackPlugin({ 32 | title: 'Custom template', 33 | template: './src/index.html', 34 | }), 35 | ], 36 | }; --------------------------------------------------------------------------------