Files
gallery.mjs/gallery.mjs

782 lines
16 KiB
JavaScript

"use strict";
/**
* @name gallery.mjs
*
* @description
* Module for creating galleries with re-ordering
*
* @class
* @public
*
* @example
* import gallery from "https://git.svoboda.works/mirzaev/gallery.mjs/raw/branch/stable/gallery.mjs";
*
* // Initializing the instance
* const instance = new gallery(
* document.getElementById("wrap"),
* document.getElementById("images"),
* document.getElementById("gallery"),
* true
* );
*
* {@link https://git.svoboda.works/mirzaev/gallery.mjs}
* {@link https://codepen.io/mirzaev-sexy/pen/RNPdYvv}
*
* @todo 1. Instead of `ascend()`` create `remember()` and `restore()` methods
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export default class gallery {
/**
* @name Wrap
*
* @description
* Wrap for the gallery
*
* @type {HTMLElement}
*
* @protected
*/
#wrap;
/**
* @name Wrap (get)
*
* @description
* Wrap for the gallery
*
* @return {HTMLElement}
*
* @public
*/
get wrap() {
return this.#wrap;
}
/**
* @name Input
*
* @description
* Input for importing images
*
* @type {HTMLInputElement}
*
* @protected
*/
#input;
/**
* @name Input (get)
*
* @description
* Input for importing images
*
* @return {HTMLInputElement}
*
* @public
*/
get input() {
return this.#input;
}
/**
* @name Gallery
*
* @description
* Wrap for images <img> elements (`flex-flow: row wrap`)
*
* @type {HTMLElement}
*
* @protected
*/
#gallery;
/**
* @name Gallery (get)
*
* @description
* Wrap for images <img> elements (`flex-flow: row wrap`)
*
* @return {HTMLElement}
*
* @public
*/
get gallery() {
return this.#gallery;
}
/**
* @name Identifiers
*
* @description
* Identifiers registry of loaded images (array proxy)
*
* @see https://stackoverflow.com/a/76599646 by Alexander Nenashev
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
*
* @type {Proxy}
*
* @protected
*/
#identifiers;
/**
* @name Identifiers (get)
*
* @description
* Identifiers registry of loaded images (array proxy)
*
* @see https://stackoverflow.com/a/76599646 by Alexander Nenashev
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
*
* @return {array}
*
* @public
*/
get identifiers() {
return this.#identifiers;
}
/**
* @name Update
*
* @type {Promise|null}
*
* @protected
*/
#update;
/**
* @name Prefixes
*
* @description
* Prefixes for identifiers
*
* @type {object}
*/
prefixes = {
wrap: "",
image: "image_",
};
/**
* @name Allowed
*
* @description
* Regular expression for checking matching to images extensions (not MIME-types)
*
* @type {RegExp}
*/
allowed = /\.(jpe?g|png|gif|webp)$/i;
/**
* @name Dragged
*
* @description
* Buffer of currently dragged wrap identifier
*
* @type {string|number}
*
* @protected
*/
#dragged;
/**
* @name Dragged (get)
*
* @description
* Buffer of currently dragged wrap identifier
*
* @type {string|number}
*
* @public
*/
get dragged() {
return this.#dragged;
}
/**
* @name Events
*
* @type {Map}
*
* @protected
*/
#events = new Map([
[
"system",
new Map([
[
"dragstart",
(event) => {
// Disabling default actions
this.#dragged = event.target.getAttribute("id");
// Allowing moving
event.dataTransfer.effectAllowed = "move";
// Processing the modification event
this.#events.get("moving").get("dragstart")(event);
},
],
[
"dragover",
(event) => {
// Disabling default actions
event.preventDefault();
// Allowing moving
event.dataTransfer.dropEffect = "move";
// Processing the modification event
this.#events.get("moving").get("dragover")(event);
},
],
[
"dragenter",
(event) => {
// Searching for the wrap
const wrap = event.target.closest("div.image");
if (wrap?.getAttribute("id") !== this.#dragged) {
// Wrap is not currently draggable wrap
// Writing class about targeting
wrap?.classList.add("target");
}
// Processing the modification event
this.#events.get("moving").get("dragenter")(event);
},
],
[
"dragleave",
(event) => {
// Searching for the closest parent wrap
const wrap = event.target.closest("div.image");
if (wrap instanceof HTMLElement) {
// Found the wrap
// Deleting class about targeting
wrap.classList.remove("target");
}
// Processing the modification event
this.#events.get("moving").get("dragleave")(event);
},
],
[
"dragend",
(event) => {
// Searching for the closest parent wrap
const wrap = event.target.closest("div.image");
if (wrap instanceof HTMLElement) {
// Found the wrap
// Deleting class about targeting
wrap.classList.remove("target");
}
// Processing the modification event
this.#events.get("moving").get("dragend")(event);
},
],
[
"drop",
(event) => {
// Searching for the closest parent wrap
const wrap = event.target.closest("div.image");
if (
wrap instanceof HTMLElement &&
wrap.getAttribute("id") &&
wrap.getAttribute("id") !== this.#dragged
) {
// Found the wrap and has it identifier and it not currently draggable wrap
// Deleting class about targeting from every wrap
this.#gallery
.querySelector("div.image.target")
?.classList.remove("target");
// Initializing indexes of wraps in the identifiers registry
const from = this.#identifiers.indexOf(this.#dragged);
const to = this.#identifiers.indexOf(wrap.getAttribute("id"));
// Swapping wraps
[this.#identifiers[from], this.#identifiers[to]] = [
this.#identifiers[to],
this.#identifiers[from],
];
}
// Processing the modification event
this.#events.get("moving").get("drop")(event);
},
],
]),
],
[
"moving",
new Map([
["dragstart", async (event) => {}],
["dragover", async (event) => {}],
["dragenter", async (event) => {}],
["dragleave", async (event) => {}],
["dragend", async (event) => {}],
["drop", async (event) => {}],
]),
],
["wrap", new Map([["generated", async (wrap, order) => {}]])],
[
"import",
new Map([
["one", async (wrap) => {}],
["total", async (wraps) => {}],
]),
],
[
"export",
new Map([
["one", async (src) => {}],
["total", async (converted) => {}],
]),
],
]);
/**
* @name Events (get)
*
* @type {Map}
*
* @public
*/
get events() {
return this.#events;
}
/**
* @name Constructor
*
* @description
* Initialize the instance
*
* @param {HTMLElement} wrap The wrap element
* @param {HTMLInputElement} input The input <input> element
* @param {HTMLElement} gallery The gallery element
* @param {boolean} [inject=false] Write the instance into the wrap element?
*/
constructor(wrap, input, gallery, inject = false) {
if (wrap instanceof HTMLElement) {
// Initialized the wrap element
// Writing the wrap
this.#wrap = wrap;
// Writing the instance into the wrap element
if (inject) this.#wrap.gallery = this;
}
if (input instanceof HTMLInputElement) {
// Initialized the input <input> element
// Writing the input
this.#input = input;
}
if (gallery instanceof HTMLElement) {
// Initialized the gallery element
// Writing the gallery
this.#gallery = gallery;
}
// Initializing the identifiers registry proxy
this.proxy();
// Synchronizing the identifiers registry with images in the gallery element
this.#identifiers.push(
...[...this.#gallery.querySelectorAll("div.image")].map((image) =>
image.getAttribute("id")
)
);
}
/**
* @name Proxy
*
* @description
* Initialize the identifiers registry proxy
*/
proxy() {
// Initializing the identifiers registry proxy
this.#identifiers = new Proxy([], {
set: (target, property, value) => {
// Postponing the update with a microtask
this.#update ??= Promise.resolve().then(() => {
// Deinitializing the update promise
this.#update = null;
// Re-ordering wraps <div> elements by the identifiers registry
target.forEach((identifier) =>
this.#gallery.appendChild(document.getElementById(identifier))
);
});
// Regenerating the identifiers registry and return (success)
return Reflect.set(target, property, value);
},
});
}
/**
* @name Ascending
*
* @description
* Sort images in ascending order
*/
ascending() {
// Sorting
this.#identifiers.sort((a, b) => a - b);
}
/**
* @name Start
*
* @description
* Start handling moving of images
*/
start() {
// Initializing events listeners
for (const [event, handler] of this.#events.get("system")) {
this.#gallery.addEventListener(event, handler);
}
}
/**
* @name Stop
*
* @description
* Stop handling moving of images
*/
stop() {
// Deinitializing events listeners
for (const [event, handler] of this.#events.get("system")) {
this.#gallery.removeEventListener(event, handler);
}
}
/**
* @name Generate
*
* @description
* Create the wrap with images and buttons
*
* @param {number|string} order Number for generating identifiers
* @param {(File|string)} target The image for `src` attribute
*
* @returns {HTMLElement} Created wrap <div> element with images and buttons
*/
generate(order, target) {
// Creating the wrap <div> element
const wrap = document.createElement("div");
wrap.classList.add("image");
wrap.setAttribute("id", this.prefixes.wrap + order);
wrap.setAttribute("draggable", true);
// Creating the image <img> element
const image = document.createElement("img");
image.setAttribute("id", this.prefixes.image + order);
image.setAttribute("draggable", false);
image.setAttribute(
"src",
target instanceof File
? window.URL.createObjectURL(target)
: target + "?updated=" + Date.now()
);
// Assembling
wrap.appendChild(image);
// Processing the `generated` wrap event function
this.#events.get("wrap")?.get("generated")(wrap, order);
// Exit (success)
return wrap;
}
/**
* @name Delete
*
* @description
* Delete the wrap element and its identifier from the registry
*
* @param {HTMLElement} wrap The wrap
*
* @returns {boolean} Is the wrap was deleted?
*/
delete(wrap) {
// Initializing identifier of the wrap
const identifier = wrap.getAttribute("id");
// Initializing index of the wrap
const index = this.#identifiers.indexOf(identifier);
if (index > -1) {
// Initialized index of the wrap
// Deleting identifier of the wrap from the identifiers registry
this.#identifiers.splice(index, 1);
// Deleting the wrap
wrap.remove();
// Exit (success)
return true;
}
// Exit (fail)
return false;
}
/**
* @name Import
*
* @description
* Creating images <img> elements by loaded images
*
* We can not change `FileList` content of `<input type="file">`,
* so if you add images via `gallery.import()`,
* do not rely on the contents of the `<input>` element
*
* Use `gallery.export()` instead
*
* @param {(FileList|object} files Files for importing (can be array of URL`s)
* @param {boolean} reverse Reverse the files registry?
* @param {boolean} silent Do not processing events?
*/
import(files, reverse = false, silent = false) {
// Stopping events handlers
this.stop();
// Initializing the identifiers registry proxy
this.proxy();
// Deleting deprecated images from the gallery
this.#gallery.innerHTML = "";
// Initializing the imported wraps registry
const imported = new Set();
// Initializing the registry of files
const registry = files instanceof FileList ? Object.entries(files) : [...files];
for (const [index, file] of reverse ? registry.reverse() : registry) {
// Iterating over files
if (file) {
// Initialized the file
// Initializing the file extension
const extension = file.name ?? file.match(/\.\w{3,4}$/)?.[0];
if (this.allowed.test(extension)) {
// Allowed the file
// Generating HTML elements
const wrap = this.generate(index, file);
// Injecting HTML elements into the document
this.#gallery.appendChild(wrap);
// Writing into the identifiers registry
this.#identifiers.push(wrap.getAttribute("id"));
// Writing into the imported wraps registry
imported.add(wrap);
if (!silent) {
// Requested processing events
// Processing the `one` import event function
this.#events.get("import")?.get("one")(wrap);
}
}
}
}
if (!silent) {
// Requested processing events
// Processing the `total` import event function
this.#events.get("import")?.get("total")(imported);
}
// Starting events handlers
this.start();
}
/**
* @name Export
*
* @description
* Collect images and convert to Blob object or Base64 string
*
* @param {(string|number)} [identifier] Identifier of the wrap
* @param {boolean} [base64=false] Convert to Base64 string instead of Blob object
* @param {boolean} silent Do not processing events?
*
* @returns {Promise}
*/
async export(identifier, base64 = false, silent = false) {
// Initializing the reader
const reader = new FileReader();
if (typeof identifier === "string" || typeof identifier === "number") {
// Requested specified image
// Initializing the image
const image = this.#gallery.querySelector(
"div.image#" + CSS.escape(identifier) + ">img"
);
if (image instanceof HTMLImageElement) {
// Initialized the image <img> element
// Initializing the image content
const content = image.getAttribute("src")?.split("?")[0];
try {
if (new URL(content).protocol === "blob:") {
// Blob
// Exit (success/fail)
return new Promise((resolve) => {
// Initializing listener for the "LoadEnd" event
reader.onloadend = () => resolve(reader.result);
fetch(content)
.then((r) => r.blob())
.then((value) => {
// Converted "blob:..." string to Blob object
if (base64) {
// Base64 string
// Converting blob to base64
const converted = reader.readAsDataURL(value);
if (!silent) {
// Requested processing events
// Processing the `one` export event function
this.#events.get("export")?.get("one")(converted);
}
// Exit (success)
resolve(converted);
} else {
// Blob object
if (!silent) {
// Requested processing events
// Processing the `one` export event function
this.#events.get("export")?.get("one")(value);
}
// Exit (success)
resolve(value);
}
});
});
} else {
// Base64 or HTTP
if (!silent) {
// Requested processing events
// Processing the `one` export event function
this.#events.get("export")?.get("one")(content);
}
// Exit (success)
return content;
}
} catch {
// Base64 or HTTP
if (!silent) {
// Requested processing events
// Processing the `one` export event function
this.#events.get("export")?.get("one")(content);
}
// Exit (success)
return content;
}
}
} else {
// Requested all images
// Initializing wraps
const wraps = this.#gallery.querySelectorAll("div.image");
if (wraps.length > 0) {
// Initialized wraps
// Initialize the converted images buffer
const converted = [];
for (const wrap of wraps) {
// Iterating over images
// Initializing the wrap identifier
const identifier = wrap.getAttribute("id");
if (
typeof identifier === "string" ||
typeof identifier === "number"
) {
// Initialized the wrap identifier
// Converting the image and writing into the converted images buffer
converted.push(
(await this.export(identifier, base64, silent)) ?? null
);
}
}
if (!silent) {
// Requested processing events
// Processing the `total` export event function
this.#events.get("export")?.get("total")(converted);
}
// Exit (success)
return converted;
}
}
}
}