mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-07-29 07:33:47 +00:00
fix: convert cf-kv and excalidraw-libraries from submodules to regular files
- Remove stale cloudflare-worker submodule entry from .gitmodules - Remove .git dirs from cf-kv and excalidraw-libraries so Git tracks them as regular files - Fixes 'No url found for submodule path cf-kv' CI error
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
// copied from excalidraw/excalidraw
|
||||
const debounce = (fn, timeout) => {
|
||||
let handle = 0;
|
||||
let lastArgs = null;
|
||||
const ret = (...args) => {
|
||||
lastArgs = args;
|
||||
clearTimeout(handle);
|
||||
handle = window.setTimeout(() => {
|
||||
lastArgs = null;
|
||||
fn(...args);
|
||||
}, timeout);
|
||||
};
|
||||
ret.flush = () => {
|
||||
clearTimeout(handle);
|
||||
if (lastArgs) {
|
||||
const _lastArgs = lastArgs;
|
||||
lastArgs = null;
|
||||
fn(..._lastArgs);
|
||||
}
|
||||
};
|
||||
ret.cancel = () => {
|
||||
lastArgs = null;
|
||||
clearTimeout(handle);
|
||||
};
|
||||
return ret;
|
||||
};
|
||||
|
||||
const fetchJSONFile = (path, callback) => {
|
||||
let httpRequest = new XMLHttpRequest();
|
||||
httpRequest.onreadystatechange = () => {
|
||||
if (httpRequest.readyState === 4) {
|
||||
if (httpRequest.status === 200) {
|
||||
let data = JSON.parse(httpRequest.responseText);
|
||||
if (callback) callback(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
httpRequest.open("GET", path);
|
||||
httpRequest.send();
|
||||
};
|
||||
|
||||
const getDate = (date) => {
|
||||
const d = new Date(date);
|
||||
const MONTHS = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
return `${d.getDate()} ${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
|
||||
};
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
const sortByDate = (property) => (a, b) => {
|
||||
const aTime = new Date(a[property]);
|
||||
const bTime = new Date(b[property]);
|
||||
const today = new Date();
|
||||
const diffA = today.getTime() - aTime.getTime();
|
||||
const diffB = today.getTime() - bTime.getTime();
|
||||
return diffB - diffA;
|
||||
};
|
||||
const sortBy = {
|
||||
default: {
|
||||
label: "Default",
|
||||
func: (items) => {
|
||||
return sortBy.downloadsTotal.func(items);
|
||||
},
|
||||
},
|
||||
new: {
|
||||
label: "New",
|
||||
func: (items) => items.sort(sortByDate("created")),
|
||||
},
|
||||
updates: {
|
||||
label: "Updated",
|
||||
func: (items) => items.sort(sortByDate("updated")),
|
||||
},
|
||||
downloadsTotal: {
|
||||
label: "Total Downloads",
|
||||
func: (items) =>
|
||||
items.sort((a, b) => {
|
||||
return a.downloads.total - b.downloads.total;
|
||||
}),
|
||||
},
|
||||
downloadsWeek: {
|
||||
label: "Downloads This Week",
|
||||
func: (items) =>
|
||||
items.sort((a, b) => {
|
||||
return a.downloads.week - b.downloads.week;
|
||||
}),
|
||||
},
|
||||
author: {
|
||||
label: "Author",
|
||||
func: (items) =>
|
||||
items.sort((a, b) => {
|
||||
return b.authors[0].name.localeCompare(a.authors[0].name);
|
||||
}),
|
||||
},
|
||||
name: {
|
||||
label: "Name",
|
||||
func: (items) =>
|
||||
items.sort((a, b) => {
|
||||
return b.name.localeCompare(a.name);
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
const APP_NAMES = {
|
||||
"Excalidraw+": "https://app.excalidraw.com",
|
||||
Excalidraw: "https://excalidraw.com",
|
||||
Excalideck: "https://app.excalideck.com",
|
||||
};
|
||||
|
||||
let appName = "";
|
||||
|
||||
const getAppName = (referrer) => {
|
||||
return (appName =
|
||||
appName ||
|
||||
Object.entries(APP_NAMES).find(([appName, domain]) => {
|
||||
return referrer.includes(domain);
|
||||
})?.[0] ||
|
||||
"Excalidraw");
|
||||
};
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let libraries_ = [];
|
||||
let currSort = null;
|
||||
|
||||
const searchKeys = ["name", "description", "itemNames"];
|
||||
|
||||
let IMG_INTERSECTION_OBSERVER = null;
|
||||
|
||||
const initImageLazyLoading = () => {
|
||||
if (IMG_INTERSECTION_OBSERVER) {
|
||||
IMG_INTERSECTION_OBSERVER.disconnect();
|
||||
}
|
||||
const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
const lazyImageObserver = new IntersectionObserver(
|
||||
function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
let lazyImage = entry.target;
|
||||
lazyImage.src = lazyImage.dataset.src;
|
||||
lazyImage.classList.remove("lazy");
|
||||
lazyImageObserver.unobserve(lazyImage);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: "0px 0px 500px 0px",
|
||||
},
|
||||
);
|
||||
IMG_INTERSECTION_OBSERVER = lazyImageObserver;
|
||||
lazyImages.forEach(function (lazyImage) {
|
||||
lazyImageObserver.observe(lazyImage);
|
||||
});
|
||||
} else {
|
||||
lazyImages.forEach(function (lazyImage) {
|
||||
lazyImage.src = lazyImage.dataset.src;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const escapeHTMLAttribute = (str) => {
|
||||
const map = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
};
|
||||
|
||||
if (typeof str !== "string") return "";
|
||||
|
||||
return str.replace(/[&<>"]/g, (char) => map[char]);
|
||||
};
|
||||
|
||||
const populateLibraryList = (filterQuery = "") => {
|
||||
const items = [
|
||||
...document.getElementById("template").parentNode.children,
|
||||
].filter((x) => x.id !== "template");
|
||||
items.forEach((x) => x.remove());
|
||||
|
||||
filterQuery = filterQuery.trim().toLowerCase();
|
||||
const hasMatch = (haystackStr) =>
|
||||
haystackStr.toLowerCase().includes(filterQuery);
|
||||
let libraries = libraries_;
|
||||
if (filterQuery) {
|
||||
libraries = libraries.filter((library) =>
|
||||
searchKeys.some((key) => {
|
||||
const haystack = library[key] || "";
|
||||
if (Array.isArray(haystack)) {
|
||||
return haystack.some((x) => hasMatch(x));
|
||||
} else {
|
||||
return hasMatch(haystack);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
const template = document.getElementById("template");
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const referrer = escapeHTMLAttribute(
|
||||
searchParams.get("referrer") || "https://excalidraw.com",
|
||||
);
|
||||
const appName = getAppName(referrer);
|
||||
const target = decodeURIComponent(
|
||||
escapeHTMLAttribute(searchParams.get("target")) || "_blank",
|
||||
);
|
||||
const useHash = searchParams.get("useHash");
|
||||
const csrfToken = escapeHTMLAttribute(searchParams.get("token"));
|
||||
for (let library of libraries) {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("library");
|
||||
div.setAttribute("id", library.id);
|
||||
let inner = template.innerHTML;
|
||||
const source = `libraries/${library.source}`;
|
||||
let authorsInnerHTML = "";
|
||||
inner = inner.replace(/\{libraryId\}/g, library.id);
|
||||
inner = inner.replace(/\{name\}/g, library.name);
|
||||
|
||||
const truncate = (str) => {
|
||||
if (str.length > 300) {
|
||||
str = str.slice(0, 300);
|
||||
return str.split(", ").slice(0, -1).join(", ") + "...";
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
let description = library.description || "";
|
||||
if (library.itemNames) {
|
||||
description += `<br/><br/><b>Items: </b> <span className="itemNames">${truncate(
|
||||
library.itemNames.join(", "),
|
||||
)}</span>`;
|
||||
}
|
||||
inner = inner.replace(/\{description\}/g, description);
|
||||
|
||||
inner = inner.replace(/\{source\}/g, source);
|
||||
for (let author of library.authors) {
|
||||
authorsInnerHTML += `<a href="${author.url}" target="_blank">@${author.name}</a> `;
|
||||
}
|
||||
inner = inner.replace(/\{authors\}/g, authorsInnerHTML);
|
||||
inner = inner.replace(
|
||||
/\{preview\}/g,
|
||||
`libraries/${library.preview}?v=${library.updated || 0}`,
|
||||
);
|
||||
inner = inner.replace(/\{created\}/g, getDate(library.created));
|
||||
if (library.created !== library.updated) {
|
||||
inner = inner.replace(/\{updated\}/g, getDate(library.updated));
|
||||
} else {
|
||||
inner = inner.replace('<p class="updated">Updated: {updated}</p>', "");
|
||||
}
|
||||
inner = inner.replace(/\{appName\}/g, appName);
|
||||
const libraryUrl = encodeURIComponent(
|
||||
`${escapeHTMLAttribute(origin)}/${source}`,
|
||||
);
|
||||
inner = inner.replace(
|
||||
"{addToLib}",
|
||||
`${referrer}${useHash ? "#" : "?"}addLibrary=${libraryUrl}${
|
||||
csrfToken ? `&token=${csrfToken}` : ""
|
||||
}`,
|
||||
);
|
||||
inner = inner.replace("{target}", target);
|
||||
inner = inner.replace(/\{total\}/g, library.downloads.total);
|
||||
inner = inner.replace(/\{week\}/g, library.downloads.week);
|
||||
div.innerHTML = inner;
|
||||
div.setAttribute("data-version", library.version || "1");
|
||||
template.after(div);
|
||||
}
|
||||
initImageLazyLoading();
|
||||
};
|
||||
|
||||
const handleSort = (sortType) => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
searchParams.set("sort", sortType);
|
||||
history.pushState("", "sort", `?` + searchParams.toString() + location.hash);
|
||||
|
||||
libraries_ = sortBy[sortType ?? "default"].func(libraries_);
|
||||
populateLibraryList();
|
||||
if (currSort) {
|
||||
const prev = document.getElementById(currSort);
|
||||
prev.classList.remove("option-selected");
|
||||
}
|
||||
const curr = document.getElementById(sortType);
|
||||
curr?.classList.add("option-selected");
|
||||
currSort = sortType;
|
||||
};
|
||||
|
||||
const populateSorts = () => {
|
||||
const sortTemplate = document.getElementById("sort-template");
|
||||
for ([key, value] of Object.entries(sortBy).filter(
|
||||
([key]) => key !== "default",
|
||||
)) {
|
||||
const spacer = document.createElement("span");
|
||||
spacer.innerHTML = ` · `;
|
||||
sortTemplate.before(spacer);
|
||||
const el = sortTemplate.cloneNode(true);
|
||||
el.setAttribute("id", key);
|
||||
el.innerText = el.innerText.replace(/\{label\}/g, value.label);
|
||||
el.setAttribute("href", "#");
|
||||
const handler = (sort) => () => {
|
||||
history.replaceState(null, null, " ");
|
||||
handleSort(sort);
|
||||
};
|
||||
el.onclick = handler(key);
|
||||
sortTemplate.before(el);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToAnchor = () => {
|
||||
if (location.hash) {
|
||||
const target = location.hash;
|
||||
const element = document.querySelector(target);
|
||||
if (element) {
|
||||
window.scrollTo(0, element.offsetTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTheme = (theme) => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
searchParams.set("theme", theme);
|
||||
history.pushState("", "theme", `?` + searchParams.toString() + location.hash);
|
||||
|
||||
if (theme === "dark") {
|
||||
document.querySelector("html").classList.add("theme--dark");
|
||||
document.querySelector("#light").classList.remove("is-hidden");
|
||||
document.querySelector("#dark").classList.add("is-hidden");
|
||||
} else if (theme === "light") {
|
||||
document.querySelector("#light").classList.add("is-hidden");
|
||||
document.querySelector("#dark").classList.remove("is-hidden");
|
||||
document.querySelector("html").classList.remove("theme--dark");
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// init
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Add listeners to handle theme change
|
||||
const themes = document.querySelectorAll("#theme .option");
|
||||
themes.forEach((theme) =>
|
||||
theme.addEventListener("click", () => handleTheme(theme.id)),
|
||||
);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const searchInput = document.getElementById("search-input");
|
||||
searchInput.addEventListener(
|
||||
"input",
|
||||
debounce((event) => {
|
||||
populateLibraryList(event.target.value);
|
||||
}, 200),
|
||||
);
|
||||
|
||||
document.documentElement.addEventListener("keypress", (event) => {
|
||||
if (
|
||||
!event.altKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
/^[a-z0-9]$/i.test(event.key)
|
||||
) {
|
||||
if (searchInput !== document.activeElement) {
|
||||
searchInput.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handleTheme(urlParams.get("theme") ?? "light");
|
||||
populateSorts();
|
||||
|
||||
fetchJSONFile("libraries.json", (libraries) => {
|
||||
fetchJSONFile("stats.json", (stats) => {
|
||||
for (let library of libraries) {
|
||||
const replaceText = { "/": "-", ".excalidrawlib": "" };
|
||||
const libraryId = library.source
|
||||
.toLowerCase()
|
||||
.replace(/\/|.excalidrawlib/g, (match) => replaceText[match]);
|
||||
library["id"] = libraryId;
|
||||
library["downloads"] = {
|
||||
total: libraryId in stats ? stats[libraryId].total : 0,
|
||||
week: libraryId in stats ? stats[libraryId].week : 0,
|
||||
};
|
||||
libraries_.push(library);
|
||||
}
|
||||
|
||||
const sort = urlParams.get("sort");
|
||||
|
||||
handleSort(sort ?? "default");
|
||||
scrollToAnchor();
|
||||
});
|
||||
});
|
||||
|
||||
// update footer with current year
|
||||
const footer = document.getElementById("footer");
|
||||
footer.innerHTML = footer.innerHTML.replace(/{currentYear}/g, () =>
|
||||
new Date().getFullYear(),
|
||||
);
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target.closest(".install-library")) {
|
||||
const libraryItemNode = event.target.closest(".library");
|
||||
const libraryVersion = parseInt(
|
||||
libraryItemNode.getAttribute("data-version") || "1",
|
||||
);
|
||||
|
||||
const referrer = urlParams.get("referrer");
|
||||
const referrerVersion = parseInt(urlParams.get("version") || "1");
|
||||
|
||||
if (referrer && referrerVersion < libraryVersion) {
|
||||
let message =
|
||||
"It seems the Excalidraw editor's version is older than the library version. Installing this library may not work correctly.";
|
||||
if (referrer.includes("excalidraw.com")) {
|
||||
message += `\n\nTo ensure you are on the latest version, hard-reload the excalidraw.com tab (Mac: Cmd-Shift-R, Window: Ctrl-F5). If that doesn't work, ensure you only have a single excalidraw.com tab open.`;
|
||||
}
|
||||
window.alert(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user