refactor: type view.ts, dom.ts and time.ts
parent
35b8baef92
commit
fa97027321
@ -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