refactor: type view.ts, dom.ts and time.ts
parent
faca657ce4
commit
5e75b64619
@ -1,92 +0,0 @@
|
|||||||
/**
|
|
||||||
* example usage:
|
|
||||||
*
|
|
||||||
* const props = {className: 'btn', onclick: async (e) => alert('hi')};
|
|
||||||
* const btn = elem('button', props, ['download']);
|
|
||||||
* document.body.append(btn);
|
|
||||||
*
|
|
||||||
* @param {string} name
|
|
||||||
* @param {HTMLElement.prototype} props
|
|
||||||
* @param {Array<HTMLElement|string>} children
|
|
||||||
* @return HTMLElement
|
|
||||||
*/
|
|
||||||
export function elem(name = 'div', {data, ...props} = {}, children = []) {
|
|
||||||
const el = document.createElement(name);
|
|
||||||
Object.assign(el, props);
|
|
||||||
if (['number', 'string'].includes(typeof children)) {
|
|
||||||
el.append(children);
|
|
||||||
} else {
|
|
||||||
el.append(...children);
|
|
||||||
}
|
|
||||||
if (data) {
|
|
||||||
Object.entries(data).forEach(([key, value]) => el.dataset[key] = value);
|
|
||||||
}
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidURL(url) {
|
|
||||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!['', '443', '80'].includes(url.port)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (url.hostname === 'localhost') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const lastDot = url.hostname.lastIndexOf('.');
|
|
||||||
if (lastDot < 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (url.hostname.slice(lastDot) === '.local') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (url.hostname.slice(lastDot + 1).match(/^[\d]+$/)) { // there should be no tld with numbers, possible ipv4
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (url.hostname.includes(':')) { // possibly an ipv6 addr; certainly an invalid hostname
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseTextContent(string) {
|
|
||||||
let firstLink;
|
|
||||||
return [string
|
|
||||||
.trimRight()
|
|
||||||
.replaceAll(/\n{3,}/g, '\n\n')
|
|
||||||
.split('\n')
|
|
||||||
.map(line => {
|
|
||||||
const words = line.split(/\s/);
|
|
||||||
return words.map(word => {
|
|
||||||
if (word.match(/^ln(tbs?|bcr?t?)[a-z0-9]+$/g)) {
|
|
||||||
return elem('a', {
|
|
||||||
href: `lightning:${word}`
|
|
||||||
}, `lightning:${word.slice(0, 24)}…`);
|
|
||||||
}
|
|
||||||
if (!word.match(/^(https?:\/\/|www\.)\S*/)) {
|
|
||||||
return word;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (!word.startsWith('http')) {
|
|
||||||
word = 'https://' + word;
|
|
||||||
}
|
|
||||||
const url = new URL(word);
|
|
||||||
if (!isValidURL(url)) {
|
|
||||||
return word;
|
|
||||||
}
|
|
||||||
firstLink = firstLink || url.href;
|
|
||||||
return elem('a', {
|
|
||||||
href: url.href,
|
|
||||||
target: '_blank',
|
|
||||||
rel: 'noopener noreferrer'
|
|
||||||
}, url.href.slice(url.protocol.length + 2));
|
|
||||||
} catch (err) {
|
|
||||||
return word;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.reduce((acc, word) => [...acc, word, ' '], []);
|
|
||||||
})
|
|
||||||
.reduce((acc, words) => [...acc, ...words, elem('br')], []),
|
|
||||||
{firstLink}];
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* throttle and debounce given function in regular time interval,
|
|
||||||
* but with the difference that the last call will be debounced and therefore never missed.
|
|
||||||
* @param {*} function to throttle and debounce
|
|
||||||
* @param {*} time desired interval to execute function
|
|
||||||
* @returns callback
|
|
||||||
*/
|
|
||||||
export const bounce = (fn, time) => {
|
|
||||||
let throttle;
|
|
||||||
let debounce;
|
|
||||||
return (/*...args*/) => {
|
|
||||||
if (throttle) {
|
|
||||||
clearTimeout(debounce);
|
|
||||||
debounce = setTimeout(() => fn(/*...args*/), time);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fn(/*...args*/);
|
|
||||||
throttle = setTimeout(() => {
|
|
||||||
throttle = false;
|
|
||||||
}, time);
|
|
||||||
};
|
|
||||||
};
|
|
@ -0,0 +1,129 @@
|
|||||||
|
type Attributes = {
|
||||||
|
[key: string]: string | number;
|
||||||
|
} & {
|
||||||
|
data?: {
|
||||||
|
[key: string]: string | number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* example usage:
|
||||||
|
*
|
||||||
|
* const props = {className: 'btn', onclick: async (e) => alert('hi')};
|
||||||
|
* const btn = elem('button', props, ['download']);
|
||||||
|
* document.body.append(btn);
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {HTMLElement.prototype} props
|
||||||
|
* @param {Array<HTMLElement|string>} children
|
||||||
|
* @return HTMLElement
|
||||||
|
*/
|
||||||
|
export const elem = (
|
||||||
|
name: keyof HTMLElementTagNameMap,
|
||||||
|
attrs: Attributes = {},
|
||||||
|
children: Array<Node> | string = []
|
||||||
|
) => {
|
||||||
|
const {data, ...props} = attrs;
|
||||||
|
const el = document.createElement(name);
|
||||||
|
Object.assign(el, props);
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
el.append(...children);
|
||||||
|
} else {
|
||||||
|
const childType = typeof children;
|
||||||
|
if (childType === 'number' || childType === 'string') {
|
||||||
|
el.append(children);
|
||||||
|
} else {
|
||||||
|
console.error('call me');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
el.dataset[key] = value as string;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidURL = (url: URL) => {
|
||||||
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!['', '443', '80'].includes(url.port)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (url.hostname === 'localhost') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const lastDot = url.hostname.lastIndexOf('.');
|
||||||
|
if (lastDot < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (url.hostname.slice(lastDot) === '.local') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (url.hostname.slice(lastDot + 1).match(/^[\d]+$/)) { // there should be no tld with numbers, possible ipv4
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (url.hostname.includes(':')) { // possibly an ipv6 addr; certainly an invalid hostname
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* example usage:
|
||||||
|
*
|
||||||
|
* const [content, {firstLink}] = parseTextContent('Hi<br>click https://nostr.ch/');
|
||||||
|
*
|
||||||
|
* @param {string} content
|
||||||
|
* @returns [Array<string | HTMLElement>, {firstLink: href}]
|
||||||
|
*/
|
||||||
|
export const parseTextContent = (
|
||||||
|
content: string,
|
||||||
|
): [
|
||||||
|
Array<string | HTMLAnchorElement | HTMLBRElement>,
|
||||||
|
{firstLink: string | undefined},
|
||||||
|
] => {
|
||||||
|
let firstLink: string | undefined;
|
||||||
|
const parsedContent = content
|
||||||
|
.trim()
|
||||||
|
.replaceAll(/\n{3,}/g, '\n\n')
|
||||||
|
.split('\n')
|
||||||
|
.map(line => {
|
||||||
|
const words = line.split(/\s/);
|
||||||
|
return words.map(word => {
|
||||||
|
if (word.match(/^ln(tbs?|bcr?t?)[a-z0-9]+$/g)) {
|
||||||
|
return elem('a', {
|
||||||
|
href: `lightning:${word}`
|
||||||
|
}, `lightning:${word.slice(0, 24)}…`);
|
||||||
|
}
|
||||||
|
if (!word.match(/^(https?:\/\/|www\.)\S*/)) {
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!word.startsWith('http')) {
|
||||||
|
word = 'https://' + word;
|
||||||
|
}
|
||||||
|
const url = new URL(word);
|
||||||
|
if (!isValidURL(url)) {
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
firstLink = firstLink || url.href;
|
||||||
|
return elem('a', {
|
||||||
|
href: url.href,
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener noreferrer'
|
||||||
|
}, url.href.slice(url.protocol.length + 2));
|
||||||
|
} catch (err) {
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reduce((acc, word) => [...acc, word, ' '], []);
|
||||||
|
})
|
||||||
|
.reduce((acc, words) => [...acc, ...words, elem('br')], []);
|
||||||
|
|
||||||
|
return [
|
||||||
|
parsedContent,
|
||||||
|
{firstLink}
|
||||||
|
];
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export {elem, parseTextContent} from './dom';
|
||||||
|
export {bounce, dateTime, formatTime} from './time';
|
@ -0,0 +1,76 @@
|
|||||||
|
import {elem} from './utils';
|
||||||
|
|
||||||
|
type Container = {
|
||||||
|
id: string;
|
||||||
|
view: HTMLSelectElement;
|
||||||
|
content: HTMLDivElement;
|
||||||
|
dom: {
|
||||||
|
[eventId: string]: HTMLElement
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const containers: Array<Container> = [];
|
||||||
|
|
||||||
|
let activeContainerIndex = -1;
|
||||||
|
|
||||||
|
export const getViewContent = () => containers[activeContainerIndex]?.content;
|
||||||
|
|
||||||
|
export const clearView = () => {
|
||||||
|
// TODO: this is clears the current view, but it should probably do this for all views
|
||||||
|
const domMap = containers[activeContainerIndex]?.dom;
|
||||||
|
Object.keys(domMap).forEach(eventId => delete domMap[eventId]);
|
||||||
|
getViewContent().replaceChildren();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getViewElem = (eventId: string) => {
|
||||||
|
return containers[activeContainerIndex]?.dom[eventId];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setViewElem = (eventId: string, node: HTMLElement) => {
|
||||||
|
const container = containers[activeContainerIndex];
|
||||||
|
if (container) {
|
||||||
|
container.dom[eventId] = node;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mainContainer = document.querySelector('main');
|
||||||
|
|
||||||
|
const getContainer = (route: string) => {
|
||||||
|
let container = containers.find(c => c.id === route);
|
||||||
|
if (container) {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
const content = elem('div', {className: 'content'});
|
||||||
|
const view = elem('section', {className: 'view'}, [content]);
|
||||||
|
mainContainer?.append(view);
|
||||||
|
container = {id: route, view, content, dom: {}};
|
||||||
|
containers.push(container);
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const view = (route: string) => {
|
||||||
|
const active = containers[activeContainerIndex];
|
||||||
|
active?.view.classList.remove('view-active');
|
||||||
|
const nextContainer = getContainer(route);
|
||||||
|
const nextContainerIndex = containers.indexOf(nextContainer);
|
||||||
|
if (nextContainerIndex === activeContainerIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
nextContainer.view.classList.add('view-next');
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
nextContainer.view.classList.remove('view-next', 'view-prev');
|
||||||
|
nextContainer.view.classList.add('view-active');
|
||||||
|
});
|
||||||
|
// // console.log(activeContainerIndex, nextContainerIndex);
|
||||||
|
getViewContent()?.querySelectorAll('.view-prev').forEach(prev => {
|
||||||
|
prev.classList.remove('view-prev');
|
||||||
|
prev.classList.add('view-next');
|
||||||
|
});
|
||||||
|
active?.view.classList.add(nextContainerIndex < activeContainerIndex ? 'view-next' : 'view-prev');
|
||||||
|
activeContainerIndex = nextContainerIndex;
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2021"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||||
|
}
|
Loading…
Reference in New Issue