1: UI works, receiving location updates
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
db/db.json
|
||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
10
model/Node.js
Normal file
10
model/Node.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export default function Node(node) {
|
||||||
|
let traits = [
|
||||||
|
"labels"
|
||||||
|
]
|
||||||
|
for(let i = 0; i < traits.length; i++) {
|
||||||
|
if(!node[traits[i]]) {
|
||||||
|
throw new Error(`Node is missing field "${traits[i]}": ${JSON.stringify(node)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "blockcatcher-admin",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server/index.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"moment": "^2.30.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
46
server/auth.js
Normal file
46
server/auth.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export default class AuthHandler {
|
||||||
|
ips = new Map()
|
||||||
|
#secret
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#secret = process.env.JWT_SECRET || 'random-id-for-now-123013123u1o23o12i3ukjdsbvkfndijnx1ijs';
|
||||||
|
}
|
||||||
|
|
||||||
|
check(req, res) {
|
||||||
|
if(this.ips.get(req.headers["x-forwarded-for"])) {
|
||||||
|
console.log(chalk.green(" ", req.headers["x-forwarded-for"]))
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red(" ", req.headers["x-forwarded-for"]))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login(req, res) {
|
||||||
|
const { login } = req.body;
|
||||||
|
if(login === process.env.LOGIN) {
|
||||||
|
this.ips.set(req.headers["x-forwarded-for"], new Date())
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sign(payload, options = {}) {
|
||||||
|
return jwt.sign(
|
||||||
|
payload,
|
||||||
|
this.#secret,
|
||||||
|
{ expiresIn: '30d', ...options }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(token) {
|
||||||
|
return jwt.verify(token, this.#secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
77
server/db.js
Normal file
77
server/db.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import chalk from 'chalk'
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import { pathToFileURL } from 'url';
|
||||||
|
import Node from "../model/Node.js"
|
||||||
|
|
||||||
|
export default class Database {
|
||||||
|
#nodes;
|
||||||
|
#edges;
|
||||||
|
#labels = {}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getData() {
|
||||||
|
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
|
||||||
|
let dbJson;
|
||||||
|
try {
|
||||||
|
dbJson = JSON.parse(dbData);
|
||||||
|
} catch {
|
||||||
|
dbJson = []
|
||||||
|
}
|
||||||
|
this.#nodes = dbJson["nodes"];
|
||||||
|
this.#edges = dbJson["edges"];
|
||||||
|
|
||||||
|
console.log(chalk.yellow("DB established."))
|
||||||
|
Object.preventExtensions(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// superKey = "nodes" || "edges"
|
||||||
|
async writeData(superKey, key, value) {
|
||||||
|
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
|
||||||
|
let dbJson;
|
||||||
|
try {
|
||||||
|
dbJson = JSON.parse(dbData);
|
||||||
|
} catch {
|
||||||
|
dbJson = []
|
||||||
|
}
|
||||||
|
|
||||||
|
dbJson[superKey][key] = value;
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(process.cwd(), 'db/db.json'), JSON.stringify(dbJson, null, 2), 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLabelModels() {
|
||||||
|
const labelHandlers = {};
|
||||||
|
const labelDir = path.join(process.cwd(), 'src/model/labels');
|
||||||
|
const files = await fs.readdir(labelDir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.endsWith('.js')) continue;
|
||||||
|
|
||||||
|
const label = path.basename(file, '.js');
|
||||||
|
const modulePath = path.join(labelDir, file);
|
||||||
|
const module = await import(pathToFileURL(modulePath).href);
|
||||||
|
labelHandlers[label] = module.default;
|
||||||
|
|
||||||
|
if (!this.#labels[label]) {
|
||||||
|
this.#labels[label] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labelHandlers
|
||||||
|
}
|
||||||
|
|
||||||
|
generateUserID() {
|
||||||
|
let id = this.#labels["User"].length + 1;
|
||||||
|
while (this.get.user(`user-${id}`)) {
|
||||||
|
id++;
|
||||||
|
}
|
||||||
|
return `user-${id}`; // O(1) most of the time
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
return { nodes: this.#nodes }
|
||||||
|
}
|
||||||
|
}
|
||||||
7
server/handlers.js
Normal file
7
server/handlers.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const handlers = {
|
||||||
|
updateLocation(req, res) {
|
||||||
|
console.log("req received")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handlers;
|
||||||
152
server/index.js
Normal file
152
server/index.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors'
|
||||||
|
import http from 'http'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import moment from 'moment'
|
||||||
|
import path from 'path';
|
||||||
|
import httpProxy from 'http-proxy';
|
||||||
|
const proxy = httpProxy.createProxyServer({});
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
import Database from "./db.js"
|
||||||
|
import AuthHandler from './auth.js';
|
||||||
|
import handlers from "./handlers.js";
|
||||||
|
|
||||||
|
// Get __dirname in ES6 environment
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
db;
|
||||||
|
auth;
|
||||||
|
UIPath = path.join(__dirname, '../ui')
|
||||||
|
|
||||||
|
registerRoutes(router) {
|
||||||
|
router.post('/api/location', handlers.updateLocation)
|
||||||
|
router.post('/login', this.login)
|
||||||
|
router.get('/*', this.get)
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
authMiddleware = (req, res, next) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader) {
|
||||||
|
return res.status(401).json({ error: 'Authorization token required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [scheme, token] = authHeader.split(' ');
|
||||||
|
if (scheme !== 'Bearer' || !token) {
|
||||||
|
return res.status(401).json({ error: 'Malformed authorization header.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = this.auth.verify(token);
|
||||||
|
req.user = payload;
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(403).json({ error: 'Invalid or expired token.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login = async (req, res) => {
|
||||||
|
if(this.auth.login(req, res)) {
|
||||||
|
res.writeHead(302, { 'Location': "/" }).end()
|
||||||
|
// res.status(200).send();
|
||||||
|
} else {
|
||||||
|
res.status(400).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get = async (req, res) => {
|
||||||
|
if(!this.auth.check(req, res)) {
|
||||||
|
if(req.url === "/") {
|
||||||
|
res.sendFile(path.join(this.UIPath, 'auth/auth.html'));
|
||||||
|
return;
|
||||||
|
} else if(req.url === "/betsyross.svg") {
|
||||||
|
res.sendFile(path.join(this.UIPath, '_/betsyross.svg'));
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let url = req.url
|
||||||
|
if(url === "/") {
|
||||||
|
url = "/index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePath;
|
||||||
|
if(url.startsWith("/_")) {
|
||||||
|
filePath = path.join(this.UIPath, url);
|
||||||
|
} else {
|
||||||
|
filePath = path.join(this.UIPath, "app", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendFile(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`Error serving ${filePath}:`, err);
|
||||||
|
res.status(err.status || 500).send('File not found or error serving file.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logRequest(req, res, next) {
|
||||||
|
const formattedDate = moment().format('M.D');
|
||||||
|
const formattedTime = moment().format('h:mma');
|
||||||
|
if(req.url.includes("/api/")) {
|
||||||
|
console.log(chalk.blue(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`));
|
||||||
|
} else {
|
||||||
|
if(req.url === "/")
|
||||||
|
console.log(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`));
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
logResponse(req, res, next) {
|
||||||
|
const originalSend = res.send;
|
||||||
|
res.send = function (body) {
|
||||||
|
if(res.statusCode >= 400) {
|
||||||
|
console.log(chalk.blue( `<-${chalk.red(res.statusCode)}- ${req.method} ${req.url} | ${chalk.red(body)}`));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.blue(`<-${res.statusCode}- ${req.method} ${req.url}`));
|
||||||
|
}
|
||||||
|
originalSend.call(this, body);
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.db = new Database()
|
||||||
|
this.auth = new AuthHandler()
|
||||||
|
const app = express();
|
||||||
|
app.use(cors({ origin: '*' }));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.use(this.logRequest);
|
||||||
|
app.use(this.logResponse);
|
||||||
|
|
||||||
|
let router = express.Router();
|
||||||
|
this.registerRoutes(router)
|
||||||
|
app.use('/', router);
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const PORT = 3008;
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log("\n")
|
||||||
|
console.log(chalk.yellow("**************America****************"))
|
||||||
|
console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`));
|
||||||
|
console.log(chalk.yellow("***************************************"))
|
||||||
|
console.log("\n")
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log(chalk.red('Closing server...'));
|
||||||
|
console.log(chalk.green('Database connection closed.'));
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.preventExtensions(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new Server()
|
||||||
826
ui/_/code/quill.js
Normal file
826
ui/_/code/quill.js
Normal file
@@ -0,0 +1,826 @@
|
|||||||
|
/* $ NAVIGATION */
|
||||||
|
let oldPushState = history.pushState;
|
||||||
|
history.pushState = function pushState() {
|
||||||
|
let ret = oldPushState.apply(this, arguments);
|
||||||
|
window.dispatchEvent(new Event('pushstate'));
|
||||||
|
window.dispatchEvent(new Event('navigate'));
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
window.dispatchEvent(new Event('navigate'));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.navigateTo = function(url) {
|
||||||
|
window.dispatchEvent(new Event('navigate'));
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* $ SELECTOR */
|
||||||
|
|
||||||
|
HTMLElement.prototype.$ = function(selector) {
|
||||||
|
return window.$(selector, this)
|
||||||
|
}
|
||||||
|
DocumentFragment.prototype.$ = function(selector) {
|
||||||
|
return window.$(selector, this)
|
||||||
|
}
|
||||||
|
window.$ = function(selector, el = document) {
|
||||||
|
return el.querySelector(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.$$ = function(selector, el = document) {
|
||||||
|
return Array.from(el.querySelectorAll(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CONSOLE */
|
||||||
|
|
||||||
|
console.red = function(message) {
|
||||||
|
this.log(`%c${message}`, "color: rgb(254, 79, 42);");
|
||||||
|
};
|
||||||
|
|
||||||
|
console.green = function(message) {
|
||||||
|
this.log(`%c${message}`, "color: rgb(79, 254, 42);");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GET CSS VARIABLES FOR DARK OR LIGHT MODE */
|
||||||
|
window.darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
document.documentElement.classList.add(darkMode ? 'dark' : 'light');
|
||||||
|
|
||||||
|
window.getColor = function(name) {
|
||||||
|
const rootStyles = getComputedStyle(document.documentElement);
|
||||||
|
const color = rootStyles.getPropertyValue(`--${name}`).trim();
|
||||||
|
if(!color) {
|
||||||
|
throw new Error("Color not found")
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MOBILE */
|
||||||
|
|
||||||
|
window.isMobile = function isMobile() {
|
||||||
|
return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.css = function css(cssString) {
|
||||||
|
let container = document.querySelector("style#pageStyle");
|
||||||
|
if(!container) {
|
||||||
|
container = document.createElement('style');
|
||||||
|
container.id = "pageStyle";
|
||||||
|
document.head.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
|
||||||
|
primarySelector = primarySelector.replace(/\*/g, "all");
|
||||||
|
primarySelector = primarySelector.replace(/#/g, "id-");
|
||||||
|
primarySelector = primarySelector.replace(/,/g, "");
|
||||||
|
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
|
||||||
|
if(!stylesheet) {
|
||||||
|
stylesheet = document.createElement('style');
|
||||||
|
stylesheet.id = primarySelector;
|
||||||
|
stylesheet.appendChild(document.createTextNode(cssString));
|
||||||
|
container.appendChild(stylesheet);
|
||||||
|
} else {
|
||||||
|
stylesheet.innerText = cssString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.html = function html(elementString) {
|
||||||
|
let parser = new DOMParser();
|
||||||
|
let doc = parser.parseFromString(elementString, 'text/html');
|
||||||
|
return doc.body.firstChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.util = {}
|
||||||
|
window.util.observeClassChange = (el, callback) => {
|
||||||
|
if (!el || !(el instanceof Element)) {
|
||||||
|
throw new Error("observeClassChange requires a valid DOM element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.type === "attributes" && mutation.attributeName === "class") {
|
||||||
|
callback(el.classList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(el, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"]
|
||||||
|
});
|
||||||
|
|
||||||
|
return observer; // Optional: return it so you can disconnect later
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PAGE SETUP */
|
||||||
|
|
||||||
|
Object.defineProperty(Array.prototype, 'last', {
|
||||||
|
get() {
|
||||||
|
return this[this.length - 1];
|
||||||
|
},
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* QUILL */
|
||||||
|
|
||||||
|
window.quill = {
|
||||||
|
rendering: [],
|
||||||
|
|
||||||
|
render: (el) => {
|
||||||
|
if(el instanceof Shadow) {
|
||||||
|
let parent = quill.rendering[quill.rendering.length-1]
|
||||||
|
if(!parent) {
|
||||||
|
parent = document.body
|
||||||
|
}
|
||||||
|
parent.appendChild(el)
|
||||||
|
} else {
|
||||||
|
if(!el.render) {el.render = () => {}}
|
||||||
|
let parent = quill.rendering[quill.rendering.length-1]
|
||||||
|
if(!parent) throw new Error("Quill: no parent for element")
|
||||||
|
parent.appendChild(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
quill.rendering.push(el)
|
||||||
|
el.render()
|
||||||
|
quill.rendering.pop(el)
|
||||||
|
},
|
||||||
|
|
||||||
|
rerender: (el) => {
|
||||||
|
Array.from(el.attributes).forEach(attr => el.removeAttribute(attr.name));
|
||||||
|
el.innerHTML = ""
|
||||||
|
el.removeAllListeners()
|
||||||
|
|
||||||
|
quill.rendering.push(el)
|
||||||
|
el.render()
|
||||||
|
quill.rendering.pop()
|
||||||
|
},
|
||||||
|
|
||||||
|
loadPage: () => {
|
||||||
|
let URL = window.location.pathname
|
||||||
|
if(!window.routes[URL]) {
|
||||||
|
throw new Error("No URL for this route: ", URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pageClass = window[routes[URL]]
|
||||||
|
document.title = pageClass.title ?? document.title
|
||||||
|
document.body.innerHTML = ""
|
||||||
|
let page = new pageClass()
|
||||||
|
quill.render(page)
|
||||||
|
},
|
||||||
|
|
||||||
|
isStack: (el) => {
|
||||||
|
return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Shadow = class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.registerShadow = (el, tagname) => {
|
||||||
|
if (typeof el.prototype.render !== 'function') {
|
||||||
|
throw new Error("Element must have a render: " + el.prototype.constructor.name)
|
||||||
|
}
|
||||||
|
if(!tagname) {
|
||||||
|
tagname = el.prototype.constructor.name.toLowerCase() + "-"
|
||||||
|
}
|
||||||
|
customElements.define(tagname, el)
|
||||||
|
if(el.css) {
|
||||||
|
css(el.css)
|
||||||
|
}
|
||||||
|
|
||||||
|
window[el.prototype.constructor.name] = function (...params) {
|
||||||
|
let instance = new el(...params)
|
||||||
|
quill.render(instance)
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.rerender = function() {
|
||||||
|
quill.rerender(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling */
|
||||||
|
|
||||||
|
window.vh = "vh"
|
||||||
|
window.vw = "vw"
|
||||||
|
window.px = "px"
|
||||||
|
window.em = "em"
|
||||||
|
window.rem = "rem"
|
||||||
|
window.inches = "in"
|
||||||
|
|
||||||
|
HTMLElement.prototype.addStyle = function(func) {
|
||||||
|
return func(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.css = function css(cssString) {
|
||||||
|
let container = document.querySelector("style#pageStyle");
|
||||||
|
if(!container) {
|
||||||
|
container = document.createElement('style');
|
||||||
|
container.id = "pageStyle";
|
||||||
|
document.head.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
|
||||||
|
primarySelector = primarySelector.replace(/\*/g, "all");
|
||||||
|
primarySelector = primarySelector.replace(/#/g, "id-");
|
||||||
|
primarySelector = primarySelector.replace(/,/g, "");
|
||||||
|
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
|
||||||
|
if(!stylesheet) {
|
||||||
|
stylesheet = document.createElement('style');
|
||||||
|
stylesheet.id = primarySelector;
|
||||||
|
stylesheet.appendChild(document.createTextNode(cssString));
|
||||||
|
container.appendChild(stylesheet);
|
||||||
|
} else {
|
||||||
|
stylesheet.innerText = cssString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendHTMLElementWithStyleSetters() {
|
||||||
|
let allStyleProps = Object.keys(document.createElement("div").style)
|
||||||
|
allStyleProps.forEach(prop => {
|
||||||
|
if(prop === "translate") return
|
||||||
|
HTMLElement.prototype[prop] = function(value) {
|
||||||
|
this.style[prop] = value;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
extendHTMLElementWithStyleSetters();
|
||||||
|
|
||||||
|
HTMLElement.prototype.padding = function(one, two, three = "px") {
|
||||||
|
|
||||||
|
const setPadding = (side, val) => {
|
||||||
|
const directionName = `padding${side.charAt(0).toUpperCase()}${side.slice(1)}`;
|
||||||
|
this.style[directionName] = (typeof val === 'number') ? `${val}${three}` : val;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(one === "horizontal" || one === "vertical") { // is one a direction
|
||||||
|
if (one === "horizontal") {
|
||||||
|
setPadding("left", two);
|
||||||
|
setPadding("right", two);
|
||||||
|
} else if (one === "vertical") {
|
||||||
|
setPadding("top", two);
|
||||||
|
setPadding("bottom", two);
|
||||||
|
}
|
||||||
|
} else { // is two a value
|
||||||
|
if(typeof one !== 'number' || isNaN(one)) {
|
||||||
|
this.style.padding = one
|
||||||
|
} else {
|
||||||
|
this.style.padding = one + two
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.paddingTop = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.paddingTop = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.paddingLeft = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.paddingLeft = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.paddingBottom = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.paddingBottom = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.paddingRight = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.paddingRight = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.margin = function(direction, value, unit = "px") {
|
||||||
|
if (!value) {
|
||||||
|
this.style.margin = direction;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setMargin = (side, val) => {
|
||||||
|
const directionName = `margin${side.charAt(0).toUpperCase()}${side.slice(1)}`;
|
||||||
|
this.style[directionName] = (typeof val === 'number') ? `${val}${unit}` : val;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (direction === "horizontal") {
|
||||||
|
setMargin("left", value);
|
||||||
|
setMargin("right", value);
|
||||||
|
} else if (direction === "vertical") {
|
||||||
|
setMargin("top", value);
|
||||||
|
setMargin("bottom", value);
|
||||||
|
} else {
|
||||||
|
setMargin(direction, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.marginTop = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.marginTop = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.marginLeft = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.marginLeft = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.marginBottom = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.marginBottom = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.marginRight = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.marginRight = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.width = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.width = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.minWidth = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.minWidth = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.height = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.height = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.minHeight = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.minHeight = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.fontSize = function(value, unit = "px") {
|
||||||
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
|
||||||
|
switch(value) {
|
||||||
|
case "6xl":
|
||||||
|
value = "3.75"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "5xl":
|
||||||
|
value = "3"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "4xl":
|
||||||
|
value = "2.25"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "3xl":
|
||||||
|
value = "1.875"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "2xl":
|
||||||
|
value = "1.5"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "xl":
|
||||||
|
value = "1.25"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "l":
|
||||||
|
value = "1.125"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "s":
|
||||||
|
value = "0.875"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "xs":
|
||||||
|
value = "0.75"; unit = "rem"
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.style.fontSize = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPositionType(el) {
|
||||||
|
let computed = window.getComputedStyle(el).position
|
||||||
|
if(!(computed === "absolute" || computed === "fixed")) {
|
||||||
|
el.style.position = "absolute"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.x = function(value, unit = "px") {
|
||||||
|
if (typeof value !== 'number' || isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
checkPositionType(this)
|
||||||
|
this.style.left = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.y = function(value, unit = "px") {
|
||||||
|
if (typeof value !== 'number' || isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
checkPositionType(this)
|
||||||
|
this.style.top = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.xRight = function(value, unit = "px") {
|
||||||
|
if (typeof value !== 'number' || isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
checkPositionType(this)
|
||||||
|
this.style.right = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.yBottom = function(value, unit = "px") {
|
||||||
|
if (typeof value !== 'number' || isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
checkPositionType(this)
|
||||||
|
this.style.bottom = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.borderRadius = function(value, unit = "px") {
|
||||||
|
if (typeof value !== 'number' || isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.borderRadius = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.positionType = function (value) {
|
||||||
|
if(!(value === "absolute" || value === "relative" || value === "static" || value === "fixed" || value === "sticky")) {
|
||||||
|
console.error("HTMLElement.overlflow: must have valid overflow value!")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.style.position = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.gap = function(value, unit = "px") {
|
||||||
|
if (typeof value !== 'number' || Number.isNaN(value))
|
||||||
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||||
|
this.style.gap = value + unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Elements */
|
||||||
|
|
||||||
|
quill.setChildren = function(el, innerContent) {
|
||||||
|
if(typeof innerContent === "string") {
|
||||||
|
el.innerText = innerContent
|
||||||
|
} else if(typeof innerContent === "function") {
|
||||||
|
el.render = innerContent
|
||||||
|
} else {
|
||||||
|
throw new Error("Children of unknown type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.a = function a( href, inner=href ) {
|
||||||
|
if(!href) throw new Error("quill a: missing href argument. Function: a( href, inner=href )")
|
||||||
|
let link = document.createElement("a")
|
||||||
|
link.setAttribute('href', href);
|
||||||
|
quill.setChildren(link, inner)
|
||||||
|
quill.render(link)
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
|
||||||
|
window.img = function img(src, width="", height="") {
|
||||||
|
let image = document.createElement("img")
|
||||||
|
|
||||||
|
if(!src || !(typeof src==="string")) {
|
||||||
|
throw new Error("img: missing first argument: src | String")
|
||||||
|
} else {
|
||||||
|
image.src = src
|
||||||
|
}
|
||||||
|
if(width && typeof width === "string") {
|
||||||
|
image.style.width = width
|
||||||
|
} else if(width) {
|
||||||
|
image.style.width = width + "px"
|
||||||
|
}
|
||||||
|
if(height && typeof height === "string") {
|
||||||
|
image.style.height = height
|
||||||
|
} else if(height) {
|
||||||
|
image.style.height = height + "px"
|
||||||
|
}
|
||||||
|
quill.render(image)
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLImageElement.prototype.backgroundColor = function(value) {
|
||||||
|
if (this.src.endsWith('.svg') || this.src.startsWith('data:image/svg+xml')) {
|
||||||
|
fetch(this.src).then(response => response.text())
|
||||||
|
.then(svgText => {
|
||||||
|
const modifiedSvg = svgText.replace(/\bfill="[^"]*"/g, `fill="${value}"`);
|
||||||
|
const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' });
|
||||||
|
this.src = URL.createObjectURL(blob);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error updating SVG fill:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.style.backgroundColor = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this; // Always returns the element itself
|
||||||
|
};
|
||||||
|
|
||||||
|
window.p = function p(innerText) {
|
||||||
|
let el = document.createElement("p")
|
||||||
|
if(typeof innerText === "function") {
|
||||||
|
el.render = innerText
|
||||||
|
} else {
|
||||||
|
el.innerText = innerText
|
||||||
|
}
|
||||||
|
el.style.margin = "0";
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.h1 = function h1(innerText) {
|
||||||
|
let el = document.createElement("h1")
|
||||||
|
el.innerText = innerText
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.h2 = function h2(innerText) {
|
||||||
|
let el = document.createElement("h2")
|
||||||
|
el.innerText = innerText
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.h3 = function h3(innerText) {
|
||||||
|
let el = document.createElement("h3")
|
||||||
|
el.innerText = innerText
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.div = function (innerText) {
|
||||||
|
let el = document.createElement("div")
|
||||||
|
el.innerText = innerText ?? ""
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.span = function (innerText) {
|
||||||
|
let el = document.createElement("span")
|
||||||
|
el.innerText = innerText
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.button = function (children) {
|
||||||
|
let el = document.createElement("button")
|
||||||
|
quill.setChildren(el, children)
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.form = function(cb) {
|
||||||
|
let el = document.createElement("form")
|
||||||
|
el.render = cb
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.input = function(placeholder, width, height) {
|
||||||
|
let el = document.createElement("input")
|
||||||
|
el.placeholder = placeholder
|
||||||
|
el.style.width = width
|
||||||
|
el.style.height = height
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.label = function(text) {
|
||||||
|
let el = document.createElement("label")
|
||||||
|
el.innerText = text
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
window.textarea = function(placeholder) {
|
||||||
|
let el = document.createElement("textarea")
|
||||||
|
el.placeholder = placeholder
|
||||||
|
quill.render(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* STACKS */
|
||||||
|
|
||||||
|
window.VStack = function (cb = () => {}) {
|
||||||
|
let styles = `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
let nowRendering = quill.rendering[quill.rendering.length-1]
|
||||||
|
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
|
||||||
|
nowRendering.style.cssText += styles
|
||||||
|
nowRendering.classList.add("VStack")
|
||||||
|
cb()
|
||||||
|
return nowRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
let div = document.createElement("div")
|
||||||
|
div.classList.add("VStack")
|
||||||
|
div.style.cssText += styles
|
||||||
|
div.render = cb
|
||||||
|
quill.render(div)
|
||||||
|
return div
|
||||||
|
}
|
||||||
|
|
||||||
|
window.HStack = function (cb = () => {}) {
|
||||||
|
let styles = `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
let nowRendering = quill.rendering[quill.rendering.length - 1];
|
||||||
|
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
|
||||||
|
nowRendering.style.cssText += styles;
|
||||||
|
nowRendering.classList.add("HStack")
|
||||||
|
cb();
|
||||||
|
return nowRendering;
|
||||||
|
}
|
||||||
|
|
||||||
|
let div = document.createElement("div");
|
||||||
|
div.classList.add("HStack");
|
||||||
|
div.style.cssText += styles;
|
||||||
|
div.render = cb;
|
||||||
|
quill.render(div);
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ZStack = function (cb = () => {}) {
|
||||||
|
let nowRendering = quill.rendering[quill.rendering.length - 1];
|
||||||
|
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
|
||||||
|
nowRendering.classList.add("ZStack")
|
||||||
|
cb();
|
||||||
|
return nowRendering;
|
||||||
|
}
|
||||||
|
|
||||||
|
let div = document.createElement("div");
|
||||||
|
div.classList.add("ZStack");
|
||||||
|
div.render = cb;
|
||||||
|
quill.render(div);
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* EVENTS */
|
||||||
|
|
||||||
|
HTMLElement.prototype.onAppear = function(func) {
|
||||||
|
func(this);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onClick = function(func) {
|
||||||
|
this._storeListener("click", func);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onMouseDown = function(func) {
|
||||||
|
this._storeListener("mousedown", func);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onMouseUp = function(func) {
|
||||||
|
this._storeListener("mouseup", func);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onRightClick = function(func) {
|
||||||
|
this._storeListener("contextmenu", func);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onHover = function(cb) {
|
||||||
|
const onEnter = () => cb.call(this, true);
|
||||||
|
const onLeave = () => cb.call(this, false);
|
||||||
|
this._storeListener("mouseover", onEnter);
|
||||||
|
this._storeListener("mouseleave", onLeave);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onFocus = function(cb) {
|
||||||
|
if (!this.matches('input, textarea, select, button')) {
|
||||||
|
throw new Error("Can't put focus event on non-form element!");
|
||||||
|
}
|
||||||
|
this._storeListener("focus", cb);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onBlur = function(cb) {
|
||||||
|
if (!this.matches('input, textarea, select, button')) {
|
||||||
|
throw new Error("Can't put blur event on non-form element!");
|
||||||
|
}
|
||||||
|
this._storeListener("blur", cb);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onKeyDown = function(cb) {
|
||||||
|
this._storeListener("keydown", cb);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* QUIRK 1:
|
||||||
|
In all the other callback functions, the user can choose the scope of "this". It can be either the parent shadow or the element itself.
|
||||||
|
This listener only allows for the latter functionality. This is because the navigate event fires on the window.
|
||||||
|
Without binding, "this" would refer only to the window. So here we are compromising on one of the two.
|
||||||
|
*/
|
||||||
|
HTMLElement.prototype.onNavigate = function(cb) {
|
||||||
|
window._storeListener(window, "navigate", cb.bind(this));
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype.onEvent = function(name, cb) {
|
||||||
|
window._storeListener(window, name, cb);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
HTMLElement.prototype._storeListener = function(type, handler, options) {
|
||||||
|
window._storeListener(this, type, handler, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__listeners = []
|
||||||
|
|
||||||
|
function _storeListener(target, type, handler, options) {
|
||||||
|
if (!target.__listeners) target.__listeners = [];
|
||||||
|
|
||||||
|
const optionsString = JSON.stringify(options);
|
||||||
|
|
||||||
|
const index = target.__listeners.findIndex(listener =>
|
||||||
|
listener.type === type &&
|
||||||
|
listener.handler.toString() === handler.toString() &&
|
||||||
|
JSON.stringify(listener.options) === optionsString
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === -1) { // Listener is new
|
||||||
|
target.addEventListener(type, handler, options);
|
||||||
|
target.__listeners.push({ type, handler, options });
|
||||||
|
} else { // Listener is a duplicate, can be replaced
|
||||||
|
const old = target.__listeners[index];
|
||||||
|
target.removeEventListener(old.type, old.handler, old.options);
|
||||||
|
|
||||||
|
// Replace with the new one
|
||||||
|
target.addEventListener(type, handler, options);
|
||||||
|
target.__listeners[index] = { type, handler, options };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLElement.prototype.removeAllListeners = function() {
|
||||||
|
if (!this.__listeners) return;
|
||||||
|
for (const { type, handler, options } of this.__listeners) {
|
||||||
|
this.removeEventListener(type, handler, options);
|
||||||
|
}
|
||||||
|
this.__listeners = [];
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ATTRIBUTES */
|
||||||
|
|
||||||
|
HTMLElement.prototype.attr = function(attributes) {
|
||||||
|
if (
|
||||||
|
typeof attributes !== "object" ||
|
||||||
|
attributes === null ||
|
||||||
|
Array.isArray(attributes)
|
||||||
|
) {
|
||||||
|
throw new TypeError("attr() expects an object with key-value pairs");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(attributes)) {
|
||||||
|
this.setAttribute(key, value);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
24
ui/_/icons/logo.svg
Normal file
24
ui/_/icons/logo.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="1235" height="650" viewBox="0 0 1235 650" version="1.1" id="svg890" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="1235" height="650" fill="#bf0a30" id="rect860" x="0" y="0" style="stroke-width:0.5" />
|
||||||
|
<g fill="#ffffff" id="g888" transform="scale(0.5)">
|
||||||
|
<g id="Stripes3">
|
||||||
|
<rect id="Stripe" width="2470" height="100" y="100" x="0" />
|
||||||
|
<use xlink:href="#Stripe" y="200" id="use863" x="0" width="100%" height="100%" />
|
||||||
|
<use xlink:href="#Stripe" y="400" id="use865" x="0" width="100%" height="100%" />
|
||||||
|
</g>
|
||||||
|
<use xlink:href="#Stripes3" y="600" id="use868" x="0" width="100%" height="100%" />
|
||||||
|
<rect width="823.33331" height="700" fill="#002868" id="rect870" x="0" y="0" />
|
||||||
|
<g transform="translate(411.66667,350)" id="g886">
|
||||||
|
<path transform="matrix(0.047,0,0,0.047,0,-233)" id="Star" d="M 0,-1000 -587.78525,809.01699 951.05652,-309.01699 H -951.05652 L 587.78525,809.01699 Z" />
|
||||||
|
<g id="Stars4">
|
||||||
|
<use xlink:href="#Star" transform="rotate(27.692308)" id="use873" x="0" y="0" width="100%" height="100%" />
|
||||||
|
<use xlink:href="#Star" transform="rotate(55.384615)" id="use875" x="0" y="0" width="100%" height="100%" />
|
||||||
|
<use xlink:href="#Star" transform="rotate(83.076923)" id="use877" x="0" y="0" width="100%" height="100%" />
|
||||||
|
<use xlink:href="#Star" transform="rotate(110.76923)" id="use879" x="0" y="0" width="100%" height="100%" />
|
||||||
|
</g>
|
||||||
|
<use xlink:href="#Stars4" transform="rotate(110.76923)" id="use882" x="0" y="0" width="100%" height="100%" />
|
||||||
|
<use xlink:href="#Stars4" transform="rotate(-138.46154)" id="use884" x="0" y="0" width="100%" height="100%" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
12
ui/app/components/Home.js
Normal file
12
ui/app/components/Home.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
class Home extends Shadow {
|
||||||
|
render() {
|
||||||
|
ZStack(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
.backgroundColor("#aebdff")
|
||||||
|
.display("block")
|
||||||
|
.width(100, vw).height(100, vh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerShadow(Home)
|
||||||
12
ui/app/index.html
Normal file
12
ui/app/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Blockcatcher</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="/_/icons/logo.svg">
|
||||||
|
<script src="/_/code/quill.js"></script>
|
||||||
|
<script type="module" src="/index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0px">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
ui/app/index.js
Normal file
2
ui/app/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import "./components/Home.js"
|
||||||
|
Home()
|
||||||
59
ui/auth/auth.html
Normal file
59
ui/auth/auth.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Login</title>
|
||||||
|
<link rel="icon" href="/_/icons/logo.svg">
|
||||||
|
<script type="module">
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const loginInput = document.getElementById('login');
|
||||||
|
|
||||||
|
// Listen for the Enter key to submit the login
|
||||||
|
loginInput.addEventListener('keydown', async (event) => {
|
||||||
|
if (event.key === 'Enter') { // Check if the key pressed is Enter
|
||||||
|
event.preventDefault(); // Prevent any default form submission behavior
|
||||||
|
|
||||||
|
// Get the value from the input field
|
||||||
|
const loginValue = loginInput.value;
|
||||||
|
|
||||||
|
// Send the login data to the backend using fetch
|
||||||
|
try {
|
||||||
|
const response = await fetch('/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ login: loginValue }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response;
|
||||||
|
|
||||||
|
// Handle the response from the backend
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById("login").style.borderBottom = "2px solid black"
|
||||||
|
window.location.reload()
|
||||||
|
} else {
|
||||||
|
document.getElementById("login").style.borderBottom = "2px solid red"
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during login:', error);
|
||||||
|
document.getElementById("login").style.borderBottom = "2px solid red"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body style="background-color: bisque">
|
||||||
|
<input id="login" type="text" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 50vh;
|
||||||
|
left: 50vw;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
|
">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user