├── 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 | 
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 |
39 |
40 |
45 |
46 |
47 |
Open
48 |
New
49 |
50 |
51 |
52 |
53 |
54 |
55 |
The quick way to share lists for anything.
56 |
57 |
58 | Create a new list
59 | or
60 | Open existing list
61 |
62 |
63 |
64 |
Recently opened lists
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
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 |
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 |
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 |
93 |
94 |
Open list
95 |
Input ID or paste link to open list:
96 |
97 |
106 |
107 |
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 | - 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 |
77 |
78 |
83 |
84 |
85 |
Open
86 |
New
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
List ID:
100 |
0 items
101 |
102 |
113 |
114 |
115 |
116 |
117 |
118 |
New item
119 |
120 |
121 |
122 |
List not found!
123 |
There is no existing list with the ID .
124 |
125 |
126 |
127 |
128 |
Open list
129 |
Input ID or paste link to open list:
130 |
131 |
140 |
141 |
142 |
143 |
144 |
145 |
Edit list name
146 |
147 |
156 |
157 |
158 |
159 |
160 |
Link was copied to clipboard. Go share it!
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
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