├── config ├── db_config.ini └── splitlist.sql ├── assets ├── img │ ├── logo-small.png │ ├── dark-mode │ │ ├── list.png │ │ └── logo.png │ ├── light-mode │ │ ├── list.png │ │ └── logo.png │ ├── create.svg │ ├── share.svg │ └── devices.svg ├── favicons │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── mstile-150x150.png │ ├── android-chrome-192x192.png │ ├── android-chrome-256x256.png │ ├── browserconfig.xml │ ├── site.webmanifest │ ├── README.md │ └── safari-pinned-tab.svg ├── fonts │ ├── OpenSans-Light.woff │ ├── OpenSans-Light.woff2 │ ├── OpenSans-Regular.woff │ ├── OpenSans-Regular.woff2 │ ├── OpenSans-SemiBold.woff │ ├── OpenSans-SemiBold.woff2 │ └── Inconsolata-SemiBold.woff ├── icons │ ├── dark-mode │ │ ├── checkbox-unchecked.svg │ │ ├── add.svg │ │ ├── dark-mode.svg │ │ ├── open.svg │ │ ├── checkbox-checked.svg │ │ ├── edit.svg │ │ ├── remove.svg │ │ └── drag-handle.svg │ ├── light-mode │ │ ├── checkbox-unchecked.svg │ │ ├── open.svg │ │ ├── add.svg │ │ ├── checkbox-checked.svg │ │ ├── edit.svg │ │ ├── dark-mode.svg │ │ ├── remove.svg │ │ └── drag-handle.svg │ ├── always-light │ │ ├── add.svg │ │ └── share.svg │ └── loading.svg ├── js │ ├── theme.js │ ├── index.js │ ├── all.js │ ├── list.js │ └── sortable.js ├── ajax │ ├── setlist.php │ └── getlist.php └── css │ └── style.css ├── Dockerfile ├── new └── index.php ├── README.md ├── index.php └── list └── index.php /config/db_config.ini: -------------------------------------------------------------------------------- 1 | servername= 2 | username= 3 | password= 4 | dbname= -------------------------------------------------------------------------------- /assets/img/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/img/logo-small.png -------------------------------------------------------------------------------- /assets/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/favicons/favicon.ico -------------------------------------------------------------------------------- /assets/img/dark-mode/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/img/dark-mode/list.png -------------------------------------------------------------------------------- /assets/img/dark-mode/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/img/dark-mode/logo.png -------------------------------------------------------------------------------- /assets/img/light-mode/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/img/light-mode/list.png -------------------------------------------------------------------------------- /assets/img/light-mode/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/img/light-mode/logo.png -------------------------------------------------------------------------------- /assets/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/fonts/OpenSans-Light.woff -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/fonts/OpenSans-Light.woff2 -------------------------------------------------------------------------------- /assets/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/fonts/OpenSans-Regular.woff -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/fonts/OpenSans-Regular.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/fonts/OpenSans-SemiBold.woff -------------------------------------------------------------------------------- /assets/fonts/OpenSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/fonts/OpenSans-SemiBold.woff2 -------------------------------------------------------------------------------- /assets/fonts/Inconsolata-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/fonts/Inconsolata-SemiBold.woff -------------------------------------------------------------------------------- /assets/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/favicons/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikkelsvartveit/splitlist/HEAD/assets/favicons/android-chrome-256x256.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0-apache 2 | WORKDIR /var/www/html 3 | ENV DOCKER=true 4 | 5 | RUN docker-php-ext-install mysqli pdo pdo_mysql 6 | 7 | COPY / . 8 | EXPOSE 80 9 | -------------------------------------------------------------------------------- /assets/icons/dark-mode/checkbox-unchecked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/light-mode/checkbox-unchecked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/splitlist.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `list` ( 2 | `idlist` VARCHAR(6) NOT NULL, 3 | `name` VARCHAR(45) NOT NULL, 4 | `lastedited` DATETIME(6) NOT NULL, 5 | `data` MEDIUMTEXT NULL, 6 | PRIMARY KEY (`idlist`)) 7 | ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci; -------------------------------------------------------------------------------- /assets/js/theme.js: -------------------------------------------------------------------------------- 1 | // Sets the theme before the body loads to reduce white "flashing" 2 | if (localStorage.getItem("theme") === "dark") { 3 | document.documentElement.setAttribute("data-theme", "dark"); 4 | document.querySelector("meta[name='theme-color']").setAttribute("content", "#2e3233"); 5 | } -------------------------------------------------------------------------------- /assets/icons/dark-mode/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/dark-mode/dark-mode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/dark-mode/open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/light-mode/open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/always-light/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/light-mode/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/dark-mode/checkbox-checked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/light-mode/checkbox-checked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00aba9 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/dark-mode/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/light-mode/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/light-mode/dark-mode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/dark-mode/remove.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/light-mode/remove.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/dark-mode/drag-handle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/light-mode/drag-handle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/always-light/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Splitlist", 3 | "short_name": "Splitlist", 4 | "icons": [ 5 | { 6 | "src": "/assets/favicons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/favicons/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /assets/icons/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/ajax/setlist.php: -------------------------------------------------------------------------------- 1 | PDO::ERRMODE_EXCEPTION, 21 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, 22 | PDO::ATTR_EMULATE_PREPARES => false 23 | ]; 24 | 25 | try { 26 | $pdo = new PDO($dsn, $username, $password, $options); 27 | } catch (\PDOException $e) { 28 | die("Connection failed: " . $e->getMessage()); 29 | } 30 | 31 | $idlist = $_POST["idlist"]; 32 | $name = $_POST["name"]; 33 | $data = $_POST["data"]; 34 | 35 | try { 36 | $sql = "UPDATE list SET name = ?, lastedited = NOW(6), data = ? WHERE idlist = ?"; 37 | $stmt = $pdo->prepare($sql); 38 | $stmt->execute([$name, $data, $idlist]); 39 | } catch (\PDOException $e) { 40 | echo ("Error: " . $e->getCode() . " - " . $e->getMessage()); 41 | } 42 | ?> 43 | -------------------------------------------------------------------------------- /assets/img/create.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/ajax/getlist.php: -------------------------------------------------------------------------------- 1 | PDO::ERRMODE_EXCEPTION, 21 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, 22 | PDO::ATTR_EMULATE_PREPARES => false 23 | ]; 24 | 25 | try { 26 | $pdo = new PDO($dsn, $username, $password, $options); 27 | } catch (\PDOException $e) { 28 | die("Connection failed: " . $e->getMessage()); 29 | } 30 | 31 | $idlist = $_POST["idlist"]; 32 | $lastedited = $_POST["lastedited"]; 33 | 34 | try { 35 | $sql = "SELECT * FROM list WHERE idlist = ?"; 36 | $stmt = $pdo->prepare($sql); 37 | $stmt->execute([$idlist]); 38 | $row = $stmt->fetch(); 39 | 40 | if ($row["lastedited"] != $lastedited) { 41 | echo json_encode($row); 42 | } else { 43 | echo false; 44 | } 45 | } catch (\PDOException $e) { 46 | echo ("Error: " . $e->getCode() . " - " . $e->getMessage()); 47 | } 48 | ?> 49 | -------------------------------------------------------------------------------- /assets/favicons/README.md: -------------------------------------------------------------------------------- 1 | # Your Favicon Package 2 | 3 | This package was generated with [RealFaviconGenerator](https://realfavicongenerator.net/) [v0.16](https://realfavicongenerator.net/change_log#v0.16) 4 | 5 | ## Install instructions 6 | 7 | To install this package: 8 | 9 | Extract this package in <web site>/assets/favicons/. If your site is http://www.example.com, you should be able to access a file named http://www.example.com/assets/favicons/favicon.ico. 10 | 11 | Insert the following code in the `head` section of your pages: 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | *Optional* - Check your favicon with the [favicon checker](https://realfavicongenerator.net/favicon_checker) -------------------------------------------------------------------------------- /assets/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/img/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /new/index.php: -------------------------------------------------------------------------------- 1 | PDO::ERRMODE_EXCEPTION, 21 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, 22 | PDO::ATTR_EMULATE_PREPARES => false 23 | ]; 24 | 25 | try { 26 | $pdo = new PDO($dsn, $username, $password, $options); 27 | } catch (\PDOException $e) { 28 | die("Connection failed: " . $e->getMessage()); 29 | } 30 | 31 | // Function for generating a random ID 32 | function generateId() { 33 | $characters = "abcdefghijklmnopqrstuvwxyz0123456789"; 34 | $id = ""; 35 | 36 | for ($i = 0; $i < 6; $i++) { 37 | $id .= $characters[random_int(0, strlen($characters) - 1)]; 38 | } 39 | 40 | return $id; 41 | } 42 | 43 | // Generates a new ID and checks that it hasn't already been used in the database 44 | $sql = "SELECT idlist FROM list WHERE idlist = ?"; 45 | $stmt = $pdo->prepare($sql); 46 | do { 47 | $id = generateId(); 48 | $stmt->execute([$id]); 49 | $row = $stmt->fetch(); 50 | } while ($row); 51 | 52 | $name = "Unnamed list"; 53 | $data = '[{"id":0,"text":"Item 1","index":0,"checked":false}]'; 54 | 55 | try { 56 | $sql = "INSERT INTO list (idlist, name, lastedited, data) VALUES(?, ?, NOW(), ?)"; 57 | $stmt = $pdo->prepare($sql); 58 | $stmt->execute([$id, $name, $data]); 59 | 60 | // Redirects to the newly created list 61 | $url = "/list/?id=$id"; 62 | header("Location: $url"); 63 | } catch (\PDOException $e) { 64 | echo ("Error: " . $e->getCode() . " - " . $e->getMessage()); 65 | } 66 | ?> 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Splitlist 2 | 3 | Splitlist is the fastest way to create, share and collaborate on to-do lists, shopping lists and more. 4 | 5 | Visit website here: [list.misva.me](https://list.misva.me) 6 | 7 | ![Splitlist screen dump](https://user-images.githubusercontent.com/30391413/96385267-14195000-1193-11eb-9bac-760c84b76517.png) 8 | 9 | ## What is Splitlist? 10 | 11 | Splitlist is a web app that lets you create lists with the click of a button. After creating a list, you can share it with your friends, family members or coworkers. Anyone with the link can create and edit items on the list. The list is updated in real time, and changes you make will sync instantly and appear on all other devices in about a second. No need to refresh! 12 | 13 | ### Features 14 | 15 | - No registration required 16 | - Create lists with a single click 17 | - Syncs instantly and automatically 18 | - View and edit lists on your phone, tablet or computer 19 | - Drag & drop to reorder list items 20 | - Full-fledged dark mode 21 | 22 | ## Setup 23 | 24 | If you want to check out the code for yourself, follow these instructions: 25 | 26 | ### 1. Clone the repository: 27 | 28 | Clone the repository to the **web root directory** of your web server. This folder is often called `htdocs` or `www`. 29 | 30 | You can do this by running this command in your web root directory: 31 | 32 | ``` 33 | git clone https://github.com/mikkelsvartveit/splitlist.git . 34 | ``` 35 | 36 | ### 2. Set up SQL database 37 | 38 | Create a new database with `utf8mb4_unicode_ci` collation (or use an existing one) and run the SQL script located at config/splitlist.sql on the database. 39 | 40 | ### 3. Set up MySQL config file 41 | 42 | Create a new directory called `private` in the **parent directory** of the web root. Copy the file `config/db_config.ini` to the `private` directory you just created. Open the file with a text editor and input your MySQL credentials there. 43 | 44 | You can do this by running these commands in the web root directory: 45 | 46 | ``` 47 | mkdir ../private 48 | cp config/db_config.ini ../private/db_config.ini 49 | nano ../private/db_config.ini 50 | ``` 51 | 52 | To give you an idea, my `db_config.ini` file looks like this: 53 | 54 | ``` 55 | servername=localhost 56 | username=root 57 | password=root 58 | dbname=splitlist 59 | ``` 60 | 61 | ## Resources 62 | 63 | - [SortableJS](https://github.com/SortableJS/Sortable) - JavaScript library for drag-and-drop sorting of HTML elements 64 | - [Google Material Icons](https://material.io/resources/icons/) - collection of icons for UI elements 65 | - [Open Sans](https://fonts.google.com/specimen/Open+Sans) - web font 66 | -------------------------------------------------------------------------------- /assets/js/index.js: -------------------------------------------------------------------------------- 1 | function timeFormat(date) { 2 | let nowDate = new Date(); 3 | if (date.toDateString() === nowDate.toDateString()) { 4 | return "Today " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 5 | } else { 6 | date.setHours(0); 7 | date.setMinutes(0); 8 | date.setSeconds(0, 0); 9 | 10 | nowDate.setHours(0); 11 | nowDate.setMinutes(0); 12 | nowDate.setSeconds(0, 0); 13 | 14 | let daysAgo = (nowDate.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); 15 | 16 | if (daysAgo === 1) { 17 | return "Yesterday"; 18 | } else if (daysAgo < 7) { 19 | return daysAgo + " days ago"; 20 | } else { 21 | const weeksAgo = Math.floor(daysAgo / 7); 22 | if (weeksAgo === 1) { 23 | return weeksAgo + " week ago"; 24 | } else { 25 | return weeksAgo + " weeks ago"; 26 | } 27 | } 28 | } 29 | } 30 | 31 | function removeRecentList() { 32 | const id = this.parentElement.getAttribute("data-list-id"); 33 | const recentLists = JSON.parse(localStorage.getItem("recentLists")); 34 | 35 | // Creates new array without removed list 36 | const newRecentLists = recentLists.filter(list => list.id !== id); 37 | localStorage.setItem("recentLists", JSON.stringify(newRecentLists)); 38 | 39 | loadRecentLists(); 40 | } 41 | 42 | function loadRecentLists() { 43 | const recentLists = JSON.parse(localStorage.getItem("recentLists")); 44 | 45 | if (recentLists && recentLists.length > 0) { 46 | recentLists.sort((a, b) => Date.parse(b.time) - Date.parse(a.time)); 47 | 48 | const listEl = document.getElementById("recent-lists"); 49 | while (listEl.firstChild) { 50 | listEl.removeChild(listEl.firstChild); 51 | } 52 | 53 | for (const list of recentLists) { 54 | const listItemEl = document.getElementById("sample-list").cloneNode(true); 55 | listItemEl.removeAttribute("id"); 56 | listItemEl.classList.remove("hidden"); 57 | listItemEl.setAttribute("data-list-id", list.id); 58 | listItemEl.querySelector("a").setAttribute("href", "list/?id=" + list.id); 59 | 60 | listItemEl.querySelector(".list-name").textContent = list.name; 61 | listItemEl.querySelector(".list-last-edited").textContent = timeFormat(new Date(list.time)); 62 | 63 | listItemEl.querySelector(".remove-button").addEventListener("click", removeRecentList); 64 | 65 | listEl.appendChild(listItemEl); 66 | } 67 | 68 | // Removes the "hidden" class to show the section 69 | document.getElementById("recent-lists-section").classList.remove("hidden"); 70 | } else { 71 | // Adds the "hidden" class to hide the section 72 | document.getElementById("recent-lists-section").classList.add("hidden"); 73 | } 74 | } 75 | 76 | loadRecentLists(); 77 | 78 | 79 | // EVENT LISTENERS: 80 | 81 | document.getElementById("new-list-button").addEventListener("click", newList); 82 | 83 | document.getElementById("open-list-button").addEventListener("click", function () { 84 | showModal("open-list-modal", true); 85 | 86 | // Set cursor to input field 87 | setTimeout(() => { 88 | document.getElementById("open-list-modal-input").focus(); 89 | document.getElementById("open-list-modal-input").select(); 90 | }, 100); 91 | }); -------------------------------------------------------------------------------- /assets/img/devices.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /assets/js/all.js: -------------------------------------------------------------------------------- 1 | function loadTheme() { 2 | // Temporarily disable CSS transitions for 200ms 3 | document.body.classList.add("no-transition"); 4 | setTimeout(() => document.body.classList.remove("no-transition"), 200); 5 | 6 | const imgs = document.querySelectorAll("img"); 7 | 8 | if (localStorage.getItem("theme") === "dark") { 9 | // Set all icons to dark mode versions 10 | imgs.forEach(img => img.src = img.src.replace("/light-mode/", "/dark-mode/")); 11 | 12 | document.documentElement.setAttribute("data-theme", "dark"); 13 | document.querySelector("meta[name='theme-color']").setAttribute("content", "#2e3233"); 14 | } 15 | 16 | else { 17 | // Set all icons to light mode versions 18 | imgs.forEach(img => img.src = img.src.replace("/dark-mode/", "/light-mode/")); 19 | 20 | document.documentElement.setAttribute("data-theme", "light"); 21 | document.querySelector("meta[name='theme-color']").setAttribute("content", "#ffffff"); 22 | } 23 | } 24 | 25 | function toggleDarkMode() { 26 | if (localStorage.getItem("theme") === "dark") { 27 | localStorage.setItem("theme", "light"); 28 | } else { 29 | localStorage.setItem("theme", "dark"); 30 | } 31 | 32 | loadTheme(); 33 | } 34 | 35 | // Open/close a modal by passing element ID and true/false 36 | function showModal(modalId, show) { 37 | if (show) { 38 | document.getElementById(modalId).classList.remove("modal-hidden"); 39 | } else { 40 | document.getElementById(modalId).classList.add("modal-hidden"); 41 | } 42 | } 43 | 44 | let wait; 45 | function showSnackbar(snackbarId) { 46 | // Clears interval if a snackbar is already displayed 47 | if (wait) { 48 | clearInterval(wait); 49 | } 50 | 51 | // Shows snackbar for 5 seconds 52 | const el = document.getElementById(snackbarId); 53 | el.classList.add("snackbar-show"); 54 | wait = setTimeout(() => el.classList.remove("snackbar-show"), 5000); 55 | } 56 | 57 | function newList() { 58 | window.location = "/new"; 59 | } 60 | 61 | function openList() { 62 | const input = document.getElementById("open-list-modal-input").value.toLowerCase().split("="); 63 | const id = input[input.length - 1]; 64 | 65 | if (id) { 66 | window.location = "/list/?id=" + id; 67 | } 68 | } 69 | 70 | // Check theme preference on load 71 | loadTheme(); 72 | 73 | // Disables CSS hover effects on touch devices 74 | function watchForHover() { 75 | let hasHoverClass = false; 76 | let lastTouchTime = 0; 77 | 78 | const enableHover = () => { 79 | // Discard emulated mouseMove events coming from touch events 80 | if (!hasHoverClass && new Date() - lastTouchTime >= 500) { 81 | document.body.classList.add("no-touch"); 82 | hasHoverClass = true; 83 | } 84 | }; 85 | 86 | const disableHover = () => { 87 | if (hasHoverClass) { 88 | document.body.classList.remove("no-touch"); 89 | hasHoverClass = false; 90 | } 91 | }; 92 | 93 | const updateLastTouchTime = () => { 94 | lastTouchTime = new Date(); 95 | }; 96 | 97 | document.addEventListener('touchstart', updateLastTouchTime, true); 98 | document.addEventListener('touchstart', disableHover, true); 99 | document.addEventListener('mousemove', enableHover, true); 100 | 101 | enableHover(); 102 | } 103 | watchForHover(); 104 | 105 | 106 | // EVENT LISTENERS: 107 | 108 | document.getElementById("dark-mode-button").addEventListener("click", toggleDarkMode); 109 | 110 | document.getElementById("nav-new-list-button").addEventListener("click", newList); 111 | 112 | document.getElementById("nav-open-list-button").addEventListener("click", function () { 113 | showModal("open-list-modal", true); 114 | 115 | // Set cursor to input field 116 | setTimeout(() => { 117 | document.getElementById("open-list-modal-input").focus(); 118 | document.getElementById("open-list-modal-input").select(); 119 | }, 100); 120 | }); 121 | 122 | document.getElementById("open-list-modal-open-button").addEventListener("click", openList); 123 | 124 | document.getElementById("open-list-modal-input").addEventListener("keypress", function (e) { 125 | // Runs function if enter is pressed 126 | if (e.keyCode === 13) { 127 | openList(); 128 | } 129 | }); 130 | 131 | // Code for closing any modal 132 | const modals = document.getElementsByClassName("modal"); 133 | for (const modal of modals) { 134 | // Close when clicking outside modal 135 | modal.addEventListener("click", function () { 136 | if (event.target.classList.contains("modal")) { 137 | showModal(this.id, false); 138 | } 139 | }); 140 | 141 | // Close when clicking button 142 | const closeButton = modal.querySelector(".close-modal"); 143 | closeButton.addEventListener("click", function () { 144 | showModal(this.parentElement.parentElement.parentElement.parentElement.id, false); 145 | }); 146 | } -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | Splitlist - The quick way to share lists for anything 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 53 | 54 |
55 |

The quick way to share lists for anything.

56 | 57 |
58 | 59 | or 60 | 61 |
62 | 63 | 69 | 70 |
71 | 72 | Splitlist on laptop and phone 73 | 74 |
75 |
76 | Create 77 |

Getting started with Splitlist couldn't be easier. No registration required - simply click the create button above to make a list, and start typing right away. It's that simple!

78 |
79 | 80 |
81 | Share 82 |

Share your list with the click of a button. Just toss the link to your friends or family members, and you're ready to start collaborating on grocery lists, to-do lists and more.

83 |
84 | 85 |
86 | Devices 87 |

Does your device have a web browser? Then it works with Splitlist. Access and edit your lists on your phone, tablet or computer. The lists will sync on all your devices in real-time.

88 |
89 |
90 |
91 | 92 | 108 | 109 | 110 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /list/index.php: -------------------------------------------------------------------------------- 1 | PDO::ERRMODE_EXCEPTION, 21 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, 22 | PDO::ATTR_EMULATE_PREPARES => false 23 | ]; 24 | 25 | try { 26 | $pdo = new PDO($dsn, $username, $password, $options); 27 | } catch (\PDOException $e) { 28 | die("Connection failed: " . $e->getMessage()); 29 | } 30 | 31 | $idlist = $_GET["id"]; 32 | 33 | $sql = "SELECT * FROM list WHERE idlist = ?"; 34 | $stmt = $pdo->prepare($sql); 35 | $stmt->execute([$idlist]); 36 | $row = $stmt->fetch(); 37 | 38 | if ($row) { 39 | $listname = $row["name"]; 40 | } else { 41 | $listname = "List not found"; 42 | } 43 | ?> 44 | 45 | 46 | 47 | 48 | 49 | <?php echo htmlspecialchars($listname); ?> - Splitlist 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 91 | 92 |
93 | Loading... 94 |
95 | 96 | 120 | 121 | 125 | 126 | 142 | 143 | 158 | 159 | 162 | 163 | 164 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | /* Light mode (default) */ 2 | :root { 3 | --color-text: #555; 4 | --color-text-secondary: #666; 5 | --color-text-tertiary: #888; 6 | --color-background: #FFF; 7 | --color-hover: #F2F2F2; 8 | --color-accent: #34AE91; 9 | --color-accent-hover: #67C2AD; 10 | --color-shadow: #888; 11 | --color-shadow-dark: #444; 12 | --color-highlight: #E6F5F1; 13 | --color-divider: #CCC; 14 | } 15 | 16 | /* Dark mode */ 17 | [data-theme="dark"] { 18 | --color-text: #EEE; 19 | --color-text-secondary: #EEE; 20 | --color-text-tertiary: #AAA; 21 | --color-background: #2E3233; 22 | --color-hover: #3D4243; 23 | --color-accent: #34AE91; 24 | --color-accent-hover: #67C2AD; 25 | --color-shadow: #000; 26 | --color-shadow-dark: #000; 27 | --color-highlight: #394743; 28 | --color-divider: #555; 29 | } 30 | 31 | @font-face { 32 | font-family: "Open Sans"; 33 | src: url("/assets/fonts/OpenSans-Light.woff2") format("woff2"), 34 | url("/assets/fonts/OpenSans-Light.woff") format("woff"); 35 | font-weight: 300; 36 | font-style: normal; 37 | } 38 | 39 | @font-face { 40 | font-family: "Open Sans"; 41 | src: url("/assets/fonts/OpenSans-Regular.woff2") format("woff2"), 42 | url("/assets/fonts/OpenSans-Regular.woff") format("woff"); 43 | font-weight: 400; 44 | font-style: normal; 45 | } 46 | 47 | @font-face { 48 | font-family: "Open Sans"; 49 | src: url("/assets/fonts/OpenSans-SemiBold.woff2") format("woff2"), 50 | url("/assets/fonts/OpenSans-SemiBold.woff") format("woff"); 51 | font-weight: 600; 52 | font-style: normal; 53 | } 54 | 55 | @font-face { 56 | font-family: "Inconsolata"; 57 | src: url("/assets/fonts/Inconsolata-SemiBold.woff") format("woff"); 58 | font-weight: 600; 59 | font-style: normal; 60 | } 61 | 62 | 63 | /* ALL PAGES */ 64 | 65 | /* Class for temporarily disabling all transitions with Javascript */ 66 | .no-transition * { 67 | transition: none !important; 68 | } 69 | 70 | body { 71 | font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 72 | font-size: 16px; 73 | line-height: 1.4; 74 | color: var(--color-text); 75 | background-color: var(--color-background); 76 | } 77 | 78 | h1 { 79 | font-size: 54px; 80 | font-weight: 300; 81 | text-align: center; 82 | letter-spacing: 1px; 83 | margin: 35px 10px 20px 10px; 84 | } 85 | 86 | @media only screen and (max-width: 600px) { 87 | h1 { 88 | font-size: 42px; 89 | } 90 | } 91 | 92 | h2 { 93 | font-size: 32px; 94 | font-weight: 300; 95 | } 96 | 97 | h3 { 98 | font-size: 24px; 99 | font-weight: 600; 100 | } 101 | 102 | nav { 103 | position: fixed; 104 | top: 0; 105 | left: 0; 106 | display: flex; 107 | width: 100%; 108 | height: 70px; 109 | background-color: var(--color-background); 110 | box-shadow: 0 0 7px var(--color-shadow); 111 | } 112 | 113 | .nav-content { 114 | display: flex; 115 | justify-content: space-between; 116 | width: 100%; 117 | padding: 20px; 118 | margin: 0 auto; 119 | box-sizing: border-box; 120 | } 121 | 122 | .nav-content>div { 123 | display: flex; 124 | align-items: center; 125 | } 126 | 127 | nav a { 128 | display: flex; 129 | } 130 | 131 | nav img { 132 | height: 30px; 133 | } 134 | 135 | nav .button { 136 | margin-left: 15px; 137 | padding: 10px 15px; 138 | } 139 | 140 | nav .button img { 141 | margin-right: 5px; 142 | } 143 | 144 | nav #dark-mode-button { 145 | margin-left: 20px; 146 | cursor: pointer; 147 | } 148 | 149 | @media only screen and (max-width: 600px) { 150 | #nav-open-list-button { 151 | display: none; 152 | } 153 | } 154 | 155 | .content { 156 | max-width: 992px; 157 | margin: 65px auto; 158 | padding: 10px 1%; 159 | } 160 | 161 | .center { 162 | margin-left: auto; 163 | margin-right: auto; 164 | } 165 | 166 | .hidden { 167 | display: none !important; 168 | } 169 | 170 | .button { 171 | display: flex; 172 | justify-content: center; 173 | align-items: center; 174 | padding: 12px 18px; 175 | border: none; 176 | border-radius: 100px; 177 | box-shadow: 0 1px 5px var(--color-shadow); 178 | color: var(--color-text-secondary); 179 | background-color: var(--color-background); 180 | font-family: inherit; 181 | font-size: 14px; 182 | font-weight: 600; 183 | text-transform: uppercase; 184 | letter-spacing: 1px; 185 | outline-color: var(--color-accent); 186 | transition: background-color 0.2s; 187 | } 188 | 189 | .button img { 190 | height: 18px; 191 | margin: 0 8px 0 -2px; 192 | } 193 | 194 | .no-touch .button:hover, 195 | .button:active { 196 | background-color: var(--color-hover); 197 | } 198 | 199 | .button-teal { 200 | background-color: var(--color-accent); 201 | color: #FFF; 202 | border: none; 203 | } 204 | 205 | .no-touch .button-teal:hover, 206 | .button-teal:active { 207 | background-color: #67C2AD; 208 | } 209 | 210 | .icon-button { 211 | background-color: transparent; 212 | border: none; 213 | margin: 0; 214 | padding: 0; 215 | } 216 | 217 | .icon-button img { 218 | vertical-align: bottom; 219 | } 220 | 221 | .button-group { 222 | display: flex; 223 | justify-content: center; 224 | align-items: center; 225 | } 226 | 227 | .button-group button { 228 | margin: 10px; 229 | } 230 | 231 | .modal { 232 | position: fixed; 233 | top: 0; 234 | left: 0; 235 | width: 100%; 236 | height: 100%; 237 | display: flex; 238 | justify-content: center; 239 | align-items: center; 240 | background-color: rgba(0, 0, 0, 0.4); 241 | transition-duration: 0.2s; 242 | } 243 | 244 | .modal-hidden { 245 | visibility: hidden; 246 | opacity: 0; 247 | transition-duration: 0.1s; 248 | } 249 | 250 | .modal-content { 251 | background-color: var(--color-background); 252 | width: 90%; 253 | max-width: 350px; 254 | padding: 20px; 255 | box-sizing: border-box; 256 | text-align: center; 257 | border-radius: 5px; 258 | box-shadow: 0 0 10px var(--color-shadow-dark); 259 | transition-duration: 0.2s; 260 | } 261 | 262 | .modal-hidden .modal-content { 263 | transform: scale(0.9); 264 | } 265 | 266 | .modal h2 { 267 | margin: 5px 0 20px 0; 268 | } 269 | 270 | .modal .divider { 271 | margin: 20px auto; 272 | } 273 | 274 | .modal input { 275 | width: 90%; 276 | padding: 7px 10px; 277 | box-sizing: border-box; 278 | font-family: inherit; 279 | font-size: inherit; 280 | color: inherit; 281 | border: 3px solid var(--color-accent); 282 | border-radius: 5px; 283 | background-color: transparent; 284 | } 285 | 286 | .modal input:focus { 287 | outline-color: var(--color-accent); 288 | } 289 | 290 | .modal-buttons { 291 | display: flex; 292 | justify-content: space-between; 293 | margin-top: 25px; 294 | } 295 | 296 | .snackbar { 297 | position: fixed; 298 | display: flex; 299 | justify-content: center; 300 | bottom: 30px; 301 | left: 0; 302 | width: 100%; 303 | opacity: 0; 304 | visibility: hidden; 305 | transition-duration: 0.5s; 306 | } 307 | 308 | .snackbar p { 309 | font-size: 14px; 310 | font-weight: 600; 311 | color: #EEE; 312 | margin: 0 25px; 313 | padding: 15px 25px; 314 | background-color: #444; 315 | border-radius: 3px; 316 | box-shadow: 0 1px 5px var(--color-shadow-dark); 317 | } 318 | 319 | .snackbar-show { 320 | opacity: 1; 321 | visibility: visible; 322 | } 323 | 324 | 325 | /* FRONT PAGE */ 326 | 327 | #front-image { 328 | display: block; 329 | width: 100%; 330 | max-width: 900px; 331 | margin: 60px auto; 332 | } 333 | 334 | .divider { 335 | width: 99%; 336 | height: 0; 337 | margin: 50px auto; 338 | border-bottom: 1px solid var(--color-divider); 339 | } 340 | 341 | p.feature { 342 | line-height: 1.8; 343 | font-weight: 600; 344 | } 345 | 346 | .row-3 { 347 | display: flex; 348 | flex-wrap: wrap; 349 | justify-content: space-between; 350 | padding: 0 5px; 351 | } 352 | 353 | .row-3>* { 354 | display: block; 355 | width: calc(33.33% - 25px); 356 | box-sizing: border-box; 357 | } 358 | 359 | .row-3 img { 360 | display: block; 361 | width: 40%; 362 | margin: 0px auto 50px auto; 363 | } 364 | 365 | @media only screen and (max-width: 992px) { 366 | .row-3 { 367 | padding: 0 10px; 368 | } 369 | 370 | .row-3>* { 371 | width: calc(50% - 25px); 372 | margin-bottom: 50px; 373 | } 374 | 375 | .row-3 img { 376 | width: 128px; 377 | margin-bottom: 30px; 378 | } 379 | } 380 | 381 | @media only screen and (max-width: 600px) { 382 | .row-3 { 383 | padding: 0 10px; 384 | } 385 | 386 | .row-3>* { 387 | width: 100%; 388 | } 389 | } 390 | 391 | #main-buttons { 392 | color: var(--color-text-secondary); 393 | font-weight: 600; 394 | text-transform: uppercase; 395 | letter-spacing: 1px; 396 | margin: 30px 0; 397 | } 398 | 399 | @media only screen and (max-width: 768px) { 400 | #main-buttons { 401 | flex-direction: column; 402 | } 403 | } 404 | 405 | #main-buttons button { 406 | font-size: inherit; 407 | margin: 20px; 408 | padding: 20px 30px; 409 | } 410 | 411 | #recent-lists-section { 412 | max-width: 400px; 413 | margin: auto; 414 | } 415 | 416 | #recent-lists-section h3 { 417 | text-align: center; 418 | color: var(--color-text); 419 | margin-top: 50px; 420 | } 421 | 422 | #recent-lists div { 423 | display: flex; 424 | align-items: center; 425 | margin-bottom: 2px; 426 | border-radius: 5px; 427 | transition-duration: 0.1s; 428 | } 429 | 430 | #recent-lists div a { 431 | display: flex; 432 | justify-content: space-between; 433 | align-items: center; 434 | flex-grow: 1; 435 | color: var(--color-text); 436 | text-decoration: none; 437 | padding: 5px 5px 5px 10px; 438 | min-width: 0; 439 | } 440 | 441 | #recent-lists div .icon-button img { 442 | width: 18px; 443 | margin: 0px 5px 0px 0px; 444 | } 445 | 446 | #recent-lists div a .list-name { 447 | font-weight: 600; 448 | margin-bottom: 1px; 449 | overflow: hidden; 450 | text-overflow: ellipsis; 451 | white-space: nowrap; 452 | min-width: 0; 453 | } 454 | 455 | #recent-lists div .list-last-edited { 456 | font-size: 12px; 457 | color: var(--color-text-tertiary); 458 | white-space: nowrap; 459 | margin-left: 10px; 460 | } 461 | 462 | .no-touch #recent-lists div:hover, 463 | #recent-lists div:active { 464 | background-color: var(--color-hover); 465 | } 466 | 467 | 468 | /* LIST PAGE */ 469 | 470 | #loading-spinner { 471 | display: block; 472 | width: 64px; 473 | margin: 30px auto; 474 | } 475 | 476 | #no-list-error { 477 | text-align: center; 478 | padding-top: 20px; 479 | } 480 | 481 | h1.list-name { 482 | margin-bottom: 10px; 483 | } 484 | 485 | #list-name { 486 | margin-bottom: 10px; 487 | cursor: pointer; 488 | } 489 | 490 | #list-id { 491 | font-size: 18px; 492 | font-weight: 400; 493 | color: var(--color-text-secondary); 494 | text-align: center; 495 | margin: 0 0 0 0; 496 | } 497 | 498 | #list-id-id { 499 | font-family: "Inconsolata", monospace; 500 | font-size: 20px; 501 | font-weight: 600; 502 | letter-spacing: 0.5px; 503 | } 504 | 505 | #item-count { 506 | font-size: 18px; 507 | font-weight: 400; 508 | color: var(--color-text-secondary); 509 | text-align: center; 510 | margin: 0 0 20px 0; 511 | } 512 | 513 | .list { 514 | font-size: 18px; 515 | max-width: 600px; 516 | margin: 30px auto 20px auto; 517 | display: flex; 518 | flex-direction: column; 519 | } 520 | 521 | .list-item { 522 | display: flex; 523 | align-items: center; 524 | padding: 10px; 525 | border-bottom: 1px solid var(--color-divider); 526 | transition: background-color 0.2s; 527 | } 528 | 529 | .list-item>.text { 530 | flex-grow: 1; 531 | padding: 3px 10px 4px 10px; 532 | font-family: inherit; 533 | font-size: 16px; 534 | color: inherit; 535 | border: none; 536 | background-color: transparent; 537 | transition-duration: 0.1s; 538 | } 539 | 540 | .list-item>.text:focus { 541 | outline-color: var(--color-accent); 542 | } 543 | 544 | .list-item.checked>.text { 545 | color: var(--color-text-tertiary); 546 | text-decoration: line-through; 547 | } 548 | 549 | .list-item .icon-button { 550 | margin: 0 0 0 5px; 551 | } 552 | 553 | .list-item .icon-button:focus { 554 | outline-color: var(--color-accent); 555 | } 556 | 557 | .list-item .drag-handle { 558 | vertical-align: bottom; 559 | cursor: grab; 560 | } 561 | 562 | .list-item .check-button:focus { 563 | outline: 0; 564 | } 565 | 566 | .sortable-ghost { 567 | background-color: var(--color-highlight); 568 | order: none; 569 | } 570 | 571 | #clipboard-placeholder { 572 | position: fixed; 573 | z-index: -1; 574 | opacity: 0; 575 | display: none; 576 | } -------------------------------------------------------------------------------- /assets/js/list.js: -------------------------------------------------------------------------------- 1 | // Function for getting URL variables, like $_GET[] in PHP 2 | function getQueryVariable(queryVariable) { 3 | const query = window.location.search.substring(1); 4 | const variables = query.split("&"); 5 | 6 | for (const variable of variables) { 7 | const pair = variable.split("="); 8 | if (pair[0] === queryVariable) { 9 | return pair[1]; 10 | } 11 | } 12 | 13 | return false; 14 | } 15 | 16 | function ajaxSetList(id, name, data) { 17 | pendingAjaxSetRequests++; 18 | 19 | const xhttp = new XMLHttpRequest(); 20 | 21 | xhttp.onreadystatechange = function () { 22 | if (this.readyState === 4 && this.status === 200) { 23 | pendingAjaxSetRequests--; 24 | } 25 | }; 26 | 27 | xhttp.open("POST", "/assets/ajax/setlist.php", true); 28 | xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 29 | xhttp.send("idlist=" + id + "&name=" + encodeURIComponent(name) + "&data=" + encodeURIComponent(data)); 30 | } 31 | 32 | function ajaxGetList(id, lastedited, callback) { 33 | pendingAjaxGetRequest = true; 34 | 35 | const xhttp = new XMLHttpRequest(); 36 | 37 | xhttp.onreadystatechange = function () { 38 | if (this.readyState === 4 && this.status === 200) { 39 | callback(this.responseText); 40 | } 41 | }; 42 | 43 | xhttp.open("POST", "/assets/ajax/getlist.php", true); 44 | xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 45 | xhttp.send("idlist=" + id + "&lastedited=" + lastedited); 46 | } 47 | 48 | // Function for refreshing DOM to match the list object 49 | function refreshDom() { 50 | // Checks if list name has changed 51 | const listNameEl = document.getElementById("list-name"); 52 | if (listNameEl.textContent !== list.name) { 53 | document.title = list.name + " - Splitlist"; 54 | listNameEl.textContent = list.name; 55 | document.getElementById("edit-name-modal-input").value = list.name; 56 | } 57 | 58 | const listArray = JSON.parse(list.data); 59 | const listEl = document.getElementById("list"); 60 | 61 | // Removes items that has been deleted 62 | const elementsToDelete = []; 63 | for (const listItemEl of listEl.children) { 64 | let isDeletedElement = true; 65 | 66 | for (const listItem of listArray) { 67 | if (Number(listItemEl.getAttribute("data-list-id")) === listItem.id) { 68 | isDeletedElement = false; 69 | break; 70 | } 71 | } 72 | 73 | if (isDeletedElement) { 74 | elementsToDelete.push(listItemEl); 75 | } 76 | } 77 | for (const element of elementsToDelete) { 78 | element.remove(); 79 | } 80 | 81 | // Refreshes current list items 82 | for (const listItemEl of listEl.children) { 83 | const listItem = listArray.find(item => item.id === Number(listItemEl.getAttribute("data-list-id"))); 84 | 85 | const listItemTextEl = listItemEl.querySelector(".text"); 86 | const checkboxEl = listItemEl.querySelector(".check-button img"); 87 | 88 | if (document.activeElement !== listItemTextEl) { 89 | listItemTextEl.value = listItem.text; 90 | } 91 | 92 | if (listItem.checked) { 93 | checkboxEl.src = checkboxEl.src.replace("checkbox-unchecked.svg", "checkbox-checked.svg"); 94 | listItemEl.classList.add("checked"); 95 | } else { 96 | checkboxEl.src = checkboxEl.src.replace("checkbox-checked.svg", "checkbox-unchecked.svg"); 97 | listItemEl.classList.remove("checked"); 98 | } 99 | 100 | listItemEl.setAttribute("data-list-index", listItem.index); 101 | } 102 | 103 | // Adds new list items 104 | for (const listItem of listArray) { 105 | // If item doesn't already exist in the DOM 106 | if (!Array.from(listEl.children).find(el => Number(el.getAttribute("data-list-id")) === listItem.id)) { 107 | const listItemEl = document.getElementById("sample-list-item").cloneNode(true); 108 | listItemEl.removeAttribute("id"); 109 | listItemEl.classList.remove("hidden"); 110 | listItemEl.setAttribute("data-list-id", listItem.id); 111 | listItemEl.setAttribute("data-list-index", listItem.index); 112 | 113 | const listItemTextEl = listItemEl.querySelector(".text"); 114 | listItemTextEl.setAttribute("value", listItem.text); 115 | listItemTextEl.addEventListener("input", updateListItem); 116 | listItemTextEl.addEventListener("focusout", refreshDom); 117 | listItemTextEl.addEventListener("keydown", function (e) { 118 | const index = Array.from(listEl.children).indexOf(listItemEl); 119 | 120 | // If arrow up is pressed 121 | if (e.keyCode === 38) { 122 | if (index > 0) { 123 | // Move cursor to previous item on list 124 | const previousItem = listEl.children[index - 1].querySelector(".text"); 125 | previousItem.focus(); 126 | } 127 | } 128 | 129 | // If arrow down is pressed 130 | if (e.keyCode === 40) { 131 | if (index < listEl.children.length - 1) { 132 | // Move cursor to next item on list 133 | const nextItem = listEl.children[index + 1].querySelector(".text"); 134 | nextItem.focus(); 135 | } 136 | } 137 | 138 | // If enter is pressed 139 | else if (e.keyCode === 13) { 140 | if (index < listEl.children.length - 1) { 141 | // Move cursor to next item on list 142 | const nextItem = listEl.children[index + 1].querySelector(".text"); 143 | nextItem.focus(); 144 | nextItem.select(); 145 | } else { 146 | // Create a new item if the cursor is on the last item in the list 147 | newListItem(); 148 | } 149 | } 150 | }); 151 | 152 | const listItemCheckButton = listItemEl.querySelector(".check-button"); 153 | if (listItem.checked) { 154 | const imgEl = listItemCheckButton.querySelector("img"); 155 | imgEl.src = imgEl.src.replace("checkbox-unchecked.svg", "checkbox-checked.svg"); 156 | listItemEl.classList.add("checked"); 157 | } 158 | listItemCheckButton.addEventListener("click", toggleCheckedListItem); 159 | 160 | const listItemDeleteButtonEl = listItemEl.querySelector(".remove-button"); 161 | listItemDeleteButtonEl.addEventListener("click", deleteListItem); 162 | 163 | listEl.appendChild(listItemEl); 164 | } 165 | } 166 | 167 | // Reorders list if order has changed 168 | if (!document.activeElement.classList.contains("text") && !isDragging) { 169 | const itemsArray = Array.from(listEl.children); 170 | const sortedItemsArray = Array.from(listEl.children).sort((a, b) => { 171 | return a.getAttribute("data-list-index") - b.getAttribute("data-list-index"); 172 | }); 173 | 174 | // Function to check if two arrays are equal 175 | const areEqual = function (array1, array2) { 176 | if (array1.length !== array2.length) { 177 | return false; 178 | } 179 | 180 | for (let i = 0; i < array1.length; i++) { 181 | if (array1[i] !== array2[i]) { 182 | return false; 183 | } 184 | } 185 | 186 | return true; 187 | }; 188 | 189 | if (!areEqual(itemsArray, sortedItemsArray)) { 190 | for (const item of sortedItemsArray) { 191 | listEl.appendChild(item); 192 | } 193 | } 194 | } 195 | 196 | // Updates item counter 197 | const itemCountEl = document.getElementById("item-count-number"); 198 | if (itemCountEl.textContent !== listEl.children.length && !isDragging) { 199 | itemCountEl.textContent = listEl.children.length; 200 | } 201 | 202 | // Updates the localstorage entry that stores previously opened lists 203 | const recentLists = JSON.parse(localStorage.getItem("recentLists")) || []; 204 | const listIndex = recentLists.findIndex(list => list.id === listId); 205 | if (listIndex >= 0) { 206 | recentLists[listIndex].name = list.name; 207 | recentLists[listIndex].time = new Date(); 208 | } else { 209 | const listObject = {}; 210 | listObject.id = listId; 211 | listObject.name = list.name; 212 | listObject.time = new Date(); 213 | 214 | recentLists.push(listObject); 215 | } 216 | localStorage.setItem("recentLists", JSON.stringify(recentLists)); 217 | } 218 | 219 | function loadList() { 220 | ajaxGetList(listId, list.lastedited, function (responseText) { 221 | pendingAjaxGetRequest = false; 222 | 223 | try { 224 | if (responseText !== "false") { 225 | list = JSON.parse(responseText); 226 | 227 | const listIdEl = document.getElementById("list-id-id"); 228 | listIdEl.textContent = listId.toUpperCase(); 229 | 230 | refreshDom(); 231 | 232 | document.getElementById("loading").classList.add("hidden"); 233 | document.getElementById("list-content").classList.remove("hidden"); 234 | } else { 235 | clearInterval(autoReload); 236 | 237 | document.getElementById("no-list-id").textContent = listId.toUpperCase(); 238 | document.getElementById("loading").classList.add("hidden"); 239 | document.getElementById("no-list-error").classList.remove("hidden"); 240 | } 241 | } catch (err) { 242 | clearInterval(autoReload); 243 | console.error(responseText); 244 | } 245 | }); 246 | } 247 | 248 | function pullList() { 249 | // Checks that no AJAX requests are currently pending 250 | if (!pendingAjaxGetRequest && pendingAjaxSetRequests === 0) { 251 | ajaxGetList(listId, list.lastedited, function (responseText) { 252 | pendingAjaxGetRequest = false; 253 | 254 | if (pendingAjaxSetRequests === 0 && responseText) { 255 | list = JSON.parse(responseText); 256 | refreshDom(); 257 | } 258 | }); 259 | } 260 | } 261 | 262 | function pushList() { 263 | ajaxSetList(listId, list.name, list.data); 264 | } 265 | 266 | function newListItem() { 267 | const listArray = JSON.parse(list.data); 268 | const newItem = {}; 269 | 270 | newItem.id = 0; 271 | for (const listItem of listArray) { 272 | if (listItem.id >= newItem.id) { 273 | newItem.id = listItem.id + 1; 274 | } 275 | } 276 | 277 | newItem.text = ""; 278 | newItem.index = listArray.length; 279 | newItem.checked = false; 280 | 281 | listArray.push(newItem); 282 | list.data = JSON.stringify(listArray); 283 | 284 | refreshDom(); 285 | pushList(); 286 | 287 | // Sets focus to the newly created list item 288 | const addedElement = listEl.querySelector("[data-list-id='" + newItem.id + "']"); 289 | addedElement.querySelector(".text").focus(); 290 | } 291 | 292 | function updateListItem() { 293 | const itemId = Number(this.parentElement.getAttribute("data-list-id")); 294 | const newText = this.value; 295 | 296 | const listArray = JSON.parse(list.data); 297 | const listItem = listArray.find(item => item.id === itemId); 298 | 299 | listItem.text = newText; 300 | list.data = JSON.stringify(listArray); 301 | 302 | pushList(); 303 | } 304 | 305 | function deleteListItem() { 306 | const itemId = Number(this.parentElement.getAttribute("data-list-id")); 307 | 308 | // Creates new array without the item that should be deleted 309 | const listArray = JSON.parse(list.data); 310 | const newListArray = listArray.filter(listItem => listItem.id !== itemId); 311 | list.data = JSON.stringify(newListArray); 312 | 313 | refreshDom(); 314 | pushList(); 315 | } 316 | 317 | function toggleCheckedListItem() { 318 | const itemId = Number(this.parentElement.getAttribute("data-list-id")); 319 | 320 | const listArray = JSON.parse(list.data); 321 | const listItem = listArray.find(item => item.id === itemId); 322 | 323 | listItem.checked = !listItem.checked; 324 | list.data = JSON.stringify(listArray); 325 | 326 | refreshDom(); 327 | pushList(); 328 | } 329 | 330 | function updateListOrder() { 331 | isDragging = false; 332 | const listEl = document.getElementById("list"); 333 | const listArray = JSON.parse(list.data); 334 | 335 | let index = 0; 336 | for (const listItemEl of listEl.children) { 337 | listItemEl.setAttribute("data-list-index", index); 338 | 339 | const listItem = listArray.find(item => item.id === Number(listItemEl.getAttribute("data-list-id"))); 340 | listItem.index = index++; 341 | } 342 | 343 | list.data = JSON.stringify(listArray); 344 | pushList(); 345 | } 346 | 347 | function editListName() { 348 | const name = document.getElementById("edit-name-modal-input").value; 349 | 350 | if (name) { 351 | list.name = name; 352 | 353 | refreshDom(); 354 | pushList(); 355 | } 356 | 357 | showModal("edit-name-modal", false); 358 | } 359 | 360 | function shareList() { 361 | const clipboardEl = document.getElementById("clipboard-placeholder"); 362 | const url = window.location.hostname + "/?i=" + listId; 363 | 364 | // Copies the variable "url" to clipboard 365 | clipboardEl.textContent = url; 366 | clipboardEl.style.display = "block"; 367 | clipboardEl.select(); 368 | document.execCommand("copy"); 369 | clipboardEl.style.display = "none"; 370 | 371 | showSnackbar("link-copied-snackbar"); 372 | } 373 | 374 | // Initializing SortableJS 375 | let isDragging = false; 376 | const listEl = document.getElementById("list"); 377 | Sortable.create(listEl, { 378 | delay: 0, 379 | delayOnTouchOnly: true, 380 | touchStartThreshold: 4, 381 | animation: 150, 382 | handle: ".drag-handle", 383 | ghostClass: "sortable-ghost", 384 | 385 | onStart: function () { 386 | isDragging = true; 387 | }, 388 | onEnd: updateListOrder 389 | }); 390 | 391 | // Tracks whether an AJAX get request is waiting for response 392 | let pendingAjaxGetRequest = false; 393 | // Tracks how many AJAX set requests are waiting for response 394 | let pendingAjaxSetRequests = 0; 395 | 396 | const listId = getQueryVariable("id"); 397 | let list = {}; 398 | 399 | // Loads list once on page load 400 | loadList(); 401 | 402 | // Checks for changes in database every second 403 | const autoReload = window.setInterval(pullList, 1000); 404 | 405 | 406 | // EVENT LISTENERS: 407 | 408 | document.getElementById("list-name").addEventListener("click", function () { 409 | showModal("edit-name-modal", true); 410 | 411 | // Set cursor to input field 412 | setTimeout(() => { 413 | document.getElementById("edit-name-modal-input").focus(); 414 | document.getElementById("edit-name-modal-input").select(); 415 | }, 100); 416 | }); 417 | 418 | document.getElementById("edit-name-button").addEventListener("click", function () { 419 | showModal("edit-name-modal", true); 420 | 421 | // Set cursor to input field 422 | setTimeout(() => { 423 | document.getElementById("edit-name-modal-input").focus(); 424 | document.getElementById("edit-name-modal-input").select(); 425 | }, 100); 426 | }); 427 | 428 | document.getElementById("share-list-button").addEventListener("click", shareList); 429 | 430 | document.getElementById("new-item-button").addEventListener("click", newListItem); 431 | 432 | document.getElementById("edit-name-modal-rename-button").addEventListener("click", editListName); 433 | 434 | document.getElementById("edit-name-modal-input").addEventListener("keypress", function (e) { 435 | // Runs function if enter is pressed 436 | if (e.keyCode === 13) { 437 | editListName(); 438 | } 439 | }); -------------------------------------------------------------------------------- /assets/js/sortable.js: -------------------------------------------------------------------------------- 1 | /*! Sortable 1.10.2 - MIT | git://github.com/SortableJS/Sortable.git */ 2 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return!1}return!1}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&h(t,e):h(t,e))||o&&t===n)return t;if(t===n)break}while(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode)}var i;return null}var f,p=/\s+/g;function k(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(p," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(p," ")}}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||-1!==e.indexOf("webkit")||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform");o&&"none"!==o&&(n=o+" "+n)}while(!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function g(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=e.left-n&&r<=e.right+n,i=a>=e.top-n&&a<=e.bottom+n;return n&&o&&i?l=t:void 0}}),l}((t=t.touches?t.touches[0]:t).clientX,t.clientY);if(e){var n={};for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[j]._onDragOver(n)}}}function kt(t){z&&z.parentNode[j]._isOutsideThisEl(t.target)}function Rt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[j]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Ot(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Rt.supportPointer&&"PointerEvent"in window,emptyInsertThreshold:5};for(var o in O.initializePlugins(this,t,n),n)o in e||(e[o]=n[o]);for(var i in At(e),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&xt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?u(t,"pointerdown",this._onTapStart):(u(t,"mousedown",this._onTapStart),u(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(u(t,"dragover",this),u(t,"dragenter",this)),bt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,T())}function Xt(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||w||E?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),d&&(c=d.call(u,s,a)),c}function Yt(t){t.draggable=!1}function Bt(){Dt=!1}function Ft(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;n--;)o+=e.charCodeAt(n);return o.toString(36)}function Ht(t){return setTimeout(t,0)}function Lt(t){return clearTimeout(t)}Rt.prototype={constructor:Rt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(ht=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(function(t){St.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&St.push(o)}}(o),!z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled||s.isContentEditable||(l=P(l,t.draggable,o,!1))&&l.animated||Z===l)){if(J=F(l),et=F(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return W({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),K("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c&&(c=c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return W({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),K("filter",n,{evt:e}),!0})))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;if(n&&!z&&n.parentNode===r){var s=X(n);if(q=r,G=(z=n).parentNode,V=z.nextSibling,Z=n,ot=a.group,rt={target:Rt.dragged=z,clientX:(e||t).clientX,clientY:(e||t).clientY},ct=rt.clientX-s.left,ut=rt.clientY-s.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,z.style["will-change"]="all",o=function(){K("delayEnded",i,{evt:t}),Rt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!c&&i.nativeDraggable&&(z.draggable=!0),i._triggerDragStart(t,e),W({sortable:i,name:"choose",originalEvent:t}),k(z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){g(z,t.trim(),Yt)}),u(l,"dragover",Pt),u(l,"mousemove",Pt),u(l,"touchmove",Pt),u(l,"mouseup",i._onDrop),u(l,"touchend",i._onDrop),u(l,"touchcancel",i._onDrop),c&&this.nativeDraggable&&(this.options.touchStartThreshold=4,z.draggable=!0),K("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(E||w))o();else{if(Rt.eventCanceled)return void this._onDrop();u(l,"mouseup",i._disableDelayedDrag),u(l,"touchend",i._disableDelayedDrag),u(l,"touchcancel",i._disableDelayedDrag),u(l,"mousemove",i._delayedDragTouchMoveHandler),u(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&u(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){z&&Yt(z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;d(t,"mouseup",this._disableDelayedDrag),d(t,"touchend",this._disableDelayedDrag),d(t,"touchcancel",this._disableDelayedDrag),d(t,"mousemove",this._delayedDragTouchMoveHandler),d(t,"touchmove",this._delayedDragTouchMoveHandler),d(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?u(document,"pointermove",this._onTouchMove):u(document,e?"touchmove":"mousemove",this._onTouchMove):(u(z,"dragend",this),u(q,"dragstart",this._onDragStart));try{document.selection?Ht(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){if(vt=!1,q&&z){K("dragStarted",this,{evt:e}),this.nativeDraggable&&u(document,"dragover",kt);var n=this.options;t||k(z,n.dragClass,!1),k(z,n.ghostClass,!0),Rt.active=this,t&&this._appendGhost(),W({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(at){this._lastX=at.clientX,this._lastY=at.clientY,Nt();for(var t=document.elementFromPoint(at.clientX,at.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(at.clientX,at.clientY))!==e;)e=t;if(z.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j]){if(e[j]._onDragOver({clientX:at.clientX,clientY:at.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);It()}},_onTouchMove:function(t){if(rt){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=U&&v(U,!0),a=U&&r&&r.a,l=U&&r&&r.d,s=Ct&>&&b(gt),c=(i.clientX-rt.clientX+o.x)/(a||1)+(s?s[0]-Et[0]:0)/(a||1),u=(i.clientY-rt.clientY+o.y)/(l||1)+(s?s[1]-Et[1]:0)/(l||1);if(!Rt.active&&!vt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))o.right+10||t.clientX<=o.right&&t.clientY>o.bottom&&t.clientX>=o.left:t.clientX>o.right&&t.clientY>o.top||t.clientX<=o.right&&t.clientY>o.bottom+10}(n,a,this)&&!g.animated){if(g===z)return A(!1);if(g&&l===n.target&&(s=g),s&&(i=X(s)),!1!==Xt(q,l,z,o,s,i,n,!!s))return O(),l.appendChild(z),G=l,N(),A(!0)}else if(s.parentNode===l){i=X(s);var v,m,b,y=z.parentNode!==l,w=!function(t,e,n){var o=n?t.left:t.top,i=n?t.right:t.bottom,r=n?t.width:t.height,a=n?e.left:e.top,l=n?e.right:e.bottom,s=n?e.width:e.height;return o===a||i===l||o+r/2===a+s/2}(z.animated&&z.toRect||o,s.animated&&s.toRect||i,a),E=a?"top":"left",D=Y(s,"top","top")||Y(z,"top","top"),S=D?D.scrollTop:void 0;if(ht!==s&&(m=i[E],yt=!1,wt=!w&&e.invertSwap||y),0!==(v=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,u=o?n.top:n.left,d=o?n.bottom:n.right,h=!1;if(!a)if(l&&pt