mirror of
https://github.com/picocss/pico.git
synced 2025-04-20 16:46:14 -04:00
V. 2.1.0 - Yohns Fork
This commit is contained in:
parent
b611b528bc
commit
42b62b10a6
10 changed files with 66 additions and 38 deletions
35
docs/js/Accordion.js
Normal file
35
docs/js/Accordion.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll('details').forEach(el => {
|
||||
let anim = null, opening = false, closing = false
|
||||
let summ = el.querySelector('summary')
|
||||
const runAnim = (height, targetHeight, callback) => {
|
||||
anim?.cancel()
|
||||
anim = el.animate({ height: [`${height}px`,`${targetHeight}px`] },{
|
||||
duration: 400,
|
||||
easing: 'ease-out'
|
||||
})
|
||||
anim.onfinish = () => {
|
||||
anim = null
|
||||
el.style.height = el.style.overflow = ''
|
||||
opening = closing = false
|
||||
callback?.()
|
||||
}
|
||||
}
|
||||
|
||||
summ.addEventListener('click', ev => {
|
||||
ev.preventDefault()
|
||||
el.style.overflow = 'hidden'
|
||||
|
||||
if(!el.open || closing) {
|
||||
el.style.height = `${el.offsetHeight}px`
|
||||
el.open = opening = true
|
||||
runAnim(el.offsetHeight, [...el.children].reduce((a,c) => a+c.offsetHeight, 0))
|
||||
anim.oncancel = () => opening = false
|
||||
} else if(el.open || opening) {
|
||||
closing = true
|
||||
runAnim(el.offsetHeight, summ.offsetHeight, () => el.open = false)
|
||||
anim.oncancel = () => closing = false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
157
docs/js/FileValidator.js
Normal file
157
docs/js/FileValidator.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
class FileValidator {
|
||||
/**
|
||||
* Initializes the FileValidator with a file input element.
|
||||
* @param {HTMLInputElement} fileInput - File input element to validate.
|
||||
* @param {boolean} displayList - Whether to display the file list (default: true).
|
||||
* @param {HTMLElement} customListContainer - Optional custom container for the file list.
|
||||
*/
|
||||
constructor(fileInput, displayList = true, customListContainer = null) {
|
||||
if (!(fileInput instanceof HTMLInputElement) || fileInput.type !== "file") {
|
||||
throw new Error("FileValidator requires an input element of type 'file'.");
|
||||
}
|
||||
|
||||
this.fileInput = fileInput;
|
||||
this.displayList = displayList;
|
||||
this.customListContainer = customListContainer;
|
||||
|
||||
this.fileInput.addEventListener("change", () => this.validateFiles());
|
||||
|
||||
// Create a container for the file list
|
||||
if (this.displayList) {
|
||||
this.fileListContainer = document.createElement("ul");
|
||||
this.fileListContainer.className = "file-list";
|
||||
if (this.customListContainer) {
|
||||
this.customListContainer.append(this.fileListContainer);
|
||||
} else {
|
||||
this.fileInput.insertAdjacentElement("afterend", this.fileListContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates files based on the accept and size attributes of the input element.
|
||||
* Displays a file list if files are valid.
|
||||
* @returns {boolean} - True if all files are valid, otherwise false.
|
||||
*/
|
||||
validateFiles() {
|
||||
const accept = this.fileInput.getAttribute("accept") || "";
|
||||
const maxSize = parseInt(this.fileInput.getAttribute("data-max-size"), 10) || Infinity;
|
||||
const files = Array.from(this.fileInput.files);
|
||||
|
||||
// Clear any existing list
|
||||
if (this.displayList) {
|
||||
this.fileListContainer.innerHTML = "";
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log("No files selected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
if (accept && !this.isFileTypeValid(file, accept)) {
|
||||
console.error(`Invalid file type: ${file.name}`);
|
||||
this.fileInput.setCustomValidity(`Invalid file type: ${file.name}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
console.error(`File too large: ${file.name} (${file.size} bytes)`);
|
||||
this.fileInput.setCustomValidity(`File too large: ${file.name}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add file to the list if valid
|
||||
if (this.displayList) {
|
||||
this.addFileToList(file);
|
||||
}
|
||||
}
|
||||
|
||||
this.fileInput.setCustomValidity("");
|
||||
console.log("All files are valid.");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the displayed list, including an image preview if the file is an image.
|
||||
* @param {File} file - The file to add to the list.
|
||||
*/
|
||||
addFileToList(file) {
|
||||
const listItem = document.createElement("li");
|
||||
listItem.textContent = `${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
|
||||
|
||||
// Add an image preview if the file is an image
|
||||
if (file.type.startsWith("image/")) {
|
||||
const imagePreview = document.createElement("img");
|
||||
imagePreview.src = URL.createObjectURL(file);
|
||||
imagePreview.style.maxWidth = "50px";
|
||||
imagePreview.style.maxHeight = "50px";
|
||||
imagePreview.onload = () => URL.revokeObjectURL(imagePreview.src); // Release memory
|
||||
listItem.prepend(imagePreview);
|
||||
}
|
||||
|
||||
// Add a remove button
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.textContent = " ";
|
||||
removeButton.className = "btn-file-rm";
|
||||
removeButton.addEventListener("click", () => this.removeFile(file, listItem));
|
||||
|
||||
listItem.append(removeButton);
|
||||
|
||||
if (this.customListContainer) {
|
||||
this.customListContainer.append(listItem);
|
||||
} else {
|
||||
this.fileListContainer.append(listItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a file from the list and updates the input file element.
|
||||
* @param {File} file - The file to remove.
|
||||
* @param {HTMLElement} listItem - The list item element to remove.
|
||||
*/
|
||||
removeFile(file, listItem) {
|
||||
const filesArray = Array.from(this.fileInput.files);
|
||||
const updatedFiles = filesArray.filter(f => f !== file);
|
||||
|
||||
// Update the input element with the new FileList
|
||||
const dataTransfer = new DataTransfer();
|
||||
updatedFiles.forEach(f => dataTransfer.items.add(f));
|
||||
this.fileInput.files = dataTransfer.files;
|
||||
|
||||
// Remove the list item from the display
|
||||
listItem.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file's type matches any of the allowed types in the accept attribute.
|
||||
* @param {File} file - File object to check.
|
||||
* @param {string} accept - Accept attribute value (e.g., ".jpg, .png, image/*").
|
||||
* @returns {boolean} - True if the file type is valid, otherwise false.
|
||||
*/
|
||||
isFileTypeValid(file, accept) {
|
||||
const acceptedTypes = accept.split(",").map(type => type.trim());
|
||||
return acceptedTypes.some(type => {
|
||||
if (type.includes("/") && file.type === type) return true;
|
||||
if (type.startsWith(".") && file.name.endsWith(type)) return true;
|
||||
if (type.endsWith("/*")) return file.type.startsWith(type.split("/")[0] + "/");
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Usage example:
|
||||
// <input type="file" id="fileInput" accept=".jpg, .png, .gif, .svg" data-max-size="1048576" multiple>
|
||||
// const fileInput = document.getElementById("fileInput");
|
||||
// new FileValidator(fileInput, false); // Hide display list
|
||||
//
|
||||
// Alternatively, with custom list container:
|
||||
// const customListContainer = document.getElementById("customListContainer");
|
||||
// new FileValidator(fileInput, true, customListContainer);
|
||||
|
||||
// Usage example2:
|
||||
// <input type="file" id="checkfile" accept=".jpg, .png, .gif, .svg" data-max-size="1048576" multiple>
|
||||
// 1048576 = 1MB
|
||||
// const checkfile = document.getElementById("checkfile");
|
||||
// new FileValidator(checkfile);
|
79
docs/js/MinimalThemeSwitcher.js
Normal file
79
docs/js/MinimalThemeSwitcher.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*!
|
||||
* Minimal theme switcher
|
||||
*
|
||||
* Pico.css - https://picocss.com
|
||||
* Copyright 2019-2024 - Licensed under MIT
|
||||
*/
|
||||
|
||||
const themeSwitcher = {
|
||||
// Config
|
||||
_scheme: "auto",
|
||||
menuTarget: "details.dropdown",
|
||||
buttonsTarget: "a[data-theme-switcher]",
|
||||
buttonAttribute: "data-theme-switcher",
|
||||
rootAttribute: "data-theme",
|
||||
localStorageKey: "picoPreferredColorScheme",
|
||||
|
||||
// Init
|
||||
init() {
|
||||
this.scheme = this.schemeFromLocalStorage;
|
||||
this.initSwitchers();
|
||||
},
|
||||
|
||||
// Get color scheme from local storage
|
||||
get schemeFromLocalStorage() {
|
||||
return window.localStorage?.getItem(this.localStorageKey) ?? this._scheme;
|
||||
},
|
||||
|
||||
// Preferred color scheme
|
||||
get preferredColorScheme() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
},
|
||||
|
||||
// Init switchers
|
||||
initSwitchers() {
|
||||
const buttons = document.querySelectorAll(this.buttonsTarget);
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener(
|
||||
"click",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
// Set scheme
|
||||
this.scheme = button.getAttribute(this.buttonAttribute);
|
||||
// Close dropdown
|
||||
document.querySelector(this.menuTarget)?.removeAttribute("open");
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
// Set scheme
|
||||
set scheme(scheme) {
|
||||
if (scheme == "auto") {
|
||||
this._scheme = this.preferredColorScheme;
|
||||
} else if (scheme == "dark" || scheme == "light") {
|
||||
this._scheme = scheme;
|
||||
}
|
||||
this.applyScheme();
|
||||
this.schemeToLocalStorage();
|
||||
},
|
||||
|
||||
// Get scheme
|
||||
get scheme() {
|
||||
return this._scheme;
|
||||
},
|
||||
|
||||
// Apply scheme
|
||||
applyScheme() {
|
||||
document.querySelector("html")?.setAttribute(this.rootAttribute, this.scheme);
|
||||
},
|
||||
|
||||
// Store scheme to local storage
|
||||
schemeToLocalStorage() {
|
||||
window.localStorage?.setItem(this.localStorageKey, this.scheme);
|
||||
},
|
||||
};
|
||||
|
||||
// Init
|
||||
themeSwitcher.init();
|
76
docs/js/Modal.js
Normal file
76
docs/js/Modal.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Modal
|
||||
*
|
||||
* Pico.css - https://picocss.com
|
||||
* Copyright 2019-2024 - Licensed under MIT
|
||||
*/
|
||||
//document.addEventListener("DOMContentLoaded", () => {
|
||||
// Config
|
||||
const isOpenClass = "modal-is-open";
|
||||
const openingClass = "modal-is-opening";
|
||||
const closingClass = "modal-is-closing";
|
||||
const scrollbarWidthCssVar = "--pico-scrollbar-width";
|
||||
const animationDuration = 400; // ms
|
||||
let visibleModal = null;
|
||||
|
||||
// Toggle modal
|
||||
const toggleModal = (event) => {
|
||||
event.preventDefault();
|
||||
const modal = document.getElementById(event.currentTarget.dataset.target);
|
||||
if (!modal) return;
|
||||
modal && (modal.open ? closeModal(modal) : openModal(modal));
|
||||
};
|
||||
|
||||
// Open modal
|
||||
const openModal = (modal) => {
|
||||
const { documentElement: html } = document;
|
||||
const scrollbarWidth = getScrollbarWidth();
|
||||
if (scrollbarWidth) {
|
||||
html.style.setProperty(scrollbarWidthCssVar, `${scrollbarWidth}px`);
|
||||
}
|
||||
html.classList.add(isOpenClass, openingClass);
|
||||
setTimeout(() => {
|
||||
visibleModal = modal;
|
||||
html.classList.remove(openingClass);
|
||||
}, animationDuration);
|
||||
modal.showModal();
|
||||
};
|
||||
|
||||
// Close modal
|
||||
const closeModal = (modal) => {
|
||||
visibleModal = null;
|
||||
const { documentElement: html } = document;
|
||||
html.classList.add(closingClass);
|
||||
setTimeout(() => {
|
||||
html.classList.remove(closingClass, isOpenClass);
|
||||
html.style.removeProperty(scrollbarWidthCssVar);
|
||||
modal.close();
|
||||
}, animationDuration);
|
||||
};
|
||||
|
||||
// Close with a click outside
|
||||
document.addEventListener("click", (event) => {
|
||||
if (visibleModal === null) return;
|
||||
const modalContent = visibleModal.querySelector("article");
|
||||
const isClickInside = modalContent.contains(event.target);
|
||||
!isClickInside && closeModal(visibleModal);
|
||||
});
|
||||
|
||||
// Close with Esc key
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && visibleModal) {
|
||||
closeModal(visibleModal);
|
||||
}
|
||||
});
|
||||
|
||||
// Get scrollbar width
|
||||
const getScrollbarWidth = () => {
|
||||
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||||
return scrollbarWidth;
|
||||
};
|
||||
|
||||
// Is scrollbar visible
|
||||
const isScrollbarVisible = () => {
|
||||
return document.body.scrollHeight > screen.height;
|
||||
};
|
||||
//})
|
20
docs/js/Notifications.js
Normal file
20
docs/js/Notifications.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
function showNotification(options = {}) {
|
||||
const dialog = document.querySelector("dialog[role='alert']");
|
||||
|
||||
if (options.text || typeof options === "string") {
|
||||
dialog.innerText = options.text || options;
|
||||
} else if (options.html) {
|
||||
dialog.innerHTML = options.html;
|
||||
}
|
||||
|
||||
dialog.showModal();
|
||||
|
||||
setTimeout(() => {
|
||||
dialog.close();
|
||||
}, options.delay || 3000);
|
||||
}
|
||||
|
||||
function closeNotification() {
|
||||
const dialog = document.querySelector("dialog[role='alert']");
|
||||
dialog.close();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue